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