a491dcdf5871f7a6a82ea6491f51a9ad9ec7562c
[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.util.*;
8 import org.mozilla.javascript.*;
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 (Log.on) Log.log(this, "  resolveAndCheckIfFirewalled: resolving " + host);
165
166         // if all scripts are trustworthy (local FS), continue
167         if (Main.originAddr == null) return;
168
169         // resolve using DNS
170         try {
171             InetAddress addr = InetAddress.getByName(host);
172             byte[] quadbyte = addr.getAddress();
173             if ((quadbyte[0] == 10 ||
174                  (quadbyte[0] == 192 && quadbyte[1] == 168) ||
175                  (quadbyte[0] == 172 && (quadbyte[1] & 0xF0) == 16)) && !addr.equals(Main.originAddr))
176                 throw new HTTPException("security violation: " + host + " [" + addr.getHostAddress() + "] is in a firewalled netblock");
177             return;
178         } catch (UnknownHostException uhe) { }
179
180         // resolve using xmlrpc.xwt.org
181         if (Platform.detectProxy() == null) throw new HTTPException("could not resolve hostname \"" + host + "\" and no proxy configured");
182         if (Log.on) Log.log(this, "  could not resolve host " + host + "; using xmlrpc.xwt.org to ensure security");
183         try {
184             Object ret = new XMLRPC("http://xmlrpc.xwt.org/RPC2/", "dns.resolve").call(new Object[] { host });
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(Function pacFunc) {
285         if (Log.verbose) Log.log(this, "evaluating PAC script");
286         String pac = null;
287         try {
288             Object obj = pacFunc.call(Context.enter(), Proxy.proxyAutoConfigRootScope, null, new Object[] { url.toString(), url.getHost() });
289             if (Log.verbose) Log.log(this, "  PAC script returned \"" + obj + "\"");
290             pac = obj.toString();
291         } catch (Throwable e) {
292             if (Log.on) Log.log(this, "PAC script threw exception " + e);
293             return null;
294         }
295
296         StringTokenizer st = new StringTokenizer(pac, ";", false);
297         while (st.hasMoreTokens()) {
298             String token = st.nextToken().trim();
299             if (Log.verbose) Log.log(this, "  trying \"" + token + "\"...");
300             try {
301                 Socket ret = null;
302                 if (token.startsWith("DIRECT"))
303                     ret = attemptDirect();
304                 else if (token.startsWith("PROXY"))
305                     ret = attemptHttpProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')),
306                                            Integer.parseInt(token.substring(token.indexOf(':') + 1)));
307                 else if (token.startsWith("SOCKS"))
308                     ret = attemptSocksProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')),
309                                             Integer.parseInt(token.substring(token.indexOf(':') + 1)));
310                 if (ret != null) return ret;
311             } catch (Throwable e) {
312                 if (Log.on) Log.log(this, "attempt at \"" + token + "\" failed due to " + e + "; trying next token");
313             }
314         }
315         if (Log.on) Log.log(this, "all PAC results exhausted");
316         return null;
317     }
318
319
320     // Everything Else ////////////////////////////////////////////////////////////////////////////
321
322     private synchronized void connect() throws IOException {
323         if (sock != null) {
324             if (in == null) in = new BufferedInputStream(sock.getInputStream());
325             return;
326         }
327         // grab the userinfo; gcj doesn't have java.net.URL.getUserInfo()
328         String url = originalUrl;
329         userInfo = url.substring(url.indexOf("://") + 3);
330         userInfo = userInfo.indexOf('/') == -1 ? userInfo : userInfo.substring(0, userInfo.indexOf('/'));
331         if (userInfo.indexOf('@') != -1) {
332             userInfo = userInfo.substring(0, userInfo.indexOf('@'));
333             url = url.substring(0, url.indexOf("://") + 3) + url.substring(url.indexOf('@') + 1);
334         } else {
335             userInfo = null;
336         }
337
338         if (url.startsWith("https:")) {
339             this.url = new URL("http" + url.substring(5));
340             ssl = true;
341         } else if (!url.startsWith("http:")) {
342             throw new MalformedURLException("HTTP only supports http/https urls");
343         } else {
344             this.url = new URL(url);
345         }
346         if (!skipResolveCheck) resolveAndCheckIfFirewalled(this.url.getHost());
347         port = this.url.getPort();
348         path = this.url.getFile();
349         if (port == -1) port = ssl ? 443 : 80;
350         host = this.url.getHost();
351         if (Log.verbose) Log.log(this, "creating HTTP object for connection to " + host + ":" + port);
352
353         Proxy pi = Platform.detectProxy();
354         if (sock == null && pi != null && pi.proxyAutoConfigFunction != null) sock = attemptPAC(pi.proxyAutoConfigFunction);
355         if (sock == null && pi != null && ssl && pi.httpsProxyHost != null) sock = attemptHttpProxy(pi.httpsProxyHost, pi.httpsProxyPort);
356         if (sock == null && pi != null && pi.httpProxyHost != null) sock = attemptHttpProxy(pi.httpProxyHost, pi.httpProxyPort);
357         if (sock == null && pi != null && pi.socksProxyHost != null) sock = attemptSocksProxy(pi.socksProxyHost, pi.socksProxyPort);
358         if (sock == null) sock = attemptDirect();
359         if (sock == null) throw new HTTPException("unable to contact host " + host);
360         if (in == null) in = new BufferedInputStream(sock.getInputStream());
361     }
362
363     public void sendRequest(String contentType, String content) throws IOException {
364
365         PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
366         if (content != null) {
367             pw.print("POST " + path + " HTTP/1.1\r\n");
368             pw.print("Content-Length: " + content.length() + "\r\n");
369             if (contentType != null) pw.print("Content-Type: " + contentType + "\r\n");
370         } else {
371             pw.print("GET " + path + " HTTP/1.1\r\n");
372         }
373         
374         pw.print("User-Agent: XWT\r\n");
375         pw.print("Host: " + (host + (port == 80 ? "" : (":" + port))) + "\r\n");
376
377         if (Proxy.Authorization.authorization != null) pw.print("Proxy-Authorization: " + Proxy.Authorization.authorization2 + "\r\n");
378         if (authCache.get(originalUrl) != null) pw.print("Authorization: " + authCache.get(originalUrl) + "\r\n");
379
380         pw.print(content == null ? "\r\n" : content);
381         pw.print("\r\n");
382         pw.flush();
383     }
384
385     private void doWebAuth(Hashtable h0, String method) throws IOException {
386         if (userInfo == null) throw new HTTPException("web server demanded username/password, but none were supplied");
387         Hashtable h = parseAuthenticationChallenge(h0.get("www-authenticate").toString());
388         
389         if (h.get("AUTHTYPE").equals("Basic")) {
390             if (authCache.get(originalUrl) != null) throw new HTTPException("username/password rejected");
391             authCache.put(originalUrl, "Basic " + new String(Base64.encode(userInfo.getBytes("US-ASCII"))));
392             
393         } else if (h.get("AUTHTYPE").equals("Digest")) {
394             if (authCache.get(originalUrl) != null && !"true".equals(h.get("stale"))) throw new HTTPException("username/password rejected");
395             String path2 = path;
396             if (path2.startsWith("http://") || path2.startsWith("https://")) {
397                 path2 = path2.substring(path2.indexOf("://") + 3);
398                 path2 = path2.substring(path2.indexOf('/'));
399             }
400             String A1 = userInfo.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" + userInfo.substring(userInfo.indexOf(':') + 1);
401             String A2 = method + ":" + path2;
402             authCache.put(originalUrl,
403                           "Digest " +
404                           "username=\"" + userInfo.substring(0, userInfo.indexOf(':')) + "\", " +
405                           "realm=\"" + h.get("realm") + "\", " +
406                           "nonce=\"" + h.get("nonce") + "\", " +
407                           "uri=\"" + path2 + "\", " +
408                           (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + 
409                           "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " +
410                           "algorithm=MD5"
411                           );
412             
413         } else {
414             throw new HTTPException("unknown authentication type: " + h.get("AUTHTYPE"));
415         }
416     }
417
418     private void doProxyAuth(Hashtable h0, String method) throws IOException {
419         if (Log.on) Log.log(this, "Proxy AuthChallenge: " + h0.get("proxy-authenticate"));
420         Hashtable h = parseAuthenticationChallenge(h0.get("proxy-authenticate").toString());
421         String style = h.get("AUTHTYPE").toString();
422         String realm = h.get("realm").toString();
423
424         if (!realm.equals("Digest") || Proxy.Authorization.authorization2 == null || !"true".equals(h.get("stale")))
425             Proxy.Authorization.getPassword(realm, style, sock.getInetAddress().getHostAddress(), Proxy.Authorization.authorization);
426
427         if (style.equals("Basic")) {
428             Proxy.Authorization.authorization2 =
429                 "Basic " + new String(Base64.encode(Proxy.Authorization.authorization.getBytes("US-ASCII")));
430             
431         } else if (style.equals("Digest")) {
432             String A1 = Proxy.Authorization.authorization.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" +
433                 Proxy.Authorization.authorization.substring(Proxy.Authorization.authorization.indexOf(':') + 1);
434             String A2 = method + ":" + path;
435             Proxy.Authorization.authorization2 = 
436                 "Digest " +
437                 "username=\"" + Proxy.Authorization.authorization.substring(0, Proxy.Authorization.authorization.indexOf(':')) + "\", " +
438                 "realm=\"" + h.get("realm") + "\", " +
439                 "nonce=\"" + h.get("nonce") + "\", " +
440                 "uri=\"" + path + "\", " +
441                 (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + 
442                 "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " +
443                 "algorithm=MD5";
444         }            
445     }
446
447
448     // HTTPException ///////////////////////////////////////////////////////////////////////////////////
449
450     static class HTTPException extends IOException {
451         public HTTPException(String s) { super(s); }
452     }
453
454
455     // HTTPInputStream ///////////////////////////////////////////////////////////////////////////////////
456
457     /** An input stream that represents a subset of a longer input stream. Supports HTTP chunking as well */
458     public class HTTPInputStream extends FilterInputStream {
459
460         /** if chunking, the number of bytes remaining in this subset; otherwise the remainder of the chunk */
461         private int length = 0;
462
463         /** this semaphore will be released when the stream is closed */
464         private Semaphore releaseMe = null;
465
466         /** indicates that we have encountered the zero-length terminator chunk */
467         boolean chunkedDone = false;
468
469         /** if we're on the first chunk, we don't pre-read a CRLF */
470         boolean firstChunk = true;
471
472         /** the length of the entire content body; -1 if chunked */
473         private int contentLength = 0;
474         public int getContentLength() { return contentLength; }
475
476         HTTPInputStream(InputStream in, int length, Semaphore releaseMe) {
477             super(in);
478             this.releaseMe = releaseMe;
479             this.contentLength = length;
480             this.length = length == -1 ? 0 : length;
481         }
482
483         private void readChunk() throws IOException {
484             if (chunkedDone) return;
485             if (!firstChunk) super.skip(2); // CRLF
486             firstChunk = false;
487             String chunkLen = "";
488             while(true) {
489                 int i = super.read();
490                 if (i == -1) throw new HTTPException("encountered end of stream while reading chunk length");
491
492                 // FIXME: handle chunking extensions
493                 if (i == '\r') {
494                     super.read();    // LF
495                     break;
496                 } else {
497                     chunkLen += (char)i;
498                 }
499             }
500             length = Integer.parseInt(chunkLen, 16);
501             if (length == 0) chunkedDone = true;
502         }
503
504         public int read(byte[] b, int off, int len) throws IOException {
505             boolean good = false;
506             try {
507                 if (length == 0 && contentLength == -1) readChunk();
508                 if (len > length) len = length;
509                 int ret = super.read(b, off, len);
510                 length -= ret;
511                 good = true;
512                 return ret;
513             } finally {
514                 if (!good) invalid = true;
515             }
516         }
517
518         public void close() throws IOException {
519             if (contentLength == -1) {
520                 while(!chunkedDone) {
521                     if (length != 0) skip(length);
522                     readChunk();
523                 }
524                 skip(2);
525             } else {
526                 skip(length);
527             }
528             if (releaseMe != null) releaseMe.release();
529         }
530     }
531
532
533     // Misc Helpers ///////////////////////////////////////////////////////////////////////////////////
534
535     /** reads a set of HTTP headers off of the input stream, returning null if the stream is already at its end */
536     private Hashtable parseHeaders(InputStream in) throws IOException {
537         Hashtable ret = new Hashtable();
538
539         // we can't use a BufferedReader directly on the input stream, since it will buffer past the end of the headers
540         byte[] buf = new byte[4096];
541         int buflen = 0;
542         while(true) {
543             int read = in.read();
544             if (read == -1 && buflen == 0) return null;
545             if (read == -1) throw new HTTPException("stream closed while reading headers");
546             buf[buflen++] = (byte)read;
547             if (buflen >= 4 && buf[buflen - 4] == '\r' && buf[buflen - 3] == '\n' && buf[buflen - 2] == '\r' && buf[buflen - 1] == '\n') break;
548             if (buflen == buf.length) {
549                 byte[] newbuf = new byte[buf.length * 2];
550                 System.arraycopy(buf, 0, newbuf, 0, buflen);
551                 buf = newbuf;
552             }
553         }
554
555         BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, buflen)));
556         String s = br.readLine();
557         if (!s.startsWith("HTTP/")) throw new HTTPException("Expected reply to start with \"HTTP/\", got: " + s);
558         ret.put("STATUSLINE", s.substring(s.indexOf(' ') + 1));
559         ret.put("HTTP", s.substring(5, s.indexOf(' ')));
560
561         while((s = br.readLine()) != null && s.length() > 0) {
562             String front = s.substring(0, s.indexOf(':')).toLowerCase();
563             String back = s.substring(s.indexOf(':') + 1).trim();
564             // ugly hack: we never replace a Digest-auth with a Basic-auth (proxy + www)
565             if (front.endsWith("-authenticate") && ret.get(front) != null && !back.equals("Digest")) continue;
566             ret.put(front, back);
567         }
568         return ret;
569     }
570
571     private Hashtable parseAuthenticationChallenge(String s) {
572         Hashtable ret = new Hashtable();
573
574         s = s.trim();
575         ret.put("AUTHTYPE", s.substring(0, s.indexOf(' ')));
576         s = s.substring(s.indexOf(' ')).trim();
577
578         while (s.length() > 0) {
579             String val = null;
580             String key = s.substring(0, s.indexOf('='));
581             s = s.substring(s.indexOf('=') + 1);
582             if (s.charAt(0) == '\"') {
583                 s = s.substring(1);
584                 val = s.substring(0, s.indexOf('\"'));
585                 s = s.substring(s.indexOf('\"') + 1);
586             } else {
587                 val = s.indexOf(',') == -1 ? s : s.substring(0, s.indexOf(','));
588                 s = s.indexOf(',') == -1 ? "" : s.substring(s.indexOf(',') + 1);
589             }
590             if (s.length() > 0 && s.charAt(0) == ',') s = s.substring(1);
591             s = s.trim();
592             ret.put(key, val);
593         }
594         return ret;
595     }
596
597     private String H(String s) throws IOException {
598         byte[] b = s.getBytes("US-ASCII");
599         MD5Digest md5 = new MD5Digest();
600         md5.update(b, 0, b.length);
601         byte[] out = new byte[md5.getDigestSize()];
602         md5.doFinal(out, 0);
603         String ret = "";
604         for(int i=0; i<out.length; i++) {
605             ret += "0123456789abcdef".charAt((out[i] & 0xf0) >> 4);
606             ret += "0123456789abcdef".charAt(out[i] & 0x0f);
607         }
608         return ret;
609     }
610
611 }