2 Copyright (C) 2004, 2005, 2006 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
.net
.BASE64
;
42 import gnu
.java
.net
.LineInputStream
;
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
;
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 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();
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 (Iterator i
= requestHeaders
.iterator(); i
.hasNext(); )
307 Headers
.HeaderElement elt
= (Headers
.HeaderElement
)i
.next();
308 line
= elt
.name
+ HEADER_SEP
+ elt
.value
+ CRLF
;
309 out
.write(line
.getBytes(US_ASCII
));
311 out
.write(CRLF
.getBytes(US_ASCII
));
313 if (requestBodyWriter
!= null && !expectingContinue
)
315 byte[] buffer
= new byte[4096];
319 requestBodyWriter
.reset();
322 len
= requestBodyWriter
.write(buffer
);
325 out
.write(buffer
, 0, len
);
329 while (len
> -1 && count
< contentLength
);
335 response
= readResponse(connection
.getInputStream());
336 int sc
= response
.getCode();
337 if (sc
== 401 && authenticator
!= null)
339 if (authenticate(response
, attempts
++))
346 if (expectingContinue
)
348 requestHeaders
.remove("Expect");
349 setHeader("Content-Length",
350 Integer
.toString(contentLength
));
351 expectingContinue
= false;
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
369 catch (IOException e
)
377 Response
readResponse(InputStream in
)
383 // Read response status line
384 LineInputStream lis
= new LineInputStream(in
);
386 line
= lis
.readLine();
389 throw new ProtocolException("Peer closed connection");
391 if (!line
.startsWith("HTTP/"))
393 throw new ProtocolException(line
);
396 int start
= 5, end
= 6;
397 while (line
.charAt(end
) != '.')
401 int majorVersion
= Integer
.parseInt(line
.substring(start
, end
));
404 while (line
.charAt(end
) != ' ')
408 int minorVersion
= Integer
.parseInt(line
.substring(start
, end
));
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;
426 body
= createResponseBodyStream(responseHeaders
, majorVersion
,
427 minorVersion
, in
, false);
430 body
= createResponseBodyStream(responseHeaders
, majorVersion
,
431 minorVersion
, in
, true);
434 // Construct response
435 Response ret
= new Response(majorVersion
, minorVersion
, code
,
436 message
, responseHeaders
, body
);
440 void notifyHeaderHandlers(Headers headers
)
442 for (Iterator i
= headers
.iterator(); i
.hasNext(); )
444 Headers
.HeaderElement entry
= (Headers
.HeaderElement
) i
.next();
446 if ("Set-Cookie".equalsIgnoreCase(entry
.name
))
447 handleSetCookie(entry
.value
);
449 ResponseHeaderHandler handler
=
450 (ResponseHeaderHandler
) responseHeaderHandlers
.get(entry
.name
);
452 handler
.setValue(entry
.value
);
456 private InputStream
createResponseBodyStream(Headers responseHeaders
,
463 long contentLength
= -1;
464 Headers trailer
= null;
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);
472 String transferCoding
= responseHeaders
.getValue("Transfer-Encoding");
473 if ("HEAD".equals(method
) || !mayHaveBody
)
475 // Special case no body.
476 in
= new LimitedLengthInputStream(in
, 0, true, connection
, doClose
);
478 else if ("chunked".equalsIgnoreCase(transferCoding
))
480 in
= new LimitedLengthInputStream(in
, -1, false, connection
, doClose
);
482 in
= new ChunkedInputStream(in
, responseHeaders
);
486 contentLength
= responseHeaders
.getLongValue("Content-Length");
488 if (contentLength
< 0)
489 doClose
= true; // No Content-Length, must close.
491 in
= new LimitedLengthInputStream(in
, contentLength
,
493 connection
, doClose
);
495 String contentCoding
= responseHeaders
.getValue("Content-Encoding");
496 if (contentCoding
!= null && !"identity".equals(contentCoding
))
498 if ("gzip".equals(contentCoding
))
500 in
= new GZIPInputStream(in
);
502 else if ("deflate".equals(contentCoding
))
504 in
= new InflaterInputStream(in
);
508 throw new ProtocolException("Unsupported Content-Encoding: " +
511 // Remove the Content-Encoding header because the content is
512 // no longer compressed.
513 responseHeaders
.remove("Content-Encoding");
518 boolean authenticate(Response response
, int attempts
)
521 String challenge
= response
.getHeader("WWW-Authenticate");
522 if (challenge
== null)
524 challenge
= response
.getHeader("Proxy-Authenticate");
526 int si
= challenge
.indexOf(' ');
527 String scheme
= (si
== -1) ? challenge
: challenge
.substring(0, si
);
528 if ("Basic".equalsIgnoreCase(scheme
))
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
);
541 else if ("Digest".equalsIgnoreCase(scheme
))
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
);
555 MessageDigest md5
= MessageDigest
.getInstance("MD5");
556 final byte[] COLON
= { 0x3a };
560 md5
.update(username
.getBytes("US-ASCII"));
562 md5
.update(realm
.getBytes("US-ASCII"));
564 md5
.update(password
.getBytes("US-ASCII"));
565 byte[] ha1
= md5
.digest();
566 if ("md5-sess".equals(algorithm
))
568 byte[] cnonce
= generateNonce();
572 md5
.update(nonce
.getBytes("US-ASCII"));
577 String ha1Hex
= toHexString(ha1
);
581 md5
.update(method
.getBytes("US-ASCII"));
583 md5
.update(digestUri
.getBytes("US-ASCII"));
584 if ("auth-int".equals(qop
))
586 byte[] hEntity
= null; // TODO hash of entity body
590 byte[] ha2
= md5
.digest();
591 String ha2Hex
= toHexString(ha2
);
593 // Calculate response
595 md5
.update(ha1Hex
.getBytes("US-ASCII"));
597 md5
.update(nonce
.getBytes("US-ASCII"));
598 if ("auth".equals(qop
) || "auth-int".equals(qop
))
600 String nc
= getNonceCount(nonce
);
601 byte[] cnonce
= generateNonce();
603 md5
.update(nc
.getBytes("US-ASCII"));
607 md5
.update(qop
.getBytes("US-ASCII"));
610 md5
.update(ha2Hex
.getBytes("US-ASCII"));
611 String digestResponse
= toHexString(md5
.digest());
613 String authorization
= scheme
+
614 " username=\"" + username
+ "\"" +
615 " realm=\"" + realm
+ "\"" +
616 " nonce=\"" + nonce
+ "\"" +
617 " uri=\"" + digestUri
+ "\"" +
618 " response=\"" + digestResponse
+ "\"";
619 setHeader("Authorization", authorization
);
622 catch (NoSuchAlgorithmException e
)
627 // Scheme not recognised
631 Properties
parseAuthParams(String text
)
633 int len
= text
.length();
635 StringBuilder buf
= new StringBuilder();
636 Properties ret
= new Properties();
637 boolean inQuote
= false;
638 for (int i
= 0; i
< len
; i
++)
640 char c
= text
.charAt(i
);
645 else if (c
== '=' && key
== null)
647 key
= buf
.toString().trim();
650 else if (c
== ' ' && !inQuote
)
652 String value
= unquote(buf
.toString().trim());
657 else if (c
!= ',' || (i
<(len
- 1) && text
.charAt(i
+ 1) != ' '))
664 String value
= unquote(buf
.toString().trim());
670 String
unquote(String text
)
672 int len
= text
.length();
673 if (len
> 0 && text
.charAt(0) == '"' && text
.charAt(len
- 1) == '"')
675 return text
.substring(1, len
- 1);
681 * Returns the number of times the specified nonce value has been seen.
682 * This always returns an 8-byte 0-padded hexadecimal string.
684 String
getNonceCount(String nonce
)
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
--)
694 return buf
.toString();
698 * Client nonce value.
703 * Generates a new client nonce value.
705 byte[] generateNonce()
706 throws IOException
, NoSuchAlgorithmException
710 long time
= System
.currentTimeMillis();
711 MessageDigest md5
= MessageDigest
.getInstance("MD5");
712 md5
.update(Long
.toString(time
).getBytes("US-ASCII"));
713 nonce
= md5
.digest();
718 String
toHexString(byte[] bytes
)
720 char[] ret
= new char[bytes
.length
* 2];
721 for (int i
= 0, j
= 0; i
< bytes
.length
; i
++)
723 int c
=(int) bytes
[i
];
728 ret
[j
++] = Character
.forDigit(c
/ 0x10, 0x10);
729 ret
[j
++] = Character
.forDigit(c
% 0x10, 0x10);
731 return new String(ret
);
735 * Parse the specified cookie list and notify the cookie manager.
737 void handleSetCookie(String text
)
739 CookieManager cookieManager
= connection
.getCookieManager();
740 if (cookieManager
== null)
746 String comment
= null;
747 String domain
= connection
.getHostName();
748 String path
= this.path
;
749 int lsi
= path
.lastIndexOf('/');
752 path
= path
.substring(0, lsi
);
754 boolean secure
= false;
757 int len
= text
.length();
759 StringBuilder buf
= new StringBuilder();
760 boolean inQuote
= false;
761 for (int i
= 0; i
<= len
; i
++)
763 char c
=(i
== len
) ?
'\u0000' : text
.charAt(i
);
770 if (c
== '=' && attr
== null)
772 attr
= buf
.toString().trim();
775 else if (c
== ';' || i
== len
|| c
== ',')
777 String val
= unquote(buf
.toString().trim());
783 else if ("Comment".equalsIgnoreCase(attr
))
787 else if ("Domain".equalsIgnoreCase(attr
))
791 else if ("Path".equalsIgnoreCase(attr
))
795 else if ("Secure".equalsIgnoreCase(val
))
799 else if ("Max-Age".equalsIgnoreCase(attr
))
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();
807 else if ("Expires".equalsIgnoreCase(attr
))
809 DateFormat dateFormat
= new HTTPDateFormat();
812 expires
= dateFormat
.parse(val
);
814 catch (ParseException e
)
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
826 if (i
== len
|| c
== ',')
828 Cookie cookie
= new Cookie(name
, value
, comment
, domain
,
829 path
, secure
, expires
);
830 cookieManager
.setCookie(cookie
);
834 // Reset cookie fields
838 domain
= connection
.getHostName();
842 path
= path
.substring(0, lsi
);