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 |