2 Copyright (C) 2004, 2005, 2006, 2007 Free Software Foundation, Inc.
4 This file is part of GNU Classpath.
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING. If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library. Thus, the terms and
23 conditions of the GNU General Public License cover the whole
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module. An independent module is a module which is not derived from
33 or based on this library. If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so. If you do not wish to do so, delete this
36 exception statement from your version. */
39 package gnu.java.net.protocol.http;
41 import gnu.java.lang.CPStringBuilder;
42 import gnu.java.net.LineInputStream;
43 import gnu.java.util.Base64;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.OutputStream;
48 import java.net.ProtocolException;
49 import java.security.MessageDigest;
50 import java.security.NoSuchAlgorithmException;
51 import java.text.DateFormat;
52 import java.text.ParseException;
53 import java.util.Calendar;
54 import java.util.Date;
55 import java.util.HashMap;
57 import java.util.Properties;
58 import java.util.zip.GZIPInputStream;
59 import java.util.zip.InflaterInputStream;
62 * A single HTTP request.
64 * @author Chris Burdess (dog@gnu.org)
70 * The connection context in which this request is invoked.
72 protected final HTTPConnection connection;
75 * The HTTP method to invoke.
77 protected final String method;
80 * The path identifying the resource.
81 * This string must conform to the abs_path definition given in RFC2396,
82 * with an optional "?query" part, and must be URI-escaped by the caller.
84 protected final String path;
87 * The headers in this request.
89 protected final Headers requestHeaders;
92 * The request body provider.
94 protected RequestBodyWriter requestBodyWriter;
97 * Map of response header handlers.
99 protected Map<String, ResponseHeaderHandler> responseHeaderHandlers;
104 protected Authenticator authenticator;
107 * Whether this request has been dispatched yet.
109 private boolean dispatched;
112 * Constructor for a new request.
113 * @param connection the connection context
114 * @param method the HTTP method
115 * @param path the resource path including query part
117 protected Request(HTTPConnection connection, String method,
120 this.connection = connection;
121 this.method = method;
123 requestHeaders = new Headers();
124 responseHeaderHandlers = new HashMap<String, ResponseHeaderHandler>();
128 * Returns the connection associated with this request.
131 public HTTPConnection getConnection()
137 * Returns the HTTP method to invoke.
140 public String getMethod()
146 * Returns the resource path.
149 public String getPath()
155 * Returns the full request-URI represented by this request, as specified
158 public String getRequestURI()
160 return connection.getURI() + path;
164 * Returns the headers in this request.
166 public Headers getHeaders()
168 return requestHeaders;
172 * Returns the value of the specified header in this request.
173 * @param name the header name
175 public String getHeader(String name)
177 return requestHeaders.getValue(name);
181 * Returns the value of the specified header in this request as an integer.
182 * @param name the header name
184 public int getIntHeader(String name)
186 return requestHeaders.getIntValue(name);
190 * Returns the value of the specified header in this request as a date.
191 * @param name the header name
193 public Date getDateHeader(String name)
195 return requestHeaders.getDateValue(name);
199 * Sets the specified header in this request.
200 * @param name the header name
201 * @param value the header value
203 public void setHeader(String name, String value)
205 requestHeaders.put(name, value);
209 * Convenience method to set the entire request body.
210 * @param requestBody the request body content
212 public void setRequestBody(byte[] requestBody)
214 setRequestBodyWriter(new ByteArrayRequestBodyWriter(requestBody));
218 * Sets the request body provider.
219 * @param requestBodyWriter the handler used to obtain the request body
221 public void setRequestBodyWriter(RequestBodyWriter requestBodyWriter)
223 this.requestBodyWriter = requestBodyWriter;
227 * Sets a callback handler to be invoked for the specified header name.
228 * @param name the header name
229 * @param handler the handler to receive the value for the header
231 public void setResponseHeaderHandler(String name,
232 ResponseHeaderHandler handler)
234 responseHeaderHandlers.put(name, handler);
238 * Sets an authenticator that can be used to handle authentication
240 * @param authenticator the authenticator
242 public void setAuthenticator(Authenticator authenticator)
244 this.authenticator = authenticator;
248 * Dispatches this request.
249 * A request can only be dispatched once; calling this method a second
250 * time results in a protocol exception.
251 * @exception IOException if an I/O error occurred
252 * @return an HTTP response object representing the result of the operation
254 public Response dispatch()
259 throw new ProtocolException("request already dispatched");
261 final String CRLF = "\r\n";
262 final String HEADER_SEP = ": ";
263 final String US_ASCII = "US-ASCII";
264 final String version = connection.getVersion();
266 int contentLength = -1;
267 boolean retry = false;
269 boolean expectingContinue = false;
270 if (requestBodyWriter != null)
272 contentLength = requestBodyWriter.getContentLength();
273 String expect = getHeader("Expect");
274 if (expect != null && expect.equals("100-continue"))
276 expectingContinue = true;
280 setHeader("Content-Length", Integer.toString(contentLength));
286 // Loop while authentication fails or continue
291 // Get socket output and input streams
292 OutputStream out = connection.getOutputStream();
295 String requestUri = path;
296 if (connection.isUsingProxy() &&
297 !"*".equals(requestUri) &&
298 !"CONNECT".equals(method))
300 requestUri = getRequestURI();
302 String line = method + ' ' + requestUri + ' ' + version + CRLF;
303 out.write(line.getBytes(US_ASCII));
305 for (Headers.HeaderElement elt : requestHeaders)
307 line = elt.name + HEADER_SEP + elt.value + CRLF;
308 out.write(line.getBytes(US_ASCII));
310 out.write(CRLF.getBytes(US_ASCII));
312 if (requestBodyWriter != null && !expectingContinue)
314 byte[] buffer = new byte[4096];
318 requestBodyWriter.reset();
321 len = requestBodyWriter.write(buffer);
324 out.write(buffer, 0, len);
328 while (len > -1 && count < contentLength);
334 response = readResponse(connection.getInputStream());
335 int sc = response.getCode();
336 if (sc == 401 && authenticator != null)
338 if (authenticate(response, attempts++))
345 if (expectingContinue)
347 requestHeaders.remove("Expect");
348 setHeader("Content-Length",
349 Integer.toString(contentLength));
350 expectingContinue = false;
355 // A conforming server can send an unsoliceted
356 // Continue response but *should* not (RFC 2616
357 // sec 8.2.3). Ignore the bogus Continue
358 // response and get the real response that
368 catch (IOException e)
376 Response readResponse(InputStream in)
382 // Read response status line
383 LineInputStream lis = new LineInputStream(in);
385 line = lis.readLine();
388 throw new ProtocolException("Peer closed connection");
390 if (!line.startsWith("HTTP/"))
392 throw new ProtocolException(line);
395 int start = 5, end = 6;
396 while (line.charAt(end) != '.')
400 int majorVersion = Integer.parseInt(line.substring(start, end));
403 while (line.charAt(end) != ' ')
407 int minorVersion = Integer.parseInt(line.substring(start, end));
410 int code = Integer.parseInt(line.substring(start, end));
411 String message = line.substring(end + 1, len - 1);
412 // Read response headers
413 Headers responseHeaders = new Headers();
414 responseHeaders.parse(lis);
415 notifyHeaderHandlers(responseHeaders);
416 InputStream body = null;
425 body = createResponseBodyStream(responseHeaders, majorVersion,
426 minorVersion, in, false);
429 body = createResponseBodyStream(responseHeaders, majorVersion,
430 minorVersion, in, true);
433 // Construct response
434 Response ret = new Response(majorVersion, minorVersion, code,
435 message, responseHeaders, body);
439 void notifyHeaderHandlers(Headers headers)
441 for (Headers.HeaderElement entry : headers)
444 if ("Set-Cookie".equalsIgnoreCase(entry.name))
445 handleSetCookie(entry.value);
447 ResponseHeaderHandler handler =
448 (ResponseHeaderHandler) responseHeaderHandlers.get(entry.name);
450 handler.setValue(entry.value);
454 private InputStream createResponseBodyStream(Headers responseHeaders,
461 long contentLength = -1;
463 // Persistent connections are the default in HTTP/1.1
464 boolean doClose = "close".equalsIgnoreCase(getHeader("Connection")) ||
465 "close".equalsIgnoreCase(responseHeaders.getValue("Connection")) ||
466 (connection.majorVersion == 1 && connection.minorVersion == 0) ||
467 (majorVersion == 1 && minorVersion == 0);
469 String transferCoding = responseHeaders.getValue("Transfer-Encoding");
470 if ("HEAD".equals(method) || !mayHaveBody)
472 // Special case no body.
473 in = new LimitedLengthInputStream(in, 0, true, connection, doClose);
475 else if ("chunked".equalsIgnoreCase(transferCoding))
477 in = new LimitedLengthInputStream(in, -1, false, connection, doClose);
479 in = new ChunkedInputStream(in, responseHeaders);
483 contentLength = responseHeaders.getLongValue("Content-Length");
485 if (contentLength < 0)
486 doClose = true; // No Content-Length, must close.
488 in = new LimitedLengthInputStream(in, contentLength,
490 connection, doClose);
492 String contentCoding = responseHeaders.getValue("Content-Encoding");
493 if (contentCoding != null && !"identity".equals(contentCoding))
495 if ("gzip".equals(contentCoding))
497 in = new GZIPInputStream(in);
499 else if ("deflate".equals(contentCoding))
501 in = new InflaterInputStream(in);
505 throw new ProtocolException("Unsupported Content-Encoding: " +
508 // Remove the Content-Encoding header because the content is
509 // no longer compressed.
510 responseHeaders.remove("Content-Encoding");
515 boolean authenticate(Response response, int attempts)
518 String challenge = response.getHeader("WWW-Authenticate");
519 if (challenge == null)
521 challenge = response.getHeader("Proxy-Authenticate");
523 int si = challenge.indexOf(' ');
524 String scheme = (si == -1) ? challenge : challenge.substring(0, si);
525 if ("Basic".equalsIgnoreCase(scheme))
527 Properties params = parseAuthParams(challenge.substring(si + 1));
528 String realm = params.getProperty("realm");
529 Credentials creds = authenticator.getCredentials(realm, attempts);
530 String userPass = creds.getUsername() + ':' + creds.getPassword();
531 byte[] b_userPass = userPass.getBytes("US-ASCII");
532 byte[] b_encoded = Base64.encode(b_userPass).getBytes("US-ASCII");
533 String authorization =
534 scheme + " " + new String(b_encoded, "US-ASCII");
535 setHeader("Authorization", authorization);
538 else if ("Digest".equalsIgnoreCase(scheme))
540 Properties params = parseAuthParams(challenge.substring(si + 1));
541 String realm = params.getProperty("realm");
542 String nonce = params.getProperty("nonce");
543 String qop = params.getProperty("qop");
544 String algorithm = params.getProperty("algorithm");
545 String digestUri = getRequestURI();
546 Credentials creds = authenticator.getCredentials(realm, attempts);
547 String username = creds.getUsername();
548 String password = creds.getPassword();
549 connection.incrementNonce(nonce);
552 MessageDigest md5 = MessageDigest.getInstance("MD5");
553 final byte[] COLON = { 0x3a };
557 md5.update(username.getBytes("US-ASCII"));
559 md5.update(realm.getBytes("US-ASCII"));
561 md5.update(password.getBytes("US-ASCII"));
562 byte[] ha1 = md5.digest();
563 if ("md5-sess".equals(algorithm))
565 byte[] cnonce = generateNonce();
569 md5.update(nonce.getBytes("US-ASCII"));
574 String ha1Hex = toHexString(ha1);
578 md5.update(method.getBytes("US-ASCII"));
580 md5.update(digestUri.getBytes("US-ASCII"));
581 if ("auth-int".equals(qop))
583 byte[] hEntity = null; // TODO hash of entity body
587 byte[] ha2 = md5.digest();
588 String ha2Hex = toHexString(ha2);
590 // Calculate response
592 md5.update(ha1Hex.getBytes("US-ASCII"));
594 md5.update(nonce.getBytes("US-ASCII"));
595 if ("auth".equals(qop) || "auth-int".equals(qop))
597 String nc = getNonceCount(nonce);
598 byte[] cnonce = generateNonce();
600 md5.update(nc.getBytes("US-ASCII"));
604 md5.update(qop.getBytes("US-ASCII"));
607 md5.update(ha2Hex.getBytes("US-ASCII"));
608 String digestResponse = toHexString(md5.digest());
610 String authorization = scheme +
611 " username=\"" + username + "\"" +
612 " realm=\"" + realm + "\"" +
613 " nonce=\"" + nonce + "\"" +
614 " uri=\"" + digestUri + "\"" +
615 " response=\"" + digestResponse + "\"";
616 setHeader("Authorization", authorization);
619 catch (NoSuchAlgorithmException e)
624 // Scheme not recognised
628 Properties parseAuthParams(String text)
630 int len = text.length();
632 CPStringBuilder buf = new CPStringBuilder();
633 Properties ret = new Properties();
634 boolean inQuote = false;
635 for (int i = 0; i < len; i++)
637 char c = text.charAt(i);
642 else if (c == '=' && key == null)
644 key = buf.toString().trim();
647 else if (c == ' ' && !inQuote)
649 String value = unquote(buf.toString().trim());
654 else if (c != ',' || (i <(len - 1) && text.charAt(i + 1) != ' '))
661 String value = unquote(buf.toString().trim());
667 String unquote(String text)
669 int len = text.length();
670 if (len > 0 && text.charAt(0) == '"' && text.charAt(len - 1) == '"')
672 return text.substring(1, len - 1);
678 * Returns the number of times the specified nonce value has been seen.
679 * This always returns an 8-byte 0-padded hexadecimal string.
681 String getNonceCount(String nonce)
683 int nc = connection.getNonceCount(nonce);
684 String hex = Integer.toHexString(nc);
685 CPStringBuilder buf = new CPStringBuilder();
686 for (int i = 8 - hex.length(); i > 0; i--)
691 return buf.toString();
695 * Client nonce value.
700 * Generates a new client nonce value.
702 byte[] generateNonce()
703 throws IOException, NoSuchAlgorithmException
707 long time = System.currentTimeMillis();
708 MessageDigest md5 = MessageDigest.getInstance("MD5");
709 md5.update(Long.toString(time).getBytes("US-ASCII"));
710 nonce = md5.digest();
715 String toHexString(byte[] bytes)
717 char[] ret = new char[bytes.length * 2];
718 for (int i = 0, j = 0; i < bytes.length; i++)
720 int c =(int) bytes[i];
725 ret[j++] = Character.forDigit(c / 0x10, 0x10);
726 ret[j++] = Character.forDigit(c % 0x10, 0x10);
728 return new String(ret);
732 * Parse the specified cookie list and notify the cookie manager.
734 void handleSetCookie(String text)
736 CookieManager cookieManager = connection.getCookieManager();
737 if (cookieManager == null)
743 String comment = null;
744 String domain = connection.getHostName();
745 String path = this.path;
746 int lsi = path.lastIndexOf('/');
749 path = path.substring(0, lsi);
751 boolean secure = false;
754 int len = text.length();
756 CPStringBuilder buf = new CPStringBuilder();
757 boolean inQuote = false;
758 for (int i = 0; i <= len; i++)
760 char c =(i == len) ? '\u0000' : text.charAt(i);
767 if (c == '=' && attr == null)
769 attr = buf.toString().trim();
772 else if (c == ';' || i == len || c == ',')
774 String val = unquote(buf.toString().trim());
780 else if ("Comment".equalsIgnoreCase(attr))
784 else if ("Domain".equalsIgnoreCase(attr))
788 else if ("Path".equalsIgnoreCase(attr))
792 else if ("Secure".equalsIgnoreCase(val))
796 else if ("Max-Age".equalsIgnoreCase(attr))
798 int delta = Integer.parseInt(val);
799 Calendar cal = Calendar.getInstance();
800 cal.setTimeInMillis(System.currentTimeMillis());
801 cal.add(Calendar.SECOND, delta);
802 expires = cal.getTime();
804 else if ("Expires".equalsIgnoreCase(attr))
806 DateFormat dateFormat = new HTTPDateFormat();
809 expires = dateFormat.parse(val);
811 catch (ParseException e)
813 // if this isn't a valid date, it may be that
814 // the value was returned unquoted; in that case, we
815 // want to continue buffering the value
823 if (i == len || c == ',')
825 Cookie cookie = new Cookie(name, value, comment, domain,
826 path, secure, expires);
827 cookieManager.setCookie(cookie);
831 // Reset cookie fields
835 domain = connection.getHostName();
839 path = path.substring(0, lsi);