001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.modules.proxy;
042:
043: import javax.net.SocketFactory;
044: import java.net.*;
045: import java.util.*;
046: import java.util.prefs.Preferences;
047: import java.util.regex.Pattern;
048: import java.io.*;
049: import java.nio.channels.SocketChannel;
050:
051: /**
052: * Creates sockets capable of connecting through HTTPS and SOCKS proxies.
053: *
054: * @author Maros Sandor
055: */
056: public class ProxySocketFactory extends SocketFactory {
057:
058: private static final int CONNECT_TIMEOUT = 1000 * 20; /// 20 seconds timeout
059:
060: private static final String USE_PROXY_AUTHENTICATION = "useProxyAuthentication";
061: private static final String PROXY_AUTHENTICATION_USERNAME = "proxyAuthenticationUsername";
062: private static final String PROXY_AUTHENTICATION_PASSWORD = "proxyAuthenticationPassword";
063:
064: private static final String AUTH_NONE = "<none>";
065: private static final String AUTH_BASIC = "Basic";
066: private static final Pattern sConnectionEstablishedPattern = Pattern
067: .compile("HTTP\\/\\d+\\.\\d+\\s+200\\s+");
068: private static final Pattern sProxyAuthRequiredPattern = Pattern
069: .compile("HTTP\\/\\d+\\.\\d+\\s+407\\s+");
070:
071: private static final ProxySocketFactory instance = new ProxySocketFactory();
072:
073: private final Map<InetSocketAddress, ConnectivitySettings> lastKnownSettings = Collections
074: .synchronizedMap(new HashMap<InetSocketAddress, ConnectivitySettings>(
075: 2));
076:
077: public static ProxySocketFactory getDefault() {
078: return instance;
079: }
080:
081: private ProxySocketFactory() {
082: }
083:
084: /**
085: * Creates probe socket that supports only
086: * connect(SocketAddressm, int timeout).
087: */
088: public Socket createSocket() throws IOException {
089: return new Socket() {
090: public void connect(SocketAddress endpoint, int timeout)
091: throws IOException {
092: Socket s = createSocket((InetSocketAddress) endpoint,
093: timeout);
094: s.close();
095: }
096:
097: public void bind(SocketAddress bindpoint) {
098: throw new UnsupportedOperationException();
099: }
100:
101: protected Object clone() {
102: throw new UnsupportedOperationException();
103: }
104:
105: public synchronized void close() {
106: }
107:
108: public void connect(SocketAddress endpoint) {
109: throw new UnsupportedOperationException();
110: }
111:
112: public SocketChannel getChannel() {
113: throw new UnsupportedOperationException();
114: }
115:
116: public InetAddress getInetAddress() {
117: throw new UnsupportedOperationException();
118: }
119:
120: public InputStream getInputStream() {
121: throw new UnsupportedOperationException();
122: }
123:
124: public boolean getKeepAlive() {
125: throw new UnsupportedOperationException();
126: }
127:
128: public InetAddress getLocalAddress() {
129: throw new UnsupportedOperationException();
130: }
131:
132: public int getLocalPort() {
133: throw new UnsupportedOperationException();
134: }
135:
136: public SocketAddress getLocalSocketAddress() {
137: throw new UnsupportedOperationException();
138: }
139:
140: public boolean getOOBInline() {
141: throw new UnsupportedOperationException();
142: }
143:
144: public OutputStream getOutputStream() {
145: throw new UnsupportedOperationException();
146: }
147:
148: public int getPort() {
149: throw new UnsupportedOperationException();
150: }
151:
152: public synchronized int getReceiveBufferSize() {
153: throw new UnsupportedOperationException();
154: }
155:
156: public SocketAddress getRemoteSocketAddress() {
157: throw new UnsupportedOperationException();
158: }
159:
160: public boolean getReuseAddress() {
161: throw new UnsupportedOperationException();
162: }
163:
164: public synchronized int getSendBufferSize() {
165: throw new UnsupportedOperationException();
166: }
167:
168: public int getSoLinger() {
169: throw new UnsupportedOperationException();
170: }
171:
172: public synchronized int getSoTimeout() {
173: throw new UnsupportedOperationException();
174: }
175:
176: public boolean getTcpNoDelay() {
177: throw new UnsupportedOperationException();
178: }
179:
180: public int getTrafficClass() {
181: throw new UnsupportedOperationException();
182: }
183:
184: public boolean isBound() {
185: throw new UnsupportedOperationException();
186: }
187:
188: public boolean isClosed() {
189: throw new UnsupportedOperationException();
190: }
191:
192: public boolean isConnected() {
193: throw new UnsupportedOperationException();
194: }
195:
196: public boolean isInputShutdown() {
197: throw new UnsupportedOperationException();
198: }
199:
200: public boolean isOutputShutdown() {
201: throw new UnsupportedOperationException();
202: }
203:
204: public void sendUrgentData(int data) {
205: throw new UnsupportedOperationException();
206: }
207:
208: public void setKeepAlive(boolean on) {
209: throw new UnsupportedOperationException();
210: }
211:
212: public void setOOBInline(boolean on) {
213: throw new UnsupportedOperationException();
214: }
215:
216: public synchronized void setReceiveBufferSize(int size) {
217: throw new UnsupportedOperationException();
218: }
219:
220: public void setReuseAddress(boolean on) {
221: throw new UnsupportedOperationException();
222: }
223:
224: public synchronized void setSendBufferSize(int size) {
225: throw new UnsupportedOperationException();
226: }
227:
228: public void setSoLinger(boolean on, int linger) {
229: throw new UnsupportedOperationException();
230: }
231:
232: public synchronized void setSoTimeout(int timeout) {
233: throw new UnsupportedOperationException();
234: }
235:
236: public void setTcpNoDelay(boolean on) {
237: throw new UnsupportedOperationException();
238: }
239:
240: public void setTrafficClass(int tc) {
241: throw new UnsupportedOperationException();
242: }
243:
244: public void shutdownInput() {
245: throw new UnsupportedOperationException();
246: }
247:
248: public void shutdownOutput() {
249: throw new UnsupportedOperationException();
250: }
251: };
252: }
253:
254: public Socket createSocket(String host, int port)
255: throws IOException {
256: return createSocket(new InetSocketAddress(host, port),
257: CONNECT_TIMEOUT);
258: }
259:
260: public Socket createSocket(InetAddress inetAddress, int port)
261: throws IOException {
262: return createSocket(new InetSocketAddress(inetAddress, port),
263: CONNECT_TIMEOUT);
264: }
265:
266: public Socket createSocket(String s, int i,
267: InetAddress inetAddress, int i1) throws IOException {
268: throw new IOException("Unsupported operation");
269: }
270:
271: public Socket createSocket(InetAddress inetAddress, int i,
272: InetAddress inetAddress1, int i1) throws IOException {
273: throw new IOException("Unsupported operation");
274: }
275:
276: /**
277: * Connects to the remote machine by establishing a tunnel through a HTTP proxy. It issues a CONNECT request and
278: * eventually authenticates with the HTTP proxy. Supported authentication methods include: Basic.
279: *
280: * @param address remote machine to connect to
281: * @return a TCP/IP socket connected to the remote machine
282: * @throws UnknownHostException if the proxy host name cannot be resolved
283: * @throws IOException if an I/O error occurs during handshake (a network problem)
284: */
285: private Socket getHttpsTunnelSocket(InetSocketAddress address,
286: ConnectivitySettings cs, int timeout) throws IOException {
287: Socket proxy = new Socket();
288: proxy.connect(new InetSocketAddress(cs.getProxyHost(), cs
289: .getProxyPort()), timeout);
290: BufferedReader r = new BufferedReader(new InputStreamReader(
291: new InterruptibleInputStream(proxy.getInputStream())));
292: DataOutputStream dos = new DataOutputStream(proxy
293: .getOutputStream());
294:
295: dos.writeBytes("CONNECT ");
296: dos.writeBytes(address.getHostName() + ":" + address.getPort());
297: dos.writeBytes(" HTTP/1.0\r\n");
298: dos.writeBytes("Connection: Keep-Alive\r\n\r\n");
299: dos.flush();
300:
301: String line;
302: line = r.readLine();
303:
304: if (sConnectionEstablishedPattern.matcher(line).find()) {
305: for (;;) {
306: line = r.readLine();
307: if (line.length() == 0)
308: break;
309: }
310: return proxy;
311: } else if (sProxyAuthRequiredPattern.matcher(line).find()) {
312: boolean authMethodSelected = false;
313: String authMethod = AUTH_NONE;
314: for (;;) {
315: line = r.readLine();
316: if (line.length() == 0)
317: break;
318: if (line.startsWith("Proxy-Authenticate:")
319: && !authMethodSelected) {
320: authMethod = line.substring(19).trim();
321: if (authMethod.equals(AUTH_BASIC)) {
322: authMethodSelected = true;
323: }
324: }
325: }
326: // TODO: need to read full response before closing connection?
327: proxy.close();
328:
329: if (authMethod.startsWith(AUTH_BASIC)) {
330: return authenticateBasic(address, cs);
331: } else {
332: throw new IOException(
333: "Unsupported authentication method: "
334: + authMethod);
335: }
336: } else {
337: proxy.close();
338: throw new IOException(
339: "HTTP proxy does not support CONNECT command. Received reply: "
340: + line);
341: }
342: }
343:
344: /**
345: * Connects to the remote machine by establishing a tunnel through a HTTP proxy with Basic authentication.
346: * It issues a CONNECT request and authenticates with the HTTP proxy with Basic protocol.
347: *
348: * @param address remote machine to connect to
349: * @return a TCP/IP socket connected to the remote machine
350: * @throws IOException if an I/O error occurs during handshake (a network problem)
351: */
352: private Socket authenticateBasic(InetSocketAddress address,
353: ConnectivitySettings cs) throws IOException {
354: Socket proxy = new Socket(cs.getProxyHost(), cs.getProxyPort());
355: BufferedReader r = new BufferedReader(new InputStreamReader(
356: new InterruptibleInputStream(proxy.getInputStream())));
357: DataOutputStream dos = new DataOutputStream(proxy
358: .getOutputStream());
359:
360: String username = cs.getProxyUsername() == null ? "" : cs
361: .getProxyUsername();
362: String password = cs.getProxyPassword() == null ? "" : String
363: .valueOf(cs.getProxyPassword());
364: String credentials = username + ":" + password;
365: String basicCookie = Base64Encoder.encode(credentials
366: .getBytes("US-ASCII"));
367:
368: dos.writeBytes("CONNECT ");
369: dos.writeBytes(address.getHostName() + ":" + address.getPort());
370: dos.writeBytes(" HTTP/1.0\r\n");
371: dos.writeBytes("Connection: Keep-Alive\r\n");
372: dos.writeBytes("Proxy-Authorization: Basic " + basicCookie
373: + "\r\n");
374: dos.writeBytes("\r\n");
375: dos.flush();
376:
377: String line = r.readLine();
378: if (sConnectionEstablishedPattern.matcher(line).find()) {
379: for (;;) {
380: line = r.readLine();
381: if (line.length() == 0)
382: break;
383: }
384: return proxy;
385: }
386: throw new IOException("Basic authentication failed: " + line);
387: }
388:
389: /**
390: * Creates a new Socket connected to the given IP address. The method uses connection settings supplied
391: * in the constructor for connecting the socket.
392: *
393: * @param address the IP address to connect to
394: * @return connected socket
395: * @throws java.net.UnknownHostException if the hostname of the address or the proxy cannot be resolved
396: * @throws java.io.IOException if an I/O error occured while connecting to the remote end or to the proxy
397: */
398: private Socket createSocket(InetSocketAddress address, int timeout)
399: throws IOException {
400: String socksProxyHost = System.getProperty("socksProxyHost");
401: System.getProperties().remove("socksProxyHost");
402: try {
403: ConnectivitySettings cs = lastKnownSettings.get(address);
404: if (cs != null) {
405: try {
406: return createSocket(cs, address, timeout);
407: } catch (IOException e) {
408: // not good anymore, try all proxies
409: lastKnownSettings.remove(address);
410: }
411: }
412:
413: URI uri = addressToURI(address, "socket");
414: try {
415: return createSocket(uri, address, timeout);
416: } catch (IOException e) {
417: // we will also try https
418: }
419:
420: uri = addressToURI(address, "https");
421: return createSocket(uri, address, timeout);
422:
423: } finally {
424: if (socksProxyHost != null) {
425: System.setProperty("socksProxyHost", socksProxyHost);
426: }
427: }
428: }
429:
430: private URI addressToURI(InetSocketAddress address, String schema) {
431: URI uri;
432: try {
433: if (address.isUnresolved()) {
434: uri = new URI(schema + "://" + address.getHostName()
435: + ":" + address.getPort());
436: } else {
437: uri = new URI(schema + "://"
438: + address.getAddress().getHostAddress() + ":"
439: + address.getPort());
440: }
441: } catch (URISyntaxException e) {
442: throw new RuntimeException(e);
443: }
444: return uri;
445: }
446:
447: private Socket createSocket(URI uri, InetSocketAddress address,
448: int timeout) throws IOException {
449: List<Proxy> proxies = ProxySelector.getDefault().select(uri);
450: IOException lastFailure = null;
451: for (Proxy proxy : proxies) {
452: ConnectivitySettings cs = proxyToCs(proxy);
453: try {
454: Socket s = createSocket(cs, address, timeout);
455: lastKnownSettings.put(address, cs);
456: return s;
457: } catch (IOException e) {
458: lastFailure = e;
459: }
460: }
461:
462: throw lastFailure;
463: }
464:
465: private ConnectivitySettings proxyToCs(Proxy proxy) {
466: ConnectivitySettings cs = new ConnectivitySettings();
467: InetSocketAddress isa = (InetSocketAddress) proxy.address();
468: switch (proxy.type()) {
469: case HTTP:
470: setupProxy(cs, ConnectivitySettings.CONNECTION_VIA_HTTPS,
471: isa);
472: break;
473: case SOCKS:
474: setupProxy(cs, ConnectivitySettings.CONNECTION_VIA_SOCKS,
475: isa);
476: break;
477: default:
478: }
479:
480: Preferences prefs = org.openide.util.NbPreferences.root().node(
481: "org/netbeans/core"); // NOI18N
482: if (prefs.getBoolean(USE_PROXY_AUTHENTICATION, false)) {
483: cs.setProxyUsername(prefs.get(
484: PROXY_AUTHENTICATION_USERNAME, null));
485: cs.setProxyPassword(prefs.get(
486: PROXY_AUTHENTICATION_PASSWORD, null).toCharArray());
487: }
488: return cs;
489: }
490:
491: private void setupProxy(ConnectivitySettings cs,
492: int connectionType, InetSocketAddress inetSocketAddress) {
493: cs.setConnectionType(connectionType);
494: InetAddress address = inetSocketAddress.getAddress();
495: cs.setProxyHost((address != null) ? address.getHostAddress()
496: : inetSocketAddress.getHostName());
497: cs.setProxyPort(inetSocketAddress.getPort());
498: }
499:
500: private Socket createSocket(ConnectivitySettings cs,
501: InetSocketAddress address, int timeout) throws IOException {
502: switch (cs.getConnectionType()) {
503: case ConnectivitySettings.CONNECTION_VIA_SOCKS:
504: case ConnectivitySettings.CONNECTION_DIRECT:
505: Socket s = new Socket();
506: s.connect(address, timeout);
507: return s;
508:
509: case ConnectivitySettings.CONNECTION_VIA_HTTPS:
510: return getHttpsTunnelSocket(address, cs, timeout);
511:
512: default:
513: throw new IllegalArgumentException(
514: "Illegal connection type: "
515: + cs.getConnectionType());
516: }
517: }
518: }
|