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