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: Created Oct. 5, 2012, 9:28 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.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 }
OLDNEW
« no previous file with comments | « src/org/adblockplus/ChunkedOutputStream.java ('k') | src/org/adblockplus/brazil/SSLConnectionHandler.java » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld