hack to allow setting the user agent in HTTP
[org.ibex.net.git] / src / org / ibex / net / HTTP.java
index 15ba899..c95183c 100644 (file)
@@ -4,10 +4,7 @@ package org.ibex.net;
 import java.net.*;
 import java.io.*;
 import java.util.*;
-import org.ibex.js.*;
 import org.ibex.util.*;
-import org.ibex.plat.*;
-import org.ibex.core.*;
 import org.ibex.crypto.*;
 
 /**
@@ -16,6 +13,89 @@ import org.ibex.crypto.*;
  */
 public class HTTP {
 
+    public static InetAddress originAddr = null;
+    public static String      originHost = null;
+
+    // FIXME: HACK
+    public static String userAgent = "Ibex";
+    
+    // Cookies //////////////////////////////////////////////////////////////////////////////
+
+    public static class Cookie {
+        public final String  name;
+        public final String  value;
+        public final String  domain;
+        public final String  path;
+        public final Date    expires;
+        public final boolean secure;
+        public Cookie(String name, String value, String domain, String path, Date expires, boolean secure) {
+            this.name = name;
+            this.value = value;
+            this.domain = domain;
+            this.path = path;
+            this.expires = expires;
+            this.secure = secure;
+        }
+
+        // FIXME: this could be much more efficient
+        // FIXME currently only implements http://wp.netscape.com/newsref/std/cookie_spec.html
+        public static class Jar {
+            private Hash h = new Hash();
+            public String getCookieHeader(String domain, String path, boolean secure) {
+                StringBuffer ret = new StringBuffer("Cookie: ");
+                Enumeration e = h.keys();
+                while (e.hasMoreElements()) {
+                    Vec v = (Vec)h.get(e.nextElement());
+                    Cookie cookie = null;
+                    for(int i=0; i<v.size(); i++) {
+                        Cookie c = (Cookie)v.elementAt(i);
+                        if (domain.endsWith(c.domain) &&
+                            (c.path == null || path.startsWith(c.path)) &&
+                            (cookie == null || c.domain.length() > cookie.domain.length()))
+                            cookie = c;
+                    }
+                    if (cookie != null) {
+                        ret.append(cookie.name);
+                        ret.append("=");
+                        ret.append(cookie.value);
+                        ret.append("; ");
+                    }
+                }
+                //ret.setLength(ret.length() - 2);
+                return ret.toString();
+            }
+            public void setCookie(String header, String defaultDomain) {
+                String  name       = null;
+                String  value      = null;
+                String  domain     = defaultDomain;
+                String  path       = "/";
+                Date    expires    = null;
+                boolean secure     = false;
+                StringTokenizer st = new StringTokenizer(header, ";");
+                while(st.hasMoreTokens()) {
+                    String s = st.nextToken();
+                    if (s.indexOf('=') == -1) {
+                        if (s.equals("secure")) secure = true;
+                        continue;
+                    }
+                    String start = s.substring(0, s.indexOf('='));
+                    String end   = s.substring(s.indexOf('=')+1);
+                    if (name == null) {
+                        name = start;
+                        value = end;
+                        continue;
+                    }
+                    //#switch(start.toLowerCase())
+                    case "domain":  domain = end;
+                    case "path":    path = end;
+                    case "expires": expires = new Date(end);
+                    //#end
+                }
+                if (h.get(name) == null) h.put(name, new Vec());
+                ((Vec)h.get(name)).addElement(new Cookie(name, value, domain, path, expires, secure));
+            }
+        }
+    }
 
     // Public Methods ////////////////////////////////////////////////////////////////////////////////////////
 
@@ -23,10 +103,12 @@ public class HTTP {
     public HTTP(String url, boolean skipResolveCheck) { originalUrl = url; this.skipResolveCheck = skipResolveCheck; }
 
     /** Performs an HTTP GET request */
-    public InputStream GET() throws IOException { return makeRequest(null, null); }
-
+    public InputStream GET(String referer, Cookie.Jar cookies) throws IOException {
+        return makeRequest(null, null, referer, cookies); }
+    
     /** 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); }
+    public InputStream POST(String contentType, String content, String referer, Cookie.Jar cookies) throws IOException {
+        return makeRequest(contentType, content, referer, cookies); }
 
     public static class HTTPException extends IOException { public HTTPException(String s) { super(s); } }
 
@@ -64,7 +146,7 @@ public class HTTP {
      *  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 {
+    private InputStream makeRequest(String contentType, String content, String referer, Cookie.Jar cookies) throws IOException {
 
         // Step 1: send the request and establish a semaphore to stop any requests that pipeline after us
         Semaphore blockOn = null;
@@ -72,7 +154,7 @@ public class HTTP {
         synchronized(this) {
             try {
                 connect();
-                sendRequest(contentType, content);
+                sendRequest(contentType, content, referer, cookies);
             } catch (IOException e) {
                 reset();
                 throw e;
@@ -92,13 +174,13 @@ public class HTTP {
             if (in == null)
                 throw new HTTPException("a previous pipelined call messed up the socket");
             
-            Hashtable h = in == null ? null : parseHeaders(in);
+            Hashtable h = in == null ? null : parseHeaders(in, cookies);
             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);
+                return makeRequest(contentType, content, referer, cookies);
             }
 
             String reply = h.get("STATUSLINE").toString();
@@ -116,7 +198,7 @@ public class HTTP {
                     new HTTPInputStream(in, cl, releaseMe).close();
                 }
                 releaseMe.release();
-                return makeRequest(contentType, content);
+                return makeRequest(contentType, content, referer, cookies);
                 
             } else if (reply.startsWith("2")) {
                 if (h.get("HTTP").equals("1.0") && h.get("content-length") == null)
@@ -150,7 +232,7 @@ public class HTTP {
         if (resolvedHosts.get(host) != null) return;
 
         // if all scripts are trustworthy (local FS), continue
-        if (Main.originAddr == null) return;
+        if (originAddr == null) return;
 
         // resolve using DNS
         try {
@@ -158,14 +240,16 @@ public class HTTP {
             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))
+                 (quadbyte[0] == 172 && (quadbyte[1] & 0xF0) == 16)) && !addr.equals(originAddr))
                 throw new HTTPException("security violation: " + host + " [" + addr.getHostAddress() +
                                         "] is in a firewalled netblock");
             return;
         } catch (UnknownHostException uhe) { }
 
+        /*
         if (Platform.detectProxy() == null)
             throw new HTTPException("could not resolve hostname \"" + host + "\" and no proxy configured");
+        */
     }
 
 
@@ -267,6 +351,7 @@ public class HTTP {
     }
 
     /** executes the PAC script and dispatches a call to one of the other attempt methods based on the result */
+    /*
     private Socket attemptPAC(org.ibex.js.JS pacFunc) {
         if (Log.verbose) Log.info(this, "evaluating PAC script");
         String pac = null;
@@ -301,7 +386,7 @@ public class HTTP {
         if (Log.on) Log.info(this, "all PAC results exhausted");
         return null;
     }
-
+    */
 
     // Everything Else ////////////////////////////////////////////////////////////////////////////
 
@@ -341,6 +426,7 @@ public class HTTP {
         host = temphost;
         if (Log.verbose) Log.info(this, "creating HTTP object for connection to " + host + ":" + port);
 
+        /*
         Proxy pi = Platform.detectProxy();
         OUTER: do {
             if (pi != null) {
@@ -351,13 +437,14 @@ public class HTTP {
                 if (sock == null && pi.socksProxyHost != null) sock = attemptSocksProxy(pi.socksProxyHost, pi.socksProxyPort);
             }
         } while (false);
+        */
         proxied = sock != null;
         if (sock == null) sock = attemptDirect();
         if (sock == null) throw new HTTPException("unable to contact host " + host);
         if (in == null) in = new BufferedInputStream(sock.getInputStream());
     }
 
-    private void sendRequest(String contentType, String content) throws IOException {
+    private void sendRequest(String contentType, String content, String referer, Cookie.Jar cookies) throws IOException {
         PrintWriter pw = new PrintWriter(new OutputStreamWriter(originalUrl.equals("stdio:") ?
                                                                 System.out : sock.getOutputStream()));
         if (content != null) {
@@ -370,11 +457,12 @@ public class HTTP {
         } else {
             pw.print("GET " + path + " HTTP/1.1\r\n");
         }
-        
-        pw.print("User-Agent: Ibex\r\n");
+
+        if (cookies != null) pw.print(cookies.getCookieHeader(host, path, ssl));
+        pw.print("User-Agent: " + userAgent + "\r\n");
         pw.print("Accept-encoding: gzip\r\n");
         pw.print("Host: " + (host + (port == 80 ? "" : (":" + port))) + "\r\n");
-        if (proxied) pw.print("X-RequestOrigin: " + Main.originHost + "\r\n");
+        if (proxied) pw.print("X-RequestOrigin: " + originHost + "\r\n");
 
         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");
@@ -430,11 +518,11 @@ public class HTTP {
             Proxy.Authorization.authorization2 = "NTLM " + Base64.encode(Proxy.NTLM.type1);
             return;
         }
-
+        /*
         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("UTF8")));
@@ -572,7 +660,7 @@ public class HTTP {
     // Misc Helpers ///////////////////////////////////////////////////////////////////////////////////
 
     /** 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 {
+    private Hashtable parseHeaders(InputStream in, Cookie.Jar cookies) 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
@@ -606,6 +694,7 @@ public class HTTP {
             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;
+            if (front.equals("set-cookie")) cookies.setCookie(back, host);
             ret.put(front, back);
         }
         return ret;
@@ -664,11 +753,14 @@ public class HTTP {
         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
+
+        // ** temporarily disabled so HTTP does not depend on org.ibex.js **
+        //public JS proxyAutoConfigFunction = null;            ///< the PAC script
+        public Object 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);
@@ -716,9 +808,11 @@ public class HTTP {
             }
         
             if (ret.httpProxyHost == null && ret.socksProxyHost == null) return null;
+            */
             return ret;
         }
-    
+
+        /*
         public static JSScope proxyAutoConfigRootScope = new ProxyAutoConfigRootScope();
         public static JS getProxyAutoConfigFunction(String url) {
             try { 
@@ -765,7 +859,7 @@ public class HTTP {
                 return null;
             }
         }
-
+        */
 
         // Authorization ///////////////////////////////////////////////////////////////////////////////////
 
@@ -775,6 +869,8 @@ public class HTTP {
             static public String authorization2 = null;
             static public Semaphore waitingForUser = new Semaphore();
 
+            // FIXME: temporarily disabled so we can use HTTP outside the core
+            /*
             public static synchronized void getPassword(final String realm, final String style,
                                                         final String proxyIP, String oldAuth) throws IOException {
 
@@ -799,17 +895,18 @@ public class HTTP {
                 waitingForUser.block();
                 if (Log.on) Log.info(Authorization.class, "got proxy authorization info; re-attempting connection");
             }
+            */
         }
 
 
         // ProxyAutoConfigRootJSScope ////////////////////////////////////////////////////////////////////
-
+        /*
         public static class ProxyAutoConfigRootScope extends JSScope.Global {
 
             public ProxyAutoConfigRootScope() { super(); }
         
             public Object get(Object name) throws JSExn {
-                //#switch(name)
+                // #switch(name)
                 case "isPlainHostName": return METHOD;
                 case "dnsDomainIs": return METHOD;
                 case "localHostOrDomainIs": return METHOD;
@@ -823,7 +920,7 @@ public class HTTP {
                 case "dateRange": return METHOD;
                 case "timeRange": return METHOD;
                 case "ProxyConfig": return ProxyConfig;
-                //#end
+                // #end
                 return super.get(name);
             }
         
@@ -836,7 +933,7 @@ public class HTTP {
                 };
 
             public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
-                //#switch(method)
+                // #switch(method)
                 case "isPlainHostName": return (a0.toString().indexOf('.') == -1) ? Boolean.TRUE : Boolean.FALSE;
                 case "dnsDomainIs": return (a0.toString().endsWith(a1.toString())) ? Boolean.TRUE : Boolean.FALSE;
                 case "localHostOrDomainIs":
@@ -904,7 +1001,7 @@ public class HTTP {
                     
                 case "dateRange": throw new JSExn("Ibex does not support dateRange() in PAC scripts");
                 case "timeRange": throw new JSExn("Ibex does not support timeRange() in PAC scripts");
-                //#end
+                // #end
                 return super.callMethod(method, a0, a1, a2, rest, nargs);
             }       
             private static boolean match(String[] arr, String s, int index) {
@@ -917,7 +1014,7 @@ public class HTTP {
             }
             public static String[] days = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
         }
-
+        */
 
         /**
          *  An implementation of Microsoft's proprietary NTLM authentication protocol.  This code was derived from Eric