2002/05/05 08:37:32
[org.ibex.core.git] / src / org / xwt / HTTP.java
index 075a641..6b054c5 100644 (file)
@@ -1,5 +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.*;
 import java.io.*;
@@ -30,7 +29,7 @@ public class HTTP {
     /** the path (URI) to retrieve on the server */
     String path = null;
 
-    /** the socket; created lazily */
+    /** the socket */
     Socket sock = null;
 
     /** the socket's inputstream */
@@ -45,81 +44,180 @@ public class HTTP {
     /** the content-length of the data being recieved */
     int contentLength = 0;
 
-    /** true iff a proxy should be used */
-    boolean proxy = false;
-
     /** additional headers to be transmitted */
     String headers = "";
 
-    public HTTP(String url) throws MalformedURLException, IOException {
-        if (url.startsWith("https:")) {
-            url = "http" + url.substring(5);
-            ssl = true;
-        }
-        if (!url.startsWith("http:")) throw new IOException("HTTP only supports http/https urls");
+    /** cache for resolveAndCheckIfFirewalled() */
+    static Hashtable resolvedHosts = new Hashtable();
+
+
+    // Constructor ////////////////////////////////////////////////////////////////////////////////////////
+
+    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);
-        host = 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);
+        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);
+    }
+
+
+    // 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".
+     *  @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;
+
+        // cached
+        if (resolvedHosts.get(host) != null) return (String)resolvedHosts.get(host);
+
+        if (Log.on) Log.log(this, "  resolveAndCheckIfFirewalled: resolving " + host);
+
+        // resolve using DNS
         try {
             InetAddress addr = InetAddress.getByName(host);
             byte[] quadbyte = addr.getAddress();
-            if (quadbyte[0] == 10 || (quadbyte[0] == 192 && quadbyte[1] == 168) ||
-                (quadbyte[0] == 172 && (quadbyte[1] & 0xF0) == 16) && !addr.equals(Main.originAddr))
-                throw new IOException("security violation: " + host + " [" + addr.getHostAddress() + "] is in a firewalled netblock");
-        } catch (UnknownHostException uhe) {
-            if (Platform.detectProxy() == null) throw new IOException("could not resolve hostname \"" + host + "\" and no proxy configured");
-            else if (Log.on) Log.log("could not resolve host " + host + "; assuming that the proxy can resolve it for us");
+            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();
+        } 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);
         }
     }
 
-    public String getContentType() throws IOException {
-        getInputStream();
-        return contentType;
-    }
 
-    public int getContentLength() throws IOException {
-        getInputStream();
-        return contentLength;
-    }
+    // Methods to attempt socket creation /////////////////////////////////////////////////////////////////
 
-    public void addHeader(String header, String value) throws IOException {
-        if (in != null) throw new IOException("attempt to add header after connection has been made");
-        headers += header + ": " + value + "\r\n";
+    /** Attempts a direct connection */
+    public 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);
+        } catch (IOException e) {
+            if (Log.on) Log.log(this, "exception in attemptDirect(): " + e);
+            return null;
+        }
     }
 
