| 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 shouldLogHeaders; | |
| 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 { | |
| 99 // use default port | |
| 100 } | |
| 101 | |
| 102 auth = props.getProperty(prefix + AUTH); | |
| 103 | |
| 104 shouldLogHeaders = (props.getProperty(prefix + "proxylog") != null); | |
| 105 | |
| 106 via = " " + server.hostName + ":" + server.listen.getLocalPort() + " (" + se rver.name + ")"; | |
| 107 | |
| 108 return true; | |
| 109 } | |
| 110 | |
| 111 @Override | |
| 112 public boolean respond(Request request) throws IOException | |
| 113 { | |
| 114 boolean block = false; | |
| 115 String reqHost = null; | |
| 116 String refHost = null; | |
| 117 | |
| 118 try | |
| 119 { | |
| 120 reqHost = (new URL(request.url)).getHost(); | |
| 121 refHost = (new URL(request.getRequestHeader("referer"))).getHost(); | |
| 122 } | |
| 123 catch (MalformedURLException e) | |
| 124 { | |
| 125 // We are transparent, it's not our deal if it's malformed. | |
| 126 } | |
| 127 | |
| 128 try | |
| 129 { | |
| 130 block = application.matches(request.url, request.query, reqHost, refHost, request.getRequestHeader("accept")); | |
| 131 } | |
| 132 catch (Exception e) | |
| 133 { | |
| 134 Log.e(prefix, "Filter error", e); | |
| 135 } | |
| 136 | |
| 137 request.log(Server.LOG_LOG, prefix, block + ": " + request.url); | |
| 138 | |
| 139 int count = request.server.requestCount; | |
| 140 if (shouldLogHeaders) | |
| 141 { | |
| 142 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
| |
| 143 } | |
| 144 | |
| 145 if (block) | |
| 146 { | |
| 147 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
| |
| 148 return true; | |
| 149 } | |
| 150 | |
| 151 // Do not further process non-http requests | |
| 152 if (request.url.startsWith("http:") == false && request.url.startsWith("http s:") == false) | |
| 153 { | |
| 154 return false; | |
| 155 } | |
| 156 | |
| 157 String url = request.url; | |
| 158 | |
| 159 if ((request.query != null) && (request.query.length() > 0)) | |
| 160 { | |
| 161 url += "?" + request.query; | |
| 162 } | |
| 163 | |
| 164 /* | |
| 165 * "Proxy-Connection" may be used (instead of just "Connection") | |
| 166 * to keep alive a connection between a client and this proxy. | |
| 167 */ | |
| 168 String pc = request.headers.get("Proxy-Connection"); | |
| 169 if (pc != null) | |
| 170 { | |
| 171 request.connectionHeader = "Proxy-Connection"; | |
| 172 request.keepAlive = pc.equalsIgnoreCase("Keep-Alive"); | |
| 173 } | |
| 174 | |
| 175 HttpRequest.removePointToPointHeaders(request.headers, false); | |
| 176 | |
| 177 HttpRequest target = new HttpRequest(url); | |
| 178 try | |
| 179 { | |
| 180 target.setMethod(request.method); | |
| 181 request.headers.copyTo(target.requestHeaders); | |
| 182 | |
| 183 if (proxyHost != null) | |
| 184 { | |
| 185 target.setProxy(proxyHost, proxyPort); | |
| 186 if (auth != null) | |
| 187 { | |
| 188 target.requestHeaders.add("Proxy-Authorization", auth); | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 if (request.postData != null) | |
| 193 { | |
| 194 OutputStream out = target.getOutputStream(); | |
| 195 out.write(request.postData); | |
| 196 out.close(); | |
| 197 } | |
| 198 | |
| 199 target.connect(); | |
| 200 | |
| 201 if (shouldLogHeaders) | |
| 202 { | |
| 203 System.err.println(" " + target.status + "\n" + dumpHeaders(count, request, target.responseHeaders, false)); | |
| 204 } | |
| 205 HttpRequest.removePointToPointHeaders(target.responseHeaders, true); | |
| 206 | |
| 207 request.setStatus(target.getResponseCode()); | |
| 208 target.responseHeaders.copyTo(request.responseHeaders); | |
| 209 try | |
| 210 { | |
| 211 request.responseHeaders.add("Via", target.status.substring(0, 8) + via); | |
| 212 } | |
| 213 catch (StringIndexOutOfBoundsException e) | |
| 214 { | |
| 215 request.responseHeaders.add("Via", via); | |
| 216 } | |
| 217 | |
| 218 // Detect if we need to add ElemHide filters | |
| 219 String type = request.responseHeaders.get("Content-Type"); | |
| 220 | |
| 221 String selectors = null; | |
| 222 if (type != null && type.toLowerCase().startsWith("text/html")) | |
| 223 { | |
| 224 selectors = application.getSelectorsForDomain(reqHost); | |
| 225 } | |
| 226 // If no filters are applicable just pass through the response | |
| 227 if (selectors == null || target.getResponseCode() != 200) | |
| 228 { | |
| 229 int contentLength = target.getContentLength(); | |
| 230 if (contentLength == 0) | |
| 231 { | |
| 232 // we do not use request.sendResponse to avoid arbitrary | |
| 233 // 200 -> 204 response code conversion | |
| 234 request.sendHeaders(-1, null, -1); | |
| 235 } | |
| 236 else | |
| 237 { | |
| 238 request.sendResponse(target.getInputStream(), contentLength, null, -1) ; | |
| 239 } | |
| 240 } | |
| 241 // Insert filters otherwise | |
| 242 else | |
| 243 { | |
| 244 HttpInputStream his = target.getInputStream(); | |
| 245 int size = target.getContentLength(); | |
| 246 if (size < 0) | |
| 247 { | |
| 248 size = Integer.MAX_VALUE; | |
| 249 } | |
| 250 | |
| 251 FilterInputStream in = null; | |
| 252 FilterOutputStream out = null; | |
| 253 | |
| 254 // Detect if content needs decoding | |
| 255 String encodingHeader = request.responseHeaders.get("Content-Encoding"); | |
| 256 if (encodingHeader != null) | |
| 257 { | |
| 258 encodingHeader = encodingHeader.toLowerCase(); | |
| 259 if (encodingHeader.equals("gzip") || encodingHeader.equals("x-gzip")) | |
| 260 { | |
| 261 in = new GZIPInputStream(his); | |
| 262 } | |
| 263 else if (encodingHeader.equals("compress") || encodingHeader.equals("x -compress")) | |
| 264 { | |
| 265 in = new InflaterInputStream(his); | |
| 266 } | |
| 267 else | |
| 268 { | |
| 269 // Unsupported encoding, proxy content as-is | |
| 270 in = his; | |
| 271 out = request.out; | |
| 272 selectors = null; | |
| 273 } | |
| 274 } | |
| 275 else | |
| 276 { | |
| 277 in = his; | |
| 278 } | |
| 279 // Use chunked encoding when injecting filters in page | |
| 280 if (out == null) | |
| 281 { | |
| 282 request.responseHeaders.remove("Content-Length"); | |
| 283 request.responseHeaders.remove("Content-Encoding"); | |
| 284 out = new ChunkedOutputStream(request.out); | |
| 285 request.responseHeaders.add("Transfer-Encoding", "chunked"); | |
| 286 size = Integer.MAX_VALUE; | |
| 287 } | |
| 288 | |
| 289 request.sendHeaders(-1, null, -1); | |
| 290 | |
| 291 byte[] buf = new byte[Math.min(4096, size)]; | |
| 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 if (out instanceof ChunkedOutputStream) | |
| 337 ((ChunkedOutputStream) out).writeFinalChunk(); | |
| 338 } | |
| 339 } | |
| 340 catch (InterruptedIOException e) | |
| 341 { | |
| 342 /* | |
| 343 * Read timeout while reading from the remote side. We use a | |
| 344 * read timeout in case the target never responds. | |
| 345 */ | |
| 346 request.sendError(408, "Timeout / No response"); | |
| 347 } | |
| 348 catch (EOFException e) | |
| 349 { | |
| 350 request.sendError(500, "No response"); | |
| 351 } | |
| 352 catch (UnknownHostException e) | |
| 353 { | |
| 354 request.sendError(500, "Unknown host"); | |
| 355 } | |
| 356 catch (ConnectException e) | |
| 357 { | |
| 358 request.sendError(500, "Connection refused"); | |
| 359 } | |
| 360 catch (IOException e) | |
| 361 { | |
| 362 /* | |
| 363 * An IOException will happen if we can't communicate with the | |
| 364 * target or the client. Rather than attempting to discriminate, | |
| 365 * just send an error message to the client, and let the send | |
| 366 * fail if the client was the one that was in error. | |
| 367 */ | |
| 368 | |
| 369 String msg = "Error from proxy"; | |
| 370 if (e.getMessage() != null) | |
| 371 { | |
| 372 msg += ": " + e.getMessage(); | |
| 373 } | |
| 374 request.sendError(500, msg); | |
| 375 Log.e(prefix, msg, e); | |
| 376 } | |
| 377 finally | |
| 378 { | |
| 379 target.close(); | |
| 380 } | |
| 381 return true; | |
| 382 } | |
| 383 | |
| 384 /** | |
| 385 * Dump the headers on stderr | |
| 386 */ | |
| 387 public static String dumpHeaders(int count, Request request, MimeHeaders heade rs, boolean sent) | |
| 388 { | |
| 389 String prompt; | |
| 390 StringBuffer sb = new StringBuffer(); | |
| 391 String label = " " + count; | |
| 392 label = label.substring(label.length() - 4); | |
| 393 if (sent) | |
| 394 { | |
| 395 prompt = label + "> "; | |
| 396 sb.append(prompt).append(request.toString()).append("\n"); | |
| 397 } | |
| 398 else | |
| 399 { | |
| 400 prompt = label + "< "; | |
| 401 } | |
| 402 | |
| 403 for (int i = 0; i < headers.size(); i++) | |
| 404 { | |
| 405 sb.append(prompt).append(headers.getKey(i)); | |
| 406 sb.append(": ").append(headers.get(i)).append("\n"); | |
| 407 } | |
| 408 return (sb.toString()); | |
| 409 } | |
| 410 } | |
| OLD | NEW |