2003/06/13 05:25:52
[org.ibex.core.git] / src / org / xwt / HTTP.java
1 // Copyright 2002 Adam Megacz, see the COPYING file for licensing [GPL]
2 package org.xwt;
3
4 import java.net.*;
5 import java.io.*;
6 import java.util.*;
7 import org.xwt.js.*;
8 import org.xwt.util.*;
9 import org.bouncycastle.util.encoders.Base64;
10 import org.bouncycastle.crypto.digests.*;
11
12 /**
13  *  This object encapsulates a *single* HTTP connection. Multiple requests may be pipelined over a connection (thread-safe),
14  *  although any IOException encountered in a request will invalidate all later requests.
15  */
16 public class HTTP {
17
18     /** the URL as passed to the original constructor; this is never changed */
19     final String originalUrl;
20
21     /** the URL to connect to; this is munged when the url is parsed */
22     URL url = null;
23
24     /** the host to connect to */
25     String host = null;
26
27     /** the port to connect on */
28     int port = -1;
29
30     /** true if SSL (HTTPS) should be used */
31     boolean ssl = false;
32
33     /** the path (URI) to retrieve on the server */
34     String path = null;
35
36     /** the socket */
37     Socket sock = null;
38
39     /** the socket's inputstream */
40     InputStream in = null;
41
42     /** the username and password portions of the URL */
43     String userInfo = null;
44
45     /** cache of userInfo strings, keyed on originalUrl */
46     private static Hashtable authCache = new Hashtable();
47
48     /** this is null if the current request is the first request on
49      *  this HTTP connection; otherwise it is a Semaphore which will be
50      *  released once the request ahead of us has recieved its response
51      */
52     Semaphore okToRecieve = null;
53
54     /** cache for resolveAndCheckIfFirewalled() */
55     static Hashtable resolvedHosts = new Hashtable();
56
57     /** if any request encounters an IOException, the entire HTTP connection is invalidated */
58     boolean invalid = false;
59
60     /** true iff we are allowed to skip the resolve check (only allowed when we're downloading the PAC script) */
61     boolean skipResolveCheck = false;
62
63
64     // Public Methods ////////////////////////////////////////////////////////////////////////////////////////
65
66     public HTTP(String url) { this(url, false); }
67     public HTTP(String url, boolean skipResolveCheck) {
68         originalUrl = url;
69         this.skipResolveCheck = skipResolveCheck;
70     }
71
72     /** Performs an HTTP GET request */
73     public HTTPInputStream GET() throws IOException { return makeRequest(null, null); }
74
75     /** Performs an HTTP POST request; content is appended to the headers (so it should include a blank line to delimit the beginning of the body) */
76     public HTTPInputStream POST(String contentType, String content) throws IOException { return makeRequest(contentType, content); }
77
78     /**
79      *  This method isn't synchronized; however, only one thread can be in the inner synchronized block at a time, and the rest of
80      *  the method is protected by in-order one-at-a-time semaphore lock-steps
81      */
82     private HTTPInputStream makeRequest(String contentType, String content) throws IOException {
83
84         // Step 1: send the request and establish a semaphore to stop any requests that pipeline after us
85         Semaphore blockOn = null;
86         Semaphore releaseMe = null;
87         synchronized(this) {
88             if (invalid) throw new HTTPException("connection failed on a previous pipelined call");
89             try {
90                 connect();
91                 sendRequest(contentType, content);
92             } catch (IOException e) {
93                 invalid = true;
94                 throw e;
95             }
96             blockOn = okToRecieve;
97             releaseMe = okToRecieve = new Semaphore();
98         }
99         
100         // Step 2: wait for requests ahead of us to complete, then read the reply off the stream
101         boolean doRelease = true;
102         try {
103             if (blockOn != null) blockOn.block();
104             if (invalid) throw new HTTPException("connection failed on a previous pipelined call");
105             
106             Hashtable h = in == null ? null : parseHeaders(in);
107             if (h == null) {
108                 // sometimes the server chooses to close the stream between requests
109                 in = null; sock = null;
110                 releaseMe.release();
111                 return makeRequest(contentType, content);
112             }
113
114             String reply = h.get("STATUSLINE").toString();
115             
116             if (reply.startsWith("407") || reply.startsWith("401")) {
117                 
118                 if (reply.startsWith("407")) doProxyAuth(h, content == null ? "GET" : "POST");
119                 else doWebAuth(h, content == null ? "GET" : "POST");
120                 
121                 if (h.get("HTTP").equals("1.0") && h.get("content-length") == null) {
122                     if (Log.on) Log.log(this, "proxy returned an HTTP/1.0 reply with no content-length...");
123                     in = null; sock = null;
124                 } else {
125                     int cl = h.get("content-length") == null ? -1 : Integer.parseInt(h.get("content-length").toString());
126                     new HTTPInputStream(in, cl, releaseMe).close();
127                 }
128                 releaseMe.release();
129                 return makeRequest(contentType, content);
130                 
131             } else if (reply.startsWith("2")) {
132                 if (h.get("HTTP").equals("1.0") && h.get("content-length") == null)
133                     throw new HTTPException("XWT does not support HTTP/1.0 servers which fail to return the Content-Length header");
134                 int cl = h.get("content-length") == null ? -1 : Integer.parseInt(h.get("content-length").toString());
135                 HTTPInputStream ret = new HTTPInputStream(in, cl, releaseMe);
136                 doRelease = false;
137                 return ret;
138                 
139             } else {
140                 throw new HTTPException("HTTP Error: " + reply);
141                 
142             }
143             
144         } catch (IOException e) { invalid = true; throw e;
145         } finally { if (doRelease) releaseMe.release();
146         }
147     }
148
149
150     // Safeguarded DNS Resolver ///////////////////////////////////////////////////////////////////////////
151
152     /**
153      *  resolves the hostname and returns it as a string in the form "x.y.z.w", except for the special case "xmlrpc.xwt.org".
154      *  @throws HTTPException if the host falls within a firewalled netblock
155      */
156     private void resolveAndCheckIfFirewalled(String host) throws HTTPException {
157
158         // special case
159         if (host.equals("xmlrpc.xwt.org")) return;
160
161         // cached
162         if (resolvedHosts.get(host) != null) return;
163
164         // if all scripts are trustworthy (local FS), continue
165         if (Main.originAddr == null) return;
166
167         // resolve using DNS
168         try {
169             InetAddress addr = InetAddress.getByName(host);
170             byte[] quadbyte = addr.getAddress();
171             if ((quadbyte[0] == 10 ||
172                  (quadbyte[0] == 192 && quadbyte[1] == 168) ||
173                  (quadbyte[0] == 172 && (quadbyte[1] & 0xF0) == 16)) && !addr.equals(Main.originAddr))
174                 throw new HTTPException("security violation: " + host + " [" + addr.getHostAddress() + "] is in a firewalled netblock");
175             return;
176         } catch (UnknownHostException uhe) { }
177
178         // resolve using xmlrpc.xwt.org
179         if (Platform.detectProxy() == null) throw new HTTPException("could not resolve hostname \"" + host + "\" and no proxy configured");
180         if (Log.on) Log.log(this, "  could not resolve host " + host + "; using xmlrpc.xwt.org to ensure security");
181         try {
182             Array args = new Array();
183             args.addElement(host);
184             Object ret = new XMLRPC("http://xmlrpc.xwt.org/RPC2/", "dns.resolve").call(args);
185             if (ret == null || !(ret instanceof String)) throw new Exception("    xmlrpc.xwt.org returned non-String: " + ret);
186             resolvedHosts.put(host, ret);
187             return;
188         } catch (Throwable e) {
189             throw new HTTPException("exception while attempting to use xmlrpc.xwt.org to resolve " + host + ": " + e);
190         }
191     }
192
193
194     // Methods to attempt socket creation /////////////////////////////////////////////////////////////////
195
196     /** Attempts a direct connection */
197     public Socket attemptDirect() {
198         try {
199             if (Log.verbose) Log.log(this, "attempting to create unproxied socket to " + host + ":" + port + (ssl ? " [ssl]" : ""));
200             return Platform.getSocket(host, port, ssl, true);
201         } catch (IOException e) {
202             if (Log.on) Log.log(this, "exception in attemptDirect(): " + e);
203             return null;
204         }
205     }
206
207     /** Attempts to use an HTTP proxy, employing the CONNECT method if HTTPS is requested */
208     public Socket attemptHttpProxy(String proxyHost, int proxyPort) {
209         try {
210             if (Log.verbose) Log.log(this, "attempting to create HTTP proxied socket using proxy " + proxyHost + ":" + proxyPort);
211
212             Socket sock = Platform.getSocket(proxyHost, proxyPort, ssl, false);
213             if (!ssl) {
214                 if (!path.startsWith("http://")) path = "http://" + host + ":" + port + path;
215             } else {
216                 PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
217                 BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
218                 pw.print("CONNECT " + host + ":" + port + " HTTP/1.1\r\n\r\n");
219                 pw.flush();
220                 String s = br.readLine();
221                 if (s.charAt(9) != '2') throw new HTTPException("proxy refused CONNECT method: \"" + s + "\"");
222                 while (br.readLine().length() > 0) { };
223                 ((TinySSL)sock).negotiate();
224             }
225             return sock;
226
227         } catch (IOException e) {
228             if (Log.on) Log.log(this, "exception in attemptHttpProxy(): " + e);
229             return null;
230         }
231     }
232
233     /**
234      *  Implements SOCKSv4 with v4a DNS extension
235      *  @see http://www.socks.nec.com/protocol/socks4.protocol
236      *  @see http://www.socks.nec.com/protocol/socks4a.protocol
237      */
238     public Socket attemptSocksProxy(String proxyHost, int proxyPort) {
239
240         // even if host is already a "x.y.z.w" string, we use this to parse it into bytes
241         InetAddress addr = null;
242         try { addr = InetAddress.getByName(host); } catch (Exception e) { }
243
244         if (Log.verbose) Log.log(this, "attempting to create SOCKSv4" + (addr == null ? "" : "a") +
245                                  " proxied socket using proxy " + proxyHost + ":" + proxyPort);
246
247         try {
248             Socket sock = Platform.getSocket(proxyHost, proxyPort, ssl, false);
249             
250             DataOutputStream dos = new DataOutputStream(sock.getOutputStream());
251             dos.writeByte(0x04);                         // SOCKSv4(a)
252             dos.writeByte(0x01);                         // CONNECT
253             dos.writeShort(port & 0xffff);               // port
254             if (addr == null) dos.writeInt(0x00000001);  // bogus IP
255             else dos.write(addr.getAddress());           // actual IP
256             dos.writeByte(0x00);                         // no userid
257             if (addr == null) {
258                 PrintWriter pw = new PrintWriter(new OutputStreamWriter(dos));
259                 pw.print(host);
260                 pw.flush();
261                 dos.writeByte(0x00);                     // hostname null terminator
262             }
263             dos.flush();
264
265             DataInputStream dis = new DataInputStream(sock.getInputStream());
266             dis.readByte();                              // reply version
267             byte success = dis.readByte();               // success/fail
268             dis.skip(6);                                 // ip/port
269             
270             if ((int)(success & 0xff) == 90) {
271                 if (ssl) ((TinySSL)sock).negotiate();
272                 return sock;
273             }
274             if (Log.on) Log.log(this, "SOCKS server denied access, code " + (success & 0xff));
275             return null;
276
277         } catch (IOException e) {
278             if (Log.on) Log.log(this, "exception in attemptSocksProxy(): " + e);
279             return null;
280         }
281     }
282
283     /** executes the PAC script and dispatches a call to one of the other attempt methods based on the result */
284     public Socket attemptPAC(org.xwt.js.JS.Callable pacFunc) {
285         if (Log.verbose) Log.log(this, "evaluating PAC script");
286         String pac = null;
287         try {
288             org.xwt.js.Array args = new org.xwt.js.Array();
289             args.addElement(url.toString());
290             args.addElement(url.getHost());
291             Object obj = pacFunc.call(args);
292             if (Log.verbose) Log.log(this, "  PAC script returned \"" + obj + "\"");
293             pac = obj.toString();
294         } catch (Throwable e) {
295             if (Log.on) Log.log(this, "PAC script threw exception " + e);
296             return null;
297         }
298
299         StringTokenizer st = new StringTokenizer(pac, ";", false);
300         while (st.hasMoreTokens()) {
301             String token = st.nextToken().trim();
302             if (Log.verbose) Log.log(this, "  trying \"" + token + "\"...");
303             try {
304                 Socket ret = null;
305                 if (token.startsWith("DIRECT"))
306                     ret = attemptDirect();
307                 else if (token.startsWith("PROXY"))
308                     ret = attemptHttpProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')),
309                                            Integer.parseInt(token.substring(token.indexOf(':') + 1)));
310                 else if (token.startsWith("SOCKS"))
311                     ret = attemptSocksProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')),
312                                             Integer.parseInt(token.substring(token.indexOf(':') + 1)));
313                 if (ret != null) return ret;
314             } catch (Throwable e) {
315                 if (Log.on) Log.log(this, "attempt at \"" + token + "\" failed due to " + e + "; trying next token");
316             }
317         }
318         if (Log.on) Log.log(this, "all PAC results exhausted");
319         return null;
320     }
321
322
323     // Everything Else ////////////////////////////////////////////////////////////////////////////
324
325     private synchronized void connect() throws IOException {
326         if (sock != null) {
327             if (in == null) in = new BufferedInputStream(sock.getInputStream());
328             return;
329         }
330         // grab the userinfo; gcj doesn't have java.net.URL.getUserInfo()
331         String url = originalUrl;
332         userInfo = url.substring(url.indexOf("://") + 3);
333         userInfo = userInfo.indexOf('/') == -1 ? userInfo : userInfo.substring(0, userInfo.indexOf('/'));
334         if (userInfo.indexOf('@') != -1) {
335             userInfo = userInfo.substring(0, userInfo.indexOf('@'));
336             url = url.substring(0, url.indexOf("://") + 3) + url.substring(url.indexOf('@') + 1);
337         } else {
338             userInfo = null;
339         }
340
341         if (url.startsWith("https:")) {
342             this.url = new URL("http" + url.substring(5));
343             ssl = true;
344         } else if (!url.startsWith("http:")) {
345             throw new MalformedURLException("HTTP only supports http/https urls");
346         } else {
347             this.url = new URL(url);
348         }
349         if (!skipResolveCheck) resolveAndCheckIfFirewalled(this.url.getHost());
350         port = this.url.getPort();
351         path = this.url.getFile();
352         if (port == -1) port = ssl ? 443 : 80;
353         host = this.url.getHost();
354         if (Log.verbose) Log.log(this, "creating HTTP object for connection to " + host + ":" + port);
355
356         Proxy pi = Platform.detectProxy();
357         if (sock == null && pi != null && pi.proxyAutoConfigFunction != null) sock = attemptPAC(pi.proxyAutoConfigFunction);
358         if (sock == null && pi != null && ssl && pi.httpsProxyHost != null) sock = attemptHttpProxy(pi.httpsProxyHost, pi.httpsProxyPort);
359         if (sock == null && pi != null && pi.httpProxyHost != null) sock = attemptHttpProxy(pi.httpProxyHost, pi.httpProxyPort);
360         if (sock == null && pi != null && pi.socksProxyHost != null) sock = attemptSocksProxy(pi.socksProxyHost, pi.socksProxyPort);
361         if (sock == null) sock = attemptDirect();
362         if (sock == null) throw new HTTPException("unable to contact host " + host);
363         if (in == null) in = new BufferedInputStream(sock.getInputStream());
364     }
365
366     public void sendRequest(String contentType, String content) throws IOException {
367
368         PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
369         if (content != null) {
370             pw.print("POST " + path + " HTTP/1.1\r\n");
371         int contentLength = content.substring(0, 2).equals("\r\n") ?
372         content.length() - 2 :
373         (content.length() - content.indexOf("\r\n\r\n") - 4);
374             pw.print("Content-Length: " + contentLength + "\r\n");
375             if (contentType != null) pw.print("Content-Type: " + contentType + "\r\n");
376         } else {
377             pw.print("GET " + path + " HTTP/1.1\r\n");
378         }
379         
380         pw.print("User-Agent: XWT\r\n");
381         pw.print("Host: " + (host + (port == 80 ? "" : (":" + port))) + "\r\n");
382
383         if (Proxy.Authorization.authorization != null) pw.print("Proxy-Authorization: " + Proxy.Authorization.authorization2 + "\r\n");
384         if (authCache.get(originalUrl) != null) pw.print("Authorization: " + authCache.get(originalUrl) + "\r\n");
385
386         pw.print(content == null ? "\r\n" : content);
387         pw.print("\r\n");
388         pw.flush();
389     }
390
391     private void doWebAuth(Hashtable h0, String method) throws IOException {
392         if (userInfo == null) throw new HTTPException("web server demanded username/password, but none were supplied");
393         Hashtable h = parseAuthenticationChallenge(h0.get("www-authenticate").toString());
394         
395         if (h.get("AUTHTYPE").equals("Basic")) {
396             if (authCache.get(originalUrl) != null) throw new HTTPException("username/password rejected");
397             authCache.put(originalUrl, "Basic " + new String(Base64.encode(userInfo.getBytes("US-ASCII"))));
398             
399         } else if (h.get("AUTHTYPE").equals("Digest")) {
400             if (authCache.get(originalUrl) != null && !"true".equals(h.get("stale"))) throw new HTTPException("username/password rejected");
401             String path2 = path;
402             if (path2.startsWith("http://") || path2.startsWith("https://")) {
403                 path2 = path2.substring(path2.indexOf("://") + 3);
404                 path2 = path2.substring(path2.indexOf('/'));
405             }
406             String A1 = userInfo.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" + userInfo.substring(userInfo.indexOf(':') + 1);
407             String A2 = method + ":" + path2;
408             authCache.put(originalUrl,
409                           "Digest " +
410                           "username=\"" + userInfo.substring(0, userInfo.indexOf(':')) + "\", " +
411                           "realm=\"" + h.get("realm") + "\", " +
412                           "nonce=\"" + h.get("nonce") + "\", " +
413                           "uri=\"" + path2 + "\", " +
414                           (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + 
415                           "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " +
416                           "algorithm=MD5"
417                           );
418             
419         } else {
420             throw new HTTPException("unknown authentication type: " + h.get("AUTHTYPE"));
421         }
422     }
423
424     private void doProxyAuth(Hashtable h0, String method) throws IOException {
425         if (Log.on) Log.log(this, "Proxy AuthChallenge: " + h0.get("proxy-authenticate"));
426         Hashtable h = parseAuthenticationChallenge(h0.get("proxy-authenticate").toString());
427         String style = h.get("AUTHTYPE").toString();
428         String realm = h.get("realm").toString();
429
430         if (!realm.equals("Digest") || Proxy.Authorization.authorization2 == null || !"true".equals(h.get("stale")))
431             Proxy.Authorization.getPassword(realm, style, sock.getInetAddress().getHostAddress(), Proxy.Authorization.authorization);
432
433         if (style.equals("Basic")) {
434             Proxy.Authorization.authorization2 =
435                 "Basic " + new String(Base64.encode(Proxy.Authorization.authorization.getBytes("US-ASCII")));
436             
437         } else if (style.equals("Digest")) {
438             String A1 = Proxy.Authorization.authorization.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" +
439                 Proxy.Authorization.authorization.substring(Proxy.Authorization.authorization.indexOf(':') + 1);
440             String A2 = method + ":" + path;
441             Proxy.Authorization.authorization2 = 
442                 "Digest " +
443                 "username=\"" + Proxy.Authorization.authorization.substring(0, Proxy.Authorization.authorization.indexOf(':')) + "\", " +
444                 "realm=\"" + h.get("realm") + "\", " +
445                 "nonce=\"" + h.get("nonce") + "\", " +
446                 "uri=\"" + path + "\", " +
447                 (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + 
448                 "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " +
449                 "algorithm=MD5";
450         }            
451     }
452
453
454     // HTTPException ///////////////////////////////////////////////////////////////////////////////////
455
456     static class HTTPException extends IOException { public HTTPException(String s) { super(s); } }
457
458
459     // HTTPInputStream ///////////////////////////////////////////////////////////////////////////////////
460
461     /** An input stream that represents a subset of a longer input stream. Supports HTTP chunking as well */
462     public class HTTPInputStream extends FilterInputStream {
463
464         /** if chunking, the number of bytes remaining in this subset; otherwise the remainder of the chunk */
465         private int length = 0;
466
467         /** this semaphore will be released when the stream is closed */
468         private Semaphore releaseMe = null;
469
470         /** indicates that we have encountered the zero-length terminator chunk */
471         boolean chunkedDone = false;
472
473         /** if we're on the first chunk, we don't pre-read a CRLF */
474         boolean firstChunk = true;
475
476         /** the length of the entire content body; -1 if chunked */
477         private int contentLength = 0;
478         public int getContentLength() { return contentLength; }
479
480         HTTPInputStream(InputStream in, int length, Semaphore releaseMe) {
481             super(in);
482             this.releaseMe = releaseMe;
483             this.contentLength = length;
484             this.length = length == -1 ? 0 : length;
485         }
486
487         public boolean markSupported() { return false; }
488         public int read(byte[] b) throws IOException { return read(b, 0, b.length); }
489         public long skip(long n) throws IOException { return read(null, -1, (int)n); }
490         public int available() throws IOException {
491             if (contentLength == -1) return java.lang.Math.min(super.available(), length);
492             return super.available();
493         }
494
495         public int read() throws IOException {
496             byte[] b = new byte[1];
497             int ret = read(b, 0, 1);
498             return ret == -1 ? -1 : b[0] & 0xff;
499         }
500
501         private void readChunk() throws IOException {
502             if (chunkedDone) return;
503             if (!firstChunk) super.skip(2); // CRLF
504             firstChunk = false;
505             String chunkLen = "";
506             while(true) {
507                 int i = super.read();
508                 if (i == -1) throw new HTTPException("encountered end of stream while reading chunk length");
509
510                 // FIXME: handle chunking extensions
511                 if (i == '\r') {
512                     super.read();    // LF
513                     break;
514                 } else {
515                     chunkLen += (char)i;
516                 }
517             }
518             length = Integer.parseInt(chunkLen, 16);
519             if (length == 0) chunkedDone = true;
520         }
521
522         public int read(byte[] b, int off, int len) throws IOException {
523             boolean good = false;
524             try {
525                 if (length == 0 && contentLength == -1) {
526                     readChunk();
527                     if (chunkedDone) { good = true; return -1; }
528                 } else {
529                     if (length == 0) { good = true; return -1; }
530                 }
531                 if (len > length) len = length;
532                 int ret = b == null ? (int)super.skip(len) : super.read(b, off, len);
533                 if (ret >= 0) {
534                     length -= ret;
535                     good = true;
536                 }
537                 return ret;
538             } finally {
539                 if (!good) invalid = true;
540             }
541         }
542
543         public void close() throws IOException {
544             if (contentLength == -1) {
545                 while(!chunkedDone) {
546                     if (length != 0) skip(length);
547                     readChunk();
548                 }
549                 skip(2);
550             } else {
551                 if (length != 0) skip(length);
552             }
553             if (releaseMe != null) releaseMe.release();
554         }
555     }
556
557
558     // Misc Helpers ///////////////////////////////////////////////////////////////////////////////////
559
560     /** reads a set of HTTP headers off of the input stream, returning null if the stream is already at its end */
561     private Hashtable parseHeaders(InputStream in) throws IOException {
562         Hashtable ret = new Hashtable();
563
564         // we can't use a BufferedReader directly on the input stream, since it will buffer past the end of the headers
565         byte[] buf = new byte[4096];
566         int buflen = 0;
567         while(true) {
568             int read = in.read();
569             if (read == -1 && buflen == 0) return null;
570             if (read == -1) throw new HTTPException("stream closed while reading headers");
571             buf[buflen++] = (byte)read;
572             if (buflen >= 4 && buf[buflen - 4] == '\r' && buf[buflen - 3] == '\n' && buf[buflen - 2] == '\r' && buf[buflen - 1] == '\n') break;
573             if (buflen == buf.length) {
574                 byte[] newbuf = new byte[buf.length * 2];
575                 System.arraycopy(buf, 0, newbuf, 0, buflen);
576                 buf = newbuf;
577             }
578         }
579
580         BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, buflen)));
581         String s = br.readLine();
582         if (!s.startsWith("HTTP/")) throw new HTTPException("Expected reply to start with \"HTTP/\", got: " + s);
583         ret.put("STATUSLINE", s.substring(s.indexOf(' ') + 1));
584         ret.put("HTTP", s.substring(5, s.indexOf(' ')));
585
586         while((s = br.readLine()) != null && s.length() > 0) {
587             String front = s.substring(0, s.indexOf(':')).toLowerCase();
588             String back = s.substring(s.indexOf(':') + 1).trim();
589             // ugly hack: we never replace a Digest-auth with a Basic-auth (proxy + www)
590             if (front.endsWith("-authenticate") && ret.get(front) != null && !back.equals("Digest")) continue;
591             ret.put(front, back);
592         }
593         return ret;
594     }
595
596     private Hashtable parseAuthenticationChallenge(String s) {
597         Hashtable ret = new Hashtable();
598
599         s = s.trim();
600         ret.put("AUTHTYPE", s.substring(0, s.indexOf(' ')));
601         s = s.substring(s.indexOf(' ')).trim();
602
603         while (s.length() > 0) {
604             String val = null;
605             String key = s.substring(0, s.indexOf('='));
606             s = s.substring(s.indexOf('=') + 1);
607             if (s.charAt(0) == '\"') {
608                 s = s.substring(1);
609                 val = s.substring(0, s.indexOf('\"'));
610                 s = s.substring(s.indexOf('\"') + 1);
611             } else {
612                 val = s.indexOf(',') == -1 ? s : s.substring(0, s.indexOf(','));
613                 s = s.indexOf(',') == -1 ? "" : s.substring(s.indexOf(',') + 1);
614             }
615             if (s.length() > 0 && s.charAt(0) == ',') s = s.substring(1);
616             s = s.trim();
617             ret.put(key, val);
618         }
619         return ret;
620     }
621
622     private String H(String s) throws IOException {
623         byte[] b = s.getBytes("US-ASCII");
624         MD5Digest md5 = new MD5Digest();
625         md5.update(b, 0, b.length);
626         byte[] out = new byte[md5.getDigestSize()];
627         md5.doFinal(out, 0);
628         String ret = "";
629         for(int i=0; i<out.length; i++) {
630             ret += "0123456789abcdef".charAt((out[i] & 0xf0) >> 4);
631             ret += "0123456789abcdef".charAt(out[i] & 0x0f);
632         }
633         return ret;
634     }
635
636 }