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