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
.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
;
56 import java
.util
.Properties
;
57 import java
.util
.zip
.GZIPInputStream
;
58 import java
.util
.zip
.InflaterInputStream
;
61 * A single HTTP request.
63 * @author Chris Burdess (dog@gnu.org)
69 * The connection context in which this request is invoked.
71 protected final HTTPConnection connection
;
74 * The HTTP method to invoke.
76 protected final String method
;
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
;
86 * The headers in this request.
88 protected final Headers requestHeaders
;
91 * The request body provider.
93 protected RequestBodyWriter requestBodyWriter
;
96 * Map of response header handlers.
98 protected Map
<String
, ResponseHeaderHandler
> responseHeaderHandlers
;
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
,
119 this.connection
= connection
;
120 this.method
= method
;
122 requestHeaders
= new Headers();
123 responseHeaderHandlers
= new HashMap
<String
, ResponseHeaderHandler
>();
127 * Returns the connection associated with this request.
130 public HTTPConnection
getConnection()
136 * Returns the HTTP method to invoke.
139 public String
getMethod()
145 * Returns the resource path.
148 public String
getPath()
154 * Returns the full request-URI represented by this request, as specified
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
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()
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();
265 int contentLength
= -1;
266 boolean retry
= false;
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;
279 setHeader("Content-Length", Integer
.toString(contentLength
));
285 // Loop while authentication fails or continue
290 // Get socket output and input streams
291 OutputStream out
= connection
.getOutputStream();
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
));
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
));
311 if (requestBodyWriter
!= null && !expectingContinue
)
313 byte[] buffer
= new byte[4096];
317 requestBodyWriter
.reset();
320 len
= requestBodyWriter
.write(buffer
);
323 out
.write(buffer
, 0, len
);
327 while (len
> -1 && count
< contentLength
);
333 response
= readResponse(connection
.getInputStream());
334 int sc
= response
.getCode();
335 if (sc
== 401 && authenticator
!= null)
337 if (authenticate(response
, attempts
++))
344 if (expectingContinue
)
346 requestHeaders
.remove("Expect");
347 setHeader("Content-Length",
348 Integer
.toString(contentLength
));
349 expectingContinue
= false;
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
367 catch (IOException e
)
375 Response
readResponse(InputStream in
)
381 // Read response status line
382 LineInputStream lis
= new LineInputStream(in
);
384 line
= lis
.readLine();
387 throw new ProtocolException("Peer closed connection");
389 if (!line
.startsWith("HTTP/"))
391 throw new ProtocolException(line
);
394 int start
= 5, end
= 6;
395 while (line
.charAt(end
) != '.')
399 int majorVersion
= Integer
.parseInt(line
.substring(start
, end
));
402 while (line
.charAt(end
) != ' ')
406 int minorVersion
= Integer
.parseInt(line
.substring(start
, end
));
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;
424 body
= createResponseBodyStream(responseHeaders
, majorVersion
,
425 minorVersion
, in
, false);
428 body
= createResponseBodyStream(responseHeaders
, majorVersion
,
429 minorVersion
, in
, true);
432 // Construct response
433 Response ret
= new Response(majorVersion
, minorVersion
, code
,
434 message
, responseHeaders
, body
);
438 void notifyHeaderHandlers(Headers headers
)
440 for (Headers
.HeaderElement entry
: headers
)
443 if ("Set-Cookie".equalsIgnoreCase(entry
.name
))
444 handleSetCookie(entry
.value
);
446 ResponseHeaderHandler handler
=
447 (ResponseHeaderHandler
) responseHeaderHandlers
.get(entry
.name
);
449 handler
.setValue(entry
.value
);
453 private InputStream
createResponseBodyStream(Headers responseHeaders
,
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
);
482 contentLength
= responseHeaders
.getLongValue("Content-Length");
484 if (contentLength
< 0)
485 doClose
= true; // No Content-Length, must close.
487 in
= new LimitedLengthInputStream(in
, contentLength
,
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
);
504 throw new ProtocolException("Unsupported Content-Encoding: " +
507 // Remove the Content-Encoding header because the content is
508 // no longer compressed.
509 responseHeaders
.remove("Content-Encoding");
514 boolean authenticate(Response response
, int attempts
)
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
);
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 };
556 md5
.update(username
.getBytes("US-ASCII"));
558 md5
.update(realm
.getBytes("US-ASCII"));
560 md5
.update(password
.getBytes("US-ASCII"));
561 byte[] ha1
= md5
.digest();
562 if ("md5-sess".equals(algorithm
))
564 byte[] cnonce
= generateNonce();
568 md5
.update(nonce
.getBytes("US-ASCII"));
573 String ha1Hex
= toHexString(ha1
);
577 md5
.update(method
.getBytes("US-ASCII"));
579 md5
.update(digestUri
.getBytes("US-ASCII"));
580 if ("auth-int".equals(qop
))
582 byte[] hEntity
= null; // TODO hash of entity body
586 byte[] ha2
= md5
.digest();
587 String ha2Hex
= toHexString(ha2
);
589 // Calculate response
591 md5
.update(ha1Hex
.getBytes("US-ASCII"));
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();
599 md5
.update(nc
.getBytes("US-ASCII"));
603 md5
.update(qop
.getBytes("US-ASCII"));
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
);
618 catch (NoSuchAlgorithmException e
)
623 // Scheme not recognised
627 Properties
parseAuthParams(String text
)
629 int len
= text
.length();
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
);
641 else if (c
== '=' && key
== null)
643 key
= buf
.toString().trim();
646 else if (c
== ' ' && !inQuote
)
648 String value
= unquote(buf
.toString().trim());
653 else if (c
!= ',' || (i
<(len
- 1) && text
.charAt(i
+ 1) != ' '))
660 String value
= unquote(buf
.toString().trim());
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);
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
--)
690 return buf
.toString();
694 * Client nonce value.
699 * Generates a new client nonce value.
701 byte[] generateNonce()
702 throws IOException
, NoSuchAlgorithmException
706 long time
= System
.currentTimeMillis();
707 MessageDigest md5
= MessageDigest
.getInstance("MD5");
708 md5
.update(Long
.toString(time
).getBytes("US-ASCII"));
709 nonce
= md5
.digest();
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
];
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)
742 String comment
= null;
743 String domain
= connection
.getHostName();
744 String path
= this.path
;
745 int lsi
= path
.lastIndexOf('/');
748 path
= path
.substring(0, lsi
);
750 boolean secure
= false;
753 int len
= text
.length();
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
);
766 if (c
== '=' && attr
== null)
768 attr
= buf
.toString().trim();
771 else if (c
== ';' || i
== len
|| c
== ',')
773 String val
= unquote(buf
.toString().trim());
779 else if ("Comment".equalsIgnoreCase(attr
))
783 else if ("Domain".equalsIgnoreCase(attr
))
787 else if ("Path".equalsIgnoreCase(attr
))
791 else if ("Secure".equalsIgnoreCase(val
))
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
822 if (i
== len
|| c
== ',')
824 Cookie cookie
= new Cookie(name
, value
, comment
, domain
,
825 path
, secure
, expires
);
826 cookieManager
.setCookie(cookie
);
830 // Reset cookie fields
834 domain
= connection
.getHostName();
838 path
= path
.substring(0, lsi
);