X-Git-Url: http://git.megacz.com/?a=blobdiff_plain;f=src%2Forg%2Fxwt%2FHTTP.java;h=681b928a614ec505b71ba822d035431a15e7493e;hb=5c8da8336c1196bda3fd5d21208a17058dab1371;hp=79528acf011041a2a6875ee8d462fbe3b8a015fb;hpb=86f887828bf61c8eecbef05b88623a16005bb845;p=org.ibex.core.git diff --git a/src/org/xwt/HTTP.java b/src/org/xwt/HTTP.java index 79528ac..681b928 100644 --- a/src/org/xwt/HTTP.java +++ b/src/org/xwt/HTTP.java @@ -1,98 +1,153 @@ -// Copyright 2002 Adam Megacz, see the COPYING file for licensing [GPL]package org.xwt; +// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] package org.xwt; import java.net.*; import java.io.*; import java.util.*; +import org.xwt.js.*; import org.xwt.util.*; -import org.mozilla.javascript.*; +import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.crypto.digests.*; /** - * A crude HTTP[S] connection implementation with support for proxies, since not all Java libraries - * (particularly GCJ's) support proxies. - * - * FEATURE: implement pipelining + * This object encapsulates a *single* HTTP connection. Multiple requests may be pipelined over a connection (thread-safe), + * although any IOException encountered in a request will invalidate all later requests. */ public class HTTP { - /** the URL to connect to */ - URL url = null; - /** the host to connect to */ - String host = null; + // Public Methods //////////////////////////////////////////////////////////////////////////////////////// - /** the port to connect on */ - int port = -1; + public HTTP(String url) { this(url, false); } + public HTTP(String url, boolean skipResolveCheck) { originalUrl = url; this.skipResolveCheck = skipResolveCheck; } - /** true if SSL (HTTPS) should be used */ - boolean ssl = false; + /** Performs an HTTP GET request */ + public InputStream GET() throws IOException { return makeRequest(null, null); } - /** the path (URI) to retrieve on the server */ - String path = null; + /** Performs an HTTP POST request; content is additional headers, blank line, and body */ + public InputStream POST(String contentType, String content) throws IOException { return makeRequest(contentType, content); } - /** the socket */ - Socket sock = null; + public static class HTTPException extends IOException { public HTTPException(String s) { super(s); } } - /** the socket's inputstream */ - InputStream in = null; - /** the socket's outputstream */ - OutputStream out = null; + // Statics /////////////////////////////////////////////////////////////////////////////////////////////// - /** the content-type of the data being received */ - String contentType = null; + static Hash resolvedHosts = new Hash(); ///< cache for resolveAndCheckIfFirewalled() + private static Hash authCache = new Hash(); ///< cache of userInfo strings, keyed on originalUrl - /** the content-length of the data being recieved */ - int contentLength = 0; - /** additional headers to be transmitted */ - String headers = ""; + // Instance Data /////////////////////////////////////////////////////////////////////////////////////////////// - /** cache for resolveAndCheckIfFirewalled() */ - static Hashtable resolvedHosts = new Hashtable(); + final String originalUrl; ///< the URL as passed to the original constructor; this is never changed + URL url = null; ///< the URL to connect to; this is munged when the url is parsed */ + String host = null; ///< the host to connect to + int port = -1; ///< the port to connect on + boolean ssl = false; ///< true if SSL (HTTPS) should be used + String path = null; ///< the path (URI) to retrieve on the server + Socket sock = null; ///< the socket + InputStream in = null; ///< the socket's inputstream + String userInfo = null; ///< the username and password portions of the URL + boolean firstRequest = true; ///< true iff this is the first request to be made on this socket + boolean skipResolveCheck = false; ///< allowed to skip the resolve check when downloading PAC script + boolean proxied = false; ///< true iff we're using a proxy + /** this is null if the current request is the first request on + * this HTTP connection; otherwise it is a Semaphore which will be + * released once the request ahead of us has recieved its response + */ + Semaphore okToRecieve = null; - // Constructor //////////////////////////////////////////////////////////////////////////////////////// + /** + * This method isn't synchronized; however, only one thread can be in the inner synchronized block at a time, and the rest of + * the method is protected by in-order one-at-a-time semaphore lock-steps + */ + private InputStream makeRequest(String contentType, String content) throws IOException { - public HTTP(String url) throws MalformedURLException, IOException { this(url, false); } - public HTTP(String url, boolean skipResolveCheck) throws MalformedURLException, IOException { - if (url.startsWith("https:")) { url = "http" + url.substring(5); ssl = true; } - if (!url.startsWith("http:")) throw new HTTPException("HTTP only supports http/https urls"); - this.url = new URL(url); - port = this.url.getPort(); - path = this.url.getFile(); - if (port == -1) port = ssl ? 443 : 80; - host = this.url.getHost(); - if (Log.on) Log.log(this, "creating HTTP object for connection to " + host + ":" + port); - addHeader("Host", host); // host header is always sent verbatim - if (!skipResolveCheck) host = resolveAndCheckIfFirewalled(host); // might have to use the strict IP if behind a proxy - - ProxyInfo pi = Platform.detectProxy(); - if (sock == null && pi != null && pi.proxyAutoConfigFunction != null) sock = attemptPAC(pi.proxyAutoConfigFunction); - if (sock == null && pi != null && ssl && pi.httpsProxyHost != null) sock = attemptHttpProxy(pi.httpsProxyHost, pi.httpsProxyPort); - if (sock == null && pi != null && pi.httpProxyHost != null) sock = attemptHttpProxy(pi.httpProxyHost, pi.httpProxyPort); - if (sock == null && pi != null && pi.socksProxyHost != null) sock = attemptSocksProxy(pi.socksProxyHost, pi.socksProxyPort); - if (sock == null) sock = attemptDirect(); - if (sock == null) throw new HTTPException("all socket creation attempts have failed"); - sock.setTcpNoDelay(true); + // Step 1: send the request and establish a semaphore to stop any requests that pipeline after us + Semaphore blockOn = null; + Semaphore releaseMe = null; + synchronized(this) { + try { + connect(); + sendRequest(contentType, content); + } catch (IOException e) { + reset(); + throw e; + } + blockOn = okToRecieve; + releaseMe = okToRecieve = new Semaphore(); + } + + // Step 2: wait for requests ahead of us to complete, then read the reply off the stream + boolean doRelease = true; + try { + if (blockOn != null) blockOn.block(); + + // previous call wrecked the socket connection, but we already sent our request, so we can't just retry -- + // this could cause the server to receive the request twice, which could be bad (think of the case where the + // server call causes Amazon.com to ship you an item with one-click purchasing). + if (sock == null) + throw new HTTPException("a previous pipelined call messed up the socket"); + + Hashtable h = in == null ? null : parseHeaders(in); + if (h == null) { + if (firstRequest) throw new HTTPException("server closed the socket with no response"); + // sometimes the server chooses to close the stream between requests + reset(); + releaseMe.release(); + return makeRequest(contentType, content); + } + + String reply = h.get("STATUSLINE").toString(); + + if (reply.startsWith("407") || reply.startsWith("401")) { + + if (reply.startsWith("407")) doProxyAuth(h, content == null ? "GET" : "POST"); + else doWebAuth(h, content == null ? "GET" : "POST"); + + if (h.get("HTTP").equals("1.0") && h.get("content-length") == null) { + if (Log.on) Log.info(this, "proxy returned an HTTP/1.0 reply with no content-length..."); + reset(); + } else { + int cl = h.get("content-length") == null ? -1 : Integer.parseInt(h.get("content-length").toString()); + new HTTPInputStream(in, cl, releaseMe).close(); + } + releaseMe.release(); + return makeRequest(contentType, content); + + } else if (reply.startsWith("2")) { + if (h.get("HTTP").equals("1.0") && h.get("content-length") == null) + throw new HTTPException("XWT does not support HTTP/1.0 servers which fail to return the Content-Length header"); + int cl = h.get("content-length") == null ? -1 : Integer.parseInt(h.get("content-length").toString()); + InputStream ret = new HTTPInputStream(in, cl, releaseMe); + if ("gzip".equals(h.get("content-encoding"))) ret = new java.util.zip.GZIPInputStream(ret); + doRelease = false; + return ret; + + } else { + throw new HTTPException("HTTP Error: " + reply); + + } + + } catch (IOException e) { reset(); throw e; + } finally { if (doRelease) releaseMe.release(); + } } // Safeguarded DNS Resolver /////////////////////////////////////////////////////////////////////////// /** - * resolves the hostname and returns it as a string in the form "x.y.z.w", except for the special case "xmlrpc.xwt.org". + * resolves the hostname and returns it as a string in the form "x.y.z.w" * @throws HTTPException if the host falls within a firewalled netblock */ - private String resolveAndCheckIfFirewalled(String host) throws HTTPException { - - // special case - if (host.equals("xmlrpc.xwt.org")) return host; + private void resolveAndCheckIfFirewalled(String host) throws HTTPException { // cached - if (resolvedHosts.get(host) != null) return (String)resolvedHosts.get(host); + if (resolvedHosts.get(host) != null) return; - if (Log.on) Log.log(this, " resolveAndCheckIfFirewalled: resolving " + host); + // if all scripts are trustworthy (local FS), continue + if (Main.originAddr == null) return; // resolve using DNS try { @@ -101,59 +156,59 @@ public class HTTP { if ((quadbyte[0] == 10 || (quadbyte[0] == 192 && quadbyte[1] == 168) || (quadbyte[0] == 172 && (quadbyte[1] & 0xF0) == 16)) && !addr.equals(Main.originAddr)) - throw new HTTPException("security violation: " + host + " [" + addr.getHostAddress() + "] is in a firewalled netblock"); - return addr.getHostAddress(); + throw new HTTPException("security violation: " + host + " [" + addr.getHostAddress() + + "] is in a firewalled netblock"); + return; } catch (UnknownHostException uhe) { } - // resolve using xmlrpc.xwt.org - if (Platform.detectProxy() == null) throw new HTTPException("could not resolve hostname \"" + host + "\" and no proxy configured"); - if (Log.on) Log.log(this, " could not resolve host " + host + "; using xmlrpc.xwt.org to ensure security"); - try { - Object ret = new XMLRPC("http://xmlrpc.xwt.org/RPC2/", "dns.resolve").call(new Object[] { host }); - if (ret == null || !(ret instanceof String)) throw new Exception(" xmlrpc.xwt.org returned non-String: " + ret); - resolvedHosts.put(host, ret); - return (String)ret; - } catch (Throwable e) { - throw new HTTPException("exception while attempting to use xmlrpc.xwt.org to resolve " + host + ": " + e); - } + if (Platform.detectProxy() == null) + throw new HTTPException("could not resolve hostname \"" + host + "\" and no proxy configured"); } // Methods to attempt socket creation ///////////////////////////////////////////////////////////////// + private Socket getSocket(String host, int port, boolean ssl, boolean negotiate) throws IOException { + Socket ret = ssl ? new SSL(host, port, negotiate) : new Socket(java.net.InetAddress.getByName(host), port); + ret.setTcpNoDelay(true); + return ret; + } + /** Attempts a direct connection */ - public Socket attemptDirect() { + private Socket attemptDirect() { try { - if (Log.on) Log.log(this, "attempting to create unproxied socket to " + host + ":" + port + (ssl ? " [ssl]" : "")); - return Platform.getSocket(host, port, ssl, true); + if (Log.verbose) Log.info(this, "attempting to create unproxied socket to " + + host + ":" + port + (ssl ? " [ssl]" : "")); + return getSocket(host, port, ssl, true); } catch (IOException e) { - if (Log.on) Log.log(this, "exception in attemptDirect(): " + e); + if (Log.on) Log.info(this, "exception in attemptDirect(): " + e); return null; } } /** Attempts to use an HTTP proxy, employing the CONNECT method if HTTPS is requested */ - public Socket attemptHttpProxy(String proxyHost, int proxyPort) { + private Socket attemptHttpProxy(String proxyHost, int proxyPort) { try { - if (Log.on) Log.log(this, "attempting to create HTTP proxied socket using proxy " + proxyHost + ":" + proxyPort); + if (Log.verbose) Log.info(this, "attempting to create HTTP proxied socket using proxy " + proxyHost + ":" + proxyPort); + Socket sock = getSocket(proxyHost, proxyPort, ssl, false); - Socket sock = Platform.getSocket(proxyHost, proxyPort, ssl, false); if (!ssl) { - path = "http://" + host + ":" + port + path; - } else { - if (Log.on) Log.log(this, "attempting to create HTTP proxied socket using proxy " + proxyHost + ":" + proxyPort); - PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream())); - BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream())); - pw.print("CONNECT " + host + ":" + port + " HTTP/1.0\r\n\r\n"); - String s = br.readLine(); - if (s.charAt(9) != '2') throw new HTTPException("proxy refused CONNECT method: \"" + s + "\""); - while (br.readLine().length() > 0) { }; - ((TinySSL)sock).negotiate(); + if (!path.startsWith("http://")) path = "http://" + host + ":" + port + path; + return sock; } + + PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream())); + BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream())); + pw.print("CONNECT " + host + ":" + port + " HTTP/1.1\r\n\r\n"); + pw.flush(); + String s = br.readLine(); + if (s.charAt(9) != '2') throw new HTTPException("proxy refused CONNECT method: \"" + s + "\""); + while (br.readLine().length() > 0) { }; + ((SSL)sock).negotiate(); return sock; } catch (IOException e) { - if (Log.on) Log.log(this, "exception in attemptHttpProxy(): " + e); + if (Log.on) Log.info(this, "exception in attemptHttpProxy(): " + e); return null; } } @@ -163,17 +218,17 @@ public class HTTP { * @see http://www.socks.nec.com/protocol/socks4.protocol * @see http://www.socks.nec.com/protocol/socks4a.protocol */ - public Socket attemptSocksProxy(String proxyHost, int proxyPort) { + private Socket attemptSocksProxy(String proxyHost, int proxyPort) { // even if host is already a "x.y.z.w" string, we use this to parse it into bytes InetAddress addr = null; try { addr = InetAddress.getByName(host); } catch (Exception e) { } - if (Log.on) Log.log(this, "attempting to create SOCKSv4" + (addr == null ? "" : "a") + - " proxied socket using proxy " + proxyHost + ":" + proxyPort); + if (Log.verbose) Log.info(this, "attempting to create SOCKSv4" + (addr == null ? "" : "a") + + " proxied socket using proxy " + proxyHost + ":" + proxyPort); try { - Socket sock = Platform.getSocket(proxyHost, proxyPort, ssl, false); + Socket sock = getSocket(proxyHost, proxyPort, ssl, false); DataOutputStream dos = new DataOutputStream(sock.getOutputStream()); dos.writeByte(0x04); // SOCKSv4(a) @@ -196,35 +251,36 @@ public class HTTP { dis.skip(6); // ip/port if ((int)(success & 0xff) == 90) { - if (ssl) ((TinySSL)sock).negotiate(); + if (ssl) ((SSL)sock).negotiate(); return sock; } - if (Log.on) Log.log(this, "SOCKS server denied access, code " + (success & 0xff)); + if (Log.on) Log.info(this, "SOCKS server denied access, code " + (success & 0xff)); return null; } catch (IOException e) { - if (Log.on) Log.log(this, "exception in attemptSocksProxy(): " + e); + if (Log.on) Log.info(this, "exception in attemptSocksProxy(): " + e); return null; } } /** executes the PAC script and dispatches a call to one of the other attempt methods based on the result */ - public Socket attemptPAC(Function pacFunc) { - if (Log.on) Log.log(this, "evaluating PAC script"); + private Socket attemptPAC(org.xwt.js.JS pacFunc) { + if (Log.verbose) Log.info(this, "evaluating PAC script"); String pac = null; try { - Object obj = pacFunc.call(Context.enter(), ProxyInfo.proxyAutoConfigRootScope, null, new Object[] { url.toString(), url.getHost() }); - if (Log.on) Log.log(this, " PAC script returned \"" + obj + "\""); + org.xwt.js.JSArray args = new org.xwt.js.JSArray(); + Object obj = pacFunc.call(url.toString(), url.getHost(), null, null, 2); + if (Log.verbose) Log.info(this, " PAC script returned \"" + obj + "\""); pac = obj.toString(); } catch (Throwable e) { - if (Log.on) Log.log(this, "PAC script threw exception " + e); + if (Log.on) Log.info(this, "PAC script threw exception " + e); return null; } StringTokenizer st = new StringTokenizer(pac, ";", false); while (st.hasMoreTokens()) { String token = st.nextToken().trim(); - if (Log.on) Log.log(this, " trying \"" + token + "\"..."); + if (Log.verbose) Log.info(this, " trying \"" + token + "\"..."); try { Socket ret = null; if (token.startsWith("DIRECT")) @@ -237,201 +293,442 @@ public class HTTP { Integer.parseInt(token.substring(token.indexOf(':') + 1))); if (ret != null) return ret; } catch (Throwable e) { - if (Log.on) Log.log(this, "attempt at \"" + token + "\" failed due to " + e + "; trying next token"); + if (Log.on) Log.info(this, "attempt at \"" + token + "\" failed due to " + e + "; trying next token"); } } - if (Log.on) Log.log(this, "all PAC results exhausted"); + if (Log.on) Log.info(this, "all PAC results exhausted"); return null; } // Everything Else //////////////////////////////////////////////////////////////////////////// - /** returns the content-type of the reply */ - public String getContentType() throws IOException { - getInputStream(); - return contentType; - } - - /** returns the content-length of the reply */ - public int getContentLength() throws IOException { - getInputStream(); - return contentLength; - } + private synchronized void connect() throws IOException { + if (originalUrl.equals("stdio:")) { in = new BufferedInputStream(System.in); return; } + if (sock != null) { + if (in == null) in = new BufferedInputStream(sock.getInputStream()); + return; + } + // grab the userinfo; gcj doesn't have java.net.URL.getUserInfo() + String url = originalUrl; + userInfo = url.substring(url.indexOf("://") + 3); + userInfo = userInfo.indexOf('/') == -1 ? userInfo : userInfo.substring(0, userInfo.indexOf('/')); + if (userInfo.indexOf('@') != -1) { + userInfo = userInfo.substring(0, userInfo.indexOf('@')); + url = url.substring(0, url.indexOf("://") + 3) + url.substring(url.indexOf('@') + 1); + } else { + userInfo = null; + } - /** adds a header to the outbound transmission */ - public void addHeader(String header, String value) throws HTTPException { - if (in != null) throw new HTTPException("attempt to add header after connection has been made"); - headers += header + ": " + value + "\r\n"; + if (url.startsWith("https:")) { + this.url = new URL("http" + url.substring(5)); + ssl = true; + } else if (!url.startsWith("http:")) { + throw new MalformedURLException("HTTP only supports http/https urls"); + } else { + this.url = new URL(url); + } + if (!skipResolveCheck) resolveAndCheckIfFirewalled(this.url.getHost()); + port = this.url.getPort(); + path = this.url.getFile(); + if (port == -1) port = ssl ? 443 : 80; + host = this.url.getHost(); + if (Log.verbose) Log.info(this, "creating HTTP object for connection to " + host + ":" + port); + + Proxy pi = Platform.detectProxy(); + OUTER: do { + if (pi != null) { + for(int i=0; i= 4 && buf[buflen - 4] == '\r' && buf[buflen - 3] == '\n' && buf[buflen - 2] == '\r' && buf[buflen - 1] == '\n') break; - if (buflen == buf.length) { - byte[] newbuf = new byte[buf.length * 2]; - System.arraycopy(buf, 0, newbuf, 0, buflen); - buf = newbuf; - } - } - - BufferedReader headerReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, buflen))); - String s = headerReader.readLine(); - if (!s.startsWith("HTTP/")) throw new HTTPException("Expected reply to start with \"HTTP/\""); - String reply = s.substring(s.indexOf(' ') + 1); - if (!reply.startsWith("2")) throw new HTTPException("HTTP Error: " + reply); - while((s = headerReader.readLine()) != null) { - if (s.length() > 15 && s.substring(0, 15).equalsIgnoreCase("content-length: ")) - contentLength = Integer.parseInt(s.substring(15)); + if (style.equals("NTLM") && Proxy.Authorization.authorization2 == null) { + Log.info(this, "Proxy identified itself as NTLM, sending Type 1 packet"); + Proxy.Authorization.authorization2 = "NTLM " + Base64.encode(Proxy.NTLM.type1); + return; } - return in; + if (!realm.equals("Digest") || Proxy.Authorization.authorization2 == null || !"true".equals(h.get("stale"))) + Proxy.Authorization.getPassword(realm, style, sock.getInetAddress().getHostAddress(), + Proxy.Authorization.authorization); + + if (style.equals("Basic")) { + Proxy.Authorization.authorization2 = + "Basic " + new String(Base64.encode(Proxy.Authorization.authorization.getBytes("US-ASCII"))); + + } else if (style.equals("Digest")) { + String A1 = Proxy.Authorization.authorization.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" + + Proxy.Authorization.authorization.substring(Proxy.Authorization.authorization.indexOf(':') + 1); + String A2 = method + ":" + path; + Proxy.Authorization.authorization2 = + "Digest " + + "username=\"" + Proxy.Authorization.authorization.substring(0, Proxy.Authorization.authorization.indexOf(':')) + + "\", " + + "realm=\"" + h.get("realm") + "\", " + + "nonce=\"" + h.get("nonce") + "\", " + + "uri=\"" + path + "\", " + + (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + + "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " + + "algorithm=MD5"; + + } else if (style.equals("NTLM")) { + Log.info(this, "Proxy identified itself as NTLM, got Type 2 packet"); + byte[] type2 = Base64.decode(((String)h0.get("proxy-authenticate")).substring(5).trim()); + for(int i=0; i length) len = length; + int ret = b == null ? (int)super.skip(len) : super.read(b, off, len); + if (ret >= 0) { + length -= ret; + good = true; + } + return ret; + } finally { + if (!good) reset(); + } + } - /** the HTTP Proxy port to use */ - public int httpProxyPort = -1; + public void close() throws IOException { + if (contentLength == -1) { + while(!chunkedDone) { + if (length != 0) skip(length); + readChunk(); + } + skip(2); + } else { + if (length != 0) skip(length); + } + if (releaseMe != null) releaseMe.release(); + } + } - /** if a seperate proxy should be used for HTTPS, this is the hostname; otherwise, httpProxyHost is used */ - public String httpsProxyHost = null; + void reset() { + firstRequest = true; + in = null; + sock = null; + } - /** if a seperate proxy should be used for HTTPS, this is the port */ - public int httpsProxyPort = -1; - /** the SOCKS Proxy Host to use */ - public String socksProxyHost = null; + // Misc Helpers /////////////////////////////////////////////////////////////////////////////////// - /** the SOCKS Proxy Port to use */ - public int socksProxyPort = -1; + /** reads a set of HTTP headers off of the input stream, returning null if the stream is already at its end */ + private Hashtable parseHeaders(InputStream in) throws IOException { + Hashtable ret = new Hashtable(); - /** hosts to be excluded from proxy use; wildcards permitted */ - public String[] excluded = null; + // we can't use a BufferedReader directly on the input stream, since it will buffer past the end of the headers + byte[] buf = new byte[4096]; + int buflen = 0; + while(true) { + int read = in.read(); + if (read == -1 && buflen == 0) return null; + if (read == -1) throw new HTTPException("stream closed while reading headers"); + buf[buflen++] = (byte)read; + if (buflen >= 4 && buf[buflen - 4] == '\r' && buf[buflen - 3] == '\n' && + buf[buflen - 2] == '\r' && buf[buflen - 1] == '\n') + break; + if (buflen == buf.length) { + byte[] newbuf = new byte[buf.length * 2]; + System.arraycopy(buf, 0, newbuf, 0, buflen); + buf = newbuf; + } + } - /** the PAC script */ - public Function proxyAutoConfigFunction = null; + BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, buflen))); + String s = br.readLine(); + if (!s.startsWith("HTTP/")) throw new HTTPException("Expected reply to start with \"HTTP/\", got: " + s); + ret.put("STATUSLINE", s.substring(s.indexOf(' ') + 1)); + ret.put("HTTP", s.substring(5, s.indexOf(' '))); + + while((s = br.readLine()) != null && s.length() > 0) { + String front = s.substring(0, s.indexOf(':')).toLowerCase(); + String back = s.substring(s.indexOf(':') + 1).trim(); + // ugly hack: we never replace a Digest-auth with a Basic-auth (proxy + www) + if (front.endsWith("-authenticate") && ret.get(front) != null && !back.equals("Digest")) continue; + ret.put(front, back); + } + return ret; + } - // this method has been disabled because it was causing problems -- some domains are set up so that *.foo.com resolves - // to a single IP, for any value of *. If the client's home domain is foo.com, then xwt-proxy-httpHost will resolve erroneously. - public static ProxyInfo detectProxyViaManual() { - return null; - /* - try { - // continue iff one of the two resolves - try { InetAddress.getByName("xwt-proxy-httpHost"); } - catch (UnknownHostException e) { InetAddress.getByName("xwt-proxy-socksHost"); } - - if (Log.on) Log.log(Platform.class, "using xwt-proxy-* configuration"); - ProxyInfo ret = new ProxyInfo(); - try { - ret.httpProxyHost = InetAddress.getByName("xwt-proxy-httpHost").getHostAddress(); - byte[] quadbyte = InetAddress.getByName("xwt-proxy-httpPort").getAddress(); - ret.httpProxyPort = ((quadbyte[1] & 0xff) * 10000) + ((quadbyte[2] & 0xff) * 100) + (quadbyte[3] & 0xff); - } catch (UnknownHostException e) { } - try { - ret.httpsProxyHost = InetAddress.getByName("xwt-proxy-httpsHost").getHostAddress(); - byte[] quadbyte = InetAddress.getByName("xwt-proxy-httpsPort").getAddress(); - ret.httpsProxyPort = ((quadbyte[1] & 0xff) * 10000) + ((quadbyte[2] & 0xff) * 100) + (quadbyte[3] & 0xff); - } catch (UnknownHostException e) { } - try { - ret.socksProxyHost = InetAddress.getByName("xwt-proxy-socksHost").getHostAddress(); - byte[] quadbyte = InetAddress.getByName("xwt-proxy-socksPort").getAddress(); - ret.socksProxyPort = ((quadbyte[1] & 0xff) * 10000) + ((quadbyte[2] & 0xff) * 100) + (quadbyte[3] & 0xff); - } catch (UnknownHostException e) { } - return ret; - } catch (UnknownHostException e) { - if (Log.on) Log.log(Platform.class, "xwt-proxy-* detection failed due to: " + e); - return null; + private Hashtable parseAuthenticationChallenge(String s) { + Hashtable ret = new Hashtable(); + + s = s.trim(); + ret.put("AUTHTYPE", s.substring(0, s.indexOf(' '))); + s = s.substring(s.indexOf(' ')).trim(); + + while (s.length() > 0) { + String val = null; + String key = s.substring(0, s.indexOf('=')); + s = s.substring(s.indexOf('=') + 1); + if (s.charAt(0) == '\"') { + s = s.substring(1); + val = s.substring(0, s.indexOf('\"')); + s = s.substring(s.indexOf('\"') + 1); + } else { + val = s.indexOf(',') == -1 ? s : s.substring(0, s.indexOf(',')); + s = s.indexOf(',') == -1 ? "" : s.substring(s.indexOf(',') + 1); } - */ + if (s.length() > 0 && s.charAt(0) == ',') s = s.substring(1); + s = s.trim(); + ret.put(key, val); } + return ret; + } - // FIXME: search up from default domain - public static ProxyInfo detectProxyViaWPAD() { - try { - InetAddress wpad = InetAddress.getByName("wpad"); - if (Log.on) Log.log(Platform.class, "using Web Proxy Auto Detection to detect proxy settings"); - ProxyInfo ret = new ProxyInfo(); - ret.proxyAutoConfigFunction = getProxyAutoConfigFunction("http://wpad/wpad.dat"); - if (ret.proxyAutoConfigFunction != null) return ret; - } catch (UnknownHostException e) { - if (Log.on) Log.log(HTTP.class, "couldn't find WPAD server: " + e); - } - return null; + private String H(String s) throws IOException { + byte[] b = s.getBytes("US-ASCII"); + MD5Digest md5 = new MD5Digest(); + md5.update(b, 0, b.length); + byte[] out = new byte[md5.getDigestSize()]; + md5.doFinal(out, 0); + String ret = ""; + for(int i=0; i> 4); + ret += "0123456789abcdef".charAt(out[i] & 0x0f); } + return ret; + } - public static Scriptable proxyAutoConfigRootScope = new ProxyAutoConfigRootScope(); - public static Function getProxyAutoConfigFunction(String url) { + // Proxy /////////////////////////////////////////////////////////// + + /** encapsulates most of the proxy logic; some is shared in HTTP.java */ + public static class Proxy { + + public Proxy() { } + + public String httpProxyHost = null; ///< the HTTP Proxy host to use + public int httpProxyPort = -1; ///< the HTTP Proxy port to use + public String httpsProxyHost = null; ///< seperate proxy for HTTPS + public int httpsProxyPort = -1; + public String socksProxyHost = null; ///< the SOCKS Proxy Host to use + public int socksProxyPort = -1; ///< the SOCKS Proxy Port to use + public String[] excluded = new String[] { }; ///< hosts to be excluded from proxy use; wildcards permitted + public JS proxyAutoConfigFunction = null; ///< the PAC script + + public static Proxy detectProxyViaManual() { + Proxy ret = new Proxy(); + + ret.httpProxyHost = Platform.getEnv("http_proxy"); + if (ret.httpProxyHost != null) { + if (ret.httpProxyHost.startsWith("http://")) ret.httpProxyHost = ret.httpProxyHost.substring(7); + if (ret.httpProxyHost.endsWith("/")) + ret.httpProxyHost = ret.httpProxyHost.substring(0, ret.httpProxyHost.length() - 1); + if (ret.httpProxyHost.indexOf(':') != -1) { + ret.httpProxyPort = Integer.parseInt(ret.httpProxyHost.substring(ret.httpProxyHost.indexOf(':') + 1)); + ret.httpProxyHost = ret.httpProxyHost.substring(0, ret.httpProxyHost.indexOf(':')); + } else { + ret.httpProxyPort = 80; + } + } + + ret.httpsProxyHost = Platform.getEnv("https_proxy"); + if (ret.httpsProxyHost != null) { + if (ret.httpsProxyHost.startsWith("https://")) ret.httpsProxyHost = ret.httpsProxyHost.substring(7); + if (ret.httpsProxyHost.endsWith("/")) + ret.httpsProxyHost = ret.httpsProxyHost.substring(0, ret.httpsProxyHost.length() - 1); + if (ret.httpsProxyHost.indexOf(':') != -1) { + ret.httpsProxyPort = Integer.parseInt(ret.httpsProxyHost.substring(ret.httpsProxyHost.indexOf(':') + 1)); + ret.httpsProxyHost = ret.httpsProxyHost.substring(0, ret.httpsProxyHost.indexOf(':')); + } else { + ret.httpsProxyPort = 80; + } + } + + ret.socksProxyHost = Platform.getEnv("socks_proxy"); + if (ret.socksProxyHost != null) { + if (ret.socksProxyHost.startsWith("socks://")) ret.socksProxyHost = ret.socksProxyHost.substring(7); + if (ret.socksProxyHost.endsWith("/")) + ret.socksProxyHost = ret.socksProxyHost.substring(0, ret.socksProxyHost.length() - 1); + if (ret.socksProxyHost.indexOf(':') != -1) { + ret.socksProxyPort = Integer.parseInt(ret.socksProxyHost.substring(ret.socksProxyHost.indexOf(':') + 1)); + ret.socksProxyHost = ret.socksProxyHost.substring(0, ret.socksProxyHost.indexOf(':')); + } else { + ret.socksProxyPort = 80; + } + } + + String noproxy = Platform.getEnv("no_proxy"); + if (noproxy != null) { + StringTokenizer st = new StringTokenizer(noproxy, ","); + ret.excluded = new String[st.countTokens()]; + for(int i=0; st.hasMoreTokens(); i++) ret.excluded[i] = st.nextToken(); + } + + if (ret.httpProxyHost == null && ret.socksProxyHost == null) return null; + return ret; + } + + public static JSScope proxyAutoConfigRootScope = new ProxyAutoConfigRootScope(); + public static JS getProxyAutoConfigFunction(String url) { try { - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); - BufferedReader br = new BufferedReader(new InputStreamReader(new HTTP(url, true).getInputStream())); + BufferedReader br = new BufferedReader(new InputStreamReader(new HTTP(url, true).GET())); String s = null; String script = ""; while((s = br.readLine()) != null) script += s + "\n"; - if (Log.on) Log.log(HTTP.ProxyInfo.class, "successfully retrieved WPAD PAC:"); - if (Log.on) Log.log(HTTP.ProxyInfo.class, script); - + if (Log.on) Log.info(Proxy.class, "successfully retrieved WPAD PAC:"); + if (Log.on) Log.info(Proxy.class, script); + // MS CARP hack Vector carpHosts = new Vector(); for(int i=0; i= d1 && day <= d2) || (d1 > d2 && (day >= d1 || day <= d2))) ? T : F; + + case "dateRange": throw new JSExn("XWT does not support dateRange() in PAC scripts"); + case "timeRange": throw new JSExn("XWT does not support timeRange() in PAC scripts"); + //#end + return super.callMethod(method, a0, a1, a2, rest, nargs); + } private static boolean match(String[] arr, String s, int index) { - if (index == arr.length) return true; + if (index >= arr.length) return true; for(int i=0; i epoch. + time *= 10000; // tenths of a microsecond. + // convert to little-endian byte array. + byte[] timestamp = new byte[8]; + for (int i = 0; i < 8; i++) { + timestamp[i] = (byte) time; + time >>>= 8; + } + byte[] blob = new byte[blobSignature.length + reserved.length + + timestamp.length + clientChallenge.length + + unknown1.length + targetInformation.length + + unknown2.length]; + int offset = 0; + System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length); + offset += blobSignature.length; + System.arraycopy(reserved, 0, blob, offset, reserved.length); + offset += reserved.length; + System.arraycopy(timestamp, 0, blob, offset, timestamp.length); + offset += timestamp.length; + System.arraycopy(clientChallenge, 0, blob, offset, + clientChallenge.length); + offset += clientChallenge.length; + System.arraycopy(unknown1, 0, blob, offset, unknown1.length); + offset += unknown1.length; + System.arraycopy(targetInformation, 0, blob, offset, + targetInformation.length); + offset += targetInformation.length; + System.arraycopy(unknown2, 0, blob, offset, unknown2.length); + return blob; + } + + /** + * Calculates the HMAC-MD5 hash of the given data using the specified + * hashing key. + * + * @param data The data for which the hash will be calculated. + * @param key The hashing key. + * + * @return The HMAC-MD5 hash of the given data. + */ + private static byte[] hmacMD5(byte[] data, byte[] key) throws Exception { + byte[] ipad = new byte[64]; + byte[] opad = new byte[64]; + for (int i = 0; i < 64; i++) { + ipad[i] = (byte) 0x36; + opad[i] = (byte) 0x5c; + } + for (int i = key.length - 1; i >= 0; i--) { + ipad[i] ^= key[i]; + opad[i] ^= key[i]; + } + byte[] content = new byte[data.length + 64]; + System.arraycopy(ipad, 0, content, 0, 64); + System.arraycopy(data, 0, content, 64, data.length); + MD5Digest md5 = new MD5Digest(); + md5.update(content, 0, content.length); + data = new byte[md5.getDigestSize()]; + md5.doFinal(data, 0); + content = new byte[data.length + 64]; + System.arraycopy(opad, 0, content, 0, 64); + System.arraycopy(data, 0, content, 64, data.length); + md5 = new MD5Digest(); + md5.update(content, 0, content.length); + byte[] ret = new byte[md5.getDigestSize()]; + md5.doFinal(ret, 0); + return ret; + } - private static final JSFunction timeRange = new JSFunction() { - public Object call(Context cx, Scriptable thisObj, Scriptable ctorObj, Object[] args) throws JavaScriptException { - throw new JavaScriptException("XWT does not support timeRange() in PAC scripts"); + /** + * Creates a DES encryption key from the given key material. + * + * @param bytes A byte array containing the DES key material. + * @param offset The offset in the given byte array at which + * the 7-byte key material starts. + * + * @return A DES encryption key created from the key material + * starting at the specified offset in the given byte array. + */ + /* + private static Key createDESKey(byte[] bytes, int offset) { + byte[] keyBytes = new byte[7]; + System.arraycopy(bytes, offset, keyBytes, 0, 7); + byte[] material = new byte[8]; + material[0] = keyBytes[0]; + material[1] = (byte) (keyBytes[0] << 7 | (keyBytes[1] & 0xff) >>> 1); + material[2] = (byte) (keyBytes[1] << 6 | (keyBytes[2] & 0xff) >>> 2); + material[3] = (byte) (keyBytes[2] << 5 | (keyBytes[3] & 0xff) >>> 3); + material[4] = (byte) (keyBytes[3] << 4 | (keyBytes[4] & 0xff) >>> 4); + material[5] = (byte) (keyBytes[4] << 3 | (keyBytes[5] & 0xff) >>> 5); + material[6] = (byte) (keyBytes[5] << 2 | (keyBytes[6] & 0xff) >>> 6); + material[7] = (byte) (keyBytes[6] << 1); + oddParity(material); + return new SecretKeySpec(material, "DES"); + } + */ + + /** + * Applies odd parity to the given byte array. + * + * @param bytes The data whose parity bits are to be adjusted for + * odd parity. + */ + private static void oddParity(byte[] bytes) { + for (int i = 0; i < bytes.length; i++) { + byte b = bytes[i]; + boolean needsParity = (((b >>> 7) ^ (b >>> 6) ^ (b >>> 5) ^ + (b >>> 4) ^ (b >>> 3) ^ (b >>> 2) ^ + (b >>> 1)) & 0x01) == 0; + if (needsParity) { + bytes[i] |= (byte) 0x01; + } else { + bytes[i] &= (byte) 0xfe; } - }; + } + } } - } - }