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