-    private void getSock() throws IOException {
-        ProxyInfo pi = Platform.detectProxy();
+    /** 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);
+
+            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();
+            }
+            return sock;
 
-        // unproxied
-        if (pi == null || (pi.proxyAutoConfigFunction == null && pi.socksProxyHost == null && pi.httpProxyHost == null)) {
-            if (Log.on) Log.log(this, "creating unproxied socket to " + host + ":" + port + (ssl ? " [ssl]" : ""));
-            sock = Platform.getSocket(host, port, ssl);
-            return;
+        } catch (IOException e) {
+            if (Log.on) Log.log(this, "exception in attemptHttpProxy(): " + e);
+            return null;
         }
+    }
+
+    /**
+     *  Implements SOCKSv4 with v4a DNS extension
+     *  @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) {
+
+        // 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) { }
 
-        // no PAC; simple config
-        if (pi.proxyAutoConfigFunction == null) {
-            String proxyHost = ssl && pi.httpsProxyHost != null ? pi.httpsProxyHost : pi.httpProxyHost;
-            int proxyPort = ssl && pi.httpsProxyHost != null ? pi.httpsProxyPort : pi.httpProxyPort;
-            if (Log.on) Log.log(this, "no proxyAutoConfigFunction; using proxy " + proxyHost + ":" + proxyPort);
-            sock = Platform.getSocket(proxyHost, proxyPort, ssl);
-            proxy = true;
-            return;
+        if (Log.on) 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);
+            
+            DataOutputStream dos = new DataOutputStream(sock.getOutputStream());
+            dos.writeByte(0x04);                         // SOCKSv4(a)
+            dos.writeByte(0x01);                         // CONNECT
+            dos.writeShort(port & 0xffff);               // port
+            if (addr == null) dos.writeInt(0x00000001);  // bogus IP
+            else dos.write(addr.getAddress());           // actual IP
+            dos.writeByte(0x00);                         // no userid
+            if (addr == null) {
+                PrintWriter pw = new PrintWriter(new OutputStreamWriter(dos));
+                pw.print(host);
+                pw.flush();
+                dos.writeByte(0x00);                     // hostname null terminator
+            }
+            dos.flush();
+
+            DataInputStream dis = new DataInputStream(sock.getInputStream());
+            dis.readByte();                              // reply version
+            byte success = dis.readByte();               // success/fail
+            dis.skip(6);                                 // ip/port
+            
+            if ((int)(success & 0xff) == 90) {
+                if (ssl) ((TinySSL)sock).negotiate();
+                return sock;
+            }
+            if (Log.on) Log.log(this, "SOCKS server denied access, code " + (success & 0xff));
+            return null;
+
+        } catch (IOException e) {
+            if (Log.on) Log.log(this, "exception in attemptSocksProxy(): " + e);
+            return null;
         }
-        
-        // PAC
+    }
+
+    /** 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");
         String pac = null;
         try {
-            Context cx = Context.enter();
-            Object obj = pi.proxyAutoConfigFunction.call(cx, ProxyInfo.base, null, new Object[] { url.toString(), host });
-            if (Log.on) Log.log(this, "PAC script returned \"" + obj + "\"");
+            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 + "\"");
             pac = obj.toString();
         } catch (Throwable e) {
-            if (Log.on) Log.log(this, "PAC script threw an exception:");
-            if (Log.on) Log.log(this, e);
-            throw new IOException("PAC script threw exception " + e);
+            if (Log.on) Log.log(this, "PAC script threw exception " + e);
+            return null;
         }
 
         StringTokenizer st = new StringTokenizer(pac, ";", false);
@@ -127,33 +225,51 @@ public class HTTP {
             String token = st.nextToken().trim();
             if (Log.on) Log.log(this, "  trying \"" + token + "\"...");
             try {
-                if (token.startsWith("DIRECT")) {
-                    proxy = false;
-                    sock = Platform.getSocket(host, port, ssl);
-                    return;
-                } else if (token.startsWith("PROXY")) { 
-                    proxy = true;
-                    sock = Platform.getSocket(token.substring(token.indexOf(' ') + 1, token.indexOf(':')),
-                                              Integer.parseInt(token.substring(token.indexOf(':') + 1)), ssl);
-                    return;
-                } else if (token.startsWith("SOCKS")) {
-                    // FIXME
-                }
+                Socket ret = null;
+                if (token.startsWith("DIRECT"))
+                    ret = attemptDirect();
+                else if (token.startsWith("PROXY"))
+                    ret = attemptHttpProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')),
+                                           Integer.parseInt(token.substring(token.indexOf(':') + 1)));
+                else if (token.startsWith("SOCKS"))
+                    ret = attemptSocksProxy(token.substring(token.indexOf(' ') + 1, token.indexOf(':')),
+                                            Integer.parseInt(token.substring(token.indexOf(':') + 1)));
+                if (ret != null) return ret;
             } catch (Throwable e) {
-                if (Log.on) Log.log(this, "attempt at \"" + proxy + "\" failed due to " + e + "; trying next one");
+                if (Log.on) Log.log(this, "attempt at \"" + token + "\" failed due to " + e + "; trying next token");
             }
         }
-        throw new IOException("all proxy options exhausted");
+        if (Log.on) Log.log(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;
+    }
+
+    /** 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";
     }
 
     public OutputStream getOutputStream(int contentLength, String contentType) throws IOException {
         if (out != null) return out;
-        if (in != null) throw new IOException("attempt to getOutputStream() after getInputStream()");
-        getSock();
-        sock.setTcpNoDelay(true);
+        if (in != null) throw new HTTPException("attempt to getOutputStream() after getInputStream()");
         out = sock.getOutputStream();
         PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
-        pw.print("POST " + (proxy ? url.toString() : path) + " HTTP/1.0\r\n");
+        pw.print("POST " + path + " HTTP/1.0\r\n");
         pw.print("Host: " + host + "\r\n");
         pw.print("User-Agent: XWT\r\n");
         pw.print("Content-length: " + contentLength + "\r\n");
@@ -169,10 +285,8 @@ public class HTTP {
         if (out != null) {
             out.flush();
         } else {
-            getSock();
-            sock.setTcpNoDelay(true);
             PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
-            pw.print("GET " + (proxy ? url.toString() : path) + " HTTP/1.0\r\n");
+            pw.print("GET " + path + " HTTP/1.0\r\n");
             pw.print("Host: " + host + "\r\n");
             pw.print("User-Agent: XWT\r\n");
             pw.print(headers);
@@ -188,7 +302,7 @@ public class HTTP {
         int buflen = 0;
         while(true) {
             int read = in.read();
-            if (read == -1) throw new IOException("stream closed while reading headers");
+            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) {
@@ -200,9 +314,9 @@ public class HTTP {
         
         BufferedReader headerReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, buflen)));
         String s = headerReader.readLine();
-        if (!s.startsWith("HTTP/")) throw new IOException("Expected reply to start with \"HTTP/\"");
+        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 IOException("HTTP Error: " + reply);
+        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));
@@ -211,6 +325,17 @@ public class HTTP {
         return in;
     }
 
+
+
+    // HTTPException ///////////////////////////////////////////////////////////////////////////////////
+
+    static class HTTPException extends IOException {
+        public HTTPException(String s) { super(s); }
+    }
+
+
+    // ProxyInfo ///////////////////////////////////////////////////////////////////////////////////
+
     public static class ProxyInfo {
 
         public ProxyInfo() { }
@@ -241,8 +366,9 @@ public class HTTP {
 
         public static ProxyInfo detectProxyViaManual() {
             try {
-                try { InetAddress.getByName("xwt-proxy-httpHost");
-                } catch (UnkownHostException unhe) { InetAddress.getByName("xwt-proxy-socksHost"); }
+                // 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();
@@ -263,7 +389,7 @@ public class HTTP {
                 } catch (UnknownHostException e) { }
                 return ret;
             } catch (UnknownHostException e) {
-                if (Log.on) Log.log(Platform.class, "xwt-proxy-* detection failed due to:");
+                if (Log.on) Log.log(Platform.class, "xwt-proxy-* detection failed due to: " + e);
                 return null;
             }
         }
@@ -288,7 +414,7 @@ public class HTTP {
             try { 
                 Context cx = Context.enter();
                 cx.setOptimizationLevel(-1);
-                BufferedReader br = new BufferedReader(new InputStreamReader(new HTTP(url).getInputStream()));
+                BufferedReader br = new BufferedReader(new InputStreamReader(new HTTP(url, true).getInputStream()));
                 String s = null;
                 String script = "";
                 while((s = br.readLine()) != null) script += s + "\n";