| OLD | NEW | 
| (Empty) |  | 
 |    1 package org.adblockplus.brazil; | 
 |    2  | 
 |    3 import java.io.EOFException; | 
 |    4 import java.io.FilterInputStream; | 
 |    5 import java.io.FilterOutputStream; | 
 |    6 import java.io.IOException; | 
 |    7 import java.io.InterruptedIOException; | 
 |    8 import java.io.OutputStream; | 
 |    9 import java.net.ConnectException; | 
 |   10 import java.net.MalformedURLException; | 
 |   11 import java.net.URL; | 
 |   12 import java.net.UnknownHostException; | 
 |   13 import java.util.List; | 
 |   14 import java.util.regex.Pattern; | 
 |   15 import java.util.zip.GZIPInputStream; | 
 |   16 import java.util.zip.InflaterInputStream; | 
 |   17  | 
 |   18 import org.adblockplus.ChunkedOutputStream; | 
 |   19 import org.adblockplus.android.AdblockPlus; | 
 |   20 import org.literateprograms.BoyerMoore; | 
 |   21  | 
 |   22 import sunlabs.brazil.server.Request; | 
 |   23 import sunlabs.brazil.server.Server; | 
 |   24 import sunlabs.brazil.util.MatchString; | 
 |   25 import sunlabs.brazil.util.http.HttpInputStream; | 
 |   26 import sunlabs.brazil.util.http.HttpRequest; | 
 |   27 import sunlabs.brazil.util.http.MimeHeaders; | 
 |   28 import android.util.Log; | 
 |   29  | 
 |   30 /** | 
 |   31  * The <code>RequestHandler</code> implements a proxy service optionally | 
 |   32  * modifying output. | 
 |   33  * The following configuration parameters are used to initialize this | 
 |   34  * <code>Handler</code>: | 
 |   35  * <dl class=props> | 
 |   36  *  | 
 |   37  * <dt>prefix, suffix, glob, match | 
 |   38  * <dd>Specify the URL that triggers this handler. (See {@link MatchString}). | 
 |   39  * <dt>auth | 
 |   40  * <dd>The value of the proxy-authenticate header (if any) sent to the upstream | 
 |   41  * proxy | 
 |   42  * <dt>proxyHost | 
 |   43  * <dd>If specified, the name of the upstream proxy | 
 |   44  * <dt>proxyPort | 
 |   45  * <dd>The upstream proxy port, if a proxyHost is specified (defaults to 80) | 
 |   46  * <dt>proxylog | 
 |   47  * <dd>If set all http headers will be logged to the console. This is for | 
 |   48  * debugging. | 
 |   49  *  | 
 |   50  * </dl> | 
 |   51  *  | 
 |   52  * A sample set of configuration parameters illustrating how to use this | 
 |   53  * handler follows: | 
 |   54  *  | 
 |   55  * <pre> | 
 |   56  * handler=adblock | 
 |   57  * adblock.class=org.adblockplus.brazil.RequestHandler | 
 |   58  * </pre> | 
 |   59  *  | 
 |   60  * See the description under {@link sunlabs.brazil.server.Handler#respond | 
 |   61  * respond} for a more detailed explanation. | 
 |   62  */ | 
 |   63  | 
 |   64 public class RequestHandler extends BaseRequestHandler | 
 |   65 { | 
 |   66   private AdblockPlus application; | 
 |   67   private String via; | 
 |   68   private static Pattern RE_HTTP = Pattern.compile("^https?:"); | 
 |   69  | 
 |   70   private boolean shouldLogHeaders; | 
 |   71  | 
 |   72   @Override | 
 |   73   public boolean init(Server server, String prefix) | 
 |   74   { | 
 |   75     super.init(server, prefix); | 
 |   76  | 
 |   77     application = AdblockPlus.getApplication(); | 
 |   78     shouldLogHeaders = (server.props.getProperty(prefix + "proxylog") != null); | 
 |   79     via = " " + server.hostName + ":" + server.listen.getLocalPort() + " (" + se
     rver.name + ")"; | 
 |   80  | 
 |   81     return true; | 
 |   82   } | 
 |   83  | 
 |   84   @Override | 
 |   85   public boolean respond(Request request) throws IOException | 
 |   86   { | 
 |   87     boolean block = false; | 
 |   88     String reqHost = null; | 
 |   89     String refHost = null; | 
 |   90  | 
 |   91     String referrer = request.getRequestHeader("referer"); | 
 |   92      | 
 |   93     try | 
 |   94     { | 
 |   95       reqHost = (new URL(request.url)).getHost(); | 
 |   96       if (referrer != null) | 
 |   97         refHost = (new URL(referrer)).getHost(); | 
 |   98     } | 
 |   99     catch (MalformedURLException e) | 
 |  100     { | 
 |  101       // We are transparent, it's not our deal if it's malformed. | 
 |  102     } | 
 |  103  | 
 |  104     try | 
 |  105     { | 
 |  106       if (referrer != null) | 
 |  107         block = application.matches(request.url, request.query, reqHost, refHost
     , request.getRequestHeader("accept")); | 
 |  108     } | 
 |  109     catch (Exception e) | 
 |  110     { | 
 |  111       Log.e(prefix, "Filter error", e); | 
 |  112     } | 
 |  113  | 
 |  114     request.log(Server.LOG_LOG, prefix, block + ": " + request.url + " ("+ refHo
     st +")"); | 
 |  115  | 
 |  116     int count = request.server.requestCount; | 
 |  117     if (shouldLogHeaders) | 
 |  118     { | 
 |  119       System.err.println(dumpHeaders(count, request, request.headers, true)); | 
 |  120     } | 
 |  121  | 
 |  122     if (block) | 
 |  123     { | 
 |  124       request.sendHeaders(204, null, 0); | 
 |  125       return true; | 
 |  126     } | 
 |  127  | 
 |  128     // Do not further process non-http requests | 
 |  129     if (!RE_HTTP.matcher(request.url).find()) | 
 |  130     { | 
 |  131       return false; | 
 |  132     } | 
 |  133  | 
 |  134     String url = request.url; | 
 |  135  | 
 |  136     if ((request.query != null) && (request.query.length() > 0)) | 
 |  137     { | 
 |  138       url += "?" + request.query; | 
 |  139     } | 
 |  140  | 
 |  141     /* | 
 |  142      * "Proxy-Connection" may be used (instead of just "Connection") | 
 |  143      * to keep alive a connection between a client and this proxy. | 
 |  144      */ | 
 |  145     String pc = request.headers.get("Proxy-Connection"); | 
 |  146     if (pc != null) | 
 |  147     { | 
 |  148       request.connectionHeader = "Proxy-Connection"; | 
 |  149       request.keepAlive = pc.equalsIgnoreCase("Keep-Alive"); | 
 |  150     } | 
 |  151  | 
 |  152     HttpRequest.removePointToPointHeaders(request.headers, false); | 
 |  153  | 
 |  154     HttpRequest target = new HttpRequest(url); | 
 |  155     try | 
 |  156     { | 
 |  157       target.setMethod(request.method); | 
 |  158       request.headers.copyTo(target.requestHeaders); | 
 |  159  | 
 |  160       if (proxyHost != null) | 
 |  161       { | 
 |  162         target.setProxy(proxyHost, proxyPort); | 
 |  163         if (auth != null) | 
 |  164         { | 
 |  165           target.requestHeaders.add("Proxy-Authorization", auth); | 
 |  166         } | 
 |  167       } | 
 |  168  | 
 |  169       if (request.postData != null) | 
 |  170       { | 
 |  171         OutputStream out = target.getOutputStream(); | 
 |  172         out.write(request.postData); | 
 |  173         out.close(); | 
 |  174       } | 
 |  175  | 
 |  176       target.connect(); | 
 |  177  | 
 |  178       if (shouldLogHeaders) | 
 |  179       { | 
 |  180         System.err.println("      " + target.status + "\n" + dumpHeaders(count, 
     request, target.responseHeaders, false)); | 
 |  181       } | 
 |  182       HttpRequest.removePointToPointHeaders(target.responseHeaders, true); | 
 |  183  | 
 |  184       request.setStatus(target.getResponseCode()); | 
 |  185       target.responseHeaders.copyTo(request.responseHeaders); | 
 |  186       try | 
 |  187       { | 
 |  188         request.responseHeaders.add("Via", target.status.substring(0, 8) + via); | 
 |  189       } | 
 |  190       catch (StringIndexOutOfBoundsException e) | 
 |  191       { | 
 |  192         request.responseHeaders.add("Via", via); | 
 |  193       } | 
 |  194  | 
 |  195       // Detect if we need to add ElemHide filters | 
 |  196       String type = request.responseHeaders.get("Content-Type"); | 
 |  197  | 
 |  198       String selectors = null; | 
 |  199       if (type != null && type.toLowerCase().startsWith("text/html")) | 
 |  200       { | 
 |  201         selectors = application.getSelectorsForDomain(reqHost); | 
 |  202       } | 
 |  203       // If no filters are applicable just pass through the response | 
 |  204       if (selectors == null || target.getResponseCode() != 200) | 
 |  205       { | 
 |  206         int contentLength = target.getContentLength(); | 
 |  207         if (contentLength == 0) | 
 |  208         { | 
 |  209           // we do not use request.sendResponse to avoid arbitrary | 
 |  210           // 200 -> 204 response code conversion | 
 |  211           request.sendHeaders(-1, null, -1); | 
 |  212         } | 
 |  213         else | 
 |  214         { | 
 |  215           request.sendResponse(target.getInputStream(), contentLength, null, -1)
     ; | 
 |  216         } | 
 |  217       } | 
 |  218       // Insert filters otherwise | 
 |  219       else | 
 |  220       { | 
 |  221         HttpInputStream his = target.getInputStream(); | 
 |  222         int size = target.getContentLength(); | 
 |  223         if (size < 0) | 
 |  224         { | 
 |  225           size = Integer.MAX_VALUE; | 
 |  226         } | 
 |  227  | 
 |  228         FilterInputStream in = null; | 
 |  229         FilterOutputStream out = null; | 
 |  230  | 
 |  231         // Detect if content needs decoding | 
 |  232         String encodingHeader = request.responseHeaders.get("Content-Encoding"); | 
 |  233         if (encodingHeader != null) | 
 |  234         { | 
 |  235           encodingHeader = encodingHeader.toLowerCase(); | 
 |  236           if (encodingHeader.equals("gzip") || encodingHeader.equals("x-gzip")) | 
 |  237           { | 
 |  238             in = new GZIPInputStream(his); | 
 |  239           } | 
 |  240           else if (encodingHeader.equals("compress") || encodingHeader.equals("x
     -compress")) | 
 |  241           { | 
 |  242             in = new InflaterInputStream(his); | 
 |  243           } | 
 |  244           else | 
 |  245           { | 
 |  246             // Unsupported encoding, proxy content as-is | 
 |  247             in = his; | 
 |  248             out = request.out; | 
 |  249             selectors = null; | 
 |  250           } | 
 |  251         } | 
 |  252         else | 
 |  253         { | 
 |  254           in = his; | 
 |  255         } | 
 |  256         // Use chunked encoding when injecting filters in page | 
 |  257         if (out == null) | 
 |  258         { | 
 |  259           request.responseHeaders.remove("Content-Length"); | 
 |  260           request.responseHeaders.remove("Content-Encoding"); | 
 |  261           out = new ChunkedOutputStream(request.out); | 
 |  262           request.responseHeaders.add("Transfer-Encoding", "chunked"); | 
 |  263           size = Integer.MAX_VALUE; | 
 |  264         } | 
 |  265  | 
 |  266         request.sendHeaders(-1, null, -1); | 
 |  267  | 
 |  268         byte[] buf = new byte[Math.min(4096, size)]; | 
 |  269  | 
 |  270         boolean sent = selectors == null; | 
 |  271         // TODO Do we need to set encoding here? | 
 |  272         BoyerMoore matcher = new BoyerMoore("<html".getBytes()); | 
 |  273  | 
 |  274         while (size > 0) | 
 |  275         { | 
 |  276           out.flush(); | 
 |  277  | 
 |  278           count = in.read(buf, 0, Math.min(buf.length, size)); | 
 |  279           if (count < 0) | 
 |  280           { | 
 |  281             break; | 
 |  282           } | 
 |  283           size -= count; | 
 |  284           try | 
 |  285           { | 
 |  286             // Search for <html> tag | 
 |  287             if (!sent && count > 0) | 
 |  288             { | 
 |  289               List<Integer> matches = matcher.match(buf, 0, count); | 
 |  290               if (!matches.isEmpty()) | 
 |  291               { | 
 |  292                 // TODO Do we need to set encoding here? | 
 |  293                 byte[] addon = selectors.getBytes(); | 
 |  294                 // Add filters right before match | 
 |  295                 int m = matches.get(0); | 
 |  296                 out.write(buf, 0, m); | 
 |  297                 out.write(addon); | 
 |  298                 out.write(buf, m, count - m); | 
 |  299                 sent = true; | 
 |  300                 continue; | 
 |  301               } | 
 |  302             } | 
 |  303             out.write(buf, 0, count); | 
 |  304           } | 
 |  305           catch (IOException e) | 
 |  306           { | 
 |  307             break; | 
 |  308           } | 
 |  309         } | 
 |  310         // The correct way would be to close ChunkedOutputStream | 
 |  311         // but we can not do it because underlying output stream is | 
 |  312         // used later in caller code. So we use this ugly hack: | 
 |  313         if (out instanceof ChunkedOutputStream) | 
 |  314           ((ChunkedOutputStream) out).writeFinalChunk(); | 
 |  315       } | 
 |  316     } | 
 |  317     catch (InterruptedIOException e) | 
 |  318     { | 
 |  319       /* | 
 |  320        * Read timeout while reading from the remote side. We use a | 
 |  321        * read timeout in case the target never responds. | 
 |  322        */ | 
 |  323       request.sendError(408, "Timeout / No response"); | 
 |  324     } | 
 |  325     catch (EOFException e) | 
 |  326     { | 
 |  327       request.sendError(500, "No response"); | 
 |  328     } | 
 |  329     catch (UnknownHostException e) | 
 |  330     { | 
 |  331       request.sendError(500, "Unknown host"); | 
 |  332     } | 
 |  333     catch (ConnectException e) | 
 |  334     { | 
 |  335       request.sendError(500, "Connection refused"); | 
 |  336     } | 
 |  337     catch (IOException e) | 
 |  338     { | 
 |  339       /* | 
 |  340        * An IOException will happen if we can't communicate with the | 
 |  341        * target or the client. Rather than attempting to discriminate, | 
 |  342        * just send an error message to the client, and let the send | 
 |  343        * fail if the client was the one that was in error. | 
 |  344        */ | 
 |  345  | 
 |  346       String msg = "Error from proxy"; | 
 |  347       if (e.getMessage() != null) | 
 |  348       { | 
 |  349         msg += ": " + e.getMessage(); | 
 |  350       } | 
 |  351       request.sendError(500, msg); | 
 |  352       Log.e(prefix, msg, e); | 
 |  353     } | 
 |  354     finally | 
 |  355     { | 
 |  356       target.close(); | 
 |  357     } | 
 |  358     return true; | 
 |  359   } | 
 |  360  | 
 |  361   /** | 
 |  362    * Dump the headers on stderr | 
 |  363    */ | 
 |  364   public static String dumpHeaders(int count, Request request, MimeHeaders heade
     rs, boolean sent) | 
 |  365   { | 
 |  366     String prompt; | 
 |  367     StringBuffer sb = new StringBuffer(); | 
 |  368     String label = "   " + count; | 
 |  369     label = label.substring(label.length() - 4); | 
 |  370     if (sent) | 
 |  371     { | 
 |  372       prompt = label + "> "; | 
 |  373       sb.append(prompt).append(request.toString()).append("\n"); | 
 |  374     } | 
 |  375     else | 
 |  376     { | 
 |  377       prompt = label + "< "; | 
 |  378     } | 
 |  379  | 
 |  380     for (int i = 0; i < headers.size(); i++) | 
 |  381     { | 
 |  382       sb.append(prompt).append(headers.getKey(i)); | 
 |  383       sb.append(": ").append(headers.get(i)).append("\n"); | 
 |  384     } | 
 |  385     return (sb.toString()); | 
 |  386   } | 
 |  387 } | 
| OLD | NEW |