| /* HTTPURLConnection.java -- |
| Copyright (C) 2004 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA |
| 02111-1307 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package gnu.java.net.protocol.http; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.InputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.net.ProtocolException; |
| import java.net.URL; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.security.cert.Certificate; |
| import java.util.Date; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import javax.net.ssl.HandshakeCompletedEvent; |
| import javax.net.ssl.HandshakeCompletedListener; |
| import javax.net.ssl.HostnameVerifier; |
| import javax.net.ssl.HttpsURLConnection; |
| import javax.net.ssl.SSLPeerUnverifiedException; |
| import javax.net.ssl.SSLSocketFactory; |
| |
| /** |
| * A URLConnection that uses the HTTPConnection class. |
| * |
| * @author Chris Burdess (dog@gnu.org) |
| */ |
| public class HTTPURLConnection |
| extends HttpsURLConnection |
| implements HandshakeCompletedListener |
| { |
| |
| /** |
| * Pool of reusable connections, used if keepAlive is true. |
| */ |
| private static final Map connectionPool = new LinkedHashMap(); |
| |
| /* |
| * The underlying connection. |
| */ |
| private HTTPConnection connection; |
| |
| private String proxyHostname; |
| private int proxyPort; |
| private String agent; |
| private boolean keepAlive; |
| private int maxConnections; |
| |
| private Request request; |
| private Headers requestHeaders; |
| private ByteArrayOutputStream requestSink; |
| private boolean requestMethodSetExplicitly; |
| |
| private Response response; |
| private ByteArrayInputStream responseSink; |
| |
| private HandshakeCompletedEvent handshakeEvent; |
| |
| /** |
| * Constructor. |
| * @param url the URL |
| */ |
| public HTTPURLConnection(URL url) |
| throws IOException |
| { |
| super(url); |
| requestHeaders = new Headers(); |
| AccessController.doPrivileged(this.new GetHTTPPropertiesAction()); |
| } |
| |
| class GetHTTPPropertiesAction |
| implements PrivilegedAction |
| { |
| |
| public Object run() |
| { |
| proxyHostname = System.getProperty("http.proxyHost"); |
| if (proxyHostname != null && proxyHostname.length() > 0) |
| { |
| String port = System.getProperty("http.proxyPort"); |
| if (port != null && port.length() > 0) |
| { |
| proxyPort = Integer.parseInt(port); |
| } |
| else |
| { |
| proxyHostname = null; |
| proxyPort = -1; |
| } |
| } |
| agent = System.getProperty("http.agent"); |
| String ka = System.getProperty("http.keepAlive"); |
| keepAlive = !(ka != null && "false".equals(ka)); |
| String mc = System.getProperty("http.maxConnections"); |
| maxConnections = (mc != null && mc.length() > 0) ? |
| Math.max(Integer.parseInt(mc), 1) : 5; |
| return null; |
| } |
| |
| } |
| |
| public void connect() |
| throws IOException |
| { |
| if (connected) |
| { |
| return; |
| } |
| String protocol = url.getProtocol(); |
| boolean secure = "https".equals(protocol); |
| String host = url.getHost(); |
| int port = url.getPort(); |
| if (port < 0) |
| { |
| port = secure ? HTTPConnection.HTTPS_PORT : |
| HTTPConnection.HTTP_PORT; |
| } |
| String file = url.getFile(); |
| String username = url.getUserInfo(); |
| String password = null; |
| if (username != null) |
| { |
| int ci = username.indexOf(':'); |
| if (ci != -1) |
| { |
| password = username.substring(ci + 1); |
| username = username.substring(0, ci); |
| } |
| } |
| final Credentials creds = (username == null) ? null : |
| new Credentials (username, password); |
| |
| boolean retry; |
| do |
| { |
| retry = false; |
| if (connection == null) |
| { |
| connection = getConnection(host, port, secure); |
| if (secure) |
| { |
| SSLSocketFactory factory = getSSLSocketFactory(); |
| HostnameVerifier verifier = getHostnameVerifier(); |
| if (factory != null) |
| { |
| connection.setSSLSocketFactory(factory); |
| } |
| connection.addHandshakeCompletedListener(this); |
| // TODO verifier |
| } |
| } |
| if (proxyHostname != null) |
| { |
| if (proxyPort < 0) |
| { |
| proxyPort = secure ? HTTPConnection.HTTPS_PORT : |
| HTTPConnection.HTTP_PORT; |
| } |
| connection.setProxy(proxyHostname, proxyPort); |
| } |
| request = connection.newRequest(method, file); |
| if (!keepAlive) |
| { |
| request.setHeader("Connection", "close"); |
| } |
| if (agent != null) |
| { |
| request.setHeader("User-Agent", agent); |
| } |
| request.getHeaders().putAll(requestHeaders); |
| if (requestSink != null) |
| { |
| byte[] content = requestSink.toByteArray(); |
| RequestBodyWriter writer = new ByteArrayRequestBodyWriter(content); |
| request.setRequestBodyWriter(writer); |
| } |
| ByteArrayResponseBodyReader reader = new ByteArrayResponseBodyReader(); |
| request.setResponseBodyReader(reader); |
| if (creds != null) |
| { |
| request.setAuthenticator(new Authenticator() { |
| public Credentials getCredentials(String realm, int attempts) |
| { |
| return (attempts < 2) ? creds : null; |
| } |
| }); |
| } |
| response = request.dispatch(); |
| if (response.getCodeClass() == 3 && getInstanceFollowRedirects()) |
| { |
| // Follow redirect |
| String location = response.getHeader("Location"); |
| String connectionUri = connection.getURI(); |
| int start = connectionUri.length(); |
| if (location.startsWith(connectionUri) && |
| location.charAt(start) == '/') |
| { |
| file = location.substring(start); |
| retry = true; |
| } |
| else if (location.startsWith("http:")) |
| { |
| connection.close(); |
| connection = null; |
| secure = false; |
| start = 7; |
| int end = location.indexOf('/', start); |
| host = location.substring(start, end); |
| int ci = host.lastIndexOf(':'); |
| if (ci != -1) |
| { |
| port = Integer.parseInt(host.substring (ci + 1)); |
| host = host.substring(0, ci); |
| } |
| else |
| { |
| port = HTTPConnection.HTTP_PORT; |
| } |
| file = location.substring(end); |
| retry = true; |
| } |
| else if (location.startsWith("https:")) |
| { |
| connection.close(); |
| connection = null; |
| secure = true; |
| start = 8; |
| int end = location.indexOf('/', start); |
| host = location.substring(start, end); |
| int ci = host.lastIndexOf(':'); |
| if (ci != -1) |
| { |
| port = Integer.parseInt(host.substring (ci + 1)); |
| host = host.substring(0, ci); |
| } |
| else |
| { |
| port = HTTPConnection.HTTPS_PORT; |
| } |
| file = location.substring(end); |
| retry = true; |
| } |
| // Otherwise this is not an HTTP redirect, can't follow |
| } |
| else |
| { |
| responseSink = new ByteArrayInputStream(reader.toByteArray ()); |
| } |
| } |
| while (retry); |
| connected = true; |
| } |
| |
| /** |
| * Returns a connection, from the pool if necessary. |
| */ |
| HTTPConnection getConnection(String host, int port, boolean secure) |
| throws IOException |
| { |
| HTTPConnection connection; |
| if (keepAlive) |
| { |
| StringBuffer buf = new StringBuffer(secure ? "https://" : "http://"); |
| buf.append(host); |
| buf.append(':'); |
| buf.append(port); |
| String key = buf.toString(); |
| synchronized (connectionPool) |
| { |
| connection = (HTTPConnection) connectionPool.get(key); |
| if (connection == null) |
| { |
| connection = new HTTPConnection(host, port, secure); |
| // Good housekeeping |
| if (connectionPool.size() == maxConnections) |
| { |
| // maxConnections must always be >= 1 |
| Object lru = connectionPool.keySet().iterator().next(); |
| connectionPool.remove(lru); |
| } |
| connectionPool.put(key, connection); |
| } |
| } |
| } |
| else |
| { |
| connection = new HTTPConnection(host, port, secure); |
| } |
| return connection; |
| } |
| |
| public void disconnect() |
| { |
| if (connection != null) |
| { |
| try |
| { |
| connection.close(); |
| } |
| catch (IOException e) |
| { |
| } |
| } |
| } |
| |
| public boolean usingProxy() |
| { |
| return (proxyHostname != null); |
| } |
| |
| /** |
| * Overrides the corresponding method in HttpURLConnection to permit |
| * arbitrary methods, as long as they're valid ASCII alphabetic |
| * characters. This is to permit WebDAV and other HTTP extensions to |
| * function. |
| * @param method the method |
| */ |
| public void setRequestMethod(String method) |
| throws ProtocolException |
| { |
| if (connected) |
| { |
| throw new ProtocolException("Already connected"); |
| } |
| // Validate |
| method = method.toUpperCase(); |
| int len = method.length(); |
| if (len == 0) |
| { |
| throw new ProtocolException("Empty method name"); |
| } |
| for (int i = 0; i < len; i++) |
| { |
| char c = method.charAt(i); |
| if (c < 0x41 || c > 0x5a) |
| { |
| throw new ProtocolException("Illegal character '" + c + |
| "' at index " + i); |
| } |
| } |
| // OK |
| this.method = method; |
| requestMethodSetExplicitly = true; |
| } |
| |
| public String getRequestProperty(String key) |
| { |
| return requestHeaders.getValue(key); |
| } |
| |
| public Map getRequestProperties() |
| { |
| return requestHeaders; |
| } |
| |
| public void setRequestProperty(String key, String value) |
| { |
| requestHeaders.put(key, value); |
| } |
| |
| public void addRequestProperty(String key, String value) |
| { |
| String old = requestHeaders.getValue(key); |
| if (old == null) |
| { |
| requestHeaders.put(key, value); |
| } |
| else |
| { |
| requestHeaders.put(key, old + "," + value); |
| } |
| } |
| |
| public OutputStream getOutputStream() |
| throws IOException |
| { |
| if (connected) |
| { |
| throw new ProtocolException("Already connected"); |
| } |
| if (!doOutput) |
| { |
| throw new ProtocolException("doOutput is false"); |
| } |
| else if (!requestMethodSetExplicitly) |
| { |
| /* |
| * Silently change the method to POST if no method was set |
| * explicitly. This is due to broken applications depending on this |
| * behaviour (Apache XMLRPC for one). |
| */ |
| method = "POST"; |
| } |
| if (requestSink == null) |
| { |
| requestSink = new ByteArrayOutputStream(); |
| } |
| return requestSink; |
| } |
| |
| // -- Response -- |
| |
| public InputStream getInputStream() |
| throws IOException |
| { |
| if (!connected) |
| { |
| connect(); |
| } |
| if (!doInput) |
| { |
| throw new ProtocolException("doInput is false"); |
| } |
| return responseSink; |
| } |
| |
| public Map getHeaderFields() |
| { |
| if (!connected) |
| { |
| try |
| { |
| connect(); |
| } |
| catch (IOException e) |
| { |
| return null; |
| } |
| } |
| Map headers = response.getHeaders(); |
| Map ret = new LinkedHashMap(); |
| ret.put("", Collections.singletonList(getStatusLine(response))); |
| for (Iterator i = headers.entrySet().iterator(); i.hasNext(); ) |
| { |
| Map.Entry entry = (Map.Entry) i.next(); |
| String key = (String) entry.getKey(); |
| String value = (String) entry.getValue(); |
| ret.put(key, Collections.singletonList(value)); |
| } |
| return ret; |
| } |
| |
| String getStatusLine(Response response) |
| { |
| return "HTTP/" + response.getMajorVersion() + |
| "." + response.getMinorVersion() + |
| " " + response.getCode() + |
| " " + response.getMessage(); |
| } |
| |
| public String getHeaderField(int index) |
| { |
| if (!connected) |
| { |
| try |
| { |
| connect(); |
| } |
| catch (IOException e) |
| { |
| return null; |
| } |
| } |
| if (index == 0) |
| { |
| return getStatusLine(response); |
| } |
| Iterator i = response.getHeaders().entrySet().iterator(); |
| Map.Entry entry; |
| int count = 1; |
| do |
| { |
| if (!i.hasNext()) |
| { |
| return null; |
| } |
| entry = (Map.Entry) i.next(); |
| count++; |
| } |
| while (count <= index); |
| return (String) entry.getValue(); |
| } |
| |
| public String getHeaderFieldKey(int index) |
| { |
| if (!connected) |
| { |
| try |
| { |
| connect(); |
| } |
| catch (IOException e) |
| { |
| return null; |
| } |
| } |
| if (index == 0) |
| { |
| return null; |
| } |
| Iterator i = response.getHeaders().entrySet().iterator(); |
| Map.Entry entry; |
| int count = 1; |
| do |
| { |
| entry = (Map.Entry) i.next(); |
| count++; |
| } |
| while (count <= index); |
| return (String) entry.getKey(); |
| } |
| |
| public String getHeaderField(String name) |
| { |
| if (!connected) |
| { |
| try |
| { |
| connect(); |
| } |
| catch (IOException e) |
| { |
| return null; |
| } |
| } |
| return (String) response.getHeader(name); |
| } |
| |
| public long getHeaderFieldDate(String name, long def) |
| { |
| if (!connected) |
| { |
| try |
| { |
| connect(); |
| } |
| catch (IOException e) |
| { |
| return def; |
| } |
| } |
| Date date = response.getDateHeader(name); |
| return (date == null) ? def : date.getTime(); |
| } |
| |
| public String getContentType() |
| { |
| return getHeaderField("Content-Type"); |
| } |
| |
| public int getResponseCode() |
| throws IOException |
| { |
| if (!connected) |
| { |
| connect(); |
| } |
| return response.getCode(); |
| } |
| |
| public String getResponseMessage() |
| throws IOException |
| { |
| if (!connected) |
| { |
| connect(); |
| } |
| return response.getMessage(); |
| } |
| |
| // -- HTTPS specific -- |
| |
| public String getCipherSuite() |
| { |
| if (!connected) |
| { |
| throw new IllegalStateException("not connected"); |
| } |
| return handshakeEvent.getCipherSuite(); |
| } |
| |
| public Certificate[] getLocalCertificates() |
| { |
| if (!connected) |
| { |
| throw new IllegalStateException("not connected"); |
| } |
| return handshakeEvent.getLocalCertificates(); |
| } |
| |
| public Certificate[] getServerCertificates() |
| throws SSLPeerUnverifiedException |
| { |
| if (!connected) |
| { |
| throw new IllegalStateException("not connected"); |
| } |
| return handshakeEvent.getPeerCertificates(); |
| } |
| |
| // HandshakeCompletedListener |
| |
| public void handshakeCompleted(HandshakeCompletedEvent event) |
| { |
| handshakeEvent = event; |
| } |
| |
| } |
| |