Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: src/org/adblockplus/brazil/RequestHandler.java

Issue 8484110: ABP/Android proxy service (Closed)
Patch Set: ABP/Android proxy service Created Nov. 1, 2012, 9:46 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld