eliminate use of String-case macro
[org.ibex.net.git] / src / org / ibex / net / HTTP.java
1 // Copyright 2000-2005 the Contributors, as shown in the revision logs.
2 // Licensed under the Apache Public Source License 2.0 ("the License").
3 // You may not use this file except in compliance with the License.
4
5 package org.ibex.net;
6
7 import java.net.*;
8 import java.io.*;
9 import java.util.*;
10 import org.ibex.util.*;
11 import org.ibex.crypto.*;
12
13 /**
14  *  This object encapsulates a *single* HTTP connection. Multiple requests may be pipelined over a connection (thread-safe),
15  *  although any IOException encountered in a request will invalidate all later requests.
16  */
17 public class HTTP {
18
19     public static InetAddress originAddr = null;
20     public static String      originHost = null;
21
22     // FIXME: HACK: these shouldn't be set globally
23     public static String userAgent = "Ibex";
24     public static boolean allowRedirects = true;
25     
26     // Cookies //////////////////////////////////////////////////////////////////////////////
27
28     public static class Cookie {
29         public final String  name;
30         public final String  value;
31         public final String  domain;
32         public final String  path;
33         public final Date    expires;
34         public final boolean secure;
35         public Cookie(String name, String value, String domain, String path, Date expires, boolean secure) {
36             this.name = name;
37             this.value = value;
38             this.domain = domain;
39             this.path = path;
40             this.expires = expires;
41             this.secure = secure;
42         }
43
44         // FIXME: this could be much more efficient
45         // FIXME currently only implements http://wp.netscape.com/newsref/std/cookie_spec.html
46         public static class Jar {
47             private Hash h = new Hash();
48             public String getCookieHeader(String domain, String path, boolean secure) {
49                 StringBuffer ret = new StringBuffer("Cookie: ");
50                 Enumeration e = h.enumerateKeys();
51                 while (e.hasMoreElements()) {
52                     Vec v = (Vec)h.get(e.nextElement());
53                     Cookie cookie = null;
54                     for(int i=0; i<v.size(); i++) {
55                         Cookie c = (Cookie)v.elementAt(i);
56                         if (domain.endsWith(c.domain) &&
57                             (c.path == null || path.startsWith(c.path)) &&
58                             (cookie == null || c.domain.length() > cookie.domain.length()))
59                             cookie = c;
60                     }
61                     if (cookie != null) {
62                         ret.append(cookie.name);
63                         ret.append("=");
64                         ret.append(cookie.value);
65                         ret.append("; ");
66                     }
67                 }
68                 //ret.setLength(ret.length() - 2);
69                 return ret.toString();
70             }
71             public void setCookie(String header, String defaultDomain) {
72                 String  name       = null;
73                 String  value      = null;
74                 String  domain     = defaultDomain;
75                 String  path       = "/";
76                 Date    expires    = null;
77                 boolean secure     = false;
78                 StringTokenizer st = new StringTokenizer(header, ";");
79                 while(st.hasMoreTokens()) {
80                     String s = st.nextToken();
81                     if (s.indexOf('=') == -1) {
82                         if (s.equals("secure")) secure = true;
83                         continue;
84                     }
85                     String start = s.substring(0, s.indexOf('='));
86                     String end   = s.substring(s.indexOf('=')+1);
87                     if (name == null) {
88                         name = start;
89                         value = end;
90                         continue;
91                     }
92                     if      (start.toLowerCase().equals("domain")) domain = end;
93                     else if (start.toLowerCase().equals("path")) path = end;
94                     else if (start.toLowerCase().equals("expires")) expires = new Date(end);
95                 }
96                 if (h.get(name) == null) h.put(name, new Vec());
97                 ((Vec)h.get(name)).addElement(new Cookie(name, value, domain, path, expires, secure));
98             }
99         }
100     }
101
102     // Public Methods ////////////////////////////////////////////////////////////////////////////////////////
103
104     public HTTP(String url) { this(url, false); }
105     public HTTP(String url, boolean skipResolveCheck) { this(url, skipResolveCheck, null); }
106     public HTTP(String url, Proxy p) { this(url, false, p); }
107     public HTTP(String url, boolean skipResolveCheck, Proxy p) { originalUrl = url; this.skipResolveCheck = skipResolveCheck; proxy = p; }
108
109     /** Performs an HTTP GET request */
110     public InputStream GET(String referer, Cookie.Jar cookies) throws IOException {
111         return makeRequest(null, null, referer, cookies); }
112     
113     /** Performs an HTTP POST request; content is additional headers, blank line, and body */
114     public InputStream POST(String contentType, String content, String referer, Cookie.Jar cookies) throws IOException {
115         return makeRequest(contentType, content, referer, cookies); }
116
117     public static class HTTPException extends IOException { public HTTPException(String s) { super(s); } }
118
119     public static HTTP stdio = new HTTP("stdio:");
120
121
122     // Statics ///////////////////////////////////////////////////////////////////////////////////////////////
123
124     static Hash resolvedHosts = new Hash();            ///< cache for resolveAndCheckIfFirewalled()
125     private static Hash authCache = new Hash();        ///< cache of userInfo strings, keyed on originalUrl
126
127
128     // Instance Data ///////////////////////////////////////////////////////////////////////////////////////////////
129
130     final String originalUrl;              ///< the URL as passed to the original constructor; this is never changed
131     String url = null;                     ///< the URL to connect to; this is munged when the url is parsed */
132     String host = null;                    ///< the host to connect to
133     int port = -1;                         ///< the port to connect on
134     boolean ssl = false;                   ///< true if SSL (HTTPS) should be used
135     String path = null;                    ///< the path (URI) to retrieve on the server
136     Socket sock = null;                    ///< the socket
137     InputStream in = null;                 ///< the socket's inputstream
138     String userInfo = null;                ///< the username and password portions of the URL
139     boolean firstRequest = true;           ///< true iff this is the first request to be made on this socket
140     boolean skipResolveCheck = false;      ///< allowed to skip the resolve check when downloading PAC script
141     boolean proxied = false;               ///< true iff we're using a proxy
142     Proxy proxy;
143
144     /** this is null if the current request is the first request on
145      *  this HTTP connection; otherwise it is a Semaphore which will be
146      *  released once the request ahead of us has recieved its response
147      */
148     Semaphore okToRecieve = null;
149
150     /**
151      *  This method isn't synchronized; however, only one thread can be in the inner synchronized block at a time, and the rest of
152      *  the method is protected by in-order one-at-a-time semaphore lock-steps
153      */
154     private InputStream makeRequest(String contentType, String content, String referer, Cookie.Jar cookies) throws IOException {
155
156         // Step 1: send the request and establish a semaphore to stop any requests that pipeline after us
157         Semaphore blockOn = null;
158         Semaphore releaseMe = null;
159         synchronized(this) {
160             try {
161                 connect();
162                 sendRequest(contentType, content, referer, cookies);
163             } catch (IOException e) {
164                 reset();
165                 throw e;
166             }
167             blockOn = okToRecieve;
168             releaseMe = okToRecieve = new Semaphore();
169         }
170         
171         // Step 2: wait for requests ahead of us to complete, then read the reply off the stream
172         boolean doRelease = true;
173         try {
174             if (blockOn != null) blockOn.block();
175             
176             // previous call wrecked the socket connection, but we already sent our request, so we can't just retry --
177             // this could cause the server to receive the request twice, which could be bad (think of the case where the
178             // server call causes Amazon.com to ship you an item with one-click purchasing).
179             if (in == null)
180                 throw new HTTPException("a previous pipelined call messed up the socket");
181             
182             Hashtable h = in == null ? null : parseHeaders(in, cookies);
183             if (h == null) {
184                 if (firstRequest) throw new HTTPException("server closed the socket with no response");
185                 // sometimes the server chooses to close the stream between requests
186                 reset();
187                 releaseMe.release();
188                 return makeRequest(contentType, content, referer, cookies);
189             }
190
191             String reply = h.get("STATUSLINE").toString();
192             
193             if (reply.startsWith("407") || reply.startsWith("401")) {
194                 
195                 if (reply.startsWith("407")) doProxyAuth(h, content == null ? "GET" : "POST");
196                 else doWebAuth(h, content == null ? "GET" : "POST");
197                 
198                 if (h.get("HTTP").equals("1.0") && h.get("content-length") == null) {
199                     if (Log.on) Log.info(this, "proxy returned an HTTP/1.0 reply with no content-length...");
200                     reset();
201                 } else {
202                     int cl = h.get("content-length") == null ? -1 : Integer.parseInt(h.get("content-length").toString());
203                     new HTTPInputStream(in, cl, releaseMe).close();
204                 }
205                 releaseMe.release();
206                 return makeRequest(contentType, content, referer, cookies);
207                 
208             } else if (reply.startsWith("3") && allowRedirects) {
209                 String location = (String)h.get("location");
210                 if (location == null) throw new HTTPException("Got HTTP " + reply.substring(0, 3) + " but no Location header");
211                 Log.info(HTTP.class, "redirecting to " + location);
212                 if (content != null)
213                     return new HTTP(location).POST(contentType, content, url, cookies);
214                 else
215                     return new HTTP(location).GET(url, cookies);
216                 
217             } else if (reply.startsWith("2")) {
218                 if (h.get("HTTP").equals("1.0") && h.get("content-length") == null)
219                     throw new HTTPException("Ibex does not support HTTP/1.0 servers which fail to return the Content-Length header");
220                 int cl = h.get("content-length") == null ? -1 : Integer.parseInt(h.get("content-length").toString());
221                 InputStream ret = new HTTPInputStream(in, cl, releaseMe);
222                 if ("gzip".equals(h.get("content-encoding"))) ret = new java.util.zip.GZIPInputStream(ret);
223                 doRelease = false;
224                 return ret;
225                 
226             } else {
227                 throw new HTTPException("HTTP Error: " + reply);
228                 
229             }
230             
231         } catch (IOException e) { reset(); throw e;
232         } finally { if (doRelease) releaseMe.release();
233         }
234     }
235
236
237     // Safeguarded DNS Resolver ///////////////////////////////////////////////////////////////////////////
238
239     /**
240      *  resolves the hostname and returns it as a string in the form "x.y.z.w"
241      *  @throws HTTPException if the host falls within a firewalled netblock
242      */
243     private void resolveAndCheckIfFirewalled(String host) throws HTTPException {
244
245         // cached
246         if (resolvedHosts.get(host) != null) return;
247
248         // if all scripts are trustworthy (local FS), continue
249         if (originAddr == null) return;
250
251         // resolve using DNS
252         try {
253             InetAddress addr = InetAddress.getByName(host);
254             byte[] quadbyte = addr.getAddress();
255             if ((quadbyte[0] == 10 ||
256                  (quadbyte[0] == 192 && quadbyte[1] == 168) ||
257                  (quadbyte[0] == 172 && (quadbyte[1] & 0xF0) == 16)) && !addr.equals(originAddr))
258                 throw new HTTPException("security violation: " + host + " [" + addr.getHostAddress() +
259                                         "] is in a firewalled netblock");
260             return;
261         } catch (UnknownHostException uhe) { }
262
263         /*
264           if (Platform.detectProxy() == null)
265           throw new HTTPException("could not resolve hostname \"" + host + "\" and no proxy configured");
266         */
267     }
268
269
270     // Methods to attempt socket creation /////////////////////////////////////////////////////////////////
271
272     private Socket getSocket(String host, int port, boolean ssl, boolean negotiate) throws IOException {
273         Socket ret = ssl ? new SSL(host, port, negotiate) : new Socket(java.net.InetAddress.getByName(host), port);
274         ret.setTcpNoDelay(true);
275         return ret;
276     }
277
278     /** Attempts a direct connection */
279     Socket attemptDirect() {
280         try {
281             Log.info(this, "attempting to create unproxied socket to " +
282                      host + ":" + port + (ssl ? " [ssl]" : ""));
283             return getSocket(host, port, ssl, true);
284         } catch (IOException e) {
285             if (Log.on) Log.info(this, "exception in attemptDirect(): " + e);
286             return null;
287         }
288     }
289
290   
291
292     // Everything Else ////////////////////////////////////////////////////////////////////////////
293
294     private synchronized void connect() throws IOException {
295         if (originalUrl.equals("stdio:")) { in = new BufferedInputStream(System.in); return; }
296         if (sock != null) {
297             if (in == null) in = new BufferedInputStream(sock.getInputStream());
298             return;
299         }
300         // grab the userinfo; gcj doesn't have java.net.URL.getUserInfo()
301         String url = originalUrl;
302         userInfo = url.substring(url.indexOf("://") + 3);
303         userInfo = userInfo.indexOf('/') == -1 ? userInfo : userInfo.substring(0, userInfo.indexOf('/'));
304         if (userInfo.indexOf('@') != -1) {
305             userInfo = userInfo.substring(0, userInfo.indexOf('@'));
306             url = url.substring(0, url.indexOf("://") + 3) + url.substring(url.indexOf('@') + 1);
307         } else {
308             userInfo = null;
309         }
310
311         if (url.startsWith("https:")) {
312             ssl = true;
313         } else if (!url.startsWith("http:")) {
314             throw new IOException("HTTP only supports http/https urls");
315         }
316         if (url.indexOf("://") == -1) throw new IOException("URLs must contain a ://");
317         String temphost = url.substring(url.indexOf("://") + 3);
318         path = temphost.substring(temphost.indexOf('/'));
319         temphost = temphost.substring(0, temphost.indexOf('/'));
320         if (temphost.indexOf(':') != -1) {
321             port = Integer.parseInt(temphost.substring(temphost.indexOf(':')+1));
322             temphost = temphost.substring(0, temphost.indexOf(':'));
323         } else {
324             port = ssl ? 443 : 80;
325         }
326         if (!skipResolveCheck) resolveAndCheckIfFirewalled(temphost);
327         host = temphost;
328         if (Log.verbose) Log.info(this, "creating HTTP object for connection to " + host + ":" + port);
329
330         proxied = proxy!=null;
331         if (proxied) sock = proxy.attempt(url, host, this);
332         if (sock==null) {
333             proxied = false;
334             sock = attemptDirect();
335         }
336         if (sock == null) throw new HTTPException("unable to contact host " + host);
337         if (in == null) in = new BufferedInputStream(sock.getInputStream());
338     }
339
340     private void sendRequest(String contentType, String content, String referer, Cookie.Jar cookies) throws IOException {
341         PrintWriter pw = new PrintWriter(new OutputStreamWriter(originalUrl.equals("stdio:") ?
342                                                                 System.out : sock.getOutputStream()));
343         if (content != null) {
344             pw.print("POST " + path + " HTTP/1.0\r\n"); // FIXME chunked encoding
345             int contentLength = content.substring(0, 2).equals("\r\n") ?
346                 content.length() - 2 :
347                 (content.length() - content.indexOf("\r\n\r\n") - 4);
348             pw.print("Content-Length: " + contentLength + "\r\n");
349             if (contentType != null) pw.print("Content-Type: " + contentType + "\r\n");
350         } else {
351             pw.print("GET " + path + " HTTP/1.1\r\n");
352         }
353
354         if (cookies != null) pw.print(cookies.getCookieHeader(host, path, ssl));
355         pw.print("User-Agent: " + userAgent + "\r\n");
356         pw.print("Accept-encoding: gzip\r\n");
357         pw.print("Host: " + (host + (port == 80 ? "" : (":" + port))) + "\r\n");
358         if (proxied) pw.print("X-RequestOrigin: " + originHost + "\r\n");
359
360         if (Proxy.Authorization.authorization != null) pw.print("Proxy-Authorization: "+Proxy.Authorization.authorization2+"\r\n");
361         if (authCache.get(originalUrl) != null) pw.print("Authorization: " + authCache.get(originalUrl) + "\r\n");
362
363         pw.print(content == null ? "\r\n" : content);
364         pw.print("\r\n");
365         pw.flush();
366     }
367
368     private void doWebAuth(Hashtable h0, String method) throws IOException {
369         if (userInfo == null) throw new HTTPException("web server demanded username/password, but none were supplied");
370         Hashtable h = parseAuthenticationChallenge(h0.get("www-authenticate").toString());
371         
372         if (h.get("AUTHTYPE").equals("Basic")) {
373             if (authCache.get(originalUrl) != null) throw new HTTPException("username/password rejected");
374             authCache.put(originalUrl, "Basic " + new String(Encode.toBase64(userInfo.getBytes("UTF8"))));
375             
376         } else if (h.get("AUTHTYPE").equals("Digest")) {
377             if (authCache.get(originalUrl) != null && !"true".equals(h.get("stale")))
378                 throw new HTTPException("username/password rejected");
379             String path2 = path;
380             if (path2.startsWith("http://") || path2.startsWith("https://")) {
381                 path2 = path2.substring(path2.indexOf("://") + 3);
382                 path2 = path2.substring(path2.indexOf('/'));
383             }
384             String A1 = userInfo.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" +
385                 userInfo.substring(userInfo.indexOf(':') + 1);
386             String A2 = method + ":" + path2;
387             authCache.put(originalUrl,
388                           "Digest " +
389                           "username=\"" + userInfo.substring(0, userInfo.indexOf(':')) + "\", " +
390                           "realm=\"" + h.get("realm") + "\", " +
391                           "nonce=\"" + h.get("nonce") + "\", " +
392                           "uri=\"" + path2 + "\", " +
393                           (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + 
394                           "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " +
395                           "algorithm=MD5"
396                           );
397             
398         } else {
399             throw new HTTPException("unknown authentication type: " + h.get("AUTHTYPE"));
400         }
401     }
402
403     private void doProxyAuth(Hashtable h0, String method) throws IOException {
404         if (Log.on) Log.info(this, "Proxy AuthChallenge: " + h0.get("proxy-authenticate"));
405         Hashtable h = parseAuthenticationChallenge(h0.get("proxy-authenticate").toString());
406         String style = h.get("AUTHTYPE").toString();
407         String realm = (String)h.get("realm");
408
409         if (style.equals("NTLM") && Proxy.Authorization.authorization2 == null) {
410             Log.info(this, "Proxy identified itself as NTLM, sending Type 1 packet");
411             Proxy.Authorization.authorization2 = "NTLM " + Encode.toBase64(Proxy.NTLM.type1);
412             return;
413         }
414         /*
415           if (!realm.equals("Digest") || Proxy.Authorization.authorization2 == null || !"true".equals(h.get("stale")))
416           Proxy.Authorization.getPassword(realm, style, sock.getInetAddress().getHostAddress(),
417           Proxy.Authorization.authorization);
418         */
419         if (style.equals("Basic")) {
420             Proxy.Authorization.authorization2 =
421                 "Basic " + new String(Encode.toBase64(Proxy.Authorization.authorization.getBytes("UTF8")));
422             
423         } else if (style.equals("Digest")) {
424             String A1 = Proxy.Authorization.authorization.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" +
425                 Proxy.Authorization.authorization.substring(Proxy.Authorization.authorization.indexOf(':') + 1);
426             String A2 = method + ":" + path;
427             Proxy.Authorization.authorization2 = 
428                 "Digest " +
429                 "username=\"" + Proxy.Authorization.authorization.substring(0, Proxy.Authorization.authorization.indexOf(':')) +
430                 "\", " +
431                 "realm=\"" + h.get("realm") + "\", " +
432                 "nonce=\"" + h.get("nonce") + "\", " +
433                 "uri=\"" + path + "\", " +
434                 (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + 
435                 "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " +
436                 "algorithm=MD5";
437
438         } else if (style.equals("NTLM")) {
439             Log.info(this, "Proxy identified itself as NTLM, got Type 2 packet");
440             byte[] type2 = Encode.fromBase64(((String)h0.get("proxy-authenticate")).substring(5).trim());
441             for(int i=0; i<type2.length; i += 4) {
442                 String log = "";
443                 if (i<type2.length) log += Integer.toString(type2[i] & 0xff, 16) + " ";
444                 if (i+1<type2.length) log += Integer.toString(type2[i+1] & 0xff, 16) + " ";
445                 if (i+2<type2.length) log += Integer.toString(type2[i+2] & 0xff, 16) + " ";
446                 if (i+3<type2.length) log += Integer.toString(type2[i+3] & 0xff, 16) + " ";
447                 Log.info(this, log);
448             }
449             // FEATURE: need to keep the connection open between type1 and type3
450             // FEATURE: finish this
451             //byte[] type3 = Proxy.NTLM.getResponse(
452             //Proxy.Authorization.authorization2 = "NTLM " + Encode.toBase64(type3));
453         }            
454     }
455
456
457     // HTTPInputStream ///////////////////////////////////////////////////////////////////////////////////
458
459     /** An input stream that represents a subset of a longer input stream. Supports HTTP chunking as well */
460     public class HTTPInputStream extends FilterInputStream implements KnownLength {
461
462         private int length = 0;              ///< if chunking, numbytes left in this subset; else the remainder of the chunk
463         private Semaphore releaseMe = null;  ///< this semaphore will be released when the stream is closed
464         boolean chunkedDone = false;         ///< indicates that we have encountered the zero-length terminator chunk
465         boolean firstChunk = true;           ///< if we're on the first chunk, we don't pre-read a CRLF
466         private int contentLength = 0;       ///< the length of the entire content body; -1 if chunked
467
468         HTTPInputStream(InputStream in, int length, Semaphore releaseMe) throws IOException {
469             super(in);
470             this.releaseMe = releaseMe;
471             this.contentLength = length;
472             this.length = length == -1 ? 0 : length;
473         }
474
475         public int getLength() { return contentLength; }
476         public boolean markSupported() { return false; }
477         public int read(byte[] b) throws IOException { return read(b, 0, b.length); }
478         public long skip(long n) throws IOException { return read(null, -1, (int)n); }
479         public int available() throws IOException {
480             if (contentLength == -1) return java.lang.Math.min(super.available(), length);
481             return super.available();
482         }
483
484         public int read() throws IOException {
485             byte[] b = new byte[1];
486             int ret = read(b, 0, 1);
487             return ret == -1 ? -1 : b[0] & 0xff;
488         }
489
490         private void readChunk() throws IOException {
491             if (chunkedDone) return;
492             if (!firstChunk) super.skip(2); // CRLF
493             firstChunk = false;
494             String chunkLen = "";
495             while(true) {
496                 int i = super.read();
497                 if (i == -1) throw new HTTPException("encountered end of stream while reading chunk length");
498
499                 // FEATURE: handle chunking extensions
500                 if (i == '\r') {
501                     super.read();    // LF
502                     break;
503                 } else {
504                     chunkLen += (char)i;
505                 }
506             }
507             length = Integer.parseInt(chunkLen.trim(), 16);
508             if (length == 0) chunkedDone = true;
509         }
510
511         public int read(byte[] b, int off, int len) throws IOException {
512             boolean good = false;
513             try {
514                 if (length == 0 && contentLength == -1) {
515                     readChunk();
516                     if (chunkedDone) { good = true; return -1; }
517                 } else {
518                     if (length == 0) { good = true; return -1; }
519                 }
520                 if (len > length) len = length;
521                 int ret = b == null ? (int)super.skip(len) : super.read(b, off, len);
522                 if (ret >= 0) {
523                     length -= ret;
524                     good = true;
525                 }
526                 return ret;
527             } finally {
528                 if (!good) reset();
529             }
530         }
531
532         public void close() throws IOException {
533             if (contentLength == -1) {
534                 while(!chunkedDone) {
535                     if (length != 0) skip(length);
536                     readChunk();
537                 }
538                 skip(2);
539             } else {
540                 if (length != 0) skip(length);
541             }
542             if (releaseMe != null) releaseMe.release();
543         }
544     }
545
546     void reset() {
547         firstRequest = true;
548         in = null;
549         sock = null;
550     }
551
552
553     // Misc Helpers ///////////////////////////////////////////////////////////////////////////////////
554
555     /** reads a set of HTTP headers off of the input stream, returning null if the stream is already at its end */
556     private Hashtable parseHeaders(InputStream in, Cookie.Jar cookies) throws IOException {
557         Hashtable ret = new Hashtable();
558
559         // we can't use a BufferedReader directly on the input stream, since it will buffer past the end of the headers
560         byte[] buf = new byte[4096];
561         int buflen = 0;
562         while(true) {
563             int read = in.read();
564             if (read == -1 && buflen == 0) return null;
565             if (read == -1) throw new HTTPException("stream closed while reading headers");
566             buf[buflen++] = (byte)read;
567             if (buflen >= 4 && buf[buflen - 4] == '\r' && buf[buflen - 3] == '\n' &&
568                 buf[buflen - 2] == '\r' && buf[buflen - 1] == '\n')
569                 break;
570             if (buflen >=2 && buf[buflen - 1] == '\n' && buf[buflen - 2] == '\n')
571                 break;  // nice for people using stdio
572             if (buflen == buf.length) {
573                 byte[] newbuf = new byte[buf.length * 2];
574                 System.arraycopy(buf, 0, newbuf, 0, buflen);
575                 buf = newbuf;
576             }
577         }
578
579         BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, buflen)));
580         String s = br.readLine();
581         if (!s.startsWith("HTTP/")) throw new HTTPException("Expected reply to start with \"HTTP/\", got: " + s);
582         ret.put("STATUSLINE", s.substring(s.indexOf(' ') + 1));
583         ret.put("HTTP", s.substring(5, s.indexOf(' ')));
584
585         while((s = br.readLine()) != null && s.length() > 0) {
586             String front = s.substring(0, s.indexOf(':')).toLowerCase();
587             String back = s.substring(s.indexOf(':') + 1).trim();
588             // ugly hack: we never replace a Digest-auth with a Basic-auth (proxy + www)
589             if (front.endsWith("-authenticate") && ret.get(front) != null && !back.equals("Digest")) continue;
590             if (front.equals("set-cookie")) cookies.setCookie(back, host);
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("UTF8");
624         MD5 md5 = new MD5();
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
637     // Proxy ///////////////////////////////////////////////////////////
638
639     /** encapsulates most of the proxy logic; some is shared in HTTP.java */
640     public static class Proxy {
641         
642         public String httpProxyHost = null;                  ///< the HTTP Proxy host to use
643         public int httpProxyPort = -1;                       ///< the HTTP Proxy port to use
644         public String httpsProxyHost = null;                 ///< seperate proxy for HTTPS
645         public int httpsProxyPort = -1;
646         public String socksProxyHost = null;                 ///< the SOCKS Proxy Host to use
647         public int socksProxyPort = -1;                      ///< the SOCKS Proxy Port to use
648         public String[] excluded = new String[] { };         ///< hosts to be excluded from proxy use; wildcards permitted
649
650         public Socket attempt(String url, String host, HTTP http) {
651             for(int i=0; i<excluded.length; i++) if (http.host.equals(excluded[i])) return null;
652             if (http.ssl && httpsProxyHost != null) return attemptHttpProxy(http, httpsProxyHost,  httpsProxyPort);
653             if (httpProxyHost != null)              return attemptHttpProxy(http, httpProxyHost,   httpProxyPort);
654             if (socksProxyHost != null)             return attemptSocksProxy(http, socksProxyHost, socksProxyPort);
655             return null;
656         }
657
658         public Proxy detectProxyViaManual(String http_proxy,
659                                           String https_proxy,
660                                           String socks_proxy,
661                                           String noproxy) {
662             Proxy ret = new Proxy();
663             ret.httpProxyHost = http_proxy;
664             if (ret.httpProxyHost != null) {
665                 if (ret.httpProxyHost.startsWith("http://")) ret.httpProxyHost = ret.httpProxyHost.substring(7);
666                 if (ret.httpProxyHost.endsWith("/"))
667                     ret.httpProxyHost = ret.httpProxyHost.substring(0, ret.httpProxyHost.length() - 1);
668                 if (ret.httpProxyHost.indexOf(':') != -1) {
669                     ret.httpProxyPort = Integer.parseInt(ret.httpProxyHost.substring(ret.httpProxyHost.indexOf(':') + 1));
670                     ret.httpProxyHost = ret.httpProxyHost.substring(0, ret.httpProxyHost.indexOf(':'));
671                 } else {
672                     ret.httpProxyPort = 80;
673                 }
674             }
675         
676             ret.httpsProxyHost = https_proxy;
677             if (ret.httpsProxyHost != null) {
678                 if (ret.httpsProxyHost.startsWith("https://")) ret.httpsProxyHost = ret.httpsProxyHost.substring(7);
679                 if (ret.httpsProxyHost.endsWith("/"))
680                     ret.httpsProxyHost = ret.httpsProxyHost.substring(0, ret.httpsProxyHost.length() - 1);
681                 if (ret.httpsProxyHost.indexOf(':') != -1) {
682                     ret.httpsProxyPort = Integer.parseInt(ret.httpsProxyHost.substring(ret.httpsProxyHost.indexOf(':') + 1));
683                     ret.httpsProxyHost = ret.httpsProxyHost.substring(0, ret.httpsProxyHost.indexOf(':'));
684                 } else {
685                     ret.httpsProxyPort = 80;
686                 }
687             }
688         
689             ret.socksProxyHost = socks_proxy;
690             if (ret.socksProxyHost != null) {
691                 if (ret.socksProxyHost.startsWith("socks://")) ret.socksProxyHost = ret.socksProxyHost.substring(7);
692                 if (ret.socksProxyHost.endsWith("/"))
693                     ret.socksProxyHost = ret.socksProxyHost.substring(0, ret.socksProxyHost.length() - 1);
694                 if (ret.socksProxyHost.indexOf(':') != -1) {
695                     ret.socksProxyPort = Integer.parseInt(ret.socksProxyHost.substring(ret.socksProxyHost.indexOf(':') + 1));
696                     ret.socksProxyHost = ret.socksProxyHost.substring(0, ret.socksProxyHost.indexOf(':'));
697                 } else {
698                     ret.socksProxyPort = 80;
699                 }
700             }
701         
702             if (noproxy != null) {
703                 StringTokenizer st = new StringTokenizer(noproxy, ",");
704                 ret.excluded = new String[st.countTokens()];
705                 for(int i=0; st.hasMoreTokens(); i++) ret.excluded[i] = st.nextToken();
706             }
707         
708             if (ret.httpProxyHost == null && ret.socksProxyHost == null) return null;
709             return ret;
710         }
711
712         // Attempts //////////////////////////////////////////////////////////////////////////////
713
714         protected Socket attemptDirect(HTTP http) { return http.attemptDirect(); }
715
716         /** Attempts to use an HTTP proxy, employing the CONNECT method if HTTPS is requested */
717         protected Socket attemptHttpProxy(HTTP http, String proxyHost, int proxyPort) {
718             try {
719                 if (Log.verbose) Log.info(this, "attempting to create HTTP proxied socket using proxy " + proxyHost + ":" + proxyPort);
720                 Socket sock = http.getSocket(proxyHost, proxyPort, http.ssl, false);
721
722                 if (!http.ssl) {
723                     if (!http.path.startsWith("http://")) http.path = "http://" + http.host + ":" + http.port + http.path;
724                     return sock;
725                 }
726
727                 PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
728                 BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
729                 pw.print("CONNECT " + http.host + ":" + http.port + " HTTP/1.1\r\n\r\n");
730                 pw.flush();
731                 String s = br.readLine();
732                 if (s.charAt(9) != '2') throw new HTTPException("proxy refused CONNECT method: \"" + s + "\"");
733                 while (br.readLine().length() > 0) { };
734                 ((SSL)sock).negotiate();
735                 return sock;
736
737             } catch (IOException e) {
738                 if (Log.on) Log.info(this, "exception in attemptHttpProxy(): " + e);
739                 return null;
740             }
741         }
742
743         /**
744          *  Implements SOCKSv4 with v4a DNS extension
745          *  @see http://www.socks.nec.com/protocol/socks4.protocol
746          *  @see http://www.socks.nec.com/protocol/socks4a.protocol
747          */
748         protected Socket attemptSocksProxy(HTTP http, String proxyHost, int proxyPort) {
749
750             // even if host is already a "x.y.z.w" string, we use this to parse it into bytes
751             InetAddress addr = null;
752             try { addr = InetAddress.getByName(http.host); } catch (Exception e) { }
753
754             if (Log.verbose) Log.info(this, "attempting to create SOCKSv4" + (addr == null ? "" : "a") +
755                                       " proxied socket using proxy " + proxyHost + ":" + proxyPort);
756
757             try {
758                 Socket sock = http.getSocket(proxyHost, proxyPort, http.ssl, false);
759             
760                 DataOutputStream dos = new DataOutputStream(sock.getOutputStream());
761                 dos.writeByte(0x04);                         // SOCKSv4(a)
762                 dos.writeByte(0x01);                         // CONNECT
763                 dos.writeShort(http.port & 0xffff);          // port
764                 if (addr == null) dos.writeInt(0x00000001);  // bogus IP
765                 else dos.write(addr.getAddress());           // actual IP
766                 dos.writeByte(0x00);                         // no userid
767                 if (addr == null) {
768                     PrintWriter pw = new PrintWriter(new OutputStreamWriter(dos));
769                     pw.print(http.host);
770                     pw.flush();
771                     dos.writeByte(0x00);                     // hostname null terminator
772                 }
773                 dos.flush();
774
775                 DataInputStream dis = new DataInputStream(sock.getInputStream());
776                 dis.readByte();                              // reply version
777                 byte success = dis.readByte();               // success/fail
778                 dis.skip(6);                                 // ip/port
779             
780                 if ((int)(success & 0xff) == 90) {
781                     if (http.ssl) ((SSL)sock).negotiate();
782                     return sock;
783                 }
784                 if (Log.on) Log.info(this, "SOCKS server denied access, code " + (success & 0xff));
785                 return null;
786
787             } catch (IOException e) {
788                 if (Log.on) Log.info(this, "exception in attemptSocksProxy(): " + e);
789                 return null;
790             }
791         }
792
793         // Authorization ///////////////////////////////////////////////////////////////////////////////////
794
795         public static class Authorization {
796
797             static public String authorization = null;
798             static public String authorization2 = null;
799             static public Semaphore waitingForUser = new Semaphore();
800
801             // FIXME: temporarily disabled so we can use HTTP outside the core
802             /*
803               public static synchronized void getPassword(final String realm, final String style,
804               final String proxyIP, String oldAuth) throws IOException {
805
806               // this handles cases where multiple threads hit the proxy auth at the same time -- all but one will block on the
807               // synchronized keyword. If 'authorization' changed while the thread was blocked, it means that the user entered
808               // a password, so we should reattempt authorization.
809
810               if (authorization != oldAuth) return;
811               if (Log.on) Log.info(Authorization.class, "displaying proxy authorization dialog");
812               Scheduler.add(new Task() {
813               public void perform() throws IOException, JSExn {
814               Box b = new Box();
815               Template t = null;
816               // FIXME
817               //Template.buildTemplate("org/ibex/builtin/proxy_authorization.ibex", Stream.getInputStream((JS)Main.builtin.get("org/ibex/builtin/proxy_authorization.ibex")), new Ibex(null));
818               t.apply(b);
819               b.put("realm", realm);
820               b.put("proxyIP", proxyIP);
821               }
822               });
823
824               waitingForUser.block();
825               if (Log.on) Log.info(Authorization.class, "got proxy authorization info; re-attempting connection");
826               }
827             */
828         }
829
830         /**
831          *  An implementation of Microsoft's proprietary NTLM authentication protocol.  This code was derived from Eric
832          *  Glass's work, and is copyright as follows:
833          *
834          *  Copyright (c) 2003 Eric Glass     (eglass1 at comcast.net). 
835          *
836          *  Permission to use, copy, modify, and distribute this document for any purpose and without any fee is hereby
837          *  granted, provided that the above copyright notice and this list of conditions appear in all copies.
838          *  The most current version of this document may be obtained from http://davenport.sourceforge.net/ntlm.html .
839          */ 
840         public static class NTLM {
841             
842             public static final byte[] type1 = new byte[] { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01,
843                                                             0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00 };
844             
845             /**
846              * Calculates the NTLM Response for the given challenge, using the
847              * specified password.
848              *
849              * @param password The user's password.
850              * @param challenge The Type 2 challenge from the server.
851              *
852              * @return The NTLM Response.
853              */
854             public static byte[] getNTLMResponse(String password, byte[] challenge)
855                 throws UnsupportedEncodingException {
856                 byte[] ntlmHash = ntlmHash(password);
857                 return lmResponse(ntlmHash, challenge);
858             }
859
860             /**
861              * Calculates the LM Response for the given challenge, using the specified
862              * password.
863              *
864              * @param password The user's password.
865              * @param challenge The Type 2 challenge from the server.
866              *
867              * @return The LM Response.
868              */
869             public static byte[] getLMResponse(String password, byte[] challenge)
870             {
871                 byte[] lmHash = lmHash(password);
872                 return lmResponse(lmHash, challenge);
873             }
874
875             /**
876              * Calculates the NTLMv2 Response for the given challenge, using the
877              * specified authentication target, username, password, target information
878              * block, and client challenge.
879              *
880              * @param target The authentication target (i.e., domain).
881              * @param user The username. 
882              * @param password The user's password.
883              * @param targetInformation The target information block from the Type 2
884              * message.
885              * @param challenge The Type 2 challenge from the server.
886              * @param clientChallenge The random 8-byte client challenge. 
887              *
888              * @return The NTLMv2 Response.
889              */
890             public static byte[] getNTLMv2Response(String target, String user,
891                                                    String password, byte[] targetInformation, byte[] challenge,
892                                                    byte[] clientChallenge) throws UnsupportedEncodingException {
893                 byte[] ntlmv2Hash = ntlmv2Hash(target, user, password);
894                 byte[] blob = createBlob(targetInformation, clientChallenge);
895                 return lmv2Response(ntlmv2Hash, blob, challenge);
896             }
897
898             /**
899              * Calculates the LMv2 Response for the given challenge, using the
900              * specified authentication target, username, password, and client
901              * challenge.
902              *
903              * @param target The authentication target (i.e., domain).
904              * @param user The username.
905              * @param password The user's password.
906              * @param challenge The Type 2 challenge from the server.
907              * @param clientChallenge The random 8-byte client challenge.
908              *
909              * @return The LMv2 Response. 
910              */
911             public static byte[] getLMv2Response(String target, String user,
912                                                  String password, byte[] challenge, byte[] clientChallenge)
913                 throws UnsupportedEncodingException {
914                 byte[] ntlmv2Hash = ntlmv2Hash(target, user, password);
915                 return lmv2Response(ntlmv2Hash, clientChallenge, challenge);
916             }
917
918             /**
919              * Calculates the NTLM2 Session Response for the given challenge, using the
920              * specified password and client challenge.
921              *
922              * @param password The user's password.
923              * @param challenge The Type 2 challenge from the server.
924              * @param clientChallenge The random 8-byte client challenge.
925              *
926              * @return The NTLM2 Session Response.  This is placed in the NTLM
927              * response field of the Type 3 message; the LM response field contains
928              * the client challenge, null-padded to 24 bytes.
929              */
930             public static byte[] getNTLM2SessionResponse(String password,
931                                                          byte[] challenge, byte[] clientChallenge) throws UnsupportedEncodingException {
932                 byte[] ntlmHash = ntlmHash(password);
933                 MD5 md5 = new MD5();
934                 md5.update(challenge, 0, challenge.length);
935                 md5.update(clientChallenge, 0, clientChallenge.length);
936                 byte[] sessionHash = new byte[8];
937                 byte[] md5_out = new byte[md5.getDigestSize()];
938                 md5.doFinal(md5_out, 0);
939                 System.arraycopy(md5_out, 0, sessionHash, 0, 8);
940                 return lmResponse(ntlmHash, sessionHash);
941             }
942
943             /**
944              * Creates the LM Hash of the user's password.
945              *
946              * @param password The password.
947              *
948              * @return The LM Hash of the given password, used in the calculation
949              * of the LM Response.
950              */
951             private static byte[] lmHash(String password) {
952                 /*
953                   byte[] oemPassword = password.toUpperCase().getBytes("UTF8");
954                   int length = java.lang.Math.min(oemPassword.length, 14);
955                   byte[] keyBytes = new byte[14];
956                   System.arraycopy(oemPassword, 0, keyBytes, 0, length);
957                   Key lowKey = createDESKey(keyBytes, 0);
958                   Key highKey = createDESKey(keyBytes, 7);
959                   byte[] magicConstant = "KGS!@#$%".getBytes("UTF8");
960                   Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
961                   des.init(Cipher.ENCRYPT_MODE, lowKey);
962                   byte[] lowHash = des.doFinal(magicConstant);
963                   des.init(Cipher.ENCRYPT_MODE, highKey);
964                   byte[] highHash = des.doFinal(magicConstant);
965                   byte[] lmHash = new byte[16];
966                   System.arraycopy(lowHash, 0, lmHash, 0, 8);
967                   System.arraycopy(highHash, 0, lmHash, 8, 8);
968                   return lmHash;
969                 */
970                 return null;
971             }
972
973             /**
974              * Creates the NTLM Hash of the user's password.
975              *
976              * @param password The password.
977              *
978              * @return The NTLM Hash of the given password, used in the calculation
979              * of the NTLM Response and the NTLMv2 and LMv2 Hashes.
980              */
981             private static byte[] ntlmHash(String password) throws UnsupportedEncodingException {
982                 // FIXME
983                 /*
984                   byte[] unicodePassword = password.getBytes("UnicodeLittleUnmarked");
985                   MD4 md4 = new MD4();
986                   md4.update(unicodePassword, 0, unicodePassword.length);
987                   byte[] ret = new byte[md4.getDigestSize()];
988                   return ret;
989                 */
990                 return null;
991             }
992
993             /**
994              * Creates the NTLMv2 Hash of the user's password.
995              *
996              * @param target The authentication target (i.e., domain).
997              * @param user The username.
998              * @param password The password.
999              *
1000              * @return The NTLMv2 Hash, used in the calculation of the NTLMv2
1001              * and LMv2 Responses. 
1002              */
1003             private static byte[] ntlmv2Hash(String target, String user,
1004                                              String password) throws UnsupportedEncodingException {
1005                 byte[] ntlmHash = ntlmHash(password);
1006                 String identity = user.toUpperCase() + target.toUpperCase();
1007                 return hmacMD5(identity.getBytes("UnicodeLittleUnmarked"), ntlmHash);
1008             }
1009
1010             /**
1011              * Creates the LM Response from the given hash and Type 2 challenge.
1012              *
1013              * @param hash The LM or NTLM Hash.
1014              * @param challenge The server challenge from the Type 2 message.
1015              *
1016              * @return The response (either LM or NTLM, depending on the provided
1017              * hash).
1018              */
1019             private static byte[] lmResponse(byte[] hash, byte[] challenge)
1020             {
1021                 /*
1022                   byte[] keyBytes = new byte[21];
1023                   System.arraycopy(hash, 0, keyBytes, 0, 16);
1024                   Key lowKey = createDESKey(keyBytes, 0);
1025                   Key middleKey = createDESKey(keyBytes, 7);
1026                   Key highKey = createDESKey(keyBytes, 14);
1027                   Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
1028                   des.init(Cipher.ENCRYPT_MODE, lowKey);
1029                   byte[] lowResponse = des.doFinal(challenge);
1030                   des.init(Cipher.ENCRYPT_MODE, middleKey);
1031                   byte[] middleResponse = des.doFinal(challenge);
1032                   des.init(Cipher.ENCRYPT_MODE, highKey);
1033                   byte[] highResponse = des.doFinal(challenge);
1034                   byte[] lmResponse = new byte[24];
1035                   System.arraycopy(lowResponse, 0, lmResponse, 0, 8);
1036                   System.arraycopy(middleResponse, 0, lmResponse, 8, 8);
1037                   System.arraycopy(highResponse, 0, lmResponse, 16, 8);
1038                   return lmResponse;
1039                 */
1040                 return null;
1041             }
1042
1043             /**
1044              * Creates the LMv2 Response from the given hash, client data, and
1045              * Type 2 challenge.
1046              *
1047              * @param hash The NTLMv2 Hash.
1048              * @param clientData The client data (blob or client challenge).
1049              * @param challenge The server challenge from the Type 2 message.
1050              *
1051              * @return The response (either NTLMv2 or LMv2, depending on the
1052              * client data).
1053              */
1054             private static byte[] lmv2Response(byte[] hash, byte[] clientData,
1055                                                byte[] challenge) {
1056                 byte[] data = new byte[challenge.length + clientData.length];
1057                 System.arraycopy(challenge, 0, data, 0, challenge.length);
1058                 System.arraycopy(clientData, 0, data, challenge.length,
1059                                  clientData.length);
1060                 byte[] mac = hmacMD5(data, hash);
1061                 byte[] lmv2Response = new byte[mac.length + clientData.length];
1062                 System.arraycopy(mac, 0, lmv2Response, 0, mac.length);
1063                 System.arraycopy(clientData, 0, lmv2Response, mac.length,
1064                                  clientData.length);
1065                 return lmv2Response;
1066             }
1067
1068             /**
1069              * Creates the NTLMv2 blob from the given target information block and
1070              * client challenge.
1071              *
1072              * @param targetInformation The target information block from the Type 2
1073              * message.
1074              * @param clientChallenge The random 8-byte client challenge.
1075              *
1076              * @return The blob, used in the calculation of the NTLMv2 Response.
1077              */
1078             private static byte[] createBlob(byte[] targetInformation,
1079                                              byte[] clientChallenge) {
1080                 byte[] blobSignature = new byte[] {
1081                     (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00
1082                 };
1083                 byte[] reserved = new byte[] {
1084                     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
1085                 };
1086                 byte[] unknown1 = new byte[] {
1087                     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
1088                 };
1089                 byte[] unknown2 = new byte[] {
1090                     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
1091                 };
1092                 long time = System.currentTimeMillis();
1093                 time += 11644473600000l; // milliseconds from January 1, 1601 -> epoch.
1094                 time *= 10000; // tenths of a microsecond.
1095                 // convert to little-endian byte array.
1096                 byte[] timestamp = new byte[8];
1097                 for (int i = 0; i < 8; i++) {
1098                     timestamp[i] = (byte) time;
1099                     time >>>= 8;
1100                 }
1101                 byte[] blob = new byte[blobSignature.length + reserved.length +
1102                                        timestamp.length + clientChallenge.length +
1103                                        unknown1.length + targetInformation.length +
1104                                        unknown2.length];
1105                 int offset = 0;
1106                 System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length);
1107                 offset += blobSignature.length;
1108                 System.arraycopy(reserved, 0, blob, offset, reserved.length);
1109                 offset += reserved.length;
1110                 System.arraycopy(timestamp, 0, blob, offset, timestamp.length);
1111                 offset += timestamp.length;
1112                 System.arraycopy(clientChallenge, 0, blob, offset,
1113                                  clientChallenge.length);
1114                 offset += clientChallenge.length;
1115                 System.arraycopy(unknown1, 0, blob, offset, unknown1.length);
1116                 offset += unknown1.length;
1117                 System.arraycopy(targetInformation, 0, blob, offset,
1118                                  targetInformation.length);
1119                 offset += targetInformation.length;
1120                 System.arraycopy(unknown2, 0, blob, offset, unknown2.length);
1121                 return blob;
1122             }
1123
1124             /**
1125              * Calculates the HMAC-MD5 hash of the given data using the specified
1126              * hashing key.
1127              *
1128              * @param data The data for which the hash will be calculated. 
1129              * @param key The hashing key.
1130              *
1131              * @return The HMAC-MD5 hash of the given data.
1132              */
1133             private static byte[] hmacMD5(byte[] data, byte[] key) {
1134                 byte[] ipad = new byte[64];
1135                 byte[] opad = new byte[64];
1136                 for (int i = 0; i < 64; i++) {
1137                     ipad[i] = (byte) 0x36;
1138                     opad[i] = (byte) 0x5c;
1139                 }
1140                 for (int i = key.length - 1; i >= 0; i--) {
1141                     ipad[i] ^= key[i];
1142                     opad[i] ^= key[i];
1143                 }
1144                 byte[] content = new byte[data.length + 64];
1145                 System.arraycopy(ipad, 0, content, 0, 64);
1146                 System.arraycopy(data, 0, content, 64, data.length);
1147                 MD5 md5 = new MD5();
1148                 md5.update(content, 0, content.length);
1149                 data = new byte[md5.getDigestSize()];
1150                 md5.doFinal(data, 0);
1151                 content = new byte[data.length + 64];
1152                 System.arraycopy(opad, 0, content, 0, 64);
1153                 System.arraycopy(data, 0, content, 64, data.length);
1154                 md5 = new MD5();
1155                 md5.update(content, 0, content.length);
1156                 byte[] ret = new byte[md5.getDigestSize()];
1157                 md5.doFinal(ret, 0);
1158                 return ret;
1159             }
1160
1161             /**
1162              * Creates a DES encryption key from the given key material.
1163              *
1164              * @param bytes A byte array containing the DES key material.
1165              * @param offset The offset in the given byte array at which
1166              * the 7-byte key material starts.
1167              *
1168              * @return A DES encryption key created from the key material
1169              * starting at the specified offset in the given byte array.
1170              */
1171             /*
1172               private static Key createDESKey(byte[] bytes, int offset) {
1173               byte[] keyBytes = new byte[7];
1174               System.arraycopy(bytes, offset, keyBytes, 0, 7);
1175               byte[] material = new byte[8];
1176               material[0] = keyBytes[0];
1177               material[1] = (byte) (keyBytes[0] << 7 | (keyBytes[1] & 0xff) >>> 1);
1178               material[2] = (byte) (keyBytes[1] << 6 | (keyBytes[2] & 0xff) >>> 2);
1179               material[3] = (byte) (keyBytes[2] << 5 | (keyBytes[3] & 0xff) >>> 3);
1180               material[4] = (byte) (keyBytes[3] << 4 | (keyBytes[4] & 0xff) >>> 4);
1181               material[5] = (byte) (keyBytes[4] << 3 | (keyBytes[5] & 0xff) >>> 5);
1182               material[6] = (byte) (keyBytes[5] << 2 | (keyBytes[6] & 0xff) >>> 6);
1183               material[7] = (byte) (keyBytes[6] << 1);
1184               oddParity(material);
1185               return new SecretKeySpec(material, "DES");
1186               }
1187             */
1188
1189             /**
1190              * Applies odd parity to the given byte array.
1191              *
1192              * @param bytes The data whose parity bits are to be adjusted for
1193              * odd parity.
1194              */
1195             private static void oddParity(byte[] bytes) {
1196                 for (int i = 0; i < bytes.length; i++) {
1197                     byte b = bytes[i];
1198                     boolean needsParity = (((b >>> 7) ^ (b >>> 6) ^ (b >>> 5) ^
1199                                             (b >>> 4) ^ (b >>> 3) ^ (b >>> 2) ^
1200                                             (b >>> 1)) & 0x01) == 0;
1201                     if (needsParity) {
1202                         bytes[i] |= (byte) 0x01;
1203                     } else {
1204                         bytes[i] &= (byte) 0xfe;
1205                     }
1206                 }
1207             }
1208
1209         }
1210     }
1211 }