| /* Request.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.java.net.BASE64; |
| import gnu.java.net.LineInputStream; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.ProtocolException; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.text.DateFormat; |
| import java.text.ParseException; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.zip.GZIPInputStream; |
| import java.util.zip.InflaterInputStream; |
| |
| /** |
| * A single HTTP request. |
| * |
| * @author Chris Burdess (dog@gnu.org) |
| */ |
| public class Request |
| { |
| |
| /** |
| * The connection context in which this request is invoked. |
| */ |
| protected final HTTPConnection connection; |
| |
| /** |
| * The HTTP method to invoke. |
| */ |
| protected final String method; |
| |
| /** |
| * The path identifying the resource. |
| * This string must conform to the abs_path definition given in RFC2396, |
| * with an optional "?query" part, and must be URI-escaped by the caller. |
| */ |
| protected final String path; |
| |
| /** |
| * The headers in this request. |
| */ |
| protected final Headers requestHeaders; |
| |
| /** |
| * The request body provider. |
| */ |
| protected RequestBodyWriter requestBodyWriter; |
| |
| /** |
| * Map of response header handlers. |
| */ |
| protected Map responseHeaderHandlers; |
| |
| /** |
| * The authenticator. |
| */ |
| protected Authenticator authenticator; |
| |
| /** |
| * Whether this request has been dispatched yet. |
| */ |
| private boolean dispatched; |
| |
| /** |
| * Constructor for a new request. |
| * @param connection the connection context |
| * @param method the HTTP method |
| * @param path the resource path including query part |
| */ |
| protected Request(HTTPConnection connection, String method, |
| String path) |
| { |
| this.connection = connection; |
| this.method = method; |
| this.path = path; |
| requestHeaders = new Headers(); |
| responseHeaderHandlers = new HashMap(); |
| } |
| |
| /** |
| * Returns the connection associated with this request. |
| * @see #connection |
| */ |
| public HTTPConnection getConnection() |
| { |
| return connection; |
| } |
| |
| /** |
| * Returns the HTTP method to invoke. |
| * @see #method |
| */ |
| public String getMethod() |
| { |
| return method; |
| } |
| |
| /** |
| * Returns the resource path. |
| * @see #path |
| */ |
| public String getPath() |
| { |
| return path; |
| } |
| |
| /** |
| * Returns the full request-URI represented by this request, as specified |
| * by HTTP/1.1. |
| */ |
| public String getRequestURI() |
| { |
| return connection.getURI() + path; |
| } |
| |
| /** |
| * Returns the headers in this request. |
| */ |
| public Headers getHeaders() |
| { |
| return requestHeaders; |
| } |
| |
| /** |
| * Returns the value of the specified header in this request. |
| * @param name the header name |
| */ |
| public String getHeader(String name) |
| { |
| return requestHeaders.getValue(name); |
| } |
| |
| /** |
| * Returns the value of the specified header in this request as an integer. |
| * @param name the header name |
| */ |
| public int getIntHeader(String name) |
| { |
| return requestHeaders.getIntValue(name); |
| } |
| |
| /** |
| * Returns the value of the specified header in this request as a date. |
| * @param name the header name |
| */ |
| public Date getDateHeader(String name) |
| { |
| return requestHeaders.getDateValue(name); |
| } |
| |
| /** |
| * Sets the specified header in this request. |
| * @param name the header name |
| * @param value the header value |
| */ |
| public void setHeader(String name, String value) |
| { |
| requestHeaders.put(name, value); |
| } |
| |
| /** |
| * Convenience method to set the entire request body. |
| * @param requestBody the request body content |
| */ |
| public void setRequestBody(byte[] requestBody) |
| { |
| setRequestBodyWriter(new ByteArrayRequestBodyWriter(requestBody)); |
| } |
| |
| /** |
| * Sets the request body provider. |
| * @param requestBodyWriter the handler used to obtain the request body |
| */ |
| public void setRequestBodyWriter(RequestBodyWriter requestBodyWriter) |
| { |
| this.requestBodyWriter = requestBodyWriter; |
| } |
| |
| /** |
| * Sets a callback handler to be invoked for the specified header name. |
| * @param name the header name |
| * @param handler the handler to receive the value for the header |
| */ |
| public void setResponseHeaderHandler(String name, |
| ResponseHeaderHandler handler) |
| { |
| responseHeaderHandlers.put(name, handler); |
| } |
| |
| /** |
| * Sets an authenticator that can be used to handle authentication |
| * automatically. |
| * @param authenticator the authenticator |
| */ |
| public void setAuthenticator(Authenticator authenticator) |
| { |
| this.authenticator = authenticator; |
| } |
| |
| /** |
| * Dispatches this request. |
| * A request can only be dispatched once; calling this method a second |
| * time results in a protocol exception. |
| * @exception IOException if an I/O error occurred |
| * @return an HTTP response object representing the result of the operation |
| */ |
| public Response dispatch() |
| throws IOException |
| { |
| if (dispatched) |
| { |
| throw new ProtocolException("request already dispatched"); |
| } |
| final String CRLF = "\r\n"; |
| final String HEADER_SEP = ": "; |
| final String US_ASCII = "US-ASCII"; |
| final String version = connection.getVersion(); |
| Response response; |
| int contentLength = -1; |
| boolean retry = false; |
| int attempts = 0; |
| boolean expectingContinue = false; |
| if (requestBodyWriter != null) |
| { |
| contentLength = requestBodyWriter.getContentLength(); |
| String expect = getHeader("Expect"); |
| if (expect != null && expect.equals("100-continue")) |
| { |
| expectingContinue = true; |
| } |
| else |
| { |
| setHeader("Content-Length", Integer.toString(contentLength)); |
| } |
| } |
| |
| try |
| { |
| // Loop while authentication fails or continue |
| do |
| { |
| retry = false; |
| |
| // Get socket output and input streams |
| OutputStream out = connection.getOutputStream(); |
| |
| // Request line |
| String requestUri = path; |
| if (connection.isUsingProxy() && |
| !"*".equals(requestUri) && |
| !"CONNECT".equals(method)) |
| { |
| requestUri = getRequestURI(); |
| } |
| String line = method + ' ' + requestUri + ' ' + version + CRLF; |
| out.write(line.getBytes(US_ASCII)); |
| // Request headers |
| for (Iterator i = requestHeaders.iterator(); i.hasNext(); ) |
| { |
| Headers.HeaderElement elt = (Headers.HeaderElement)i.next(); |
| line = elt.name + HEADER_SEP + elt.value + CRLF; |
| out.write(line.getBytes(US_ASCII)); |
| } |
| out.write(CRLF.getBytes(US_ASCII)); |
| // Request body |
| if (requestBodyWriter != null && !expectingContinue) |
| { |
| byte[] buffer = new byte[4096]; |
| int len; |
| int count = 0; |
| |
| requestBodyWriter.reset(); |
| do |
| { |
| len = requestBodyWriter.write(buffer); |
| if (len > 0) |
| { |
| out.write(buffer, 0, len); |
| } |
| count += len; |
| } |
| while (len > -1 && count < contentLength); |
| } |
| out.flush(); |
| // Get response |
| while(true) |
| { |
| response = readResponse(connection.getInputStream()); |
| int sc = response.getCode(); |
| if (sc == 401 && authenticator != null) |
| { |
| if (authenticate(response, attempts++)) |
| { |
| retry = true; |
| } |
| } |
| else if (sc == 100) |
| { |
| if (expectingContinue) |
| { |
| requestHeaders.remove("Expect"); |
| setHeader("Content-Length", |
| Integer.toString(contentLength)); |
| expectingContinue = false; |
| retry = true; |
| } |
| else |
| { |
| // A conforming server can send an unsoliceted |
| // Continue response but *should* not (RFC 2616 |
| // sec 8.2.3). Ignore the bogus Continue |
| // response and get the real response that |
| // should follow |
| continue; |
| } |
| } |
| break; |
| } |
| } |
| while (retry); |
| } |
| catch (IOException e) |
| { |
| connection.close(); |
| throw e; |
| } |
| return response; |
| } |
| |
| Response readResponse(InputStream in) |
| throws IOException |
| { |
| String line; |
| int len; |
| |
| // Read response status line |
| LineInputStream lis = new LineInputStream(in); |
| |
| line = lis.readLine(); |
| if (line == null) |
| { |
| throw new ProtocolException("Peer closed connection"); |
| } |
| if (!line.startsWith("HTTP/")) |
| { |
| throw new ProtocolException(line); |
| } |
| len = line.length(); |
| int start = 5, end = 6; |
| while (line.charAt(end) != '.') |
| { |
| end++; |
| } |
| int majorVersion = Integer.parseInt(line.substring(start, end)); |
| start = end + 1; |
| end = start + 1; |
| while (line.charAt(end) != ' ') |
| { |
| end++; |
| } |
| int minorVersion = Integer.parseInt(line.substring(start, end)); |
| start = end + 1; |
| end = start + 3; |
| int code = Integer.parseInt(line.substring(start, end)); |
| String message = line.substring(end + 1, len - 1); |
| // Read response headers |
| Headers responseHeaders = new Headers(); |
| responseHeaders.parse(lis); |
| notifyHeaderHandlers(responseHeaders); |
| InputStream body = null; |
| |
| switch (code) |
| { |
| case 100: |
| break; |
| case 204: |
| case 205: |
| case 304: |
| body = createResponseBodyStream(responseHeaders, majorVersion, |
| minorVersion, in, false); |
| break; |
| default: |
| body = createResponseBodyStream(responseHeaders, majorVersion, |
| minorVersion, in, true); |
| } |
| |
| // Construct response |
| Response ret = new Response(majorVersion, minorVersion, code, |
| message, responseHeaders, body); |
| return ret; |
| } |
| |
| void notifyHeaderHandlers(Headers headers) |
| { |
| for (Iterator i = headers.iterator(); i.hasNext(); ) |
| { |
| Headers.HeaderElement entry = (Headers.HeaderElement) i.next(); |
| // Handle Set-Cookie |
| if ("Set-Cookie".equalsIgnoreCase(entry.name)) |
| handleSetCookie(entry.value); |
| |
| ResponseHeaderHandler handler = |
| (ResponseHeaderHandler) responseHeaderHandlers.get(entry.name); |
| if (handler != null) |
| handler.setValue(entry.value); |
| } |
| } |
| |
| private InputStream createResponseBodyStream(Headers responseHeaders, |
| int majorVersion, |
| int minorVersion, |
| InputStream in, |
| boolean mayHaveBody) |
| throws IOException |
| { |
| long contentLength = -1; |
| Headers trailer = null; |
| |
| // Persistent connections are the default in HTTP/1.1 |
| boolean doClose = "close".equalsIgnoreCase(getHeader("Connection")) || |
| "close".equalsIgnoreCase(responseHeaders.getValue("Connection")) || |
| (connection.majorVersion == 1 && connection.minorVersion == 0) || |
| (majorVersion == 1 && minorVersion == 0); |
| |
| String transferCoding = responseHeaders.getValue("Transfer-Encoding"); |
| if ("HEAD".equals(method) || !mayHaveBody) |
| { |
| // Special case no body. |
| in = new LimitedLengthInputStream(in, 0, true, connection, doClose); |
| } |
| else if ("chunked".equalsIgnoreCase(transferCoding)) |
| { |
| in = new LimitedLengthInputStream(in, -1, false, connection, doClose); |
| |
| in = new ChunkedInputStream(in, responseHeaders); |
| } |
| else |
| { |
| contentLength = responseHeaders.getLongValue("Content-Length"); |
| |
| if (contentLength < 0) |
| doClose = true; // No Content-Length, must close. |
| |
| in = new LimitedLengthInputStream(in, contentLength, |
| contentLength >= 0, |
| connection, doClose); |
| } |
| String contentCoding = responseHeaders.getValue("Content-Encoding"); |
| if (contentCoding != null && !"identity".equals(contentCoding)) |
| { |
| if ("gzip".equals(contentCoding)) |
| { |
| in = new GZIPInputStream(in); |
| } |
| else if ("deflate".equals(contentCoding)) |
| { |
| in = new InflaterInputStream(in); |
| } |
| else |
| { |
| throw new ProtocolException("Unsupported Content-Encoding: " + |
| contentCoding); |
| } |
| // Remove the Content-Encoding header because the content is |
| // no longer compressed. |
| responseHeaders.remove("Content-Encoding"); |
| } |
| return in; |
| } |
| |
| boolean authenticate(Response response, int attempts) |
| throws IOException |
| { |
| String challenge = response.getHeader("WWW-Authenticate"); |
| if (challenge == null) |
| { |
| challenge = response.getHeader("Proxy-Authenticate"); |
| } |
| int si = challenge.indexOf(' '); |
| String scheme = (si == -1) ? challenge : challenge.substring(0, si); |
| if ("Basic".equalsIgnoreCase(scheme)) |
| { |
| Properties params = parseAuthParams(challenge.substring(si + 1)); |
| String realm = params.getProperty("realm"); |
| Credentials creds = authenticator.getCredentials(realm, attempts); |
| String userPass = creds.getUsername() + ':' + creds.getPassword(); |
| byte[] b_userPass = userPass.getBytes("US-ASCII"); |
| byte[] b_encoded = BASE64.encode(b_userPass); |
| String authorization = |
| scheme + " " + new String(b_encoded, "US-ASCII"); |
| setHeader("Authorization", authorization); |
| return true; |
| } |
| else if ("Digest".equalsIgnoreCase(scheme)) |
| { |
| Properties params = parseAuthParams(challenge.substring(si + 1)); |
| String realm = params.getProperty("realm"); |
| String nonce = params.getProperty("nonce"); |
| String qop = params.getProperty("qop"); |
| String algorithm = params.getProperty("algorithm"); |
| String digestUri = getRequestURI(); |
| Credentials creds = authenticator.getCredentials(realm, attempts); |
| String username = creds.getUsername(); |
| String password = creds.getPassword(); |
| connection.incrementNonce(nonce); |
| try |
| { |
| MessageDigest md5 = MessageDigest.getInstance("MD5"); |
| final byte[] COLON = { 0x3a }; |
| |
| // Calculate H(A1) |
| md5.reset(); |
| md5.update(username.getBytes("US-ASCII")); |
| md5.update(COLON); |
| md5.update(realm.getBytes("US-ASCII")); |
| md5.update(COLON); |
| md5.update(password.getBytes("US-ASCII")); |
| byte[] ha1 = md5.digest(); |
| if ("md5-sess".equals(algorithm)) |
| { |
| byte[] cnonce = generateNonce(); |
| md5.reset(); |
| md5.update(ha1); |
| md5.update(COLON); |
| md5.update(nonce.getBytes("US-ASCII")); |
| md5.update(COLON); |
| md5.update(cnonce); |
| ha1 = md5.digest(); |
| } |
| String ha1Hex = toHexString(ha1); |
| |
| // Calculate H(A2) |
| md5.reset(); |
| md5.update(method.getBytes("US-ASCII")); |
| md5.update(COLON); |
| md5.update(digestUri.getBytes("US-ASCII")); |
| if ("auth-int".equals(qop)) |
| { |
| byte[] hEntity = null; // TODO hash of entity body |
| md5.update(COLON); |
| md5.update(hEntity); |
| } |
| byte[] ha2 = md5.digest(); |
| String ha2Hex = toHexString(ha2); |
| |
| // Calculate response |
| md5.reset(); |
| md5.update(ha1Hex.getBytes("US-ASCII")); |
| md5.update(COLON); |
| md5.update(nonce.getBytes("US-ASCII")); |
| if ("auth".equals(qop) || "auth-int".equals(qop)) |
| { |
| String nc = getNonceCount(nonce); |
| byte[] cnonce = generateNonce(); |
| md5.update(COLON); |
| md5.update(nc.getBytes("US-ASCII")); |
| md5.update(COLON); |
| md5.update(cnonce); |
| md5.update(COLON); |
| md5.update(qop.getBytes("US-ASCII")); |
| } |
| md5.update(COLON); |
| md5.update(ha2Hex.getBytes("US-ASCII")); |
| String digestResponse = toHexString(md5.digest()); |
| |
| String authorization = scheme + |
| " username=\"" + username + "\"" + |
| " realm=\"" + realm + "\"" + |
| " nonce=\"" + nonce + "\"" + |
| " uri=\"" + digestUri + "\"" + |
| " response=\"" + digestResponse + "\""; |
| setHeader("Authorization", authorization); |
| return true; |
| } |
| catch (NoSuchAlgorithmException e) |
| { |
| return false; |
| } |
| } |
| // Scheme not recognised |
| return false; |
| } |
| |
| Properties parseAuthParams(String text) |
| { |
| int len = text.length(); |
| String key = null; |
| StringBuilder buf = new StringBuilder(); |
| Properties ret = new Properties(); |
| boolean inQuote = false; |
| for (int i = 0; i < len; i++) |
| { |
| char c = text.charAt(i); |
| if (c == '"') |
| { |
| inQuote = !inQuote; |
| } |
| else if (c == '=' && key == null) |
| { |
| key = buf.toString().trim(); |
| buf.setLength(0); |
| } |
| else if (c == ' ' && !inQuote) |
| { |
| String value = unquote(buf.toString().trim()); |
| ret.put(key, value); |
| key = null; |
| buf.setLength(0); |
| } |
| else if (c != ',' || (i <(len - 1) && text.charAt(i + 1) != ' ')) |
| { |
| buf.append(c); |
| } |
| } |
| if (key != null) |
| { |
| String value = unquote(buf.toString().trim()); |
| ret.put(key, value); |
| } |
| return ret; |
| } |
| |
| String unquote(String text) |
| { |
| int len = text.length(); |
| if (len > 0 && text.charAt(0) == '"' && text.charAt(len - 1) == '"') |
| { |
| return text.substring(1, len - 1); |
| } |
| return text; |
| } |
| |
| /** |
| * Returns the number of times the specified nonce value has been seen. |
| * This always returns an 8-byte 0-padded hexadecimal string. |
| */ |
| String getNonceCount(String nonce) |
| { |
| int nc = connection.getNonceCount(nonce); |
| String hex = Integer.toHexString(nc); |
| StringBuilder buf = new StringBuilder(); |
| for (int i = 8 - hex.length(); i > 0; i--) |
| { |
| buf.append('0'); |
| } |
| buf.append(hex); |
| return buf.toString(); |
| } |
| |
| /** |
| * Client nonce value. |
| */ |
| byte[] nonce; |
| |
| /** |
| * Generates a new client nonce value. |
| */ |
| byte[] generateNonce() |
| throws IOException, NoSuchAlgorithmException |
| { |
| if (nonce == null) |
| { |
| long time = System.currentTimeMillis(); |
| MessageDigest md5 = MessageDigest.getInstance("MD5"); |
| md5.update(Long.toString(time).getBytes("US-ASCII")); |
| nonce = md5.digest(); |
| } |
| return nonce; |
| } |
| |
| String toHexString(byte[] bytes) |
| { |
| char[] ret = new char[bytes.length * 2]; |
| for (int i = 0, j = 0; i < bytes.length; i++) |
| { |
| int c =(int) bytes[i]; |
| if (c < 0) |
| { |
| c += 0x100; |
| } |
| ret[j++] = Character.forDigit(c / 0x10, 0x10); |
| ret[j++] = Character.forDigit(c % 0x10, 0x10); |
| } |
| return new String(ret); |
| } |
| |
| /** |
| * Parse the specified cookie list and notify the cookie manager. |
| */ |
| void handleSetCookie(String text) |
| { |
| CookieManager cookieManager = connection.getCookieManager(); |
| if (cookieManager == null) |
| { |
| return; |
| } |
| String name = null; |
| String value = null; |
| String comment = null; |
| String domain = connection.getHostName(); |
| String path = this.path; |
| int lsi = path.lastIndexOf('/'); |
| if (lsi != -1) |
| { |
| path = path.substring(0, lsi); |
| } |
| boolean secure = false; |
| Date expires = null; |
| |
| int len = text.length(); |
| String attr = null; |
| StringBuilder buf = new StringBuilder(); |
| boolean inQuote = false; |
| for (int i = 0; i <= len; i++) |
| { |
| char c =(i == len) ? '\u0000' : text.charAt(i); |
| if (c == '"') |
| { |
| inQuote = !inQuote; |
| } |
| else if (!inQuote) |
| { |
| if (c == '=' && attr == null) |
| { |
| attr = buf.toString().trim(); |
| buf.setLength(0); |
| } |
| else if (c == ';' || i == len || c == ',') |
| { |
| String val = unquote(buf.toString().trim()); |
| if (name == null) |
| { |
| name = attr; |
| value = val; |
| } |
| else if ("Comment".equalsIgnoreCase(attr)) |
| { |
| comment = val; |
| } |
| else if ("Domain".equalsIgnoreCase(attr)) |
| { |
| domain = val; |
| } |
| else if ("Path".equalsIgnoreCase(attr)) |
| { |
| path = val; |
| } |
| else if ("Secure".equalsIgnoreCase(val)) |
| { |
| secure = true; |
| } |
| else if ("Max-Age".equalsIgnoreCase(attr)) |
| { |
| int delta = Integer.parseInt(val); |
| Calendar cal = Calendar.getInstance(); |
| cal.setTimeInMillis(System.currentTimeMillis()); |
| cal.add(Calendar.SECOND, delta); |
| expires = cal.getTime(); |
| } |
| else if ("Expires".equalsIgnoreCase(attr)) |
| { |
| DateFormat dateFormat = new HTTPDateFormat(); |
| try |
| { |
| expires = dateFormat.parse(val); |
| } |
| catch (ParseException e) |
| { |
| // if this isn't a valid date, it may be that |
| // the value was returned unquoted; in that case, we |
| // want to continue buffering the value |
| buf.append(c); |
| continue; |
| } |
| } |
| attr = null; |
| buf.setLength(0); |
| // case EOL |
| if (i == len || c == ',') |
| { |
| Cookie cookie = new Cookie(name, value, comment, domain, |
| path, secure, expires); |
| cookieManager.setCookie(cookie); |
| } |
| if (c == ',') |
| { |
| // Reset cookie fields |
| name = null; |
| value = null; |
| comment = null; |
| domain = connection.getHostName(); |
| path = this.path; |
| if (lsi != -1) |
| { |
| path = path.substring(0, lsi); |
| } |
| secure = false; |
| expires = null; |
| } |
| } |
| else |
| { |
| buf.append(c); |
| } |
| } |
| else |
| { |
| buf.append(c); |
| } |
| } |
| } |
| |
| } |
| |