| /* 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.List; | |
| import java.util.Map; | |
| import javax.net.ssl.HandshakeCompletedEvent; | |
| import javax.net.ssl.HandshakeCompletedListener; | |
| 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 = -1; | |
| 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(); | |
| String proxy = SystemProperties.getProperty("http.proxyHost"); | |
| if (proxy != null && proxy.length() > 0) | |
| { | |
| String port = SystemProperties.getProperty("http.proxyPort"); | |
| if (port != null && port.length() > 0) | |
| { | |
| try | |
| { | |
| proxyPort = Integer.parseInt(port); | |
| proxyHostname = proxy; | |
| } | |
| catch (NumberFormatException _) | |
| { | |
| // Ignore. | |
| } | |
| } | |
| } | |
| 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(); | |
| // FIXME: use the verifier | |
| // 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(), | |
| getReadTimeout()); | |
| } | |
| else | |
| { | |
| connection = new HTTPConnection(host, port, secure, | |
| getConnectTimeout(), getReadTimeout()); | |
| } | |
| 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<String, List<String>> getRequestProperties() | |
| { | |
| if (connected) | |
| throw new IllegalStateException("Already connected"); | |
| Map<String, List<String>> 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<String,List<String>> getHeaderFields() | |
| { | |
| if (!connected) | |
| { | |
| try | |
| { | |
| connect(); | |
| } | |
| catch (IOException e) | |
| { | |
| return null; | |
| } | |
| } | |
| Map<String,List<String>> 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 read timeout, in milliseconds, or zero if the timeout | |
| * is to be considered infinite. | |
| * | |
| * Overloaded. | |
| * | |
| */ | |
| public void setReadTimeout(int timeout) | |
| throws IllegalArgumentException | |
| { | |
| super.setReadTimeout(timeout); | |
| if (connection == null) | |
| return; | |
| try | |
| { | |
| connection.getSocket().setSoTimeout(timeout); | |
| } | |
| catch (IOException se) | |
| { | |
| // Ignore socket exceptions. | |
| } | |
| } | |
| } |