| Index: src/org/adblockplus/brazil/RequestHandler.java | 
| =================================================================== | 
| new file mode 100644 | 
| --- /dev/null | 
| +++ b/src/org/adblockplus/brazil/RequestHandler.java | 
| @@ -0,0 +1,410 @@ | 
| +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 shouldLogHeaders; | 
| + | 
| + @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) | 
| + { | 
| + // use default port | 
| + } | 
| + | 
| + auth = props.getProperty(prefix + AUTH); | 
| + | 
| + shouldLogHeaders = (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) | 
| + { | 
| + // We are transparent, it's not our deal if it's malformed. | 
| + } | 
| + | 
| + 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); | 
| + | 
| + int count = request.server.requestCount; | 
| + if (shouldLogHeaders) | 
| + { | 
| + System.err.println(dumpHeaders(count, request, request.headers, true)); | 
| 
 
Felix Dahlke
2012/11/09 14:40:46
Haven't seen this code before, it was further down
 
Andrey Novikov
2012/11/12 08:53:16
For the same reason. It's not Android specific dom
 
 | 
| + } | 
| + | 
| + if (block) | 
| + { | 
| + request.sendHeaders(204, null, 0); | 
| 
 
Felix Dahlke
2012/11/09 14:40:46
This part is new, I presume it wasn't to address a
 
Andrey Novikov
2012/11/12 08:53:16
No, it's a fix I introduced. I can not wait for re
 
 | 
| + return true; | 
| + } | 
| + | 
| + // Do not further process non-http requests | 
| + if (request.url.startsWith("http:") == false && request.url.startsWith("https:") == false) | 
| + { | 
| + return false; | 
| + } | 
| + | 
| + String url = request.url; | 
| + | 
| + if ((request.query != null) && (request.query.length() > 0)) | 
| + { | 
| + url += "?" + request.query; | 
| + } | 
| + | 
| + /* | 
| + * "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 (shouldLogHeaders) | 
| + { | 
| + 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)]; | 
| + | 
| + 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: | 
| + if (out instanceof ChunkedOutputStream) | 
| + ((ChunkedOutputStream) out).writeFinalChunk(); | 
| + } | 
| + } | 
| + 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()); | 
| + } | 
| +} |