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