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

Powered by Google App Engine
This is Rietveld