re PR classpath/28580 (HTTP HEAD fails on chuncked encoding)
[gcc.git] / libjava / classpath / gnu / java / net / protocol / http / Request.java
1 /* Request.java --
2 Copyright (C) 2004, 2005, 2006 Free Software Foundation, Inc.
3
4 This file is part of GNU Classpath.
5
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.
10
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.
15
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.
20
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.
25
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. */
37
38
39 package gnu.java.net.protocol.http;
40
41 import gnu.java.net.BASE64;
42 import gnu.java.net.LineInputStream;
43
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.Iterator;
56 import java.util.Map;
57 import java.util.Properties;
58 import java.util.zip.GZIPInputStream;
59 import java.util.zip.InflaterInputStream;
60
61 /**
62 * A single HTTP request.
63 *
64 * @author Chris Burdess (dog@gnu.org)
65 */
66 public class Request
67 {
68
69 /**
70 * The connection context in which this request is invoked.
71 */
72 protected final HTTPConnection connection;
73
74 /**
75 * The HTTP method to invoke.
76 */
77 protected final String method;
78
79 /**
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.
83 */
84 protected final String path;
85
86 /**
87 * The headers in this request.
88 */
89 protected final Headers requestHeaders;
90
91 /**
92 * The request body provider.
93 */
94 protected RequestBodyWriter requestBodyWriter;
95
96 /**
97 * Map of response header handlers.
98 */
99 protected Map responseHeaderHandlers;
100
101 /**
102 * The authenticator.
103 */
104 protected Authenticator authenticator;
105
106 /**
107 * Whether this request has been dispatched yet.
108 */
109 private boolean dispatched;
110
111 /**
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
116 */
117 protected Request(HTTPConnection connection, String method,
118 String path)
119 {
120 this.connection = connection;
121 this.method = method;
122 this.path = path;
123 requestHeaders = new Headers();
124 responseHeaderHandlers = new HashMap();
125 }
126
127 /**
128 * Returns the connection associated with this request.
129 * @see #connection
130 */
131 public HTTPConnection getConnection()
132 {
133 return connection;
134 }
135
136 /**
137 * Returns the HTTP method to invoke.
138 * @see #method
139 */
140 public String getMethod()
141 {
142 return method;
143 }
144
145 /**
146 * Returns the resource path.
147 * @see #path
148 */
149 public String getPath()
150 {
151 return path;
152 }
153
154 /**
155 * Returns the full request-URI represented by this request, as specified
156 * by HTTP/1.1.
157 */
158 public String getRequestURI()
159 {
160 return connection.getURI() + path;
161 }
162
163 /**
164 * Returns the headers in this request.
165 */
166 public Headers getHeaders()
167 {
168 return requestHeaders;
169 }
170
171 /**
172 * Returns the value of the specified header in this request.
173 * @param name the header name
174 */
175 public String getHeader(String name)
176 {
177 return requestHeaders.getValue(name);
178 }
179
180 /**
181 * Returns the value of the specified header in this request as an integer.
182 * @param name the header name
183 */
184 public int getIntHeader(String name)
185 {
186 return requestHeaders.getIntValue(name);
187 }
188
189 /**
190 * Returns the value of the specified header in this request as a date.
191 * @param name the header name
192 */
193 public Date getDateHeader(String name)
194 {
195 return requestHeaders.getDateValue(name);
196 }
197
198 /**
199 * Sets the specified header in this request.
200 * @param name the header name
201 * @param value the header value
202 */
203 public void setHeader(String name, String value)
204 {
205 requestHeaders.put(name, value);
206 }
207
208 /**
209 * Convenience method to set the entire request body.
210 * @param requestBody the request body content
211 */
212 public void setRequestBody(byte[] requestBody)
213 {
214 setRequestBodyWriter(new ByteArrayRequestBodyWriter(requestBody));
215 }
216
217 /**
218 * Sets the request body provider.
219 * @param requestBodyWriter the handler used to obtain the request body
220 */
221 public void setRequestBodyWriter(RequestBodyWriter requestBodyWriter)
222 {
223 this.requestBodyWriter = requestBodyWriter;
224 }
225
226 /**
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
230 */
231 public void setResponseHeaderHandler(String name,
232 ResponseHeaderHandler handler)
233 {
234 responseHeaderHandlers.put(name, handler);
235 }
236
237 /**
238 * Sets an authenticator that can be used to handle authentication
239 * automatically.
240 * @param authenticator the authenticator
241 */
242 public void setAuthenticator(Authenticator authenticator)
243 {
244 this.authenticator = authenticator;
245 }
246
247 /**
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
253 */
254 public Response dispatch()
255 throws IOException
256 {
257 if (dispatched)
258 {
259 throw new ProtocolException("request already dispatched");
260 }
261 final String CRLF = "\r\n";
262 final String HEADER_SEP = ": ";
263 final String US_ASCII = "US-ASCII";
264 final String version = connection.getVersion();
265 Response response;
266 int contentLength = -1;
267 boolean retry = false;
268 int attempts = 0;
269 boolean expectingContinue = false;
270 if (requestBodyWriter != null)
271 {
272 contentLength = requestBodyWriter.getContentLength();
273 String expect = getHeader("Expect");
274 if (expect != null && expect.equals("100-continue"))
275 {
276 expectingContinue = true;
277 }
278 else
279 {
280 setHeader("Content-Length", Integer.toString(contentLength));
281 }
282 }
283
284 try
285 {
286 // Loop while authentication fails or continue
287 do
288 {
289 retry = false;
290
291 // Get socket output and input streams
292 OutputStream out = connection.getOutputStream();
293
294 // Request line
295 String requestUri = path;
296 if (connection.isUsingProxy() &&
297 !"*".equals(requestUri) &&
298 !"CONNECT".equals(method))
299 {
300 requestUri = getRequestURI();
301 }
302 String line = method + ' ' + requestUri + ' ' + version + CRLF;
303 out.write(line.getBytes(US_ASCII));
304 // Request headers
305 for (Iterator i = requestHeaders.iterator(); i.hasNext(); )
306 {
307 Headers.HeaderElement elt = (Headers.HeaderElement)i.next();
308 line = elt.name + HEADER_SEP + elt.value + CRLF;
309 out.write(line.getBytes(US_ASCII));
310 }
311 out.write(CRLF.getBytes(US_ASCII));
312 // Request body
313 if (requestBodyWriter != null && !expectingContinue)
314 {
315 byte[] buffer = new byte[4096];
316 int len;
317 int count = 0;
318
319 requestBodyWriter.reset();
320 do
321 {
322 len = requestBodyWriter.write(buffer);
323 if (len > 0)
324 {
325 out.write(buffer, 0, len);
326 }
327 count += len;
328 }
329 while (len > -1 && count < contentLength);
330 }
331 out.flush();
332 // Get response
333 while(true)
334 {
335 response = readResponse(connection.getInputStream());
336 int sc = response.getCode();
337 if (sc == 401 && authenticator != null)
338 {
339 if (authenticate(response, attempts++))
340 {
341 retry = true;
342 }
343 }
344 else if (sc == 100)
345 {
346 if (expectingContinue)
347 {
348 requestHeaders.remove("Expect");
349 setHeader("Content-Length",
350 Integer.toString(contentLength));
351 expectingContinue = false;
352 retry = true;
353 }
354 else
355 {
356 // A conforming server can send an unsoliceted
357 // Continue response but *should* not (RFC 2616
358 // sec 8.2.3). Ignore the bogus Continue
359 // response and get the real response that
360 // should follow
361 continue;
362 }
363 }
364 break;
365 }
366 }
367 while (retry);
368 }
369 catch (IOException e)
370 {
371 connection.close();
372 throw e;
373 }
374 return response;
375 }
376
377 Response readResponse(InputStream in)
378 throws IOException
379 {
380 String line;
381 int len;
382
383 // Read response status line
384 LineInputStream lis = new LineInputStream(in);
385
386 line = lis.readLine();
387 if (line == null)
388 {
389 throw new ProtocolException("Peer closed connection");
390 }
391 if (!line.startsWith("HTTP/"))
392 {
393 throw new ProtocolException(line);
394 }
395 len = line.length();
396 int start = 5, end = 6;
397 while (line.charAt(end) != '.')
398 {
399 end++;
400 }
401 int majorVersion = Integer.parseInt(line.substring(start, end));
402 start = end + 1;
403 end = start + 1;
404 while (line.charAt(end) != ' ')
405 {
406 end++;
407 }
408 int minorVersion = Integer.parseInt(line.substring(start, end));
409 start = end + 1;
410 end = start + 3;
411 int code = Integer.parseInt(line.substring(start, end));
412 String message = line.substring(end + 1, len - 1);
413 // Read response headers
414 Headers responseHeaders = new Headers();
415 responseHeaders.parse(lis);
416 notifyHeaderHandlers(responseHeaders);
417 InputStream body = null;
418
419 switch (code)
420 {
421 case 100:
422 break;
423 case 204:
424 case 205:
425 case 304:
426 body = createResponseBodyStream(responseHeaders, majorVersion,
427 minorVersion, in, false);
428 break;
429 default:
430 body = createResponseBodyStream(responseHeaders, majorVersion,
431 minorVersion, in, true);
432 }
433
434 // Construct response
435 Response ret = new Response(majorVersion, minorVersion, code,
436 message, responseHeaders, body);
437 return ret;
438 }
439
440 void notifyHeaderHandlers(Headers headers)
441 {
442 for (Iterator i = headers.iterator(); i.hasNext(); )
443 {
444 Headers.HeaderElement entry = (Headers.HeaderElement) i.next();
445 // Handle Set-Cookie
446 if ("Set-Cookie".equalsIgnoreCase(entry.name))
447 handleSetCookie(entry.value);
448
449 ResponseHeaderHandler handler =
450 (ResponseHeaderHandler) responseHeaderHandlers.get(entry.name);
451 if (handler != null)
452 handler.setValue(entry.value);
453 }
454 }
455
456 private InputStream createResponseBodyStream(Headers responseHeaders,
457 int majorVersion,
458 int minorVersion,
459 InputStream in,
460 boolean mayHaveBody)
461 throws IOException
462 {
463 long contentLength = -1;
464 Headers trailer = null;
465
466 // Persistent connections are the default in HTTP/1.1
467 boolean doClose = "close".equalsIgnoreCase(getHeader("Connection")) ||
468 "close".equalsIgnoreCase(responseHeaders.getValue("Connection")) ||
469 (connection.majorVersion == 1 && connection.minorVersion == 0) ||
470 (majorVersion == 1 && minorVersion == 0);
471
472 String transferCoding = responseHeaders.getValue("Transfer-Encoding");
473 if ("HEAD".equals(method) || !mayHaveBody)
474 {
475 // Special case no body.
476 in = new LimitedLengthInputStream(in, 0, true, connection, doClose);
477 }
478 else if ("chunked".equalsIgnoreCase(transferCoding))
479 {
480 in = new LimitedLengthInputStream(in, -1, false, connection, doClose);
481
482 in = new ChunkedInputStream(in, responseHeaders);
483 }
484 else
485 {
486 contentLength = responseHeaders.getLongValue("Content-Length");
487
488 if (contentLength < 0)
489 doClose = true; // No Content-Length, must close.
490
491 in = new LimitedLengthInputStream(in, contentLength,
492 contentLength >= 0,
493 connection, doClose);
494 }
495 String contentCoding = responseHeaders.getValue("Content-Encoding");
496 if (contentCoding != null && !"identity".equals(contentCoding))
497 {
498 if ("gzip".equals(contentCoding))
499 {
500 in = new GZIPInputStream(in);
501 }
502 else if ("deflate".equals(contentCoding))
503 {
504 in = new InflaterInputStream(in);
505 }
506 else
507 {
508 throw new ProtocolException("Unsupported Content-Encoding: " +
509 contentCoding);
510 }
511 // Remove the Content-Encoding header because the content is
512 // no longer compressed.
513 responseHeaders.remove("Content-Encoding");
514 }
515 return in;
516 }
517
518 boolean authenticate(Response response, int attempts)
519 throws IOException
520 {
521 String challenge = response.getHeader("WWW-Authenticate");
522 if (challenge == null)
523 {
524 challenge = response.getHeader("Proxy-Authenticate");
525 }
526 int si = challenge.indexOf(' ');
527 String scheme = (si == -1) ? challenge : challenge.substring(0, si);
528 if ("Basic".equalsIgnoreCase(scheme))
529 {
530 Properties params = parseAuthParams(challenge.substring(si + 1));
531 String realm = params.getProperty("realm");
532 Credentials creds = authenticator.getCredentials(realm, attempts);
533 String userPass = creds.getUsername() + ':' + creds.getPassword();
534 byte[] b_userPass = userPass.getBytes("US-ASCII");
535 byte[] b_encoded = BASE64.encode(b_userPass);
536 String authorization =
537 scheme + " " + new String(b_encoded, "US-ASCII");
538 setHeader("Authorization", authorization);
539 return true;
540 }
541 else if ("Digest".equalsIgnoreCase(scheme))
542 {
543 Properties params = parseAuthParams(challenge.substring(si + 1));
544 String realm = params.getProperty("realm");
545 String nonce = params.getProperty("nonce");
546 String qop = params.getProperty("qop");
547 String algorithm = params.getProperty("algorithm");
548 String digestUri = getRequestURI();
549 Credentials creds = authenticator.getCredentials(realm, attempts);
550 String username = creds.getUsername();
551 String password = creds.getPassword();
552 connection.incrementNonce(nonce);
553 try
554 {
555 MessageDigest md5 = MessageDigest.getInstance("MD5");
556 final byte[] COLON = { 0x3a };
557
558 // Calculate H(A1)
559 md5.reset();
560 md5.update(username.getBytes("US-ASCII"));
561 md5.update(COLON);
562 md5.update(realm.getBytes("US-ASCII"));
563 md5.update(COLON);
564 md5.update(password.getBytes("US-ASCII"));
565 byte[] ha1 = md5.digest();
566 if ("md5-sess".equals(algorithm))
567 {
568 byte[] cnonce = generateNonce();
569 md5.reset();
570 md5.update(ha1);
571 md5.update(COLON);
572 md5.update(nonce.getBytes("US-ASCII"));
573 md5.update(COLON);
574 md5.update(cnonce);
575 ha1 = md5.digest();
576 }
577 String ha1Hex = toHexString(ha1);
578
579 // Calculate H(A2)
580 md5.reset();
581 md5.update(method.getBytes("US-ASCII"));
582 md5.update(COLON);
583 md5.update(digestUri.getBytes("US-ASCII"));
584 if ("auth-int".equals(qop))
585 {
586 byte[] hEntity = null; // TODO hash of entity body
587 md5.update(COLON);
588 md5.update(hEntity);
589 }
590 byte[] ha2 = md5.digest();
591 String ha2Hex = toHexString(ha2);
592
593 // Calculate response
594 md5.reset();
595 md5.update(ha1Hex.getBytes("US-ASCII"));
596 md5.update(COLON);
597 md5.update(nonce.getBytes("US-ASCII"));
598 if ("auth".equals(qop) || "auth-int".equals(qop))
599 {
600 String nc = getNonceCount(nonce);
601 byte[] cnonce = generateNonce();
602 md5.update(COLON);
603 md5.update(nc.getBytes("US-ASCII"));
604 md5.update(COLON);
605 md5.update(cnonce);
606 md5.update(COLON);
607 md5.update(qop.getBytes("US-ASCII"));
608 }
609 md5.update(COLON);
610 md5.update(ha2Hex.getBytes("US-ASCII"));
611 String digestResponse = toHexString(md5.digest());
612
613 String authorization = scheme +
614 " username=\"" + username + "\"" +
615 " realm=\"" + realm + "\"" +
616 " nonce=\"" + nonce + "\"" +
617 " uri=\"" + digestUri + "\"" +
618 " response=\"" + digestResponse + "\"";
619 setHeader("Authorization", authorization);
620 return true;
621 }
622 catch (NoSuchAlgorithmException e)
623 {
624 return false;
625 }
626 }
627 // Scheme not recognised
628 return false;
629 }
630
631 Properties parseAuthParams(String text)
632 {
633 int len = text.length();
634 String key = null;
635 StringBuilder buf = new StringBuilder();
636 Properties ret = new Properties();
637 boolean inQuote = false;
638 for (int i = 0; i < len; i++)
639 {
640 char c = text.charAt(i);
641 if (c == '"')
642 {
643 inQuote = !inQuote;
644 }
645 else if (c == '=' && key == null)
646 {
647 key = buf.toString().trim();
648 buf.setLength(0);
649 }
650 else if (c == ' ' && !inQuote)
651 {
652 String value = unquote(buf.toString().trim());
653 ret.put(key, value);
654 key = null;
655 buf.setLength(0);
656 }
657 else if (c != ',' || (i <(len - 1) && text.charAt(i + 1) != ' '))
658 {
659 buf.append(c);
660 }
661 }
662 if (key != null)
663 {
664 String value = unquote(buf.toString().trim());
665 ret.put(key, value);
666 }
667 return ret;
668 }
669
670 String unquote(String text)
671 {
672 int len = text.length();
673 if (len > 0 && text.charAt(0) == '"' && text.charAt(len - 1) == '"')
674 {
675 return text.substring(1, len - 1);
676 }
677 return text;
678 }
679
680 /**
681 * Returns the number of times the specified nonce value has been seen.
682 * This always returns an 8-byte 0-padded hexadecimal string.
683 */
684 String getNonceCount(String nonce)
685 {
686 int nc = connection.getNonceCount(nonce);
687 String hex = Integer.toHexString(nc);
688 StringBuilder buf = new StringBuilder();
689 for (int i = 8 - hex.length(); i > 0; i--)
690 {
691 buf.append('0');
692 }
693 buf.append(hex);
694 return buf.toString();
695 }
696
697 /**
698 * Client nonce value.
699 */
700 byte[] nonce;
701
702 /**
703 * Generates a new client nonce value.
704 */
705 byte[] generateNonce()
706 throws IOException, NoSuchAlgorithmException
707 {
708 if (nonce == null)
709 {
710 long time = System.currentTimeMillis();
711 MessageDigest md5 = MessageDigest.getInstance("MD5");
712 md5.update(Long.toString(time).getBytes("US-ASCII"));
713 nonce = md5.digest();
714 }
715 return nonce;
716 }
717
718 String toHexString(byte[] bytes)
719 {
720 char[] ret = new char[bytes.length * 2];
721 for (int i = 0, j = 0; i < bytes.length; i++)
722 {
723 int c =(int) bytes[i];
724 if (c < 0)
725 {
726 c += 0x100;
727 }
728 ret[j++] = Character.forDigit(c / 0x10, 0x10);
729 ret[j++] = Character.forDigit(c % 0x10, 0x10);
730 }
731 return new String(ret);
732 }
733
734 /**
735 * Parse the specified cookie list and notify the cookie manager.
736 */
737 void handleSetCookie(String text)
738 {
739 CookieManager cookieManager = connection.getCookieManager();
740 if (cookieManager == null)
741 {
742 return;
743 }
744 String name = null;
745 String value = null;
746 String comment = null;
747 String domain = connection.getHostName();
748 String path = this.path;
749 int lsi = path.lastIndexOf('/');
750 if (lsi != -1)
751 {
752 path = path.substring(0, lsi);
753 }
754 boolean secure = false;
755 Date expires = null;
756
757 int len = text.length();
758 String attr = null;
759 StringBuilder buf = new StringBuilder();
760 boolean inQuote = false;
761 for (int i = 0; i <= len; i++)
762 {
763 char c =(i == len) ? '\u0000' : text.charAt(i);
764 if (c == '"')
765 {
766 inQuote = !inQuote;
767 }
768 else if (!inQuote)
769 {
770 if (c == '=' && attr == null)
771 {
772 attr = buf.toString().trim();
773 buf.setLength(0);
774 }
775 else if (c == ';' || i == len || c == ',')
776 {
777 String val = unquote(buf.toString().trim());
778 if (name == null)
779 {
780 name = attr;
781 value = val;
782 }
783 else if ("Comment".equalsIgnoreCase(attr))
784 {
785 comment = val;
786 }
787 else if ("Domain".equalsIgnoreCase(attr))
788 {
789 domain = val;
790 }
791 else if ("Path".equalsIgnoreCase(attr))
792 {
793 path = val;
794 }
795 else if ("Secure".equalsIgnoreCase(val))
796 {
797 secure = true;
798 }
799 else if ("Max-Age".equalsIgnoreCase(attr))
800 {
801 int delta = Integer.parseInt(val);
802 Calendar cal = Calendar.getInstance();
803 cal.setTimeInMillis(System.currentTimeMillis());
804 cal.add(Calendar.SECOND, delta);
805 expires = cal.getTime();
806 }
807 else if ("Expires".equalsIgnoreCase(attr))
808 {
809 DateFormat dateFormat = new HTTPDateFormat();
810 try
811 {
812 expires = dateFormat.parse(val);
813 }
814 catch (ParseException e)
815 {
816 // if this isn't a valid date, it may be that
817 // the value was returned unquoted; in that case, we
818 // want to continue buffering the value
819 buf.append(c);
820 continue;
821 }
822 }
823 attr = null;
824 buf.setLength(0);
825 // case EOL
826 if (i == len || c == ',')
827 {
828 Cookie cookie = new Cookie(name, value, comment, domain,
829 path, secure, expires);
830 cookieManager.setCookie(cookie);
831 }
832 if (c == ',')
833 {
834 // Reset cookie fields
835 name = null;
836 value = null;
837 comment = null;
838 domain = connection.getHostName();
839 path = this.path;
840 if (lsi != -1)
841 {
842 path = path.substring(0, lsi);
843 }
844 secure = false;
845 expires = null;
846 }
847 }
848 else
849 {
850 buf.append(c);
851 }
852 }
853 else
854 {
855 buf.append(c);
856 }
857 }
858 }
859
860 }
861