22a07611e7777ca5bc9a99e6b09e42c9b0855ccf
[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 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             Object ret = new XMLRPC("http://xmlrpc.xwt.org/RPC2/", "dns.resolve").call(new Object[] { host });
183             if (ret == null || !(ret instanceof String)) throw new Exception("    xmlrpc.xwt.org returned non-String: " + ret);
184             resolvedHosts.put(host, ret);
185             return;
186         } catch (Throwable e) {
187             throw new HTTPException("exception while attempting to use xmlrpc.xwt.org to resolve " + host + ": " + e);
188         }
189     }
190
191
192     // Methods to attempt socket creation /////////////////////////////////////////////////////////////////
193
194     /** Attempts a direct connection */
195     public Socket attemptDirect() {
196         try {
197             if (Log.verbose) Log.log(this, "attempting to create unproxied socket to " + host + ":" + port + (ssl ? " [ssl]" : ""));
198             return Platform.getSocket(host, port, ssl, true);
199         } catch (IOException e) {
200             if (Log.on) Log.log(this, "exception in attemptDirect(): " + e);
201             return null;
202         }
203     }
204
205     /** Attempts to use an HTTP proxy, employing the CONNECT method if HTTPS is requested */
206     public Socket attemptHttpProxy(String proxyHost, int proxyPort) {
207         try {
208             if (Log.verbose) Log.log(this, "attempting to create HTTP proxied socket using proxy " + proxyHost + ":" + proxyPort);
209
210             Socket sock = Platform.getSocket(proxyHost, proxyPort, ssl, false);
211             if (!ssl) {
212                 if (!path.startsWith("http://")) path = "http://" + host + ":" + port + path;
213             } else {
214                 PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
215                 BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
216                 pw.print("CONNECT " + host + ":" + port + " HTTP/1.1\r\n\r\n");
217                 pw.flush();
218                 String s = br.readLine();
219                 if (s.charAt(9) != '2') throw new HTTPException("proxy refused CONNECT method: \"" + s + "\"");
220                 while (br.readLine().length() > 0) { };
221                 ((TinySSL)sock).negotiate();
222             }
223             return sock;
224
225         } catch (IOException e) {
226             if (Log.on) Log.log(this, "exception in attemptHttpProxy(): " + e);
227             return null;
228         }
229     }
230
231     /**
232      *  Implements SOCKSv4 with v4a DNS extension
233      *  @see http://www.socks.nec.com/protocol/socks4.protocol
234      *  @see http://www.socks.nec.com/protocol/socks4a.protocol
235      */
236     public Socket attemptSocksProxy(String proxyHost, int proxyPort) {
237
238         // even if host is already a "x.y.z.w" string, we use this to parse it into bytes
239         InetAddress addr = null;
240         try { addr = InetAddress.getByName(host); } catch (Exception e) { }
241
242         if (Log.verbose) Log.log(this, "attempting to create SOCKSv4" + (addr == null ? "" : "a") +
243                                  " proxied socket using proxy " + proxyHost + ":" + proxyPort);
244
245         try {
246             Socket sock = Platform.getSocket(proxyHost, proxyPort, ssl, false);
247             
248             DataOutputStream dos = new DataOutputStream(sock.getOutputStream());
249             dos.writeByte(0x04);                         // SOCKSv4(a)
250             dos.writeByte(0x01);                         // CONNECT
251             dos.writeShort(port & 0xffff);               // port
252             if (addr == null) dos.writeInt(0x00000001);  // bogus IP
253             else dos.write(addr.getAddress());           // actual IP
254             dos.writeByte(0x00);                         // no userid
255             if (addr == null) {
256                 PrintWriter pw = new PrintWriter(new OutputStreamWriter(dos));
257                 pw.print(host);
258                 pw.flush();
259                 dos.writeByte(0x00);                     // hostname null terminator
260             }
261             dos.flush();
262
263             DataInputStream dis = new DataInputStream(sock.getInputStream());
264             dis.readByte();                              // reply version
265             byte success = dis.readByte();               // success/fail
266             dis.skip(6);                                 // ip/port
267             
268             if ((int)(success & 0xff) == 90) {
269                 if (ssl) ((TinySSL)sock).negotiate();
270                 return sock;
271             }
272             if (Log.on) Log.log(this, "SOCKS server denied access, code " + (success & 0xff));
273             return null;
274
275         } catch (IOException e) {
276             if (Log.on) Log.log(this, "exception in attemptSocksProxy(): " + e);
277             return null;
278         }
279     }
280
281     /** executes the PAC script and dispatches a call to one of the other attempt methods based on the result */
282     public Socket attemptPAC(Function pacFunc) {
283         if (Log.verbose) Log.log(this, "evaluating PAC script");
284         String pac = null;
285         try {
286             Object obj = pacFunc.call(Context.enter(), Proxy.proxyAutoConfigRootScope, null, new Object[] { url.toString(), url.getHost() });
287             if (Log.verbose) Log.log(this, "  PAC script returned \"" + obj + "\"");
288             pac = obj.toString();
289         } catch (Throwable e) {
290             if (Log.on) Log.log(this, "PAC script threw exception " + e);
291             return null;
292         }
293
294         StringTokenizer st = new StringTokenizer(pac, ";", false);
295         while (st.hasMoreTokens()) {
296             String token = st.nextToken().trim();
297             if (Log.verbose) Log.log(this, "  trying \"" + token + "\"...");
298             try {
299                 Socket ret = null;
300                 if (token.startsWith("DIRECT"))
301                     ret = attemptDirect();
302                 else if (token.startsWith("PROXY"))
303                     ret = attemptHttpProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')),
304                                            Integer.parseInt(token.substring(token.indexOf(':') + 1)));
305                 else if (token.startsWith("SOCKS"))
306                     ret = attemptSocksProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')),
307                                             Integer.parseInt(token.substring(token.indexOf(':') + 1)));
308                 if (ret != null) return ret;
309             } catch (Throwable e) {
310                 if (Log.on) Log.log(this, "attempt at \"" + token + "\" failed due to " + e + "; trying next token");
311             }
312         }
313         if (Log.on) Log.log(this, "all PAC results exhausted");
314         return null;
315     }
316
317
318     // Everything Else ////////////////////////////////////////////////////////////////////////////
319
320     private synchronized void connect() throws IOException {
321         if (sock != null) {
322             if (in == null) in = new BufferedInputStream(sock.getInputStream());
323             return;
324         }
325         // grab the userinfo; gcj doesn't have java.net.URL.getUserInfo()
326         String url = originalUrl;
327         userInfo = url.substring(url.indexOf("://") + 3);
328         userInfo = userInfo.indexOf('/') == -1 ? userInfo : userInfo.substring(0, userInfo.indexOf('/'));
329         if (userInfo.indexOf('@') != -1) {
330             userInfo = userInfo.substring(0, userInfo.indexOf('@'));
331             url = url.substring(0, url.indexOf("://") + 3) + url.substring(url.indexOf('@') + 1);
332         } else {
333             userInfo = null;
334         }
335
336         if (url.startsWith("https:")) {
337             this.url = new URL("http" + url.substring(5));
338             ssl = true;
339         } else if (!url.startsWith("http:")) {
340             throw new MalformedURLException("HTTP only supports http/https urls");
341         } else {
342             this.url = new URL(url);
343         }
344         if (!skipResolveCheck) resolveAndCheckIfFirewalled(this.url.getHost());
345         port = this.url.getPort();
346         path = this.url.getFile();
347         if (port == -1) port = ssl ? 443 : 80;
348         host = this.url.getHost();
349         if (Log.verbose) Log.log(this, "creating HTTP object for connection to " + host + ":" + port);
350
351         Proxy pi = Platform.detectProxy();
352         if (sock == null && pi != null && pi.proxyAutoConfigFunction != null) sock = attemptPAC(pi.proxyAutoConfigFunction);
353         if (sock == null && pi != null && ssl && pi.httpsProxyHost != null) sock = attemptHttpProxy(pi.httpsProxyHost, pi.httpsProxyPort);
354         if (sock == null && pi != null && pi.httpProxyHost != null) sock = attemptHttpProxy(pi.httpProxyHost, pi.httpProxyPort);
355         if (sock == null && pi != null && pi.socksProxyHost != null) sock = attemptSocksProxy(pi.socksProxyHost, pi.socksProxyPort);
356         if (sock == null) sock = attemptDirect();
357         if (sock == null) throw new HTTPException("unable to contact host " + host);
358         if (in == null) in = new BufferedInputStream(sock.getInputStream());
359     }
360
361     public void sendRequest(String contentType, String content) throws IOException {
362
363         PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
364         if (content != null) {
365             pw.print("POST " + path + " HTTP/1.1\r\n");
366         int contentLength = content.substring(0, 2).equals("\r\n") ?
367         content.length() - 2 :
368         (content.length() - content.indexOf("\r\n\r\n") - 4);
369             pw.print("Content-Length: " + contentLength + "\r\n");
370             if (contentType != null) pw.print("Content-Type: " + contentType + "\r\n");
371         } else {
372             pw.print("GET " + path + " HTTP/1.1\r\n");
373         }
374         
375         pw.print("User-Agent: XWT\r\n");
376         pw.print("Host: " + (host + (port == 80 ? "" : (":" + port))) + "\r\n");
377
378         if (Proxy.Authorization.authorization != null) pw.print("Proxy-Authorization: " + Proxy.Authorization.authorization2 + "\r\n");
379         if (authCache.get(originalUrl) != null) pw.print("Authorization: " + authCache.get(originalUrl) + "\r\n");
380
381         pw.print(content == null ? "\r\n" : content);
382         pw.print("\r\n");
383         pw.flush();
384     }
385
386     private void doWebAuth(Hashtable h0, String method) throws IOException {
387         if (userInfo == null) throw new HTTPException("web server demanded username/password, but none were supplied");
388         Hashtable h = parseAuthenticationChallenge(h0.get("www-authenticate").toString());
389         
390         if (h.get("AUTHTYPE").equals("Basic")) {
391             if (authCache.get(originalUrl) != null) throw new HTTPException("username/password rejected");
392             authCache.put(originalUrl, "Basic " + new String(Base64.encode(userInfo.getBytes("US-ASCII"))));
393             
394         } else if (h.get("AUTHTYPE").equals("Digest")) {
395             if (authCache.get(originalUrl) != null && !"true".equals(h.get("stale"))) throw new HTTPException("username/password rejected");
396             String path2 = path;
397             if (path2.startsWith("http://") || path2.startsWith("https://")) {
398                 path2 = path2.substring(path2.indexOf("://") + 3);
399                 path2 = path2.substring(path2.indexOf('/'));
400             }
401             String A1 = userInfo.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" + userInfo.substring(userInfo.indexOf(':') + 1);
402             String A2 = method + ":" + path2;
403             authCache.put(originalUrl,
404                           "Digest " +
405                           "username=\"" + userInfo.substring(0, userInfo.indexOf(':')) + "\", " +
406                           "realm=\"" + h.get("realm") + "\", " +
407                           "nonce=\"" + h.get("nonce") + "\", " +
408                           "uri=\"" + path2 + "\", " +
409                           (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + 
410                           "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " +
411                           "algorithm=MD5"
412                           );
413             
414         } else {
415             throw new HTTPException("unknown authentication type: " + h.get("AUTHTYPE"));
416         }
417     }
418
419     private void doProxyAuth(Hashtable h0, String method) throws IOException {
420         if (Log.on) Log.log(this, "Proxy AuthChallenge: " + h0.get("proxy-authenticate"));
421         Hashtable h = parseAuthenticationChallenge(h0.get("proxy-authenticate").toString());
422         String style = h.get("AUTHTYPE").toString();
423         String realm = h.get("realm").toString();
424
425         if (!realm.equals("Digest") || Proxy.Authorization.authorization2 == null || !"true".equals(h.get("stale")))
426             Proxy.Authorization.getPassword(realm, style, sock.getInetAddress().getHostAddress(), Proxy.Authorization.authorization);
427
428         if (style.equals("Basic")) {
429             Proxy.Authorization.authorization2 =
430                 "Basic " + new String(Base64.encode(Proxy.Authorization.authorization.getBytes("US-ASCII")));
431             
432         } else if (style.equals("Digest")) {
433             String A1 = Proxy.Authorization.authorization.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" +
434                 Proxy.Authorization.authorization.substring(Proxy.Authorization.authorization.indexOf(':') + 1);
435             String A2 = method + ":" + path;
436             Proxy.Authorization.authorization2 = 
437                 "Digest " +
438                 "username=\"" + Proxy.Authorization.authorization.substring(0, Proxy.Authorization.authorization.indexOf(':')) + "\", " +
439                 "realm=\"" + h.get("realm") + "\", " +
440                 "nonce=\"" + h.get("nonce") + "\", " +
441                 "uri=\"" + path + "\", " +
442                 (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + 
443                 "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " +
444                 "algorithm=MD5";
445         }            
446     }
447
448
449     // HTTPException ///////////////////////////////////////////////////////////////////////////////////
450
451     static class HTTPException extends IOException {
452         public HTTPException(String s) { super(s); }
453     }
454
455
456     // HTTPInputStream ///////////////////////////////////////////////////////////////////////////////////
457
458     /** An input stream that represents a subset of a longer input stream. Supports HTTP chunking as well */
459     public class HTTPInputStream extends FilterInputStream {
460
461         /** if chunking, the number of bytes remaining in this subset; otherwise the remainder of the chunk */
462         private int length = 0;
463
464         /** this semaphore will be released when the stream is closed */
465         private Semaphore releaseMe = null;
466
467         /** indicates that we have encountered the zero-length terminator chunk */
468         boolean chunkedDone = false;
469
470         /** if we're on the first chunk, we don't pre-read a CRLF */
471         boolean firstChunk = true;
472
473         /** the length of the entire content body; -1 if chunked */
474         private int contentLength = 0;
475         public int getContentLength() { return contentLength; }
476
477         HTTPInputStream(InputStream in, int length, Semaphore releaseMe) {
478             super(in);
479             this.releaseMe = releaseMe;
480             this.contentLength = length;
481             this.length = length == -1 ? 0 : length;
482         }
483
484         private void readChunk() throws IOException {
485             if (chunkedDone) return;
486             if (!firstChunk) super.skip(2); // CRLF
487             firstChunk = false;
488             String chunkLen = "";
489             while(true) {
490                 int i = super.read();
491                 if (i == -1) throw new HTTPException("encountered end of stream while reading chunk length");
492
493                 // FIXME: handle chunking extensions
494                 if (i == '\r') {
495                     super.read();    // LF
496                     break;
497                 } else {
498                     chunkLen += (char)i;
499                 }
500             }
501             length = Integer.parseInt(chunkLen, 16);
502             if (length == 0) chunkedDone = true;
503         }
504
505         public int read(byte[] b, int off, int len) throws IOException {
506             boolean good = false;
507             try {
508                 if (length == 0 && contentLength == -1) readChunk();
509                 if (len > length) len = length;
510                 int ret = super.read(b, off, len);
511                 length -= ret;
512                 good = true;
513                 return ret;
514             } finally {
515                 if (!good) invalid = true;
516             }
517         }
518
519         public void close() throws IOException {
520             if (contentLength == -1) {
521                 while(!chunkedDone) {
522                     if (length != 0) skip(length);
523                     readChunk();
524                 }
525                 skip(2);
526             } else {
527                 if (length != 0) skip(length);
528             }
529             if (releaseMe != null) releaseMe.release();
530         }
531     }
532
533
534     // Misc Helpers ///////////////////////////////////////////////////////////////////////////////////
535
536     /** reads a set of HTTP headers off of the input stream, returning null if the stream is already at its end */
537     private Hashtable parseHeaders(InputStream in) throws IOException {
538         Hashtable ret = new Hashtable();
539
540         // we can't use a BufferedReader directly on the input stream, since it will buffer past the end of the headers
541         byte[] buf = new byte[4096];
542         int buflen = 0;
543         while(true) {
544             int read = in.read();
545             if (read == -1 && buflen == 0) return null;
546             if (read == -1) throw new HTTPException("stream closed while reading headers");
547             buf[buflen++] = (byte)read;
548             if (buflen >= 4 && buf[buflen - 4] == '\r' && buf[buflen - 3] == '\n' && buf[buflen - 2] == '\r' && buf[buflen - 1] == '\n') break;
549             if (buflen == buf.length) {
550                 byte[] newbuf = new byte[buf.length * 2];
551                 System.arraycopy(buf, 0, newbuf, 0, buflen);
552                 buf = newbuf;
553             }
554         }
555
556         BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, buflen)));
557         String s = br.readLine();
558         if (!s.startsWith("HTTP/")) throw new HTTPException("Expected reply to start with \"HTTP/\", got: " + s);
559         ret.put("STATUSLINE", s.substring(s.indexOf(' ') + 1));
560         ret.put("HTTP", s.substring(5, s.indexOf(' ')));
561
562         while((s = br.readLine()) != null && s.length() > 0) {
563             String front = s.substring(0, s.indexOf(':')).toLowerCase();
564             String back = s.substring(s.indexOf(':') + 1).trim();
565             // ugly hack: we never replace a Digest-auth with a Basic-auth (proxy + www)
566             if (front.endsWith("-authenticate") && ret.get(front) != null && !back.equals("Digest")) continue;
567             ret.put(front, back);
568         }
569         return ret;
570     }
571
572     private Hashtable parseAuthenticationChallenge(String s) {
573         Hashtable ret = new Hashtable();
574
575         s = s.trim();
576         ret.put("AUTHTYPE", s.substring(0, s.indexOf(' ')));
577         s = s.substring(s.indexOf(' ')).trim();
578
579         while (s.length() > 0) {
580             String val = null;
581             String key = s.substring(0, s.indexOf('='));
582             s = s.substring(s.indexOf('=') + 1);
583             if (s.charAt(0) == '\"') {
584                 s = s.substring(1);
585                 val = s.substring(0, s.indexOf('\"'));
586                 s = s.substring(s.indexOf('\"') + 1);
587             } else {
588                 val = s.indexOf(',') == -1 ? s : s.substring(0, s.indexOf(','));
589                 s = s.indexOf(',') == -1 ? "" : s.substring(s.indexOf(',') + 1);
590             }
591             if (s.length() > 0 && s.charAt(0) == ',') s = s.substring(1);
592             s = s.trim();
593             ret.put(key, val);
594         }
595         return ret;
596     }
597
598     private String H(String s) throws IOException {
599         byte[] b = s.getBytes("US-ASCII");
600         MD5Digest md5 = new MD5Digest();
601         md5.update(b, 0, b.length);
602         byte[] out = new byte[md5.getDigestSize()];
603         md5.doFinal(out, 0);
604         String ret = "";
605         for(int i=0; i<out.length; i++) {
606             ret += "0123456789abcdef".charAt((out[i] & 0xf0) >> 4);
607             ret += "0123456789abcdef".charAt(out[i] & 0x0f);
608         }
609         return ret;
610     }
611
612 }