| /* HTTPURLConnection.java -- |
| Copyright (C) 2004, 2005, 2006 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., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 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 gnu.classpath.SystemProperties; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.ProtocolException; |
| import java.net.URL; |
| import java.security.cert.Certificate; |
| import java.util.Collections; |
| import java.util.Date; |
| 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 |
| { |
| /* |
| * The underlying connection. |
| */ |
| private HTTPConnection connection; |
| |
| // These are package private for use in anonymous inner classes. |
| String proxyHostname; |
| int proxyPort; |
| String agent; |
| boolean keepAlive; |
| |
| private Request request; |
| private Headers requestHeaders; |
| private ByteArrayOutputStream requestSink; |
| private boolean requestMethodSetExplicitly; |
| |
| private Response response; |
| private InputStream responseSink; |
| private InputStream errorSink; |
| |
| private HandshakeCompletedEvent handshakeEvent; |
| |
| /** |
| * Constructor. |
| * @param url the URL |
| */ |
| public HTTPURLConnection(URL url) |
| throws IOException |
| { |
| super(url); |
| requestHeaders = new Headers(); |
| proxyHostname = SystemProperties.getProperty("http.proxyHost"); |
| if (proxyHostname != null && proxyHostname.length() > 0) |
| { |
| String port = SystemProperties.getProperty("http.proxyPort"); |
| if (port != null && port.length() > 0) |
| { |
| proxyPort = Integer.parseInt(port); |
| } |
| else |
| { |
| proxyHostname = null; |
| proxyPort = -1; |
| } |
| } |
| agent = SystemProperties.getProperty("http.agent"); |
| String ka = SystemProperties.getProperty("http.keepAlive"); |
| keepAlive = !(ka != null && "false".equals(ka)); |
| } |
| |
| 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); |
| |
| if ("POST".equals(method)) |
| { |
| String contentType = requestHeaders.getValue("Content-Type"); |
| if (null == contentType) |
| requestHeaders.addValue("Content-Type", |
| "application/x-www-form-urlencoded"); |
| } |
| |
| 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); |
| } |
| try |
| { |
| 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); |
| } |
| if (creds != null) |
| { |
| request.setAuthenticator(new Authenticator() { |
| public Credentials getCredentials(String realm, int attempts) |
| { |
| return (attempts < 2) ? creds : null; |
| } |
| }); |
| } |
| response = request.dispatch(); |
| } |
| catch (IOException ioe) |
| { |
| if (connection.useCount > 0) |
| { |
| // Connection re-use failed: Try a new connection. |
| try |
| { |
| connection.close(); |
| } |
| catch (IOException _) |
| { |
| // Ignore. |
| } |
| connection = null; |
| retry = true; |
| continue; |
| } |
| else |
| { |
| // First time the connection was used: Hard failure. |
| throw ioe; |
| } |
| } |
| |
| if (response.isRedirect() && getInstanceFollowRedirects()) |
| { |
| // Read the response body, if there is one. If the |
| // redirect points us back at the same server, we will use |
| // the cached connection, so we must make sure there is no |
| // pending data in it. |
| InputStream body = response.getBody(); |
| if (body != null) |
| { |
| byte[] ignore = new byte[1024]; |
| while (true) |
| { |
| int n = body.read(ignore, 0, ignore.length); |
| if (n == -1) |
| break; |
| } |
| } |
| |
| // Follow redirect |
| String location = response.getHeader("Location"); |
| if (location != null) |
| { |
| 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); |
| if (end == -1) |
| end = location.length(); |
| 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); |
| if (end == -1) |
| end = location.length(); |
| 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; |
| } |
| else if (location.length() > 0) |
| { |
| // Malformed absolute URI, treat as file part of URI |
| if (location.charAt(0) == '/') |
| { |
| // Absolute path |
| file = location; |
| } |
| else |
| { |
| // Relative path |
| int lsi = file.lastIndexOf('/'); |
| file = (lsi == -1) ? "/" : file.substring(0, lsi + 1); |
| file += location; |
| } |
| retry = true; |
| } |
| } |
| } |
| else |
| { |
| responseSink = response.getBody(); |
| |
| if (response.isError()) |
| errorSink = responseSink; |
| } |
| } |
| 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) |
| { |
| connection = HTTPConnection.Pool.instance.get(host, port, secure, getConnectTimeout(), 0); |
| } |
| else |
| { |
| connection = new HTTPConnection(host, port, secure, 0, getConnectTimeout()); |
| } |
| 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() |
| { |
| if (connected) |
| throw new IllegalStateException("Already connected"); |
| |
| Map m = requestHeaders.getAsMap(); |
| return Collections.unmodifiableMap(m); |
| } |
| |
| public void setRequestProperty(String key, String value) |
| { |
| super.setRequestProperty(key, value); |
| |
| requestHeaders.put(key, value); |
| } |
| |
| public void addRequestProperty(String key, String value) |
| { |
| super.addRequestProperty(key, value); |
| requestHeaders.addValue(key, 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"); |
| } |
| |
| if (response.isError()) |
| { |
| int code = response.getCode(); |
| if (code == 404 || code == 410) |
| throw new FileNotFoundException(url.toString()); |
| |
| throw new IOException("Server returned HTTP response code " + code |
| + " for URL " + url.toString()); |
| } |
| |
| return responseSink; |
| } |
| |
| public InputStream getErrorStream() |
| { |
| return errorSink; |
| } |
| |
| public Map getHeaderFields() |
| { |
| if (!connected) |
| { |
| try |
| { |
| connect(); |
| } |
| catch (IOException e) |
| { |
| return null; |
| } |
| } |
| Map m = response.getHeaders().getAsMap(); |
| m.put(null, Collections.singletonList(getStatusLine(response))); |
| return Collections.unmodifiableMap(m); |
| } |
| |
| 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); |
| } |
| return response.getHeaders().getHeaderValue(index - 1); |
| } |
| |
| public String getHeaderFieldKey(int index) |
| { |
| if (!connected) |
| { |
| try |
| { |
| connect(); |
| } |
| catch (IOException e) |
| { |
| return null; |
| } |
| } |
| // index of zero is the status line. |
| return response.getHeaders().getHeaderName(index - 1); |
| } |
| |
| public String getHeaderField(String name) |
| { |
| if (!connected) |
| { |
| try |
| { |
| connect(); |
| } |
| catch (IOException e) |
| { |
| return null; |
| } |
| } |
| return 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; |
| } |
| |
| /** |
| * Set the connection timeout speed, in milliseconds, or zero if the timeout |
| * is to be considered infinite. |
| * |
| * Overloaded. |
| * |
| */ |
| public void setConnectTimeout(int timeout) |
| throws IllegalArgumentException |
| { |
| super.setConnectTimeout( timeout ); |
| if( connection == null ) |
| return; |
| try |
| { |
| connection.getSocket().setSoTimeout( timeout ); |
| } |
| catch(IOException se) |
| { |
| // Ignore socket exceptions. |
| } |
| } |
| } |
| |