OSDN Git Service

Normalise whitespace in GNU Classpath.
[pf3gnuchains/gcc-fork.git] / libjava / classpath / gnu / java / net / protocol / http / Request.java
1 /* Request.java --
2    Copyright (C) 2004, 2005, 2006, 2007 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.lang.CPStringBuilder;
42 import gnu.java.net.LineInputStream;
43 import gnu.java.util.Base64;
44
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.OutputStream;
48 import java.net.ProtocolException;
49 import java.security.MessageDigest;
50 import java.security.NoSuchAlgorithmException;
51 import java.text.DateFormat;
52 import java.text.ParseException;
53 import java.util.Calendar;
54 import java.util.Date;
55 import java.util.HashMap;
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<String, ResponseHeaderHandler> 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<String, ResponseHeaderHandler>();
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 (Headers.HeaderElement elt : requestHeaders)
306               {
307                 line = elt.name + HEADER_SEP + elt.value + CRLF;
308                 out.write(line.getBytes(US_ASCII));
309               }
310             out.write(CRLF.getBytes(US_ASCII));
311             // Request body
312             if (requestBodyWriter != null && !expectingContinue)
313               {
314                 byte[] buffer = new byte[4096];
315                 int len;
316                 int count = 0;
317
318                 requestBodyWriter.reset();
319                 do
320                   {
321                     len = requestBodyWriter.write(buffer);
322                     if (len > 0)
323                       {
324                         out.write(buffer, 0, len);
325                       }
326                     count += len;
327                   }
328                 while (len > -1 && count < contentLength);
329               }
330             out.flush();
331             // Get response
332             while(true)
333             {
334               response = readResponse(connection.getInputStream());
335               int sc = response.getCode();
336               if (sc == 401 && authenticator != null)
337                 {
338                   if (authenticate(response, attempts++))
339                     {
340                       retry = true;
341                     }
342                 }
343               else if (sc == 100)
344                 {
345                   if (expectingContinue)
346                     {
347                       requestHeaders.remove("Expect");
348                       setHeader("Content-Length",
349                                 Integer.toString(contentLength));
350                       expectingContinue = false;
351                       retry = true;
352                     }
353                   else
354                     {
355                       // A conforming server can send an unsoliceted
356                       // Continue response but *should* not (RFC 2616
357                       // sec 8.2.3).  Ignore the bogus Continue
358                       // response and get the real response that
359                       // should follow
360                       continue;
361                     }
362                 }
363               break;
364             }
365           }
366         while (retry);
367       }
368     catch (IOException e)
369       {
370         connection.close();
371         throw e;
372       }
373     return response;
374   }
375
376   Response readResponse(InputStream in)
377     throws IOException
378   {
379     String line;
380     int len;
381
382     // Read response status line
383     LineInputStream lis = new LineInputStream(in);
384
385     line = lis.readLine();
386     if (line == null)
387       {
388         throw new ProtocolException("Peer closed connection");
389       }
390     if (!line.startsWith("HTTP/"))
391       {
392         throw new ProtocolException(line);
393       }
394     len = line.length();
395     int start = 5, end = 6;
396     while (line.charAt(end) != '.')
397       {
398         end++;
399       }
400     int majorVersion = Integer.parseInt(line.substring(start, end));
401     start = end + 1;
402     end = start + 1;
403     while (line.charAt(end) != ' ')
404       {
405         end++;
406       }
407     int minorVersion = Integer.parseInt(line.substring(start, end));
408     start = end + 1;
409     end = start + 3;
410     int code = Integer.parseInt(line.substring(start, end));
411     String message = line.substring(end + 1, len - 1);
412     // Read response headers
413     Headers responseHeaders = new Headers();
414     responseHeaders.parse(lis);
415     notifyHeaderHandlers(responseHeaders);
416     InputStream body = null;
417
418     switch (code)
419       {
420       case 100:
421         break;
422       case 204:
423       case 205:
424       case 304:
425         body = createResponseBodyStream(responseHeaders, majorVersion,
426                                         minorVersion, in, false);
427         break;
428       default:
429         body = createResponseBodyStream(responseHeaders, majorVersion,
430                                         minorVersion, in, true);
431       }
432
433     // Construct response
434     Response ret = new Response(majorVersion, minorVersion, code,
435                                 message, responseHeaders, body);
436     return ret;
437   }
438
439   void notifyHeaderHandlers(Headers headers)
440   {
441     for (Headers.HeaderElement entry : headers)
442       {
443         // Handle Set-Cookie
444         if ("Set-Cookie".equalsIgnoreCase(entry.name))
445             handleSetCookie(entry.value);
446
447         ResponseHeaderHandler handler =
448           (ResponseHeaderHandler) responseHeaderHandlers.get(entry.name);
449         if (handler != null)
450             handler.setValue(entry.value);
451       }
452   }
453
454   private InputStream createResponseBodyStream(Headers responseHeaders,
455                                                int majorVersion,
456                                                int minorVersion,
457                                                InputStream in,
458                                                boolean mayHaveBody)
459     throws IOException
460   {
461     long contentLength = -1;
462
463     // Persistent connections are the default in HTTP/1.1
464     boolean doClose = "close".equalsIgnoreCase(getHeader("Connection")) ||
465       "close".equalsIgnoreCase(responseHeaders.getValue("Connection")) ||
466       (connection.majorVersion == 1 && connection.minorVersion == 0) ||
467       (majorVersion == 1 && minorVersion == 0);
468
469     String transferCoding = responseHeaders.getValue("Transfer-Encoding");
470     if ("HEAD".equals(method) || !mayHaveBody)
471       {
472         // Special case no body.
473         in = new LimitedLengthInputStream(in, 0, true, connection, doClose);
474       }
475     else if ("chunked".equalsIgnoreCase(transferCoding))
476       {
477         in = new LimitedLengthInputStream(in, -1, false, connection, doClose);
478
479         in = new ChunkedInputStream(in, responseHeaders);
480       }
481     else
482       {
483         contentLength = responseHeaders.getLongValue("Content-Length");
484
485         if (contentLength < 0)
486           doClose = true;  // No Content-Length, must close.
487
488         in = new LimitedLengthInputStream(in, contentLength,
489                                           contentLength >= 0,
490                                           connection, doClose);
491       }
492     String contentCoding = responseHeaders.getValue("Content-Encoding");
493     if (contentCoding != null && !"identity".equals(contentCoding))
494       {
495         if ("gzip".equals(contentCoding))
496           {
497             in = new GZIPInputStream(in);
498           }
499         else if ("deflate".equals(contentCoding))
500           {
501             in = new InflaterInputStream(in);
502           }
503         else
504           {
505             throw new ProtocolException("Unsupported Content-Encoding: " +
506                                         contentCoding);
507           }
508         // Remove the Content-Encoding header because the content is
509         // no longer compressed.
510         responseHeaders.remove("Content-Encoding");
511       }
512     return in;
513   }
514
515   boolean authenticate(Response response, int attempts)
516     throws IOException
517   {
518     String challenge = response.getHeader("WWW-Authenticate");
519     if (challenge == null)
520       {
521         challenge = response.getHeader("Proxy-Authenticate");
522       }
523     int si = challenge.indexOf(' ');
524     String scheme = (si == -1) ? challenge : challenge.substring(0, si);
525     if ("Basic".equalsIgnoreCase(scheme))
526       {
527         Properties params = parseAuthParams(challenge.substring(si + 1));
528         String realm = params.getProperty("realm");
529         Credentials creds = authenticator.getCredentials(realm, attempts);
530         String userPass = creds.getUsername() + ':' + creds.getPassword();
531         byte[] b_userPass = userPass.getBytes("US-ASCII");
532         byte[] b_encoded = Base64.encode(b_userPass).getBytes("US-ASCII");
533         String authorization =
534           scheme + " " + new String(b_encoded, "US-ASCII");
535         setHeader("Authorization", authorization);
536         return true;
537       }
538     else if ("Digest".equalsIgnoreCase(scheme))
539       {
540         Properties params = parseAuthParams(challenge.substring(si + 1));
541         String realm = params.getProperty("realm");
542         String nonce = params.getProperty("nonce");
543         String qop = params.getProperty("qop");
544         String algorithm = params.getProperty("algorithm");
545         String digestUri = getRequestURI();
546         Credentials creds = authenticator.getCredentials(realm, attempts);
547         String username = creds.getUsername();
548         String password = creds.getPassword();
549         connection.incrementNonce(nonce);
550         try
551           {
552             MessageDigest md5 = MessageDigest.getInstance("MD5");
553             final byte[] COLON = { 0x3a };
554
555             // Calculate H(A1)
556             md5.reset();
557             md5.update(username.getBytes("US-ASCII"));
558             md5.update(COLON);
559             md5.update(realm.getBytes("US-ASCII"));
560             md5.update(COLON);
561             md5.update(password.getBytes("US-ASCII"));
562             byte[] ha1 = md5.digest();
563             if ("md5-sess".equals(algorithm))
564               {
565                 byte[] cnonce = generateNonce();
566                 md5.reset();
567                 md5.update(ha1);
568                 md5.update(COLON);
569                 md5.update(nonce.getBytes("US-ASCII"));
570                 md5.update(COLON);
571                 md5.update(cnonce);
572                 ha1 = md5.digest();
573               }
574             String ha1Hex = toHexString(ha1);
575
576             // Calculate H(A2)
577             md5.reset();
578             md5.update(method.getBytes("US-ASCII"));
579             md5.update(COLON);
580             md5.update(digestUri.getBytes("US-ASCII"));
581             if ("auth-int".equals(qop))
582               {
583                 byte[] hEntity = null; // TODO hash of entity body
584                 md5.update(COLON);
585                 md5.update(hEntity);
586               }
587             byte[] ha2 = md5.digest();
588             String ha2Hex = toHexString(ha2);
589
590             // Calculate response
591             md5.reset();
592             md5.update(ha1Hex.getBytes("US-ASCII"));
593             md5.update(COLON);
594             md5.update(nonce.getBytes("US-ASCII"));
595             if ("auth".equals(qop) || "auth-int".equals(qop))
596               {
597                 String nc = getNonceCount(nonce);
598                 byte[] cnonce = generateNonce();
599                 md5.update(COLON);
600                 md5.update(nc.getBytes("US-ASCII"));
601                 md5.update(COLON);
602                 md5.update(cnonce);
603                 md5.update(COLON);
604                 md5.update(qop.getBytes("US-ASCII"));
605               }
606             md5.update(COLON);
607             md5.update(ha2Hex.getBytes("US-ASCII"));
608             String digestResponse = toHexString(md5.digest());
609
610             String authorization = scheme +
611               " username=\"" + username + "\"" +
612               " realm=\"" + realm + "\"" +
613               " nonce=\"" + nonce + "\"" +
614               " uri=\"" + digestUri + "\"" +
615               " response=\"" + digestResponse + "\"";
616             setHeader("Authorization", authorization);
617             return true;
618           }
619         catch (NoSuchAlgorithmException e)
620           {
621             return false;
622           }
623       }
624     // Scheme not recognised
625     return false;
626   }
627
628   Properties parseAuthParams(String text)
629   {
630     int len = text.length();
631     String key = null;
632     CPStringBuilder buf = new CPStringBuilder();
633     Properties ret = new Properties();
634     boolean inQuote = false;
635     for (int i = 0; i < len; i++)
636       {
637         char c = text.charAt(i);
638         if (c == '"')
639           {
640             inQuote = !inQuote;
641           }
642         else if (c == '=' && key == null)
643           {
644             key = buf.toString().trim();
645             buf.setLength(0);
646           }
647         else if (c == ' ' && !inQuote)
648           {
649             String value = unquote(buf.toString().trim());
650             ret.put(key, value);
651             key = null;
652             buf.setLength(0);
653           }
654         else if (c != ',' || (i <(len - 1) && text.charAt(i + 1) != ' '))
655           {
656             buf.append(c);
657           }
658       }
659     if (key != null)
660       {
661         String value = unquote(buf.toString().trim());
662         ret.put(key, value);
663       }
664     return ret;
665   }
666
667   String unquote(String text)
668   {
669     int len = text.length();
670     if (len > 0 && text.charAt(0) == '"' && text.charAt(len - 1) == '"')
671       {
672         return text.substring(1, len - 1);
673       }
674     return text;
675   }
676
677   /**
678    * Returns the number of times the specified nonce value has been seen.
679    * This always returns an 8-byte 0-padded hexadecimal string.
680    */
681   String getNonceCount(String nonce)
682   {
683     int nc = connection.getNonceCount(nonce);
684     String hex = Integer.toHexString(nc);
685     CPStringBuilder buf = new CPStringBuilder();
686     for (int i = 8 - hex.length(); i > 0; i--)
687       {
688         buf.append('0');
689       }
690     buf.append(hex);
691     return buf.toString();
692   }
693
694   /**
695    * Client nonce value.
696    */
697   byte[] nonce;
698
699   /**
700    * Generates a new client nonce value.
701    */
702   byte[] generateNonce()
703     throws IOException, NoSuchAlgorithmException
704   {
705     if (nonce == null)
706       {
707         long time = System.currentTimeMillis();
708         MessageDigest md5 = MessageDigest.getInstance("MD5");
709         md5.update(Long.toString(time).getBytes("US-ASCII"));
710         nonce = md5.digest();
711       }
712     return nonce;
713   }
714
715   String toHexString(byte[] bytes)
716   {
717     char[] ret = new char[bytes.length * 2];
718     for (int i = 0, j = 0; i < bytes.length; i++)
719       {
720         int c =(int) bytes[i];
721         if (c < 0)
722           {
723             c += 0x100;
724           }
725         ret[j++] = Character.forDigit(c / 0x10, 0x10);
726         ret[j++] = Character.forDigit(c % 0x10, 0x10);
727       }
728     return new String(ret);
729   }
730
731   /**
732    * Parse the specified cookie list and notify the cookie manager.
733    */
734   void handleSetCookie(String text)
735   {
736     CookieManager cookieManager = connection.getCookieManager();
737     if (cookieManager == null)
738       {
739         return;
740       }
741     String name = null;
742     String value = null;
743     String comment = null;
744     String domain = connection.getHostName();
745     String path = this.path;
746     int lsi = path.lastIndexOf('/');
747     if (lsi != -1)
748       {
749         path = path.substring(0, lsi);
750       }
751     boolean secure = false;
752     Date expires = null;
753
754     int len = text.length();
755     String attr = null;
756     CPStringBuilder buf = new CPStringBuilder();
757     boolean inQuote = false;
758     for (int i = 0; i <= len; i++)
759       {
760         char c =(i == len) ? '\u0000' : text.charAt(i);
761         if (c == '"')
762           {
763             inQuote = !inQuote;
764           }
765         else if (!inQuote)
766           {
767             if (c == '=' && attr == null)
768               {
769                 attr = buf.toString().trim();
770                 buf.setLength(0);
771               }
772             else if (c == ';' || i == len || c == ',')
773               {
774                 String val = unquote(buf.toString().trim());
775                 if (name == null)
776                   {
777                     name = attr;
778                     value = val;
779                   }
780                 else if ("Comment".equalsIgnoreCase(attr))
781                   {
782                     comment = val;
783                   }
784                 else if ("Domain".equalsIgnoreCase(attr))
785                   {
786                     domain = val;
787                   }
788                 else if ("Path".equalsIgnoreCase(attr))
789                   {
790                     path = val;
791                   }
792                 else if ("Secure".equalsIgnoreCase(val))
793                   {
794                     secure = true;
795                   }
796                 else if ("Max-Age".equalsIgnoreCase(attr))
797                   {
798                     int delta = Integer.parseInt(val);
799                     Calendar cal = Calendar.getInstance();
800                     cal.setTimeInMillis(System.currentTimeMillis());
801                     cal.add(Calendar.SECOND, delta);
802                     expires = cal.getTime();
803                   }
804                 else if ("Expires".equalsIgnoreCase(attr))
805                   {
806                     DateFormat dateFormat = new HTTPDateFormat();
807                     try
808                       {
809                         expires = dateFormat.parse(val);
810                       }
811                     catch (ParseException e)
812                       {
813                         // if this isn't a valid date, it may be that
814                         // the value was returned unquoted; in that case, we
815                         // want to continue buffering the value
816                         buf.append(c);
817                         continue;
818                       }
819                   }
820                 attr = null;
821                 buf.setLength(0);
822                 // case EOL
823                 if (i == len || c == ',')
824                   {
825                     Cookie cookie = new Cookie(name, value, comment, domain,
826                                                path, secure, expires);
827                     cookieManager.setCookie(cookie);
828                   }
829                 if (c == ',')
830                   {
831                     // Reset cookie fields
832                     name = null;
833                     value = null;
834                     comment = null;
835                     domain = connection.getHostName();
836                     path = this.path;
837                     if (lsi != -1)
838                       {
839                         path = path.substring(0, lsi);
840                       }
841                     secure = false;
842                     expires = null;
843                   }
844               }
845             else
846               {
847                 buf.append(c);
848               }
849           }
850         else
851           {
852             buf.append(c);
853           }
854       }
855   }
856
857 }