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