2 Copyright (C) 2004, 2005 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 * Request body negotiation threshold for 100-continue expectations.
99 protected int requestBodyNegotiationThreshold
;
102 * Map of response header handlers.
104 protected Map responseHeaderHandlers
;
109 protected Authenticator authenticator
;
112 * Whether this request has been dispatched yet.
114 private boolean dispatched
;
117 * Constructor for a new request.
118 * @param connection the connection context
119 * @param method the HTTP method
120 * @param path the resource path including query part
122 protected Request(HTTPConnection connection
, String method
,
125 this.connection
= connection
;
126 this.method
= method
;
128 requestHeaders
= new Headers();
129 responseHeaderHandlers
= new HashMap();
130 requestBodyNegotiationThreshold
= 4096;
134 * Returns the connection associated with this request.
137 public HTTPConnection
getConnection()
143 * Returns the HTTP method to invoke.
146 public String
getMethod()
152 * Returns the resource path.
155 public String
getPath()
161 * Returns the full request-URI represented by this request, as specified
164 public String
getRequestURI()
166 return connection
.getURI() + path
;
170 * Returns the headers in this request.
172 public Headers
getHeaders()
174 return requestHeaders
;
178 * Returns the value of the specified header in this request.
179 * @param name the header name
181 public String
getHeader(String name
)
183 return requestHeaders
.getValue(name
);
187 * Returns the value of the specified header in this request as an integer.
188 * @param name the header name
190 public int getIntHeader(String name
)
192 return requestHeaders
.getIntValue(name
);
196 * Returns the value of the specified header in this request as a date.
197 * @param name the header name
199 public Date
getDateHeader(String name
)
201 return requestHeaders
.getDateValue(name
);
205 * Sets the specified header in this request.
206 * @param name the header name
207 * @param value the header value
209 public void setHeader(String name
, String value
)
211 requestHeaders
.put(name
, value
);
215 * Convenience method to set the entire request body.
216 * @param requestBody the request body content
218 public void setRequestBody(byte[] requestBody
)
220 setRequestBodyWriter(new ByteArrayRequestBodyWriter(requestBody
));
224 * Sets the request body provider.
225 * @param requestBodyWriter the handler used to obtain the request body
227 public void setRequestBodyWriter(RequestBodyWriter requestBodyWriter
)
229 this.requestBodyWriter
= requestBodyWriter
;
233 * Sets a callback handler to be invoked for the specified header name.
234 * @param name the header name
235 * @param handler the handler to receive the value for the header
237 public void setResponseHeaderHandler(String name
,
238 ResponseHeaderHandler handler
)
240 responseHeaderHandlers
.put(name
, handler
);
244 * Sets an authenticator that can be used to handle authentication
246 * @param authenticator the authenticator
248 public void setAuthenticator(Authenticator authenticator
)
250 this.authenticator
= authenticator
;
254 * Sets the request body negotiation threshold.
255 * If this is set, it determines the maximum size that the request body
256 * may be before body negotiation occurs(via the
257 * <code>100-continue</code> expectation). This ensures that a large
258 * request body is not sent when the server wouldn't have accepted it
260 * @param threshold the body negotiation threshold, or <=0 to disable
261 * request body negotation entirely
263 public void setRequestBodyNegotiationThreshold(int threshold
)
265 requestBodyNegotiationThreshold
= threshold
;
269 * Dispatches this request.
270 * A request can only be dispatched once; calling this method a second
271 * time results in a protocol exception.
272 * @exception IOException if an I/O error occurred
273 * @return an HTTP response object representing the result of the operation
275 public Response
dispatch()
280 throw new ProtocolException("request already dispatched");
282 final String CRLF
= "\r\n";
283 final String HEADER_SEP
= ": ";
284 final String US_ASCII
= "US-ASCII";
285 final String version
= connection
.getVersion();
287 int contentLength
= -1;
288 boolean retry
= false;
290 boolean expectingContinue
= false;
291 if (requestBodyWriter
!= null)
293 contentLength
= requestBodyWriter
.getContentLength();
294 if (contentLength
> requestBodyNegotiationThreshold
)
296 expectingContinue
= true;
297 setHeader("Expect", "100-continue");
301 setHeader("Content-Length", Integer
.toString(contentLength
));
307 // Loop while authentication fails or continue
312 // Get socket output and input streams
313 OutputStream out
= connection
.getOutputStream();
316 String requestUri
= path
;
317 if (connection
.isUsingProxy() &&
318 !"*".equals(requestUri
) &&
319 !"CONNECT".equals(method
))
321 requestUri
= getRequestURI();
323 String line
= method
+ ' ' + requestUri
+ ' ' + version
+ CRLF
;
324 out
.write(line
.getBytes(US_ASCII
));
326 for (Iterator i
= requestHeaders
.keySet().iterator();
329 String name
=(String
) i
.next();
330 String value
=(String
) requestHeaders
.get(name
);
331 line
= name
+ HEADER_SEP
+ value
+ CRLF
;
332 out
.write(line
.getBytes(US_ASCII
));
334 out
.write(CRLF
.getBytes(US_ASCII
));
336 if (requestBodyWriter
!= null && !expectingContinue
)
338 byte[] buffer
= new byte[4096];
342 requestBodyWriter
.reset();
345 len
= requestBodyWriter
.write(buffer
);
348 out
.write(buffer
, 0, len
);
352 while (len
> -1 && count
< contentLength
);
358 response
= readResponse(connection
.getInputStream());
359 int sc
= response
.getCode();
360 if (sc
== 401 && authenticator
!= null)
362 if (authenticate(response
, attempts
++))
369 if (expectingContinue
)
371 requestHeaders
.remove("Expect");
372 setHeader("Content-Length",
373 Integer
.toString(contentLength
));
374 expectingContinue
= false;
379 // A conforming server can send an unsoliceted
380 // Continue response but *should* not (RFC 2616
381 // sec 8.2.3). Ignore the bogus Continue
382 // response and get the real response that
392 catch (IOException e
)
400 Response
readResponse(InputStream in
)
406 // Read response status line
407 LineInputStream lis
= new LineInputStream(in
);
409 line
= lis
.readLine();
412 throw new ProtocolException("Peer closed connection");
414 if (!line
.startsWith("HTTP/"))
416 throw new ProtocolException(line
);
419 int start
= 5, end
= 6;
420 while (line
.charAt(end
) != '.')
424 int majorVersion
= Integer
.parseInt(line
.substring(start
, end
));
427 while (line
.charAt(end
) != ' ')
431 int minorVersion
= Integer
.parseInt(line
.substring(start
, end
));
434 int code
= Integer
.parseInt(line
.substring(start
, end
));
435 String message
= line
.substring(end
+ 1, len
- 1);
436 // Read response headers
437 Headers responseHeaders
= new Headers();
438 responseHeaders
.parse(lis
);
439 notifyHeaderHandlers(responseHeaders
);
440 InputStream body
= null;
450 body
= createResponseBodyStream(responseHeaders
, majorVersion
,
454 // Construct response
455 Response ret
= new Response(majorVersion
, minorVersion
, code
,
456 message
, responseHeaders
, body
);
460 void notifyHeaderHandlers(Headers headers
)
462 for (Iterator i
= headers
.entrySet().iterator(); i
.hasNext(); )
464 Map
.Entry entry
= (Map
.Entry
) i
.next();
465 String name
=(String
) entry
.getKey();
467 if ("Set-Cookie".equalsIgnoreCase(name
))
469 String value
= (String
) entry
.getValue();
470 handleSetCookie(value
);
472 ResponseHeaderHandler handler
=
473 (ResponseHeaderHandler
) responseHeaderHandlers
.get(name
);
476 String value
= (String
) entry
.getValue();
477 handler
.setValue(value
);
482 private InputStream
createResponseBodyStream(Headers responseHeaders
,
488 long contentLength
= -1;
489 Headers trailer
= null;
491 // Persistent connections are the default in HTTP/1.1
492 boolean doClose
= "close".equalsIgnoreCase(getHeader("Connection")) ||
493 "close".equalsIgnoreCase(responseHeaders
.getValue("Connection")) ||
494 (connection
.majorVersion
== 1 && connection
.minorVersion
== 0) ||
495 (majorVersion
== 1 && minorVersion
== 0);
497 String transferCoding
= responseHeaders
.getValue("Transfer-Encoding");
498 if ("chunked".equalsIgnoreCase(transferCoding
))
500 in
= new LimitedLengthInputStream(in
, -1, false, connection
, doClose
);
502 in
= new ChunkedInputStream(in
, responseHeaders
);
506 contentLength
= responseHeaders
.getLongValue("Content-Length");
508 if (contentLength
< 0)
509 doClose
= true; // No Content-Length, must close.
511 in
= new LimitedLengthInputStream(in
, contentLength
,
513 connection
, doClose
);
515 String contentCoding
= responseHeaders
.getValue("Content-Encoding");
516 if (contentCoding
!= null && !"identity".equals(contentCoding
))
518 if ("gzip".equals(contentCoding
))
520 in
= new GZIPInputStream(in
);
522 else if ("deflate".equals(contentCoding
))
524 in
= new InflaterInputStream(in
);
528 throw new ProtocolException("Unsupported Content-Encoding: " +
535 boolean authenticate(Response response
, int attempts
)
538 String challenge
= response
.getHeader("WWW-Authenticate");
539 if (challenge
== null)
541 challenge
= response
.getHeader("Proxy-Authenticate");
543 int si
= challenge
.indexOf(' ');
544 String scheme
= (si
== -1) ? challenge
: challenge
.substring(0, si
);
545 if ("Basic".equalsIgnoreCase(scheme
))
547 Properties params
= parseAuthParams(challenge
.substring(si
+ 1));
548 String realm
= params
.getProperty("realm");
549 Credentials creds
= authenticator
.getCredentials(realm
, attempts
);
550 String userPass
= creds
.getUsername() + ':' + creds
.getPassword();
551 byte[] b_userPass
= userPass
.getBytes("US-ASCII");
552 byte[] b_encoded
= BASE64
.encode(b_userPass
);
553 String authorization
=
554 scheme
+ " " + new String(b_encoded
, "US-ASCII");
555 setHeader("Authorization", authorization
);
558 else if ("Digest".equalsIgnoreCase(scheme
))
560 Properties params
= parseAuthParams(challenge
.substring(si
+ 1));
561 String realm
= params
.getProperty("realm");
562 String nonce
= params
.getProperty("nonce");
563 String qop
= params
.getProperty("qop");
564 String algorithm
= params
.getProperty("algorithm");
565 String digestUri
= getRequestURI();
566 Credentials creds
= authenticator
.getCredentials(realm
, attempts
);
567 String username
= creds
.getUsername();
568 String password
= creds
.getPassword();
569 connection
.incrementNonce(nonce
);
572 MessageDigest md5
= MessageDigest
.getInstance("MD5");
573 final byte[] COLON
= { 0x3a };
577 md5
.update(username
.getBytes("US-ASCII"));
579 md5
.update(realm
.getBytes("US-ASCII"));
581 md5
.update(password
.getBytes("US-ASCII"));
582 byte[] ha1
= md5
.digest();
583 if ("md5-sess".equals(algorithm
))
585 byte[] cnonce
= generateNonce();
589 md5
.update(nonce
.getBytes("US-ASCII"));
594 String ha1Hex
= toHexString(ha1
);
598 md5
.update(method
.getBytes("US-ASCII"));
600 md5
.update(digestUri
.getBytes("US-ASCII"));
601 if ("auth-int".equals(qop
))
603 byte[] hEntity
= null; // TODO hash of entity body
607 byte[] ha2
= md5
.digest();
608 String ha2Hex
= toHexString(ha2
);
610 // Calculate response
612 md5
.update(ha1Hex
.getBytes("US-ASCII"));
614 md5
.update(nonce
.getBytes("US-ASCII"));
615 if ("auth".equals(qop
) || "auth-int".equals(qop
))
617 String nc
= getNonceCount(nonce
);
618 byte[] cnonce
= generateNonce();
620 md5
.update(nc
.getBytes("US-ASCII"));
624 md5
.update(qop
.getBytes("US-ASCII"));
627 md5
.update(ha2Hex
.getBytes("US-ASCII"));
628 String digestResponse
= toHexString(md5
.digest());
630 String authorization
= scheme
+
631 " username=\"" + username
+ "\"" +
632 " realm=\"" + realm
+ "\"" +
633 " nonce=\"" + nonce
+ "\"" +
634 " uri=\"" + digestUri
+ "\"" +
635 " response=\"" + digestResponse
+ "\"";
636 setHeader("Authorization", authorization
);
639 catch (NoSuchAlgorithmException e
)
644 // Scheme not recognised
648 Properties
parseAuthParams(String text
)
650 int len
= text
.length();
652 StringBuilder buf
= new StringBuilder();
653 Properties ret
= new Properties();
654 boolean inQuote
= false;
655 for (int i
= 0; i
< len
; i
++)
657 char c
= text
.charAt(i
);
662 else if (c
== '=' && key
== null)
664 key
= buf
.toString().trim();
667 else if (c
== ' ' && !inQuote
)
669 String value
= unquote(buf
.toString().trim());
674 else if (c
!= ',' || (i
<(len
- 1) && text
.charAt(i
+ 1) != ' '))
681 String value
= unquote(buf
.toString().trim());
687 String
unquote(String text
)
689 int len
= text
.length();
690 if (len
> 0 && text
.charAt(0) == '"' && text
.charAt(len
- 1) == '"')
692 return text
.substring(1, len
- 1);
698 * Returns the number of times the specified nonce value has been seen.
699 * This always returns an 8-byte 0-padded hexadecimal string.
701 String
getNonceCount(String nonce
)
703 int nc
= connection
.getNonceCount(nonce
);
704 String hex
= Integer
.toHexString(nc
);
705 StringBuilder buf
= new StringBuilder();
706 for (int i
= 8 - hex
.length(); i
> 0; i
--)
711 return buf
.toString();
715 * Client nonce value.
720 * Generates a new client nonce value.
722 byte[] generateNonce()
723 throws IOException
, NoSuchAlgorithmException
727 long time
= System
.currentTimeMillis();
728 MessageDigest md5
= MessageDigest
.getInstance("MD5");
729 md5
.update(Long
.toString(time
).getBytes("US-ASCII"));
730 nonce
= md5
.digest();
735 String
toHexString(byte[] bytes
)
737 char[] ret
= new char[bytes
.length
* 2];
738 for (int i
= 0, j
= 0; i
< bytes
.length
; i
++)
740 int c
=(int) bytes
[i
];
745 ret
[j
++] = Character
.forDigit(c
/ 0x10, 0x10);
746 ret
[j
++] = Character
.forDigit(c
% 0x10, 0x10);
748 return new String(ret
);
752 * Parse the specified cookie list and notify the cookie manager.
754 void handleSetCookie(String text
)
756 CookieManager cookieManager
= connection
.getCookieManager();
757 if (cookieManager
== null)
763 String comment
= null;
764 String domain
= connection
.getHostName();
765 String path
= this.path
;
766 int lsi
= path
.lastIndexOf('/');
769 path
= path
.substring(0, lsi
);
771 boolean secure
= false;
774 int len
= text
.length();
776 StringBuilder buf
= new StringBuilder();
777 boolean inQuote
= false;
778 for (int i
= 0; i
<= len
; i
++)
780 char c
=(i
== len
) ?
'\u0000' : text
.charAt(i
);
787 if (c
== '=' && attr
== null)
789 attr
= buf
.toString().trim();
792 else if (c
== ';' || i
== len
|| c
== ',')
794 String val
= unquote(buf
.toString().trim());
800 else if ("Comment".equalsIgnoreCase(attr
))
804 else if ("Domain".equalsIgnoreCase(attr
))
808 else if ("Path".equalsIgnoreCase(attr
))
812 else if ("Secure".equalsIgnoreCase(val
))
816 else if ("Max-Age".equalsIgnoreCase(attr
))
818 int delta
= Integer
.parseInt(val
);
819 Calendar cal
= Calendar
.getInstance();
820 cal
.setTimeInMillis(System
.currentTimeMillis());
821 cal
.add(Calendar
.SECOND
, delta
);
822 expires
= cal
.getTime();
824 else if ("Expires".equalsIgnoreCase(attr
))
826 DateFormat dateFormat
= new HTTPDateFormat();
829 expires
= dateFormat
.parse(val
);
831 catch (ParseException e
)
833 // if this isn't a valid date, it may be that
834 // the value was returned unquoted; in that case, we
835 // want to continue buffering the value
843 if (i
== len
|| c
== ',')
845 Cookie cookie
= new Cookie(name
, value
, comment
, domain
,
846 path
, secure
, expires
);
847 cookieManager
.setCookie(cookie
);
851 // Reset cookie fields
855 domain
= connection
.getHostName();
859 path
= path
.substring(0, lsi
);