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 |