| OLD | NEW |
| 1 /* | 1 /* |
| 2 * This file is part of Adblock Plus <http://adblockplus.org/>, | 2 * This file is part of Adblock Plus <http://adblockplus.org/>, |
| 3 * Copyright (C) 2006-2014 Eyeo GmbH | 3 * Copyright (C) 2006-2014 Eyeo GmbH |
| 4 * | 4 * |
| 5 * Adblock Plus is free software: you can redistribute it and/or modify | 5 * Adblock Plus is free software: you can redistribute it and/or modify |
| 6 * it under the terms of the GNU General Public License version 3 as | 6 * it under the terms of the GNU General Public License version 3 as |
| 7 * published by the Free Software Foundation. | 7 * published by the Free Software Foundation. |
| 8 * | 8 * |
| 9 * Adblock Plus is distributed in the hope that it will be useful, | 9 * Adblock Plus is distributed in the hope that it will be useful, |
| 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| (...skipping 30 matching lines...) Expand all Loading... |
| 41 | 41 |
| 42 import sunlabs.brazil.server.Request; | 42 import sunlabs.brazil.server.Request; |
| 43 import sunlabs.brazil.server.Server; | 43 import sunlabs.brazil.server.Server; |
| 44 import sunlabs.brazil.util.MatchString; | 44 import sunlabs.brazil.util.MatchString; |
| 45 import sunlabs.brazil.util.http.HttpInputStream; | 45 import sunlabs.brazil.util.http.HttpInputStream; |
| 46 import sunlabs.brazil.util.http.HttpRequest; | 46 import sunlabs.brazil.util.http.HttpRequest; |
| 47 import android.util.Log; | 47 import android.util.Log; |
| 48 | 48 |
| 49 /** | 49 /** |
| 50 * The <code>RequestHandler</code> implements a proxy service optionally | 50 * The <code>RequestHandler</code> implements a proxy service optionally |
| 51 * modifying output. | 51 * modifying output. The following configuration parameters are used to |
| 52 * The following configuration parameters are used to initialize this | 52 * initialize this <code>Handler</code>: |
| 53 * <code>Handler</code>: | |
| 54 * <dl class=props> | 53 * <dl class=props> |
| 55 * | 54 * |
| 56 * <dt>prefix, suffix, glob, match | 55 * <dt>prefix, suffix, glob, match |
| 57 * <dd>Specify the URL that triggers this handler. (See {@link MatchString}). | 56 * <dd>Specify the URL that triggers this handler. (See {@link MatchString}). |
| 58 * <dt>auth | 57 * <dt>auth |
| 59 * <dd>The value of the proxy-authenticate header (if any) sent to the upstream | 58 * <dd>The value of the proxy-authenticate header (if any) sent to the upstream |
| 60 * proxy | 59 * proxy |
| 61 * <dt>proxyHost | 60 * <dt>proxyHost |
| 62 * <dd>If specified, the name of the upstream proxy | 61 * <dd>If specified, the name of the upstream proxy |
| 63 * <dt>proxyPort | 62 * <dt>proxyPort |
| 64 * <dd>The upstream proxy port, if a proxyHost is specified (defaults to 80) | 63 * <dd>The upstream proxy port, if a proxyHost is specified (defaults to 80) |
| 65 * <dt>proxylog | 64 * <dt>proxylog |
| 66 * <dd>If set all http headers will be logged to the console. This is for | 65 * <dd>If set all http headers will be logged to the console. This is for |
| 67 * debugging. | 66 * debugging. |
| 68 * | 67 * |
| 69 * </dl> | 68 * </dl> |
| 70 * | 69 * |
| 71 * A sample set of configuration parameters illustrating how to use this | 70 * A sample set of configuration parameters illustrating how to use this handler |
| 72 * handler follows: | 71 * follows: |
| 73 * | 72 * |
| 74 * <pre> | 73 * <pre> |
| 75 * handler=adblock | 74 * handler=adblock |
| 76 * adblock.class=org.adblockplus.brazil.RequestHandler | 75 * adblock.class=org.adblockplus.brazil.RequestHandler |
| 77 * </pre> | 76 * </pre> |
| 78 * | 77 * |
| 79 * See the description under {@link sunlabs.brazil.server.Handler#respond | 78 * See the description under {@link sunlabs.brazil.server.Handler#respond |
| 80 * respond} for a more detailed explanation. | 79 * respond} for a more detailed explanation. |
| 81 */ | 80 */ |
| 82 | 81 |
| 83 public class RequestHandler extends BaseRequestHandler | 82 public class RequestHandler extends BaseRequestHandler |
| 84 { | 83 { |
| 84 private final static Pattern RE_HTTP = Pattern.compile("^https?:"); |
| 85 |
| 85 private AdblockPlus application; | 86 private AdblockPlus application; |
| 86 private String via; | 87 private String via; |
| 87 private static Pattern RE_HTTP = Pattern.compile("^https?:"); | |
| 88 | 88 |
| 89 @Override | 89 @Override |
| 90 public boolean init(Server server, String prefix) | 90 public boolean init(final Server server, final String prefix) |
| 91 { | 91 { |
| 92 super.init(server, prefix); | 92 super.init(server, prefix); |
| 93 | 93 |
| 94 application = AdblockPlus.getApplication(); | 94 this.application = AdblockPlus.getApplication(); |
| 95 via = " " + server.hostName + ":" + server.listen.getLocalPort() + " (" + se
rver.name + ")"; | 95 this.via = " " + server.hostName + ":" + server.listen.getLocalPort() + " ("
+ server.name + ")"; |
| 96 | 96 |
| 97 return true; | 97 return true; |
| 98 } | 98 } |
| 99 | 99 |
| 100 @Override | 100 @Override |
| 101 public boolean respond(Request request) throws IOException | 101 public boolean respond(final Request request) throws IOException |
| 102 { | 102 { |
| 103 boolean block = false; | 103 boolean block = false; |
| 104 | 104 |
| 105 try | 105 try |
| 106 { | 106 { |
| 107 block = application.matches(request.url, request.query, request.getRequest
Header("referer"), request.getRequestHeader("accept")); | 107 block = this.application.matches(request.url, request.query, request.getRe
questHeader("referer"), request.getRequestHeader("accept")); |
| 108 } | 108 } |
| 109 catch (Exception e) | 109 catch (final Exception e) |
| 110 { | 110 { |
| 111 Log.e(prefix, "Filter error", e); | 111 Log.e(this.prefix, "Filter error", e); |
| 112 } | 112 } |
| 113 | 113 |
| 114 request.log(Server.LOG_LOG, prefix, block + ": " + request.url); | 114 request.log(Server.LOG_LOG, this.prefix, block + ": " + request.url); |
| 115 | 115 |
| 116 int count = request.server.requestCount; | 116 int count = request.server.requestCount; |
| 117 if (shouldLogHeaders) | 117 if (this.shouldLogHeaders) |
| 118 { | 118 { |
| 119 System.err.println(dumpHeaders(count, request, request.headers, true)); | 119 System.err.println(dumpHeaders(count, request, request.headers, true)); |
| 120 } | 120 } |
| 121 | 121 |
| 122 if (block) | 122 if (block) |
| 123 { | 123 { |
| 124 request.sendHeaders(204, null, 0); | 124 request.sendHeaders(204, null, 0); |
| 125 return true; | 125 return true; |
| 126 } | 126 } |
| 127 | 127 |
| 128 // Do not further process non-http requests | 128 // Do not further process non-http requests |
| 129 if (!RE_HTTP.matcher(request.url).find()) | 129 if (!RE_HTTP.matcher(request.url).find()) |
| 130 { | 130 { |
| 131 return false; | 131 return false; |
| 132 } | 132 } |
| 133 | 133 |
| 134 String url = request.url; | 134 String url = request.url; |
| 135 | 135 |
| 136 if ((request.query != null) && (request.query.length() > 0)) | 136 if ((request.query != null) && (request.query.length() > 0)) |
| 137 { | 137 { |
| 138 url += "?" + request.query; | 138 url += "?" + request.query; |
| 139 } | 139 } |
| 140 | 140 |
| 141 /* | 141 /* |
| 142 * "Proxy-Connection" may be used (instead of just "Connection") | 142 * "Proxy-Connection" may be used (instead of just "Connection") to keep |
| 143 * to keep alive a connection between a client and this proxy. | 143 * alive a connection between a client and this proxy. |
| 144 */ | 144 */ |
| 145 String pc = request.headers.get("Proxy-Connection"); | 145 final String pc = request.headers.get("Proxy-Connection"); |
| 146 if (pc != null) | 146 if (pc != null) |
| 147 { | 147 { |
| 148 request.connectionHeader = "Proxy-Connection"; | 148 request.connectionHeader = "Proxy-Connection"; |
| 149 request.keepAlive = pc.equalsIgnoreCase("Keep-Alive"); | 149 request.keepAlive = pc.equalsIgnoreCase("Keep-Alive"); |
| 150 } | 150 } |
| 151 | 151 |
| 152 HttpRequest.removePointToPointHeaders(request.headers, false); | 152 HttpRequest.removePointToPointHeaders(request.headers, false); |
| 153 | 153 |
| 154 HttpRequest target = new HttpRequest(url); | 154 final HttpRequest target = new HttpRequest(url); |
| 155 try | 155 try |
| 156 { | 156 { |
| 157 target.setMethod(request.method); | 157 target.setMethod(request.method); |
| 158 request.headers.copyTo(target.requestHeaders); | 158 request.headers.copyTo(target.requestHeaders); |
| 159 | 159 |
| 160 if (proxyHost != null) | 160 if (this.proxyHost != null) |
| 161 { | 161 { |
| 162 target.setProxy(proxyHost, proxyPort); | 162 target.setProxy(this.proxyHost, this.proxyPort); |
| 163 if (auth != null) | 163 if (this.auth != null) |
| 164 { | 164 { |
| 165 target.requestHeaders.add("Proxy-Authorization", auth); | 165 target.requestHeaders.add("Proxy-Authorization", this.auth); |
| 166 } | 166 } |
| 167 } | 167 } |
| 168 | 168 |
| 169 if (request.postData != null) | 169 if (request.postData != null) |
| 170 { | 170 { |
| 171 OutputStream out = target.getOutputStream(); | 171 final OutputStream out = target.getOutputStream(); |
| 172 out.write(request.postData); | 172 out.write(request.postData); |
| 173 out.close(); | 173 out.close(); |
| 174 } | 174 } |
| 175 else | 175 else |
| 176 { | 176 { |
| 177 target.setHttpInputStream(request.in); | 177 target.setHttpInputStream(request.in); |
| 178 } | 178 } |
| 179 target.connect(); | 179 target.connect(); |
| 180 | 180 |
| 181 if (shouldLogHeaders) | 181 if (this.shouldLogHeaders) |
| 182 { | 182 { |
| 183 System.err.println(" " + target.status + "\n" + dumpHeaders(count,
request, target.responseHeaders, false)); | 183 System.err.println(" " + target.status + "\n" + dumpHeaders(count,
request, target.responseHeaders, false)); |
| 184 } | 184 } |
| 185 HttpRequest.removePointToPointHeaders(target.responseHeaders, true); | 185 HttpRequest.removePointToPointHeaders(target.responseHeaders, true); |
| 186 | 186 |
| 187 request.setStatus(target.getResponseCode()); | 187 request.setStatus(target.getResponseCode()); |
| 188 target.responseHeaders.copyTo(request.responseHeaders); | 188 target.responseHeaders.copyTo(request.responseHeaders); |
| 189 try | 189 try |
| 190 { | 190 { |
| 191 request.responseHeaders.add("Via", target.status.substring(0, 8) + via); | 191 request.responseHeaders.add("Via", target.status.substring(0, 8) + this.
via); |
| 192 } | 192 } |
| 193 catch (StringIndexOutOfBoundsException e) | 193 catch (final StringIndexOutOfBoundsException e) |
| 194 { | 194 { |
| 195 request.responseHeaders.add("Via", via); | 195 request.responseHeaders.add("Via", this.via); |
| 196 } | 196 } |
| 197 | 197 |
| 198 // Detect if we need to add ElemHide filters | 198 // Detect if we need to add ElemHide filters |
| 199 String type = request.responseHeaders.get("Content-Type"); | 199 final String type = request.responseHeaders.get("Content-Type"); |
| 200 | 200 |
| 201 String[] selectors = null; | 201 String[] selectors = null; |
| 202 if (type != null && type.toLowerCase().startsWith("text/html")) | 202 if (type != null && type.toLowerCase().startsWith("text/html")) |
| 203 { | 203 { |
| 204 String reqHost = ""; | 204 String reqHost = ""; |
| 205 | 205 |
| 206 try | 206 try |
| 207 { | 207 { |
| 208 reqHost = (new URL(request.url)).getHost(); | 208 reqHost = (new URL(request.url)).getHost(); |
| 209 } | 209 } |
| 210 catch (MalformedURLException e) | 210 catch (final MalformedURLException e) |
| 211 { | 211 { |
| 212 // We are transparent, it's not our deal if it's malformed. | 212 // We are transparent, it's not our deal if it's malformed. |
| 213 } | 213 } |
| 214 | 214 |
| 215 selectors = application.getSelectorsForDomain(reqHost); | 215 selectors = this.application.getSelectorsForDomain(reqHost); |
| 216 } | 216 } |
| 217 // If no filters are applicable just pass through the response | 217 // If no filters are applicable just pass through the response |
| 218 if (selectors == null || target.getResponseCode() != 200) | 218 if (selectors == null || target.getResponseCode() != 200) |
| 219 { | 219 { |
| 220 int contentLength = target.getContentLength(); | 220 final int contentLength = target.getContentLength(); |
| 221 if (contentLength == 0) | 221 if (contentLength == 0) |
| 222 { | 222 { |
| 223 // we do not use request.sendResponse to avoid arbitrary | 223 // we do not use request.sendResponse to avoid arbitrary |
| 224 // 200 -> 204 response code conversion | 224 // 200 -> 204 response code conversion |
| 225 request.sendHeaders(-1, null, -1); | 225 request.sendHeaders(-1, null, -1); |
| 226 } | 226 } |
| 227 else | 227 else |
| 228 { | 228 { |
| 229 request.sendResponse(target.getInputStream(), contentLength, null, -1)
; | 229 request.sendResponse(target.getInputStream(), contentLength, null, -1)
; |
| 230 } | 230 } |
| 231 } | 231 } |
| 232 // Insert filters otherwise | 232 // Insert filters otherwise |
| 233 else | 233 else |
| 234 { | 234 { |
| 235 HttpInputStream his = target.getInputStream(); | 235 final HttpInputStream his = target.getInputStream(); |
| 236 int size = target.getContentLength(); | 236 int size = target.getContentLength(); |
| 237 if (size < 0) | 237 if (size < 0) |
| 238 { | 238 { |
| 239 size = Integer.MAX_VALUE; | 239 size = Integer.MAX_VALUE; |
| 240 } | 240 } |
| 241 | 241 |
| 242 FilterInputStream in = null; | 242 FilterInputStream in = null; |
| 243 FilterOutputStream out = null; | 243 FilterOutputStream out = null; |
| 244 | 244 |
| 245 // Detect if content needs decoding | 245 // Detect if content needs decoding |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 283 { | 283 { |
| 284 final Matcher matcher = Pattern.compile("charset=([^;]*)").matcher(con
tentType); | 284 final Matcher matcher = Pattern.compile("charset=([^;]*)").matcher(con
tentType); |
| 285 if (matcher.matches()) | 285 if (matcher.matches()) |
| 286 { | 286 { |
| 287 try | 287 try |
| 288 { | 288 { |
| 289 final String extractedCharsetName = matcher.group(0); | 289 final String extractedCharsetName = matcher.group(0); |
| 290 Charset.forName(extractedCharsetName); | 290 Charset.forName(extractedCharsetName); |
| 291 charsetName = extractedCharsetName; | 291 charsetName = extractedCharsetName; |
| 292 } | 292 } |
| 293 catch (IllegalArgumentException e) | 293 catch (final IllegalArgumentException e) |
| 294 { | 294 { |
| 295 Log.e(prefix, "Unsupported site charset, falling back to " + chars
etName, e); | 295 Log.e(this.prefix, "Unsupported site charset, falling back to " +
charsetName, e); |
| 296 } | 296 } |
| 297 } | 297 } |
| 298 } | 298 } |
| 299 | 299 |
| 300 request.sendHeaders(-1, null, -1); | 300 request.sendHeaders(-1, null, -1); |
| 301 | 301 |
| 302 byte[] buf = new byte[Math.min(4096, size)]; | 302 final byte[] buf = new byte[Math.min(4096, size)]; |
| 303 | 303 |
| 304 boolean sent = selectors == null; | 304 boolean sent = selectors == null; |
| 305 BoyerMoore matcher = new BoyerMoore("<html".getBytes()); | 305 final BoyerMoore matcher = new BoyerMoore("<html".getBytes()); |
| 306 | 306 |
| 307 while (size > 0) | 307 while (size > 0) |
| 308 { | 308 { |
| 309 out.flush(); | 309 out.flush(); |
| 310 | 310 |
| 311 count = in.read(buf, 0, Math.min(buf.length, size)); | 311 count = in.read(buf, 0, Math.min(buf.length, size)); |
| 312 if (count < 0) | 312 if (count < 0) |
| 313 { | 313 { |
| 314 break; | 314 break; |
| 315 } | 315 } |
| 316 size -= count; | 316 size -= count; |
| 317 try | 317 try |
| 318 { | 318 { |
| 319 // Search for <html> tag | 319 // Search for <html> tag |
| 320 if (!sent && count > 0) | 320 if (!sent && count > 0) |
| 321 { | 321 { |
| 322 List<Integer> matches = matcher.match(buf, 0, count); | 322 final List<Integer> matches = matcher.match(buf, 0, count); |
| 323 if (!matches.isEmpty()) | 323 if (!matches.isEmpty()) |
| 324 { | 324 { |
| 325 // Add filters right before match | 325 // Add filters right before match |
| 326 int m = matches.get(0); | 326 final int m = matches.get(0); |
| 327 out.write(buf, 0, m); | 327 out.write(buf, 0, m); |
| 328 out.write("<style type=\"text/css\">\n".getBytes()); | 328 out.write("<style type=\"text/css\">\n".getBytes()); |
| 329 out.write(StringUtils.join(selectors, ",\r\n").getBytes(charsetN
ame)); | 329 out.write(StringUtils.join(selectors, ",\r\n").getBytes(charsetN
ame)); |
| 330 out.write("{ display: none !important }</style>\n".getBytes()); | 330 out.write("{ display: none !important }</style>\n".getBytes()); |
| 331 out.write(buf, m, count - m); | 331 out.write(buf, m, count - m); |
| 332 sent = true; | 332 sent = true; |
| 333 continue; | 333 continue; |
| 334 } | 334 } |
| 335 } | 335 } |
| 336 out.write(buf, 0, count); | 336 out.write(buf, 0, count); |
| 337 } | 337 } |
| 338 catch (IOException e) | 338 catch (final IOException e) |
| 339 { | 339 { |
| 340 break; | 340 break; |
| 341 } | 341 } |
| 342 } | 342 } |
| 343 // The correct way would be to close ChunkedOutputStream | 343 // The correct way would be to close ChunkedOutputStream |
| 344 // but we can not do it because underlying output stream is | 344 // but we can not do it because underlying output stream is |
| 345 // used later in caller code. So we use this ugly hack: | 345 // used later in caller code. So we use this ugly hack: |
| 346 if (out instanceof ChunkedOutputStream) | 346 if (out instanceof ChunkedOutputStream) |
| 347 ((ChunkedOutputStream) out).writeFinalChunk(); | 347 { |
| 348 ((ChunkedOutputStream)out).writeFinalChunk(); |
| 349 } |
| 348 } | 350 } |
| 349 } | 351 } |
| 350 catch (InterruptedIOException e) | 352 catch (final InterruptedIOException e) |
| 351 { | 353 { |
| 352 /* | 354 /* |
| 353 * Read timeout while reading from the remote side. We use a | 355 * Read timeout while reading from the remote side. We use a read timeout |
| 354 * read timeout in case the target never responds. | 356 * in case the target never responds. |
| 355 */ | 357 */ |
| 356 request.sendError(408, "Timeout / No response"); | 358 request.sendError(408, "Timeout / No response"); |
| 357 } | 359 } |
| 358 catch (EOFException e) | 360 catch (final EOFException e) |
| 359 { | 361 { |
| 360 request.sendError(500, "No response"); | 362 request.sendError(500, "No response"); |
| 361 } | 363 } |
| 362 catch (UnknownHostException e) | 364 catch (final UnknownHostException e) |
| 363 { | 365 { |
| 364 request.sendError(500, "Unknown host"); | 366 request.sendError(500, "Unknown host"); |
| 365 } | 367 } |
| 366 catch (ConnectException e) | 368 catch (final ConnectException e) |
| 367 { | 369 { |
| 368 request.sendError(500, "Connection refused"); | 370 request.sendError(500, "Connection refused"); |
| 369 } | 371 } |
| 370 catch (IOException e) | 372 catch (final IOException e) |
| 371 { | 373 { |
| 372 /* | 374 /* |
| 373 * An IOException will happen if we can't communicate with the | 375 * An IOException will happen if we can't communicate with the target or |
| 374 * target or the client. Rather than attempting to discriminate, | 376 * the client. Rather than attempting to discriminate, just send an error |
| 375 * just send an error message to the client, and let the send | 377 * message to the client, and let the send fail if the client was the one |
| 376 * fail if the client was the one that was in error. | 378 * that was in error. |
| 377 */ | 379 */ |
| 378 | 380 |
| 379 String msg = "Error from proxy"; | 381 String msg = "Error from proxy"; |
| 380 if (e.getMessage() != null) | 382 if (e.getMessage() != null) |
| 381 { | 383 { |
| 382 msg += ": " + e.getMessage(); | 384 msg += ": " + e.getMessage(); |
| 383 } | 385 } |
| 384 request.sendError(500, msg); | 386 request.sendError(500, msg); |
| 385 Log.e(prefix, msg, e); | 387 Log.e(this.prefix, msg, e); |
| 386 } | 388 } |
| 387 finally | 389 finally |
| 388 { | 390 { |
| 389 target.close(); | 391 target.close(); |
| 390 } | 392 } |
| 391 return true; | 393 return true; |
| 392 } | 394 } |
| 393 } | 395 } |
| OLD | NEW |