/** * Shadow - Anonymous web browser for Android devices * Copyright (C) 2009 Connell Gauld * * Thanks to University of Cambridge, * Alastair Beresford and Andrew Rice * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package info.guardianproject.browser; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpVersion; import org.apache.http.StatusLine; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.params.ConnRoutePNames; import org.apache.http.conn.routing.HttpRoute; import org.apache.http.conn.routing.HttpRoutePlanner; import org.apache.http.conn.routing.RouteInfo; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.cookie.Cookie; import org.apache.http.cookie.CookieOrigin; import org.apache.http.cookie.MalformedCookieException; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.impl.cookie.BrowserCompatSpec; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import org.apache.http.protocol.HttpContext; import android.util.Log; /** * Provides HTTP request functionality for the Shadow browser. * Performs requests through a SOCKS proxy. * @author cmg47 * */ public class AnonProxy { private DefaultHttpClient mClient = null; // The PostProcessor is used to rewrite POST forms as GET private PostProcessor mPostProcessor = new PostProcessor(); private CacheManager mCacheManager = CacheManager.getCacheManager(); private BrowserCompatSpec mCookieSpec = new BrowserCompatSpec(); private CookieDomainManager mCookieManager = CookieDomainManager.getInstance(); // Settings private boolean mSendReferrer = true; private ArrayList<HttpRequestBase> mLatestRequests = new ArrayList<HttpRequestBase>(); /** * Set the port for the HTTP proxy * @param port */ public AnonProxy () { HttpHost proxy = new HttpHost(Browser.DEFAULT_PROXY_HOST, Integer.parseInt(Browser.DEFAULT_PROXY_PORT), "http"); SchemeRegistry supportedSchemes = new SchemeRegistry(); // Register the "http" and "https" protocol schemes, they are // required by the default operator to look up socket factories. supportedSchemes.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); supportedSchemes.register(new Scheme("https", ModSSLSocketFactory.getSocketFactory(), 443)); // prepare parameters HttpParams hparams = new BasicHttpParams(); HttpProtocolParams.setVersion(hparams, HttpVersion.HTTP_1_1); HttpProtocolParams.setContentCharset(hparams, "UTF-8"); HttpProtocolParams.setUseExpectContinue(hparams, true); ClientConnectionManager ccm = new ThreadSafeClientConnManager(hparams, supportedSchemes); mClient = new DefaultHttpClient(ccm, hparams); mClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); } public HttpHost makeHttpHost (String url) { String TAG = "AnonProxy"; URI rURI = null; try { rURI = new URI(url); } catch (URISyntaxException e) { Log.e("AnonProxy","error parsing uri: " + url,e); return null; } int port = rURI.getPort(); if (port == -1) { if (rURI.getScheme().equalsIgnoreCase("http")) port = 80; else if (rURI.getScheme().equalsIgnoreCase("https")) port = 443; } return new HttpHost(rURI.getHost(),port, rURI.getScheme()); } /** * Perform an HTTP request * @param url the URL to get * @param headers the request headers * @return structure containing the response * @throws ClientProtocolException * @throws IOException */ public HttpResponse getHttpResponse (String url, Map<String, String> headers) throws Exception { //if (true) throw new Exception("Aaaaahh"); //Log.w("AnonProxy", "Using port " + mPort); // If the port hasn't been set don't allow any requests to be made if (mClient == null) throw new IOException(); Log.i("Orweb","fetching: " + url); boolean isPost = false; HttpRequestBase g = null; HttpHost host = makeHttpHost(url); URI uri = new URI(url); // POST processing try { boolean makePost = false; String query = uri.getQuery(); if (query != null) { // There is a querystring. Search for magic POST identifier String[] pairs = query.split("&"); for (int i=0; i<pairs.length; i++) { String[] thisPair = pairs[i].split("="); if (thisPair.length == 2) { if (mPostProcessor.isPostProcessorIdentifier(thisPair[0], thisPair[1])) { makePost = true; break; } } } // If this was supposed to be a POST, turn it into one if (makePost) { HttpPost p = new HttpPost(uri.getPath()); p.setEntity(new StringEntity(query)); g = p; isPost = true; } } } catch (Exception e1) { // Not much we can do but just send the request... } CacheObject cacheObj = null; Date requestTime = new Date(); // If we're not doing a POST, we're doing a GET if (g == null) { g = new HttpGet(uri.getPath()); } synchronized(mLatestRequests) { mLatestRequests.add(g); } // Check cookie sending boolean acceptCookies = mCookieManager.sendCookiesFor(url); // Add headers if (headers != null) { Iterator<Map.Entry<String, String>> i = headers.entrySet().iterator(); while(i.hasNext()) { Map.Entry<String, String> entry = i.next(); String key = entry.getKey(); String lowercaseKey = key.toLowerCase(); if (lowercaseKey.equals("cookie")) { if (!acceptCookies) { //Log.i("AnonProxy", "Not sending cookie: " + entry.getValue()); break; } } else if (lowercaseKey.equals("referer")) { if (!mSendReferrer) { //Log.d("AnonProxy", "Referrer stripped"); break; } } //Log.d("AnonProxy", entry.getKey() + ": " + entry.getValue()); g.setHeader(entry.getKey(), entry.getValue()); } } // Set conditional headers if required by the cache if ((!isPost) && (cacheObj != null)) { String[] conditionalHeader = cacheObj.getConditionalHeader(); g.setHeader(conditionalHeader[0], conditionalHeader[1]); } HttpResponse r; mClient.getCookieStore().clear(); r = mClient.execute(host,g); return r; } /** * Perform an HTTP request * @param url the URL to get * @param headers the request headers * @return structure containing the response * @throws ClientProtocolException * @throws IOException */ /* public PluginData get(String url, Map<String, String> headers) throws Exception { //if (true) throw new Exception("Aaaaahh"); //Log.w("AnonProxy", "Using port " + mPort); // If the port hasn't been set don't allow any requests to be made if (mClient == null) throw new IOException(); Log.i("Orweb","fetching: " + url); boolean isPost = false; HttpRequestBase g = null; HttpHost host = makeHttpHost(url); URI uri = new URI(url); // POST processing try { boolean makePost = false; String query = uri.getQuery(); if (query != null) { // There is a querystring. Search for magic POST identifier String[] pairs = query.split("&"); for (int i=0; i<pairs.length; i++) { String[] thisPair = pairs[i].split("="); if (thisPair.length == 2) { if (mPostProcessor.isPostProcessorIdentifier(thisPair[0], thisPair[1])) { makePost = true; break; } } } // If this was supposed to be a POST, turn it into one if (makePost) { HttpPost p = new HttpPost(uri.getPath()); p.setEntity(new StringEntity(query)); g = p; isPost = true; } } } catch (Exception e1) { // Not much we can do but just send the request... } CacheObject cacheObj = null; Date requestTime = new Date(); // If we're not doing a POST, we're doing a GET if (g == null) { // Check if this is in the cache // Never cache POST requests cacheObj = mCacheManager.getCacheObject(url); if (cacheObj != null) { if (!cacheObj.isStale(requestTime)) { // Can serve directly from cache //Log.i("AnonProxy", "Served directly from cache" + url); return new PluginData(cacheObj.getNewInputStream(), cacheObj.getContentLength(), cacheObj.getHeaders(), cacheObj.getStatus()); } } g = new HttpGet(uri.getPath()); } synchronized(mLatestRequests) { mLatestRequests.add(g); } // Check cookie sending boolean acceptCookies = mCookieManager.sendCookiesFor(url); // Add headers if (headers != null) { Iterator<Map.Entry<String, String>> i = headers.entrySet().iterator(); while(i.hasNext()) { Map.Entry<String, String> entry = i.next(); String key = entry.getKey(); String lowercaseKey = key.toLowerCase(); if (lowercaseKey.equals("cookie")) { if (!acceptCookies) { //Log.i("AnonProxy", "Not sending cookie: " + entry.getValue()); break; } } else if (lowercaseKey.equals("referer")) { if (!mSendReferrer) { //Log.d("AnonProxy", "Referrer stripped"); break; } } //Log.d("AnonProxy", entry.getKey() + ": " + entry.getValue()); g.setHeader(entry.getKey(), entry.getValue()); } } // Set conditional headers if required by the cache if ((!isPost) && (cacheObj != null)) { String[] conditionalHeader = cacheObj.getConditionalHeader(); g.setHeader(conditionalHeader[0], conditionalHeader[1]); } HttpResponse r; mClient.getCookieStore().clear(); r = mClient.execute(host,g); //Log.d("AnonProxy", "Execution done"); URI requestUri = uri;// g.getURI(); int port = requestUri.getPort(); if (port == -1) port = 80; // TODO fix last parameter for HTTPS CookieOrigin origin = new CookieOrigin(requestUri.getHost(), port, requestUri.getPath(), false); // Package up the response headers for PluginData HashMap<String, String[]> rpHeadersMap = new HashMap<String, String[]>(); Header[] rpHeaders = r.getAllHeaders(); for (int i=0; i<rpHeaders.length; i++) { Header c = rpHeaders[i]; String[] value = new String[2]; value[0] = c.getName(); value[1] = c.getValue(); String lowerCaseHeader = value[0].toLowerCase(); boolean returnThisHeader = true; if ((lowerCaseHeader.equals("set-cookie")) ||(lowerCaseHeader.equals("set-cookie2"))) { try { List<Cookie> cookies = mCookieSpec.parse(c, origin); int size = cookies.size(); for (int z = 0; z<size; z++) { Cookie cookie = cookies.get(z); if (!mCookieManager.setCookieForDomain(cookie.getDomain())) { returnThisHeader = false; mCookieManager.cookieBlocked(cookie, value[1], url); } } } catch (MalformedCookieException e1) { returnThisHeader = false; } } if (returnThisHeader) rpHeadersMap.put(lowerCaseHeader, value); } StatusLine stat = r.getStatusLine(); //Log.d("AnonProxy", "Statusline got"); if (stat.getStatusCode() == 304) { //Log.i("AnonProxy", "Not modified so serving from cache: " + url); // Not modified so serve from cache return new PluginData(cacheObj.getNewInputStream(), cacheObj.getContentLength(), cacheObj.getHeaders(), cacheObj.getStatus()); } HttpEntity e = r.getEntity(); InputStream content = null; Header type = null; long contentLength = 0; if (e != null) { type = e.getContentType(); content = e.getContent(); // Perform POST rewriting, if appropriate if (type != null) { if (PostProcessor.canProcessMime(type.getValue())) { content = mPostProcessor.rewriteIncoming(content); } } ByteArrayOutputStream outS = new ByteArrayOutputStream(); InputStream inS = content; byte[] buffer = new byte[498]; int read = 0; while (read != -1) { outS.write(buffer, 0, read); read = inS.read(buffer); } // Grab back all of the data as an array buffer = outS.toByteArray(); contentLength = buffer.length; Date responseTime = new Date(); // Let's cache it //Log.i("AnonProxy", "Adding to cache: " + url); cacheObj = new CacheObject(url, rpHeadersMap, buffer, stat.getStatusCode(), requestTime, responseTime); content = cacheObj.getNewInputStream(); mCacheManager.addCacheObject(url, cacheObj); } return new PluginData(content, contentLength, rpHeadersMap, stat.getStatusCode()); }*/ public void stop() { synchronized(mLatestRequests) { int size = mLatestRequests.size(); for (int i=0; i<size; i++) { try { mLatestRequests.get(i).abort(); } catch (Exception e) { // Well, we tried } } mLatestRequests.clear(); } } public void setSendReferrer(boolean value) { this.mSendReferrer = value; } }