/** * 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 uk.ac.cam.cl.dtg.android.tor.Shadow; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; 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 org.apache.http.Header; import org.apache.http.HttpEntity; 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.conn.ClientConnectionManager; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; 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.cookie.BrowserCompatSpec; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import uk.ac.cam.cl.dtg.android.tor.Shadow.workaround.MyThreadSafeClientConnManager; import android.webkit.PluginData; /** * Provides HTTP request functionality for the Shadow browser. * Performs requests through a SOCKS proxy. * @author cmg47 * */ public class AnonProxy { private int mPort = 0; 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 CookieManager mCookieManager = CookieManager.getInstance(); // Settings private boolean mSendReferrer = true; private ArrayList<HttpRequestBase> mLatestRequests = new ArrayList<HttpRequestBase>(); /** * Set the port for the HTTP proxy * @param port */ public void setPort(int port) { if (this.mPort != port) { // Proxy port has changed so set up appropriate HttpClient this.mPort = port; SchemeRegistry supportedSchemes = new SchemeRegistry(); supportedSchemes.register(new Scheme("http", new SocksSocketFactory("127.0.0.1", port), 80)); // SSL support is broken at the moment supportedSchemes.register(new Scheme("https", SSLSocketFactory.getSocketFactory("127.0.0.1", port), 443)); // prepare parameters HttpParams params = new BasicHttpParams(); HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); HttpProtocolParams.setContentCharset(params, "UTF-8"); HttpProtocolParams.setUseExpectContinue(params, true); ClientConnectionManager ccm = new MyThreadSafeClientConnManager(params, supportedSchemes); mClient = new DefaultHttpClient(ccm, params); } } /** * 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(); boolean isPost = false; HttpRequestBase g = null; // POST processing try { boolean makePost = false; URI u = new URI(url); String query = u.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(url); p.setEntity(new StringEntity(query)); g = p; isPost = true; } } } catch (URISyntaxException 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(url); } 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(g); //Log.d("AnonProxy", "Execution done"); URI requestUri = 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; } }