/************************************************************************** * Copyright (c) 2005, 2008, 2009, 2011, 2016 by Chris Gray, KIFFER Ltd. * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without * * modification, are permitted provided that the following conditions * * are met: * * 1. Redistributions of source code must retain the above copyright * * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * * notice, this list of conditions and the following disclaimer in the * * documentation and/or other materials provided with the distribution. * * 3. Neither the name of KIFFER Ltd nor the names of other contributors * * may be used to endorse or promote products derived from this * * software without specific prior written permission. * * * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED * * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * * IN NO EVENT SHALL KIFFER LTD OR OTHER CONTRIBUTORS BE LIABLE FOR * * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT * * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * **************************************************************************/ package wonka.net.http; import java.io.*; import java.net.*; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.jar.Attributes; import java.util.zip.GZIPInputStream; import wonka.decoders.Latin1Decoder; import wonka.encoder.Base64Encoder; /** ** A simple implementation of HttpURLConnection. ** This implementation does not support connection re-use. ** Requests will be sent using transfer-encoding: chunked iff the ** content-length request header has not been set at the time of connecting. ** HTTP proxies are supported, but the only proxy-authentication method ** supported is Basic Authentication. ** ** System property wonka.net.http.timeout can be used to set the timeout ** applied to all socket read() operations. The default is 60000 milliseconds, ** and a value of 0 means that no timeout will be applied. */ public class BasicHttpURLConnection extends HttpURLConnection { private static final String POST = "POST"; private static final String GET = "GET"; private static final String PUT = "PUT"; private static final String HEAD = "HEAD"; private static final String DELETE = "DELETE"; private static final String OPTIONS = "OPTIONS"; private static final String TRACE = "TRACE"; private static final String ACCEPT_ENCODING_PROPERTY = "Accept-Encoding"; private static final String AUTHORIZATION_PROPERTY = "Authorization"; private static final String CONNECTION_PROPERTY = "Connection"; private static final String CONTENT_LENGTH_PROPERTY = "Content-Length"; private static final String HOST_PROPERTY = "Host"; private static final String IF_MODIFIED_SINCE_PROPERTY = "If-Modified-Since"; private static final String PROXY_AUTHORIZATION_PROPERTY = "Proxy-Authorization"; private static final String USER_AGENT_PROPERTY = "User-Agent"; /** ** If set to <code>true</code>, cache the proxy user name and password ** until we detect that system property <code>http.proxyHost</code> ** has changed. */ private final static boolean CACHE_PROXY_AUTH = true; private static final int MAX_RECONNECTS = 20; /** ** This is the date format we always use when sending a request, **and the first one we try when parsing the date in a response. */ private static SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'"); /** ** First fall-back when parsing the date in a response. */ private static SimpleDateFormat rfc850Parser = new SimpleDateFormat("EEEE, dd-MMM-yy HH:mm:ss 'GMT'"); /** ** Second fall-back when parsing the date in a response (and the last for now). */ private static SimpleDateFormat asctimeParser = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy"); /** ** Timeout (in milliseconds) to be applied to all socket read()s. ** Determined by system property wonka.net.http.timeout, default = 60000. */ private static int timeout; /** ** A decoder for ISO 8859-1, used for converting chars to bytes. */ private static Latin1Decoder decoder = new Latin1Decoder(); /** ** The value of the <code>http.proxyHost</code> system property. ** Empty string (<code>""</code>) if property is not defined. */ private static String proxyHost; /** ** The proxy host, resolved as an InetAddress. Only set when ** <code>getProxyAddr()</code> has been called. */ private static InetAddress proxyAddr; /** ** The value of the <code>http.proxyUser</code> system property. ** Empty string (<code>""</code>) if property is not defined. */ private static String proxyUser; /** ** The value of the <code>http.proxyPassword</code> system property. ** Empty string (<code>""</code>) if property is not defined. */ private static String proxyPassword; /** ** Mapping of protection spaces (represented as InetAddress ":" realm) ** onto credentials (represented as base64-encoded userid:password). */ private static HashMap basicCredentials = new HashMap(); /** ** Default value for instanceFollowRedirects. */ private static boolean defaultFollowRedirects = true; /** ** Package access so other classes can also see it. */ static boolean verbose = (System.getProperty("mika.verbose", "").indexOf("http") >= 0); /** ** Implement verbosity */ static void debug(String s) { if (verbose) { System.err.println(s); } } private boolean requestSent; /** ** Set up the static variables <code>proxyHost</code>, <code>proxyUser</code>, ** and <code>proxyPassword</code>. We also set <code>proxyAddr</code> to null, ** so that if this value is needed the proxy address must be resolved again. */ private static void setProxyFields() { proxyHost = System.getProperty("http.proxyHost", "").trim(); proxyAddr = null; proxyUser = System.getProperty("http.proxyUser", "").trim(); proxyPassword = System.getProperty("http.proxyPassword", "").trim(); } /** ** Returns the address of the proxy as a <code>InetAddress</code>. ** <code>proxyHost</code> will be resolved if not already done. */ private static synchronized InetAddress getProxyAddr() throws UnknownHostException { if (proxyAddr == null) { proxyAddr = InetAddress.getByName(proxyHost); } return proxyAddr; } /** ** Set up the proxy fields, the date formatters and the socket timeout. */ static { //setProxyFields(); try { TimeZone tz = TimeZone.getTimeZone("GMT"); dateFormatter.setTimeZone(tz); rfc850Parser.setTimeZone(tz); asctimeParser.setTimeZone(tz); } catch(RuntimeException rt){} timeout = Integer.getInteger("wonka.net.http.timeout", 60).intValue() * 1000; } /** ** The socket over which the request is sent and the response received. */ private Socket socket; /** ** The InputStream associated with <var>socket</var>. */ private InputStream in; /** ** The stream used to write to <var>socket</var>. */ private OutputStream out; /** ** The status line (e.g. "HTTP 200 OK") received from the server. */ private String responseLine; /** ** The request headers, as a Map from key to value. ** Only valid after <code>connect()</code>. */ private Map requestHeaders; /** ** The response headers, as a Map from key to value. ** Only valid after <code>connect()</code>. */ private Map responseHeaders; /** ** A list of valid keys into <var>responseHeaders</var>. ** Only valid after <code>connect()</code>. */ private ArrayList keys; /** ** The URL host, resolved as an InetAddress. Call <code>resolveHost()</code> ** to perform the resolution. */ InetAddress hostAddr; /** ** The realm extracted from a WWW-Authenticate challenge, or null if no challenge has been received. */ private String realm; /** ** The value of the <code>Content-length</code> request header, ** or -1 if none has been set. Package-protected, so HttpInputStream ** can see it. */ int requestContentLength; /** ** True iff parseResponse() has been called. */ private boolean responseParsed; /** ** True iff we inserted an Accept-Encoding: gzip header. */ private boolean autogzip; /** ** Number of reconnects (redirections or authorisation attempts) so far. */ private int reconnectCount; /** ** Prepare the connection (we only connect later, "lazily"). ** The <var>requestProperties</var> are pre-loaded with: ** <ul> ** <li><code>host=<own hostname>[:port]</code> ** <li><code>user-agent=Mika-HTTP</code> ** </ul> */ public BasicHttpURLConnection(URL url) { super(url); String newProxyHost = System.getProperty("http.proxyHost", "").trim(); if (!CACHE_PROXY_AUTH || !newProxyHost.equalsIgnoreCase(proxyHost)) { setProxyFields(); } setRequestProperty(HOST_PROPERTY, url.getHost()+(url.getPort()== -1 ? "" : ":"+String.valueOf(url.getPort()))); setRequestProperty(USER_AGENT_PROPERTY,"Mika-HTTP"); requestContentLength = -1; instanceFollowRedirects = defaultFollowRedirects; } /* ** Get the attribute name of the n'th header field. Note that if n == 0 ** null will be returned, because we associate 0 with the HTTP status line. */ public String getHeaderFieldKey(int n) { // ensure that response has been read try { getInputStream(); } catch (IOException ioe) {} if (responseHeaders == null) { return null; } if (keys == null || n <= 0 || n > keys.size()) { return null; } return (String)keys.get(n - 1); } /* ** Get the attribute value of the n'th header field. Note that if n == 0 ** the HTTP status line will be returned. */ public String getHeaderField(int n) { // ensure that response has been read try { getInputStream(); } catch (IOException ioe) { } if (responseHeaders == null) { return null; } if (keys == null || n < 0 || n > keys.size()) { return null; } if (n == 0) { return responseLine; } return (String)responseHeaders.get(keys.get(n - 1)); } /** ** Resolve URL host as an <code>InetAddress</code> ** and store it in <code>hostAddr</code>. */ private void resolveHost() throws UnknownHostException { String hostname = url.getHost(); hostAddr = InetAddress.getByName(hostname); } /** ** Get the port on the HTTP proxy to which requests should be sent. ** If there is no HTTP proxy (system property <code>http.proxyPort</code> ** is undefined or empty), returns 80. */ protected int getProxyPort() throws IllegalArgumentException { String proxyPortStr = System.getProperty("http.proxyPort"); if((proxyPortStr==null) || (proxyPortStr.trim().equals(""))) { return 80; } else { try { return Integer.parseInt(proxyPortStr); } catch(NumberFormatException ex) { throw new IllegalArgumentException("http.proxyPort is not a number"); } } } /** ** Connect to the remote host, send the request, and get the response. */ public void connect() throws IOException { if (connected) { return; } keys = new ArrayList(); int port = url.getPort(); if (port < 0) { port = url.getDefaultPort(); } resolveHost(); if (usingProxy()) { int proxyport = getProxyPort(); debug("HTTP: connecting to proxy " + proxyHost + ":" + proxyport); socket = new Socket(proxyHost, proxyport); } else { String host = url.getHost(); debug("HTTP: connecting to " + host + ":" + port); socket = new Socket(host, port); } socket.setSoTimeout(timeout); in = new BufferedInputStream(socket.getInputStream(),4096); out = socket.getOutputStream(); requestHeaders = getRequestProperties(); connected = true; } protected void doRequest() throws IOException { if (requestSent) { // Clean up and get out OutputStream local_out = out; if ((local_out != null) && !responseParsed) { debug("HTTP: flushing " + local_out); try { ((HttpOutputStream)local_out).flush_internal(); } catch (ClassCastException ioe) { // Ignore - we were writing directly to the socket } catch (IOException ioe) { // Ignore - probably stream was already closed } } return; } if(PUT.equals(method) || POST.equals(method)){ out.write(getRequestLine(requestHeaders).getBytes()); sendPartialHeaders(out); doOutput = true; requestSent = true; return; } else if(GET.equals(method)) { requestGET(); } else if(HEAD.equals(method)) { requestGET(); } else if(DELETE.equals(method)) { requestGET(); } else if(OPTIONS.equals(method)) { requestGET(); } else if(TRACE.equals(method)) { requestGET(); } else { throw new IOException("invalid method '"+method+"'"); } requestSent = true; parseResponse(); while (true) { if (responseCode == HTTP_UNAUTHORIZED) { String challenge = (String) responseHeaders.get("WWW-Authenticate"); if (challenge == null) { throw new ProtocolException("HTTP_UNAUTHORIZED response contained no WWW-Authenticate header"); } else if (getAuthorisation(challenge.trim(), url.toString(), hostAddr, url.getPort())) { debug("HTTP: retrying with authorization"); if (!reconnect()) { break; } } else { throw new IOException(url + " requires authorisation, but we have no credentials"); } } else if (responseCode == HTTP_PROXY_AUTH && usingProxy()) { String challenge = (String) responseHeaders.get("Proxy-Authenticate"); if (challenge == null) { throw new ProtocolException("HTTP_PROXY_AUTH response contained no Proxy-Authenticate header"); } else if (getAuthorisation(challenge.trim(), "Proxy", getProxyAddr(), getProxyPort())) { debug("HTTP: retrying with proxy authorization"); if (!reconnect()) { break; } } else { throw new IOException("Proxy server requested authentication, but we have no credentials"); } } else break; } } /** ** Disconnect from the host, by closing the socket. */ public void disconnect(){ debug("HTTP: disconnecting " + this); // [CG 20090226] Close input stream InputStream local_in = in; in = null; if (local_in != null) { debug("HTTP: closing " + local_in); try { local_in.close(); } catch(IOException ioe){} } // [CG 20090702] Close output stream OutputStream local_out = out; out = null; if (local_out != null) { debug("HTTP: closing " + local_out); try { local_out.close(); } catch(IOException ioe){} } Socket local_socket = socket; socket = null; if(local_socket != null){ debug("HTTP: disconnecting " + local_socket); try { local_socket.close(); } catch(IOException ioe){} } connected = false; } /** ** Get the response header field named <var>name</var>. */ public String getHeaderField(String name){ // ensure that response has been read try { getInputStream(); } catch (IOException ioe) {} if (responseHeaders == null) { return null; } return (String) responseHeaders.get(normaliseName(name)); } /** ** Get the HTTP response code. Implies connecting and parsing the HTTP ** response headers. */ public int getResponseCode() throws IOException { connect(); doRequest(); parseResponse(); return responseCode; } /** ** Get the HTTP response message. Implies connecting and parsing the HTTP ** response headers. */ public String getResponseMessage() throws IOException { connect(); doRequest(); parseResponse(); return responseMessage; } /** ** Parse the response header field named <var>name</var> and return the ** result as milliseconds since the start of the epoch. If no such header ** found, return <var>def</def>. */ public long getHeaderFieldDate(String name, long def){ String field = getHeaderField(name); if(field != null){ try { return dateFormatter.parse(field).getTime(); } catch(ParseException pe1){ try { return rfc850Parser.parse(field).getTime(); } catch(ParseException pe2){ try { return asctimeParser.parse(field).getTime(); } catch(ParseException pe3){} } } } return def; } /** ** Get the InputStream on which the response is received. ** If necessary, <code>connect()</code> first. */ public InputStream getInputStream() throws IOException { connect(); doRequest(); OutputStream local_out = out; out = null; if (local_out != null && local_out instanceof HttpOutputStream) { debug("HTTP: closing " + local_out); try { local_out.close(); } catch(IOException ioe){} } parseResponse(); checkConnection(); return in; } /** ** Get the OutputStream to which the request content should be written. ** If necessary, <code>connect()</code> first. */ public OutputStream getOutputStream() throws IOException { if(!doOutput){ throw new IOException("output is disabled"); } if (method.equals(GET)) { method = POST; } else if (!(PUT.equals(method) || POST.equals(method))) { throw new ProtocolException("can only call getOutputStream() on PUT or POST"); } connect(); doRequest(); if (!(out instanceof HttpOutputStream)) { out = new HttpOutputStream(out, requestContentLength); } return out; } /** ** Check whether a response header is present. */ private boolean internal_checkResponsePropertyPresent (String key) { return responseHeaders.get(normaliseName(key)) != null; } /** ** Check whether a response header is present and has a given value. */ private boolean internal_checkResponsePropertyValue (String key, String value) { String rawValue = (String) responseHeaders.get(normaliseName(key)); return rawValue != null && rawValue.equalsIgnoreCase(value); } /** ** Convert a name to "normalised form", in which the first letter, and ** the first letter after each embedded hyphen, are upper ** case and all others are lower case. This conversion should always be ** performed before using a String as key into <code>responseHeaders</code>. */ private String normaliseName(String s) { StringBuffer sb = new StringBuffer(s.toLowerCase()); sb.setCharAt(0, Character.toUpperCase(sb.charAt(0))); int l = sb.length() - 1; for (int i = 1; i < l; ++i) { if (sb.charAt(i) == '-') { ++i; sb.setCharAt(i, Character.toUpperCase(sb.charAt(i))); } else if (sb.charAt(i) == 'w' && sb.charAt(i - 1) == 'W') { sb.setCharAt(i, 'W'); } } return sb.toString(); } /** ** Set the <code>if-modified-since</code> request header. ** Must be called before <code>connect()</code>. */ public void setIfModifiedSince(long time){ super.setIfModifiedSince(time); setRequestProperty(IF_MODIFIED_SINCE_PROPERTY, dateFormatter.format(new Date(time))); } /** ** Get the request header named <var>name</var>. ** Must be called before <code>connect()</code>, which is stupid. */ public String getRequestProperty(String name){ String norm = normaliseName(name); return super.getRequestProperty(norm); } /** ** Set the request header named <var>name</var>. ** Must be called before <code>connect()</code>. */ public void setRequestProperty(String name, String value){ String norm = normaliseName(name); if (CONTENT_LENGTH_PROPERTY.equals(norm)) { requestContentLength = Integer.parseInt(value); } super.setRequestProperty(norm, value); } /** ** Returns true iff system property <code>http.proxyHost</code> is defined ** and non-empty. */ public boolean usingProxy(){ return !proxyHost.equals(""); } /** ** Generate the HTTP Request-Line. ** When no proxy is used, the Request-Line consists of the following: ** <ul> ** <li>The request token, e.g. <code>GET</code>. ** <li>A space. ** <li>The "file" part of the URL, or "/" if this is empty. ** (Note: the "file" part includes the query part, if any). ** <li>If the URL has a fragment part, "#" followed by the fragment part. ** <li>A space. ** <li><code>HTTP/1.1</code> ** <li>Carriage return, line feed. ** </ul> ** When a proxy is used the Request-Line consists of the following: ** <ul> ** <li>The request token, e.g. <code>GET</code>. ** <li>A space. ** <li>The URL originally requested. ** <li>A space. ** <li><code>HTTP/1.1</code> ** <li>Carriage return, line feed. ** </ul> */ private String getRequestLine(Map requestHeaders){ StringBuffer requestLine = new StringBuffer(method); requestLine.append(' '); if (usingProxy()) { requestLine.append(url.toString()); } else { if(url.getFile().equals("")) { requestLine.append('/'); } else { requestLine.append(url.getFile()); } String ref = url.getRef(); if(ref != null){ requestLine.append('#'); requestLine.append(ref); } } requestLine.append(" HTTP/1.1\r\n"); debug("HTTP: request: " + requestLine.substring(0, requestLine.length() - 2)); return requestLine.toString(); } /** ** Dump the request geaders to be sent as a series of debug messages. */ private void dumpRequestHeaders(String headers) { try { BufferedReader r = new BufferedReader(new StringReader(headers)); String h = r.readLine(); while (h != null) { debug("HTTP: " + h); h = r.readLine(); } } catch (IOException ioe) {} } /** ** <p>Marshal the request headers into a String. ** <p>If the followinng headers are provided by the client they are ignored: ** <ul> ** <li><code>Connection</code> ** </ul> ** <p>We always add two headers: ** <ul> ** <li><code>Connection: close</code> ** <li><code>Date: </code><i>current date and time</i> ** </ul> ** <p>The following headers will also be added if needed: ** <ul> ** <li>If a proxy is being used and proxyUser is non-empty, we add ** a proxy authentication header. ** <li>If a challenge was received, we add basic authentication. ** <li>If no Accept-Encoding header was supplied, we add one for gzip. ** </ul> */ private String getRequestHeaders(Map requestHeaders) throws UnknownHostException { addProxyAuthenticationHeader(requestHeaders); addBasicAuthenticationHeader(requestHeaders); addAcceptEncodingGzipHeader(requestHeaders); StringBuffer request = new StringBuffer(1024); request.append("Connection: close\r\n"); request.append("Date: "); dateFormatter.format(new Date(), request, new java.text.FieldPosition(0)); request.append("\r\n"); Iterator it = requestHeaders.entrySet().iterator(); boolean skip; while(it.hasNext()){ Map.Entry entry = (Map.Entry)it.next(); Object key = entry.getKey(); try { skip = ((String)key).equalsIgnoreCase("connection"); } catch (ClassCastException cce) { skip = false; } if (!skip) { request.append(key); request.append(": "); request.append(formatHeaderValue(entry.getValue())); request.append("\r\n"); } } request.append("\r\n"); if (verbose) { dumpRequestHeaders(request.toString()); } return request.toString(); } /** ** If we have basic credentials for this protection space, add basic ** authentication headers to the request headers held in <var>buffer</var>. */ private void addBasicAuthenticationHeader(Map headers) throws UnknownHostException { if (realm != null) { String credentials = (String)basicCredentials.get(hostAddr + ":" + realm); if (credentials != null) { addHeader(headers, AUTHORIZATION_PROPERTY, "Basic " + credentials); } } } /** ** If no Accept-Encoding header is present, add one with parameter "gzip" ** and set the <var>autogzip</var> flag. */ private void addAcceptEncodingGzipHeader(Map headers) throws UnknownHostException { if (GET.equals(method) && headers.get(ACCEPT_ENCODING_PROPERTY) == null) { addHeader(headers, ACCEPT_ENCODING_PROPERTY, "gzip"); autogzip = true; } } /** ** If a proxy is being used and proxyUser is non-empty, add proxy ** authentication headers to the request headers held in <var>buffer</var>. */ private void addProxyAuthenticationHeader(Map headers) throws UnknownHostException { if (usingProxy() && proxyUser != null && proxyUser.length() > 0) { //int port = getProxyPort(); StringBuffer unencoded = new StringBuffer(proxyUser); unencoded.append(':'); unencoded.append(proxyPassword); StringBuffer buffer = new StringBuffer(64); buffer.append("Basic "); buffer.append(Base64Encoder.encode(unencoded.toString())); addHeader(headers, PROXY_AUTHORIZATION_PROPERTY, buffer.toString()); } } private void addHeader(Map headers, String name, String value) { List values = new ArrayList(1); values.add(value); headers.put(name, values); } /** ** Send the headers for [the first chunk of] a PUT/POST request. ** We always send <code>Connection: close</code>, regardless of ** what was specified using setRequestHeader(). We also always add ** a <code>Date:<code> header. */ private void sendPartialHeaders(OutputStream out) throws IOException { StringBuffer request = new StringBuffer(1024); Map temp = new HashMap(requestHeaders); temp.remove(CONNECTION_PROPERTY); request.append(CONNECTION_PROPERTY + ": close\r\n"); //connection will be closed after the response addProxyAuthenticationHeader(temp); addBasicAuthenticationHeader(temp); addAcceptEncodingGzipHeader(temp); request.append("Date: "); dateFormatter.format(new Date(), request, new java.text.FieldPosition(0)); request.append("\r\n"); Iterator it = temp.entrySet().iterator(); while(it.hasNext()){ Map.Entry entry = (Map.Entry)it.next(); request.append(entry.getKey().toString()); request.append(": "); request.append(formatHeaderValue(entry.getValue())); request.append("\r\n"); } if (verbose) { dumpRequestHeaders(request.toString()); } int length = request.length(); char[] chars = new char[length]; request.getChars(0,length,chars,0); out.write(decoder.cToB(chars,0,length)); } private String formatHeaderValue(Object value) { List list = (List) value; if (list == null || list.size() == 0) { return ""; } StringBuffer sb = new StringBuffer((String) list.get(0)); for (int i = 1; i < list.size(); ++i) { sb.append(','); sb.append(list.get(i)); } return sb.toString(); } /** * Add username:password to the table as an authorisation for addr:realm. */ private void addAuthorisation(InetAddress addr, String realm, String username, char[] password) { StringBuffer unencoded = new StringBuffer(username); unencoded.append(':'); unencoded.append(password); basicCredentials.put(addr + ":" + realm, Base64Encoder.encode(unencoded.toString())); } /** * Remove username:password from the table as an authorisation for addr:realm. */ private void removeAuthorisation(InetAddress addr, String realm) { basicCredentials.remove(addr + ":" + realm); } /** ** Get the information required for WWW or Proxy Authorisation, by invoking ** the default Authenticator. ** <param>challenge ** The value of the WWW-Authenticate or Proxy-Authenticate header in the ** server's 401 or 407 response. ** <param>name ** Either "Proxy" for Proxy Authorisation, or the string form of the URL ** for which authorisation is required. ** <param>addr ** The address of the proxy server (proxy authorisation) or origin server ** (WWW authorisation). ** <param>port ** The port used for the proxy server (proxy authorisation) or origin server ** (WWW authorisation). */ private boolean getAuthorisation(String challenge, String name, InetAddress addr, int port) { String prompt = name + " requires authorization"; String scheme = "[not set]"; int space1 = challenge.indexOf(' '); if (space1 >= 0) { scheme = challenge.substring(0, space1); String rest = challenge.substring(space1 + 1); int realmstart = rest.indexOf("realm=\""); int realmend = (realmstart < 0) ? -1 : rest.indexOf("\"", realmstart + 7); if (realmend >= 0) { realm = rest.substring(realmstart + 6, realmend + 1); prompt += " for realm " + realm; } } PasswordAuthentication passwordAuth = Authenticator.requestPasswordAuthentication(addr, port, "HTTP", prompt, scheme); if (passwordAuth!=null) { if ("Proxy".equalsIgnoreCase(name)) { proxyUser = passwordAuth.getUserName(); proxyPassword = new String(passwordAuth.getPassword()); } else { addAuthorisation(addr, realm, passwordAuth.getUserName(), passwordAuth.getPassword()); } return true; } return false; } /** ** Perform a GET-style request, i.e. one with no body. We send all the ** headers except Content-Length. */ private void requestGET() throws IOException { Map temp = new HashMap(requestHeaders); temp.remove(new Attributes.Name(CONTENT_LENGTH_PROPERTY)); out.write(getRequestLine(temp).getBytes()); out.write(getRequestHeaders(temp).getBytes()); doOutput = false; } /** ** Parse an HTTP response into a response code, a response message (if any), ** and a set of response headers. */ void parseResponse() throws IOException { if (responseParsed) { return; } responseHeaders = new HashMap(); // TODO: should this code be moved to checkConnection()? if (out != null) { try { ((HttpOutputStream)out).flush_internal(); } catch (ClassCastException ioe) { // Ignore - we were writing directly to the socket } catch (IOException ioe) { // Ignore - probably stream was already closed } } try { responseParsed = true; String line = readLine(false); responseLine = line; debug("HTTP: response: " + line); int space1 = line.indexOf(' '); if (space1 >= 0) { String codeString; int space2 = line.indexOf(' ',space1 + 1); if (space2 >= 0) { codeString = line.substring(space1 + 1, space2); try { responseCode = Integer.parseInt(codeString); responseMessage = line.substring(space2 + 1); } catch (NumberFormatException nfe) { } } else { // Technically this is wrong by RFC 2068, but whatever ... codeString = line.substring(space1 + 1); try { responseCode = Integer.parseInt(codeString); } catch (NumberFormatException nfe) { } } } while(!"".equals(line)){ Object key; int colon = line.indexOf(':'); if(colon != -1) { String name = line.substring(0,colon); if(name.trim().equals("")){ key = new Integer(keys.size()); } else { key = normaliseName(name); } responseHeaders.put(key, line.substring(colon+1).trim()); keys.add(key); } else { key = new Integer(keys.size()); responseHeaders.put(key, line.trim() + ")"); keys.add(key); } line = readLine(true); debug("HTTP: " + line); } } catch(RuntimeException rt){ rt.printStackTrace(); throw new IOException(); } // TODO: should this code be moved to checkConnection()? if(internal_checkResponsePropertyValue("Transfer-encoding", "chunked")){ in = new ChunkedInputStream(in); debug("HTTP: switched to chunked input"); } // TODO: should this code be moved to checkConnection()? if(autogzip && internal_checkResponsePropertyValue("Content-encoding", "gzip")){ in = new GZIPInputStream(in); responseHeaders.remove("Content-encoding"); responseHeaders.remove("Content-length"); debug("HTTP: gunzipping input"); } if(responseCode>=100 && responseCode<200) { // Continue recognized. Parse next header responseParsed = false; parseResponse(); } else if (((responseCode==HTTP_MOVED_PERM) || (responseCode==HTTP_MOVED_TEMP) || (responseCode==HTTP_SEE_OTHER)) && instanceFollowRedirects && ++reconnectCount < MAX_RECONNECTS) { if (!(GET == method || HEAD == method)) { if (responseCode == HTTP_SEE_OTHER) { method = GET; } else { throw new IOException(responseCode + " redirection response not allowed for " + method); } } String location = (String) responseHeaders.get("Location"); debug("HTTP: redirecting to " + location); if(location==null) { throw new IOException("HTTP redirect (" + responseCode + ") has no 'Location' header."); } this.url = new URL(location.trim()); disconnect(); setRequestProperty(HOST_PROPERTY, url.getHost()+(url.getPort()== -1 ? "" : ":"+String.valueOf(url.getPort()))); requestSent = false; responseParsed = false; if (++reconnectCount < MAX_RECONNECTS) { connect(); doRequest(); } } } boolean reconnect() throws IOException { disconnect(); requestSent = false; responseParsed = false; if (++reconnectCount >= MAX_RECONNECTS) { return false; } connect(); doRequest(); return true; } /** ** Throw an exception if it is not possible to read data from the connection. ** */ void checkConnection() throws IOException { if (responseCode == HTTP_OK) { return; } if (++reconnectCount >= MAX_RECONNECTS) { throw new ProtocolException("Server redirected too many times (" + MAX_RECONNECTS + ")"); } if (responseCode == HTTP_NOT_FOUND) { throw new FileNotFoundException(url.toString()); } throw new IOException("Server returned HTTP response code " + responseCode + " for " + method + " to " + url); } /** ** Read one line of the response headers, allowing for "extension lines" ** (lines beginning with a space or tab, which continue the previous ** line). Needs to read a few bytes ahead in order to determine whether ** the following line is a continuation line, so <var>in</var> needs to ** support <code>mark()/reset()</code>. */ private String readLine(boolean continued) throws IOException { StringBuffer buf = new StringBuffer(64); // [CG 20090226] Guard against close() in another thread InputStream local_in = in; int ch = local_in.read(); while(ch != -1){ boolean stop = false; if(ch == '\r'){ stop = true; local_in.mark(1); ch = local_in.read(); } if(ch == '\n'){ if(buf.length() == 0){ break; } stop = true; local_in.mark(1); ch = local_in.read(); } if(stop){ if(continued && (ch == ' ' || ch == '\t')){ continue; } else { local_in.reset(); break; } } buf.append((char)ch); ch = local_in.read(); } return buf.toString(); } }