Merge with trank @ 137446
[official-gcc.git] / libjava / classpath / gnu / java / net / protocol / http / Request.java
blob90e3b7a0d64670ef14c0e38dc900db9f16d48887
1 /* Request.java --
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)
9 any later version.
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
19 02110-1301 USA.
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
24 combination.
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.net.LineInputStream;
42 import gnu.java.util.Base64;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.OutputStream;
47 import java.net.ProtocolException;
48 import java.security.MessageDigest;
49 import java.security.NoSuchAlgorithmException;
50 import java.text.DateFormat;
51 import java.text.ParseException;
52 import java.util.Calendar;
53 import java.util.Date;
54 import java.util.HashMap;
55 import java.util.Map;
56 import java.util.Properties;
57 import java.util.zip.GZIPInputStream;
58 import java.util.zip.InflaterInputStream;
60 /**
61 * A single HTTP request.
63 * @author Chris Burdess (dog@gnu.org)
65 public class Request
68 /**
69 * The connection context in which this request is invoked.
71 protected final HTTPConnection connection;
73 /**
74 * The HTTP method to invoke.
76 protected final String method;
78 /**
79 * The path identifying the resource.
80 * This string must conform to the abs_path definition given in RFC2396,
81 * with an optional "?query" part, and must be URI-escaped by the caller.
83 protected final String path;
85 /**
86 * The headers in this request.
88 protected final Headers requestHeaders;
90 /**
91 * The request body provider.
93 protected RequestBodyWriter requestBodyWriter;
95 /**
96 * Map of response header handlers.
98 protected Map<String, ResponseHeaderHandler> responseHeaderHandlers;
101 * The authenticator.
103 protected Authenticator authenticator;
106 * Whether this request has been dispatched yet.
108 private boolean dispatched;
111 * Constructor for a new request.
112 * @param connection the connection context
113 * @param method the HTTP method
114 * @param path the resource path including query part
116 protected Request(HTTPConnection connection, String method,
117 String path)
119 this.connection = connection;
120 this.method = method;
121 this.path = path;
122 requestHeaders = new Headers();
123 responseHeaderHandlers = new HashMap<String, ResponseHeaderHandler>();
127 * Returns the connection associated with this request.
128 * @see #connection
130 public HTTPConnection getConnection()
132 return connection;
136 * Returns the HTTP method to invoke.
137 * @see #method
139 public String getMethod()
141 return method;
145 * Returns the resource path.
146 * @see #path
148 public String getPath()
150 return path;
154 * Returns the full request-URI represented by this request, as specified
155 * by HTTP/1.1.
157 public String getRequestURI()
159 return connection.getURI() + path;
163 * Returns the headers in this request.
165 public Headers getHeaders()
167 return requestHeaders;
171 * Returns the value of the specified header in this request.
172 * @param name the header name
174 public String getHeader(String name)
176 return requestHeaders.getValue(name);
180 * Returns the value of the specified header in this request as an integer.
181 * @param name the header name
183 public int getIntHeader(String name)
185 return requestHeaders.getIntValue(name);
189 * Returns the value of the specified header in this request as a date.
190 * @param name the header name
192 public Date getDateHeader(String name)
194 return requestHeaders.getDateValue(name);
198 * Sets the specified header in this request.
199 * @param name the header name
200 * @param value the header value
202 public void setHeader(String name, String value)
204 requestHeaders.put(name, value);
208 * Convenience method to set the entire request body.
209 * @param requestBody the request body content
211 public void setRequestBody(byte[] requestBody)
213 setRequestBodyWriter(new ByteArrayRequestBodyWriter(requestBody));
217 * Sets the request body provider.
218 * @param requestBodyWriter the handler used to obtain the request body
220 public void setRequestBodyWriter(RequestBodyWriter requestBodyWriter)
222 this.requestBodyWriter = requestBodyWriter;
226 * Sets a callback handler to be invoked for the specified header name.
227 * @param name the header name
228 * @param handler the handler to receive the value for the header
230 public void setResponseHeaderHandler(String name,
231 ResponseHeaderHandler handler)
233 responseHeaderHandlers.put(name, handler);
237 * Sets an authenticator that can be used to handle authentication
238 * automatically.
239 * @param authenticator the authenticator
241 public void setAuthenticator(Authenticator authenticator)
243 this.authenticator = authenticator;
247 * Dispatches this request.
248 * A request can only be dispatched once; calling this method a second
249 * time results in a protocol exception.
250 * @exception IOException if an I/O error occurred
251 * @return an HTTP response object representing the result of the operation
253 public Response dispatch()
254 throws IOException
256 if (dispatched)
258 throw new ProtocolException("request already dispatched");
260 final String CRLF = "\r\n";
261 final String HEADER_SEP = ": ";
262 final String US_ASCII = "US-ASCII";
263 final String version = connection.getVersion();
264 Response response;
265 int contentLength = -1;
266 boolean retry = false;
267 int attempts = 0;
268 boolean expectingContinue = false;
269 if (requestBodyWriter != null)
271 contentLength = requestBodyWriter.getContentLength();
272 String expect = getHeader("Expect");
273 if (expect != null && expect.equals("100-continue"))
275 expectingContinue = true;
277 else
279 setHeader("Content-Length", Integer.toString(contentLength));
285 // Loop while authentication fails or continue
288 retry = false;
290 // Get socket output and input streams
291 OutputStream out = connection.getOutputStream();
293 // Request line
294 String requestUri = path;
295 if (connection.isUsingProxy() &&
296 !"*".equals(requestUri) &&
297 !"CONNECT".equals(method))
299 requestUri = getRequestURI();
301 String line = method + ' ' + requestUri + ' ' + version + CRLF;
302 out.write(line.getBytes(US_ASCII));
303 // Request headers
304 for (Headers.HeaderElement elt : requestHeaders)
306 line = elt.name + HEADER_SEP + elt.value + CRLF;
307 out.write(line.getBytes(US_ASCII));
309 out.write(CRLF.getBytes(US_ASCII));
310 // Request body
311 if (requestBodyWriter != null && !expectingContinue)
313 byte[] buffer = new byte[4096];
314 int len;
315 int count = 0;
317 requestBodyWriter.reset();
320 len = requestBodyWriter.write(buffer);
321 if (len > 0)
323 out.write(buffer, 0, len);
325 count += len;
327 while (len > -1 && count < contentLength);
329 out.flush();
330 // Get response
331 while(true)
333 response = readResponse(connection.getInputStream());
334 int sc = response.getCode();
335 if (sc == 401 && authenticator != null)
337 if (authenticate(response, attempts++))
339 retry = true;
342 else if (sc == 100)
344 if (expectingContinue)
346 requestHeaders.remove("Expect");
347 setHeader("Content-Length",
348 Integer.toString(contentLength));
349 expectingContinue = false;
350 retry = true;
352 else
354 // A conforming server can send an unsoliceted
355 // Continue response but *should* not (RFC 2616
356 // sec 8.2.3). Ignore the bogus Continue
357 // response and get the real response that
358 // should follow
359 continue;
362 break;
365 while (retry);
367 catch (IOException e)
369 connection.close();
370 throw e;
372 return response;
375 Response readResponse(InputStream in)
376 throws IOException
378 String line;
379 int len;
381 // Read response status line
382 LineInputStream lis = new LineInputStream(in);
384 line = lis.readLine();
385 if (line == null)
387 throw new ProtocolException("Peer closed connection");
389 if (!line.startsWith("HTTP/"))
391 throw new ProtocolException(line);
393 len = line.length();
394 int start = 5, end = 6;
395 while (line.charAt(end) != '.')
397 end++;
399 int majorVersion = Integer.parseInt(line.substring(start, end));
400 start = end + 1;
401 end = start + 1;
402 while (line.charAt(end) != ' ')
404 end++;
406 int minorVersion = Integer.parseInt(line.substring(start, end));
407 start = end + 1;
408 end = start + 3;
409 int code = Integer.parseInt(line.substring(start, end));
410 String message = line.substring(end + 1, len - 1);
411 // Read response headers
412 Headers responseHeaders = new Headers();
413 responseHeaders.parse(lis);
414 notifyHeaderHandlers(responseHeaders);
415 InputStream body = null;
417 switch (code)
419 case 100:
420 break;
421 case 204:
422 case 205:
423 case 304:
424 body = createResponseBodyStream(responseHeaders, majorVersion,
425 minorVersion, in, false);
426 break;
427 default:
428 body = createResponseBodyStream(responseHeaders, majorVersion,
429 minorVersion, in, true);
432 // Construct response
433 Response ret = new Response(majorVersion, minorVersion, code,
434 message, responseHeaders, body);
435 return ret;
438 void notifyHeaderHandlers(Headers headers)
440 for (Headers.HeaderElement entry : headers)
442 // Handle Set-Cookie
443 if ("Set-Cookie".equalsIgnoreCase(entry.name))
444 handleSetCookie(entry.value);
446 ResponseHeaderHandler handler =
447 (ResponseHeaderHandler) responseHeaderHandlers.get(entry.name);
448 if (handler != null)
449 handler.setValue(entry.value);
453 private InputStream createResponseBodyStream(Headers responseHeaders,
454 int majorVersion,
455 int minorVersion,
456 InputStream in,
457 boolean mayHaveBody)
458 throws IOException
460 long contentLength = -1;
462 // Persistent connections are the default in HTTP/1.1
463 boolean doClose = "close".equalsIgnoreCase(getHeader("Connection")) ||
464 "close".equalsIgnoreCase(responseHeaders.getValue("Connection")) ||
465 (connection.majorVersion == 1 && connection.minorVersion == 0) ||
466 (majorVersion == 1 && minorVersion == 0);
468 String transferCoding = responseHeaders.getValue("Transfer-Encoding");
469 if ("HEAD".equals(method) || !mayHaveBody)
471 // Special case no body.
472 in = new LimitedLengthInputStream(in, 0, true, connection, doClose);
474 else if ("chunked".equalsIgnoreCase(transferCoding))
476 in = new LimitedLengthInputStream(in, -1, false, connection, doClose);
478 in = new ChunkedInputStream(in, responseHeaders);
480 else
482 contentLength = responseHeaders.getLongValue("Content-Length");
484 if (contentLength < 0)
485 doClose = true; // No Content-Length, must close.
487 in = new LimitedLengthInputStream(in, contentLength,
488 contentLength >= 0,
489 connection, doClose);
491 String contentCoding = responseHeaders.getValue("Content-Encoding");
492 if (contentCoding != null && !"identity".equals(contentCoding))
494 if ("gzip".equals(contentCoding))
496 in = new GZIPInputStream(in);
498 else if ("deflate".equals(contentCoding))
500 in = new InflaterInputStream(in);
502 else
504 throw new ProtocolException("Unsupported Content-Encoding: " +
505 contentCoding);
507 // Remove the Content-Encoding header because the content is
508 // no longer compressed.
509 responseHeaders.remove("Content-Encoding");
511 return in;
514 boolean authenticate(Response response, int attempts)
515 throws IOException
517 String challenge = response.getHeader("WWW-Authenticate");
518 if (challenge == null)
520 challenge = response.getHeader("Proxy-Authenticate");
522 int si = challenge.indexOf(' ');
523 String scheme = (si == -1) ? challenge : challenge.substring(0, si);
524 if ("Basic".equalsIgnoreCase(scheme))
526 Properties params = parseAuthParams(challenge.substring(si + 1));
527 String realm = params.getProperty("realm");
528 Credentials creds = authenticator.getCredentials(realm, attempts);
529 String userPass = creds.getUsername() + ':' + creds.getPassword();
530 byte[] b_userPass = userPass.getBytes("US-ASCII");
531 byte[] b_encoded = Base64.encode(b_userPass).getBytes("US-ASCII");
532 String authorization =
533 scheme + " " + new String(b_encoded, "US-ASCII");
534 setHeader("Authorization", authorization);
535 return true;
537 else if ("Digest".equalsIgnoreCase(scheme))
539 Properties params = parseAuthParams(challenge.substring(si + 1));
540 String realm = params.getProperty("realm");
541 String nonce = params.getProperty("nonce");
542 String qop = params.getProperty("qop");
543 String algorithm = params.getProperty("algorithm");
544 String digestUri = getRequestURI();
545 Credentials creds = authenticator.getCredentials(realm, attempts);
546 String username = creds.getUsername();
547 String password = creds.getPassword();
548 connection.incrementNonce(nonce);
551 MessageDigest md5 = MessageDigest.getInstance("MD5");
552 final byte[] COLON = { 0x3a };
554 // Calculate H(A1)
555 md5.reset();
556 md5.update(username.getBytes("US-ASCII"));
557 md5.update(COLON);
558 md5.update(realm.getBytes("US-ASCII"));
559 md5.update(COLON);
560 md5.update(password.getBytes("US-ASCII"));
561 byte[] ha1 = md5.digest();
562 if ("md5-sess".equals(algorithm))
564 byte[] cnonce = generateNonce();
565 md5.reset();
566 md5.update(ha1);
567 md5.update(COLON);
568 md5.update(nonce.getBytes("US-ASCII"));
569 md5.update(COLON);
570 md5.update(cnonce);
571 ha1 = md5.digest();
573 String ha1Hex = toHexString(ha1);
575 // Calculate H(A2)
576 md5.reset();
577 md5.update(method.getBytes("US-ASCII"));
578 md5.update(COLON);
579 md5.update(digestUri.getBytes("US-ASCII"));
580 if ("auth-int".equals(qop))
582 byte[] hEntity = null; // TODO hash of entity body
583 md5.update(COLON);
584 md5.update(hEntity);
586 byte[] ha2 = md5.digest();
587 String ha2Hex = toHexString(ha2);
589 // Calculate response
590 md5.reset();
591 md5.update(ha1Hex.getBytes("US-ASCII"));
592 md5.update(COLON);
593 md5.update(nonce.getBytes("US-ASCII"));
594 if ("auth".equals(qop) || "auth-int".equals(qop))
596 String nc = getNonceCount(nonce);
597 byte[] cnonce = generateNonce();
598 md5.update(COLON);
599 md5.update(nc.getBytes("US-ASCII"));
600 md5.update(COLON);
601 md5.update(cnonce);
602 md5.update(COLON);
603 md5.update(qop.getBytes("US-ASCII"));
605 md5.update(COLON);
606 md5.update(ha2Hex.getBytes("US-ASCII"));
607 String digestResponse = toHexString(md5.digest());
609 String authorization = scheme +
610 " username=\"" + username + "\"" +
611 " realm=\"" + realm + "\"" +
612 " nonce=\"" + nonce + "\"" +
613 " uri=\"" + digestUri + "\"" +
614 " response=\"" + digestResponse + "\"";
615 setHeader("Authorization", authorization);
616 return true;
618 catch (NoSuchAlgorithmException e)
620 return false;
623 // Scheme not recognised
624 return false;
627 Properties parseAuthParams(String text)
629 int len = text.length();
630 String key = null;
631 StringBuilder buf = new StringBuilder();
632 Properties ret = new Properties();
633 boolean inQuote = false;
634 for (int i = 0; i < len; i++)
636 char c = text.charAt(i);
637 if (c == '"')
639 inQuote = !inQuote;
641 else if (c == '=' && key == null)
643 key = buf.toString().trim();
644 buf.setLength(0);
646 else if (c == ' ' && !inQuote)
648 String value = unquote(buf.toString().trim());
649 ret.put(key, value);
650 key = null;
651 buf.setLength(0);
653 else if (c != ',' || (i <(len - 1) && text.charAt(i + 1) != ' '))
655 buf.append(c);
658 if (key != null)
660 String value = unquote(buf.toString().trim());
661 ret.put(key, value);
663 return ret;
666 String unquote(String text)
668 int len = text.length();
669 if (len > 0 && text.charAt(0) == '"' && text.charAt(len - 1) == '"')
671 return text.substring(1, len - 1);
673 return text;
677 * Returns the number of times the specified nonce value has been seen.
678 * This always returns an 8-byte 0-padded hexadecimal string.
680 String getNonceCount(String nonce)
682 int nc = connection.getNonceCount(nonce);
683 String hex = Integer.toHexString(nc);
684 StringBuilder buf = new StringBuilder();
685 for (int i = 8 - hex.length(); i > 0; i--)
687 buf.append('0');
689 buf.append(hex);
690 return buf.toString();
694 * Client nonce value.
696 byte[] nonce;
699 * Generates a new client nonce value.
701 byte[] generateNonce()
702 throws IOException, NoSuchAlgorithmException
704 if (nonce == null)
706 long time = System.currentTimeMillis();
707 MessageDigest md5 = MessageDigest.getInstance("MD5");
708 md5.update(Long.toString(time).getBytes("US-ASCII"));
709 nonce = md5.digest();
711 return nonce;
714 String toHexString(byte[] bytes)
716 char[] ret = new char[bytes.length * 2];
717 for (int i = 0, j = 0; i < bytes.length; i++)
719 int c =(int) bytes[i];
720 if (c < 0)
722 c += 0x100;
724 ret[j++] = Character.forDigit(c / 0x10, 0x10);
725 ret[j++] = Character.forDigit(c % 0x10, 0x10);
727 return new String(ret);
731 * Parse the specified cookie list and notify the cookie manager.
733 void handleSetCookie(String text)
735 CookieManager cookieManager = connection.getCookieManager();
736 if (cookieManager == null)
738 return;
740 String name = null;
741 String value = null;
742 String comment = null;
743 String domain = connection.getHostName();
744 String path = this.path;
745 int lsi = path.lastIndexOf('/');
746 if (lsi != -1)
748 path = path.substring(0, lsi);
750 boolean secure = false;
751 Date expires = null;
753 int len = text.length();
754 String attr = null;
755 StringBuilder buf = new StringBuilder();
756 boolean inQuote = false;
757 for (int i = 0; i <= len; i++)
759 char c =(i == len) ? '\u0000' : text.charAt(i);
760 if (c == '"')
762 inQuote = !inQuote;
764 else if (!inQuote)
766 if (c == '=' && attr == null)
768 attr = buf.toString().trim();
769 buf.setLength(0);
771 else if (c == ';' || i == len || c == ',')
773 String val = unquote(buf.toString().trim());
774 if (name == null)
776 name = attr;
777 value = val;
779 else if ("Comment".equalsIgnoreCase(attr))
781 comment = val;
783 else if ("Domain".equalsIgnoreCase(attr))
785 domain = val;
787 else if ("Path".equalsIgnoreCase(attr))
789 path = val;
791 else if ("Secure".equalsIgnoreCase(val))
793 secure = true;
795 else if ("Max-Age".equalsIgnoreCase(attr))
797 int delta = Integer.parseInt(val);
798 Calendar cal = Calendar.getInstance();
799 cal.setTimeInMillis(System.currentTimeMillis());
800 cal.add(Calendar.SECOND, delta);
801 expires = cal.getTime();
803 else if ("Expires".equalsIgnoreCase(attr))
805 DateFormat dateFormat = new HTTPDateFormat();
808 expires = dateFormat.parse(val);
810 catch (ParseException e)
812 // if this isn't a valid date, it may be that
813 // the value was returned unquoted; in that case, we
814 // want to continue buffering the value
815 buf.append(c);
816 continue;
819 attr = null;
820 buf.setLength(0);
821 // case EOL
822 if (i == len || c == ',')
824 Cookie cookie = new Cookie(name, value, comment, domain,
825 path, secure, expires);
826 cookieManager.setCookie(cookie);
828 if (c == ',')
830 // Reset cookie fields
831 name = null;
832 value = null;
833 comment = null;
834 domain = connection.getHostName();
835 path = this.path;
836 if (lsi != -1)
838 path = path.substring(0, lsi);
840 secure = false;
841 expires = null;
844 else
846 buf.append(c);
849 else
851 buf.append(c);