Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Unified Diff: src/org/adblockplus/brazil/RequestHandler.java

Issue 8484110: ABP/Android proxy service (Closed)
Patch Set: ABP/Android proxy service Created Nov. 1, 2012, 9:46 a.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: src/org/adblockplus/brazil/RequestHandler.java
===================================================================
new file mode 100644
--- /dev/null
+++ b/src/org/adblockplus/brazil/RequestHandler.java
@@ -0,0 +1,416 @@
+package org.adblockplus.brazil;
+
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.ConnectException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Properties;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+import org.adblockplus.ChunkedOutputStream;
+import org.adblockplus.android.AdblockPlus;
+import org.literateprograms.BoyerMoore;
+
+import sunlabs.brazil.server.Handler;
+import sunlabs.brazil.server.Request;
+import sunlabs.brazil.server.Server;
+import sunlabs.brazil.util.MatchString;
+import sunlabs.brazil.util.http.HttpInputStream;
+import sunlabs.brazil.util.http.HttpRequest;
+import sunlabs.brazil.util.http.MimeHeaders;
+import android.util.Log;
+
+/**
+ * The <code>RequestHandler</code> implements a proxy service optionally
+ * modifying output.
+ * The following configuration parameters are used to initialize this
+ * <code>Handler</code>:
+ * <dl class=props>
+ *
+ * <dt>prefix, suffix, glob, match
+ * <dd>Specify the URL that triggers this handler. (See {@link MatchString}).
+ * <dt>auth
+ * <dd>The value of the proxy-authenticate header (if any) sent to the upstream
+ * proxy
+ * <dt>proxyHost
+ * <dd>If specified, the name of the upstream proxy
+ * <dt>proxyPort
+ * <dd>The upstream proxy port, if a proxyHost is specified (defaults to 80)
+ * <dt>proxylog
+ * <dd>If set all http headers will be logged to the console. This is for
+ * debugging.
+ *
+ * </dl>
+ *
+ * A sample set of configuration parameters illustrating how to use this
+ * handler follows:
+ *
+ * <pre>
+ * handler=adblock
+ * adblock.class=org.adblockplus.brazil.RequestHandler
+ * </pre>
+ *
+ * See the description under {@link sunlabs.brazil.server.Handler#respond
+ * respond} for a more detailed explanation.
+ */
+
+public class RequestHandler implements Handler
+{
+ public static final String PROXY_HOST = "proxyHost";
+ public static final String PROXY_PORT = "proxyPort";
+ public static final String AUTH = "auth";
+
+ private AdblockPlus application;
+ private String prefix;
+
+ private String via;
+
+ private String proxyHost;
+ private int proxyPort = 80;
+ private String auth;
+
+ private boolean shouldLog; // if true, log all headers
Felix Dahlke 2012/11/09 06:04:32 Maybe call it shouldLogHeaders then? That'd make t
Andrey Novikov 2012/11/09 09:23:47 Done.
+
+ @Override
+ public boolean init(Server server, String prefix)
+ {
+ this.prefix = prefix;
+ application = AdblockPlus.getApplication();
+
+ Properties props = server.props;
+
+ proxyHost = props.getProperty(prefix + PROXY_HOST);
+
+ String s = props.getProperty(prefix + PROXY_PORT);
+ try
+ {
+ proxyPort = Integer.decode(s).intValue();
+ }
+ catch (Exception e)
+ {
Felix Dahlke 2012/11/09 06:04:32 It seems this case occurs when the port is not set
Andrey Novikov 2012/11/09 09:23:47 Done.
+ }
+
+ auth = props.getProperty(prefix + AUTH);
+
+ shouldLog = (props.getProperty(prefix + "proxylog") != null);
+
+ via = " " + server.hostName + ":" + server.listen.getLocalPort() + " (" + server.name + ")";
+
+ return true;
+ }
+
+ @Override
+ public boolean respond(Request request) throws IOException
+ {
+ boolean block = false;
+ String reqHost = null;
+ String refHost = null;
+
+ try
+ {
+ reqHost = (new URL(request.url)).getHost();
+ refHost = (new URL(request.getRequestHeader("referer"))).getHost();
+ }
+ catch (MalformedURLException e)
+ {
Felix Dahlke 2012/11/09 06:04:32 Can we really safely go on in this case? At least
Andrey Novikov 2012/11/09 09:23:47 We are transparent, it's not our deal if it's malf
Felix Dahlke 2012/11/09 14:40:46 Hm, but should we just go on with a null reqHost a
Andrey Novikov 2012/11/12 08:53:12 In fact this exception is for referrer, request ca
+ }
+
+ try
+ {
+ block = application.matches(request.url, request.query, reqHost, refHost, request.getRequestHeader("accept"));
+ }
+ catch (Exception e)
+ {
+ Log.e(prefix, "Filter error", e);
+ }
+
+ request.log(Server.LOG_LOG, prefix, block + ": " + request.url);
+
+ if (block)
+ {
+ request.sendError(502, "Blocked by Adblock Plus");
+ return true;
+ }
+
+ // Do not further process non-http requests
+ if (request.url.startsWith("http:") == false && request.url.startsWith("https:") == false)
Felix Dahlke 2012/11/09 06:04:32 How about if (!request.url.matches("^https?:")) ?
Andrey Novikov 2012/11/09 09:23:47 I thought it would be faster...
Felix Dahlke 2012/11/09 14:40:46 Premature optimisation? :) I think you should go w
Andrey Novikov 2012/11/12 08:53:12 Done.
+ {
+ return false;
+ }
+
+ String url = request.url;
+
+ if ((request.query != null) && (request.query.length() > 0))
+ {
+ url += "?" + request.query;
+ }
+
+ int count = request.server.requestCount;
+ if (shouldLog)
+ {
+ System.err.println(dumpHeaders(count, request, request.headers, true));
+ }
+
+ /*
+ * "Proxy-Connection" may be used (instead of just "Connection")
+ * to keep alive a connection between a client and this proxy.
+ */
+ String pc = request.headers.get("Proxy-Connection");
+ if (pc != null)
+ {
+ request.connectionHeader = "Proxy-Connection";
+ request.keepAlive = pc.equalsIgnoreCase("Keep-Alive");
+ }
+
+ HttpRequest.removePointToPointHeaders(request.headers, false);
+
+ HttpRequest target = new HttpRequest(url);
+ try
+ {
+ target.setMethod(request.method);
+ request.headers.copyTo(target.requestHeaders);
+
+ if (proxyHost != null)
+ {
+ target.setProxy(proxyHost, proxyPort);
+ if (auth != null)
+ {
+ target.requestHeaders.add("Proxy-Authorization", auth);
+ }
+ }
+
+ if (request.postData != null)
+ {
+ OutputStream out = target.getOutputStream();
+ out.write(request.postData);
+ out.close();
+ }
+
+ target.connect();
+
+ if (shouldLog)
+ {
+ System.err.println(" " + target.status + "\n" + dumpHeaders(count, request, target.responseHeaders, false));
+ }
+ HttpRequest.removePointToPointHeaders(target.responseHeaders, true);
+
+ request.setStatus(target.getResponseCode());
+ target.responseHeaders.copyTo(request.responseHeaders);
+ try
+ {
+ request.responseHeaders.add("Via", target.status.substring(0, 8) + via);
+ }
+ catch (StringIndexOutOfBoundsException e)
+ {
+ request.responseHeaders.add("Via", via);
+ }
+
+ // Detect if we need to add ElemHide filters
+ String type = request.responseHeaders.get("Content-Type");
+
+ String selectors = null;
+ if (type != null && type.toLowerCase().startsWith("text/html"))
+ {
+ selectors = application.getSelectorsForDomain(reqHost);
+ }
+ // If no filters are applicable just pass through the response
+ if (selectors == null || target.getResponseCode() != 200)
+ {
+ int contentLength = target.getContentLength();
+ if (contentLength == 0)
+ {
+ // we do not use request.sendResponse to avoid arbitrary
+ // 200 -> 204 response code conversion
+ request.sendHeaders(-1, null, -1);
+ }
+ else
+ {
+ request.sendResponse(target.getInputStream(), contentLength, null, -1);
+ }
+ }
+ // Insert filters otherwise
+ else
+ {
+ HttpInputStream his = target.getInputStream();
+ int size = target.getContentLength();
+ if (size < 0)
+ {
+ size = Integer.MAX_VALUE;
+ }
+
+ FilterInputStream in = null;
+ FilterOutputStream out = null;
+
+ // Detect if content needs decoding
+ String encodingHeader = request.responseHeaders.get("Content-Encoding");
+ if (encodingHeader != null)
+ {
+ encodingHeader = encodingHeader.toLowerCase();
+ if (encodingHeader.equals("gzip") || encodingHeader.equals("x-gzip"))
+ {
+ in = new GZIPInputStream(his);
+ }
+ else if (encodingHeader.equals("compress") || encodingHeader.equals("x-compress"))
+ {
+ in = new InflaterInputStream(his);
+ }
+ else
+ {
+ // Unsupported encoding, proxy content as-is
+ in = his;
+ out = request.out;
+ selectors = null;
+ }
+ }
+ else
+ {
+ in = his;
+ }
+ // Use chunked encoding when injecting filters in page
+ if (out == null)
+ {
+ request.responseHeaders.remove("Content-Length");
+ request.responseHeaders.remove("Content-Encoding");
+ out = new ChunkedOutputStream(request.out);
+ request.responseHeaders.add("Transfer-Encoding", "chunked");
+ size = Integer.MAX_VALUE;
+ }
+
+ request.sendHeaders(-1, null, -1);
+
+ byte[] buf = new byte[Math.min(4096, size)];
+
+ Log.e(prefix, request.url);
Felix Dahlke 2012/11/09 06:04:32 Log.d()?
Andrey Novikov 2012/11/09 09:23:47 Done.
+
+ boolean sent = selectors == null;
+ // TODO Do we need to set encoding here?
+ BoyerMoore matcher = new BoyerMoore("<html".getBytes());
+
+ while (size > 0)
+ {
+ out.flush();
+
+ count = in.read(buf, 0, Math.min(buf.length, size));
+ if (count < 0)
+ {
+ break;
+ }
+ size -= count;
+ try
+ {
+ // Search for <html> tag
+ if (!sent && count > 0)
+ {
+ List<Integer> matches = matcher.match(buf, 0, count);
+ if (!matches.isEmpty())
+ {
+ // TODO Do we need to set encoding here?
+ byte[] addon = selectors.getBytes();
+ // Add filters right before match
+ int m = matches.get(0);
+ out.write(buf, 0, m);
+ out.write(addon);
+ out.write(buf, m, count - m);
+ sent = true;
+ continue;
+ }
+ }
+ out.write(buf, 0, count);
+ }
+ catch (IOException e)
+ {
+ break;
+ }
+ }
+ // The correct way would be to close ChunkedOutputStream
+ // but we can not do it because underlying output stream is
+ // used later in caller code. So we use this ugly hack:
+ try
+ {
+ ((ChunkedOutputStream) out).writeFinalChunk();
Felix Dahlke 2012/11/09 06:04:32 Why use instanceof? That would avoid class cast ex
Andrey Novikov 2012/11/09 09:23:47 Done.
+ }
+ catch (ClassCastException e)
+ {
+ // ignore
+ }
+ }
+ }
+ catch (InterruptedIOException e)
+ {
+ /*
+ * Read timeout while reading from the remote side. We use a
+ * read timeout in case the target never responds.
+ */
+ request.sendError(408, "Timeout / No response");
+ }
+ catch (EOFException e)
+ {
+ request.sendError(500, "No response");
+ }
+ catch (UnknownHostException e)
+ {
+ request.sendError(500, "Unknown host");
+ }
+ catch (ConnectException e)
+ {
+ request.sendError(500, "Connection refused");
+ }
+ catch (IOException e)
+ {
+ /*
+ * An IOException will happen if we can't communicate with the
+ * target or the client. Rather than attempting to discriminate,
+ * just send an error message to the client, and let the send
+ * fail if the client was the one that was in error.
+ */
+
+ String msg = "Error from proxy";
+ if (e.getMessage() != null)
+ {
+ msg += ": " + e.getMessage();
+ }
+ request.sendError(500, msg);
+ Log.e(prefix, msg, e);
+ }
+ finally
+ {
+ target.close();
+ }
+ return true;
+ }
+
+ /**
+ * Dump the headers on stderr
+ */
+ public static String dumpHeaders(int count, Request request, MimeHeaders headers, boolean sent)
+ {
+ String prompt;
+ StringBuffer sb = new StringBuffer();
+ String label = " " + count;
+ label = label.substring(label.length() - 4);
+ if (sent)
+ {
+ prompt = label + "> ";
+ sb.append(prompt).append(request.toString()).append("\n");
+ }
+ else
+ {
+ prompt = label + "< ";
+ }
+
+ for (int i = 0; i < headers.size(); i++)
+ {
+ sb.append(prompt).append(headers.getKey(i));
+ sb.append(": ").append(headers.get(i)).append("\n");
+ }
+ return (sb.toString());
+ }
+}

Powered by Google App Engine
This is Rietveld