From: megacz Date: Fri, 30 Jan 2004 06:49:55 +0000 (+0000) Subject: 2002/08/07 06:39:18 X-Git-Tag: RC3~1574 X-Git-Url: http://git.megacz.com/?p=org.ibex.core.git;a=commitdiff_plain;h=e309d2b5134b39e3cbe66d02403b97d3d8ff11cd 2002/08/07 06:39:18 darcs-hash:20040130064955-2ba56-70d6d900a27fa056a174fdc6252248dc18b0272f.gz --- diff --git a/CHANGES b/CHANGES index 53820e0..b8ce31a 100644 --- a/CHANGES +++ b/CHANGES @@ -352,4 +352,6 @@ 06-Jul megacz SOAP.java: new HTTP stack +06-Jul megacz HTTP.java: total rewrite: BasicAuth, DigestAuth, KeepAlive, Pipelining. + diff --git a/src/org/xwt/HTTP.java b/src/org/xwt/HTTP.java index 526f0fc..b39240d 100644 --- a/src/org/xwt/HTTP.java +++ b/src/org/xwt/HTTP.java @@ -1,4 +1,4 @@ -// Copyright 2002 Adam Megacz, see the COPYING file for licensing [GPL]package org.xwt; +// Copyright 2002 Adam Megacz, see the COPYING file for licensing [GPL] package org.xwt; import java.net.*; @@ -6,16 +6,19 @@ import java.io.*; import java.util.*; 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 */ + /** the URL as passed to the original constructor; this is never changed */ + final String originalUrl; + + /** the URL to connect to; this is munged when the url is parsed */ URL url = null; /** the host to connect to */ @@ -36,59 +39,114 @@ public class HTTP { /** the socket's inputstream */ InputStream in = null; - /** the socket's outputstream */ - OutputStream out = null; - - /** the content-type of the data being received */ - String contentType = null; + /** the username and password portions of the URL */ + String userInfo = null; - /** the content-length of the data being recieved */ - int contentLength = 0; + /** cache of userInfo strings, keyed on originalUrl */ + private static Hashtable authCache = new Hashtable(); - /** additional headers to be transmitted */ - String headers = ""; + /** 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; /** cache for resolveAndCheckIfFirewalled() */ static Hashtable resolvedHosts = new Hashtable(); + /** if any request encounters an IOException, the entire HTTP connection is invalidated */ + boolean invalid = false; - // Constructor //////////////////////////////////////////////////////////////////////////////////////// + /** true iff we are allowed to skip the resolve check (only allowed when we're downloading the PAC script) */ + boolean skipResolveCheck = false; - public HTTP(String url) throws MalformedURLException, IOException { this(url, false); } - public HTTP(String url, boolean skipResolveCheck) throws MalformedURLException, IOException { - if (url.startsWith("https:")) { - this.url = new URL("http" + url.substring(5)); - ssl = true; - } else if (!url.startsWith("http:")) { - throw new HTTPException("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.on) Log.log(this, "creating HTTP object for connection to " + host + ":" + port); - init(); + + // Public Methods //////////////////////////////////////////////////////////////////////////////////////// + + public HTTP(String url) { this(url, false); } + public HTTP(String url, boolean skipResolveCheck) { + originalUrl = url; + this.skipResolveCheck = skipResolveCheck; } - /** this method initializes the HTTP object, resetting if needed (in case of a reconnect) */ - public void init() throws IOException { - headers = ""; - sock = null; - in = null; - out = null; - addHeader("Host", host); - 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("unable to contact host " + host); + /** Performs an HTTP GET request */ + public HTTPInputStream GET() throws IOException { return makeRequest(null, null); } + + /** 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) */ + public HTTPInputStream POST(String contentType, String content) throws IOException { return makeRequest(contentType, content); } + + /** + * 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 HTTPInputStream makeRequest(String contentType, String content) throws IOException { + + // 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) { + if (invalid) throw new HTTPException("connection failed on a previous pipelined call"); + try { + connect(); + sendRequest(contentType, content); + } catch (IOException e) { + invalid = true; + 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(); + if (invalid) throw new HTTPException("connection failed on a previous pipelined call"); + + Hashtable h = in == null ? null : parseHeaders(in); + if (h == null) { + // sometimes the server chooses to close the stream between requests + in = null; sock = null; + 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.log(this, "proxy returned an HTTP/1.0 reply with no content-length..."); + in = null; sock = null; + } 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()); + HTTPInputStream ret = new HTTPInputStream(in, cl, releaseMe); + doRelease = false; + return ret; + + } else { + throw new HTTPException("HTTP Error: " + reply); + + } + + } catch (IOException e) { invalid = true; throw e; + } finally { if (doRelease) releaseMe.release(); + } } + // Safeguarded DNS Resolver /////////////////////////////////////////////////////////////////////////// /** @@ -138,7 +196,7 @@ public class HTTP { /** Attempts a direct connection */ public Socket attemptDirect() { try { - if (Log.on) Log.log(this, "attempting to create unproxied socket to " + host + ":" + port + (ssl ? " [ssl]" : "")); + if (Log.verbose) Log.log(this, "attempting to create unproxied socket to " + host + ":" + port + (ssl ? " [ssl]" : "")); return Platform.getSocket(host, port, ssl, true); } catch (IOException e) { if (Log.on) Log.log(this, "exception in attemptDirect(): " + e); @@ -149,11 +207,11 @@ public class HTTP { /** Attempts to use an HTTP proxy, employing the CONNECT method if HTTPS is requested */ public 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.log(this, "attempting to create HTTP proxied socket using proxy " + proxyHost + ":" + proxyPort); Socket sock = Platform.getSocket(proxyHost, proxyPort, ssl, false); if (!ssl) { - path = "http://" + host + ":" + port + path; + if (!path.startsWith("http://")) path = "http://" + host + ":" + port + path; } else { PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream())); BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream())); @@ -183,8 +241,8 @@ public class HTTP { 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.log(this, "attempting to create SOCKSv4" + (addr == null ? "" : "a") + + " proxied socket using proxy " + proxyHost + ":" + proxyPort); try { Socket sock = Platform.getSocket(proxyHost, proxyPort, ssl, false); @@ -224,11 +282,11 @@ public class HTTP { /** 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"); + if (Log.verbose) Log.log(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 + "\""); + Object obj = pacFunc.call(Context.enter(), Proxy.proxyAutoConfigRootScope, null, new Object[] { url.toString(), url.getHost() }); + if (Log.verbose) Log.log(this, " PAC script returned \"" + obj + "\""); pac = obj.toString(); } catch (Throwable e) { if (Log.on) Log.log(this, "PAC script threw exception " + e); @@ -238,7 +296,7 @@ public class HTTP { 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.log(this, " trying \"" + token + "\"..."); try { Socket ret = null; if (token.startsWith("DIRECT")) @@ -261,119 +319,131 @@ public class HTTP { // Everything Else //////////////////////////////////////////////////////////////////////////// - /** returns the content-type of the reply */ - public String getContentType() throws IOException { - getInputStream(); - return contentType; - } + private synchronized void connect() throws IOException { + 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; + } - /** returns the content-length of the reply */ - public int getContentLength() throws IOException { - getInputStream(); - return contentLength; - } + 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.log(this, "creating HTTP object for connection to " + host + ":" + port); - /** 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"; + Proxy 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("unable to contact host " + host); + if (in == null) in = new BufferedInputStream(sock.getInputStream()); } - public OutputStream getOutputStream(int contentLength, String contentType) throws IOException { - if (out != null) return out; - if (in != null) throw new HTTPException("attempt to getOutputStream() after getInputStream()"); - out = sock.getOutputStream(); - PrintWriter pw = new PrintWriter(new OutputStreamWriter(out)); - pw.print("POST " + path + " HTTP/1.0\r\n"); + public void sendRequest(String contentType, String content) throws IOException { + + PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream())); + if (content != null) { + pw.print("POST " + path + " HTTP/1.1\r\n"); + pw.print("Content-Length: " + content.length() + "\r\n"); + if (contentType != null) pw.print("Content-Type: " + contentType + "\r\n"); + } else { + pw.print("GET " + path + " HTTP/1.1\r\n"); + } + pw.print("User-Agent: XWT\r\n"); - pw.print("Content-length: " + contentLength + "\r\n"); - pw.print(headers); - if (ProxyAuthorization.authorization != null) pw.print("Proxy-Authorization: " + ProxyAuthorization.authorization + "\r\n"); - if (contentType != null) pw.print("Content-Type: " + contentType + "\r\n"); - pw.print("\r\n"); + pw.print("Host: " + (host + (port == 80 ? "" : (":" + port))) + "\r\n"); - // FIXME: check for HTTP "ok, go ahead" here, in case we need proxy authorization this can happen if the xwar is - // on the local disk and the first HTTP request is through an auth-requiring proxy + if (Proxy.Authorization.authorization != null) pw.print("Proxy-Authorization: " + Proxy.Authorization.authorization2 + "\r\n"); + if (authCache.get(originalUrl) != null) pw.print("Authorization: " + authCache.get(originalUrl) + "\r\n"); + pw.print(content == null ? "\r\n" : content); + pw.print("\r\n"); pw.flush(); - return out; } - public InputStream getInputStream() throws IOException { - - if (in != null) return in; - if (out != null) { - out.flush(); - } else { - PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream())); - pw.print("GET " + path + " HTTP/1.0\r\n"); - pw.print("User-Agent: XWT\r\n"); - pw.print(headers); - System.out.print(headers); - if (ProxyAuthorization.authorization != null) pw.print("Proxy-Authorization: " + ProxyAuthorization.authorization + "\r\n"); - pw.print("\r\n"); - pw.flush(); - } - - in = new BufferedInputStream(sock.getInputStream()); - - // 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) 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; + private void doWebAuth(Hashtable h0, String method) throws IOException { + if (userInfo == null) throw new HTTPException("web server demanded username/password, but none were supplied"); + Hashtable h = parseAuthenticationChallenge(h0.get("www-authenticate").toString()); + + if (h.get("AUTHTYPE").equals("Basic")) { + if (authCache.get(originalUrl) != null) throw new HTTPException("username/password rejected"); + authCache.put(originalUrl, "Basic " + new String(Base64.encode(userInfo.getBytes("US-ASCII")))); + + } else if (h.get("AUTHTYPE").equals("Digest")) { + if (authCache.get(originalUrl) != null && !"true".equals(h.get("stale"))) throw new HTTPException("username/password rejected"); + String path2 = path; + if (path2.startsWith("http://") || path2.startsWith("https://")) { + path2 = path2.substring(path2.indexOf("://") + 3); + path2 = path2.substring(path2.indexOf('/')); } + String A1 = userInfo.substring(0, userInfo.indexOf(':')) + ":" + h.get("realm") + ":" + userInfo.substring(userInfo.indexOf(':') + 1); + String A2 = method + ":" + path2; + authCache.put(originalUrl, + "Digest " + + "username=\"" + userInfo.substring(0, userInfo.indexOf(':')) + "\", " + + "realm=\"" + h.get("realm") + "\", " + + "nonce=\"" + h.get("nonce") + "\", " + + "uri=\"" + path2 + "\", " + + (h.get("opaque") == null ? "" : ("opaque=\"" + h.get("opaque") + "\", ")) + + "response=\"" + H(H(A1) + ":" + h.get("nonce") + ":" + H(A2)) + "\", " + + "algorithm=MD5" + ); + + } else { + throw new HTTPException("unknown authentication type: " + h.get("AUTHTYPE")); } - - 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("407")) { - if (Log.on) Log.log(this, "Proxy Auth Required: HTTP " + reply); - String realm = ""; - String style = "Basic"; - - while((s = headerReader.readLine()) != null) - if (s.startsWith("Proxy-Authenticate:")) { - s = s.substring(19); - while(s.charAt(0) == ' ') s = s.substring(1); - style = s.substring(0, s.indexOf(' ')); - s = s.substring(s.indexOf(' ')); - s = s.substring(s.indexOf("realm")); - s = s.substring(s.indexOf('\"') + 1); - s = s.substring(0, s.indexOf('\"')); - realm = s; - } - - ProxyAuthorization.getPassword(realm, style, sock.getInetAddress().getHostAddress(), ProxyAuthorization.authorization); + } - // reset and re-try - init(); - return getInputStream(); + private void doProxyAuth(Hashtable h0, String method) throws IOException { + Hashtable h = parseAuthenticationChallenge(h0.get("proxy-authenticate").toString()); + String style = h.get("AUTHTYPE").toString(); + String realm = h.get("realm").toString(); - } else if (!reply.startsWith("2")) { - throw new HTTPException("HTTP Error: " + reply); - } + if (!realm.equals("Digest") || Proxy.Authorization.authorization2 == null || !"true".equals(h.get("stale"))) + Proxy.Authorization.getPassword(realm, style, sock.getInetAddress().getHostAddress(), Proxy.Authorization.authorization); - while((s = headerReader.readLine()) != null) - if (s.length() > 15 && s.substring(0, 16).equalsIgnoreCase("content-length: ")) - contentLength = Integer.parseInt(s.substring(16)); - - return in; + 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"; + } } - // HTTPException /////////////////////////////////////////////////////////////////////////////////// static class HTTPException extends IOException { @@ -381,350 +451,160 @@ public class HTTP { } - // ProxyInfo /////////////////////////////////////////////////////////////////////////////////// - - public static class ProxyInfo { - - public ProxyInfo() { } + // HTTPInputStream /////////////////////////////////////////////////////////////////////////////////// - /** the HTTP Proxy host to use */ - public String httpProxyHost = null; + /** An input stream that represents a subset of a longer input stream. Supports HTTP chunking as well */ + public class HTTPInputStream extends FilterInputStream { - /** the HTTP Proxy port to use */ - public int httpProxyPort = -1; + /** if chunking, the number of bytes remaining in this subset; otherwise the remainder of the chunk */ + private int length = 0; - /** if a seperate proxy should be used for HTTPS, this is the hostname; otherwise, httpProxyHost is used */ - public String httpsProxyHost = null; + /** this semaphore will be released when the stream is closed */ + private Semaphore releaseMe = null; - /** if a seperate proxy should be used for HTTPS, this is the port */ - public int httpsProxyPort = -1; + /** indicates that we have encountered the zero-length terminator chunk */ + boolean chunkedDone = false; - /** the SOCKS Proxy Host to use */ - public String socksProxyHost = null; + /** if we're on the first chunk, we don't pre-read a CRLF */ + boolean firstChunk = true; - /** the SOCKS Proxy Port to use */ - public int socksProxyPort = -1; + /** the length of the entire content body; -1 if chunked */ + private int contentLength = 0; + public int getContentLength() { return contentLength; } - /** hosts to be excluded from proxy use; wildcards permitted */ - public String[] excluded = null; - - /** the PAC script */ - public Function proxyAutoConfigFunction = null; + HTTPInputStream(InputStream in, int length, Semaphore releaseMe) { + super(in); + this.releaseMe = releaseMe; + this.contentLength = length; + this.length = length == -1 ? 0 : length; + } - // 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() { - ProxyInfo ret = new ProxyInfo(); - - 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(':')); + private void readChunk() throws IOException { + if (chunkedDone) return; + if (!firstChunk) super.skip(2); // CRLF + firstChunk = false; + String chunkLen = ""; + while(true) { + int i = super.read(); + if (i == -1) throw new HTTPException("encountered end of stream while reading chunk length"); + + // FIXME: handle chunking extensions + if (i == '\r') { + super.read(); // LF + break; } else { - ret.httpsProxyPort = 80; + chunkLen += (char)i; } } - - 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; + length = Integer.parseInt(chunkLen, 16); + if (length == 0) chunkedDone = true; } - // FIXME: search up from default domain - public static ProxyInfo detectProxyViaWPAD() { + public int read(byte[] b, int off, int len) throws IOException { + boolean good = false; 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); + if (length == 0 && contentLength == -1) readChunk(); + if (len > length) len = length; + int ret = super.read(b, off, len); + length -= ret; + good = true; + return ret; + } finally { + if (!good) invalid = true; } - return null; } - public static Scriptable proxyAutoConfigRootScope = new ProxyAutoConfigRootScope(); - - public static Function getProxyAutoConfigFunction(String url) { - try { - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); - BufferedReader br = new BufferedReader(new InputStreamReader(new HTTP(url, true).getInputStream())); - String s = null; - String script = ""; - while((s = br.readLine()) != null) script += s + "\n"; - if (Log.on) Log.log(ProxyInfo.class, "successfully retrieved WPAD PAC:"); - if (Log.on) Log.log(ProxyInfo.class, script); - - // MS CARP hack - Vector carpHosts = new Vector(); - for(int i=0; i= d1 && day <= d2) || - (d1 > d2 && (day >= d1 || day <= d2))) ? - Boolean.TRUE : Boolean.FALSE; - } - }; - - private static final JSFunction dateRange = new JSFunction() { - public Object call(Context cx, Scriptable thisObj, Scriptable ctorObj, Object[] args) throws JavaScriptException { - throw new JavaScriptException("XWT does not support dateRange() in PAC scripts"); - } - }; - - 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"); - } - }; + /** 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(); + // 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; + } } + 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; } - // ProxyAuthorization /////////////////////////////////////////////////////////////////////////////////// - - public static class ProxyAuthorization { - - static public String authorization = null; - static public Semaphore waitingForUser = new Semaphore(); - - // FIXME: Digest and NTLM - public static synchronized void getPassword(final String realm, final String style, final String proxyIP, String oldAuth) { - - // this handles cases where multiple threads hit the proxy auth at the same time -- all but one will block on the - // synchronized keyword. If 'authorization' changed while the thread was blocked, it means that the user entered - // a password, so we should reattempt authorization. - - if (authorization != oldAuth) return; - if (Log.on) Log.log(ProxyAuthorization.class, "displaying proxy authorization dialog"); - MessageQueue.add(new Message() { - public void perform() { - Box b = new Box("org.xwt.builtin.proxy_authorization", null); - b.put("realm", realm); - b.put("proxyIP", proxyIP); - } - }); + 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; + } - waitingForUser.block(); - if (Log.on) Log.log(ProxyAuthorization.class, "got proxy authorization info; re-attempting connection"); - + 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; } }