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. 12, 2012, 8:53 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.regex.Pattern;
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.Request;
23 import sunlabs.brazil.server.Server;
24 import sunlabs.brazil.util.MatchString;
25 import sunlabs.brazil.util.http.HttpInputStream;
26 import sunlabs.brazil.util.http.HttpRequest;
27 import sunlabs.brazil.util.http.MimeHeaders;
28 import android.util.Log;
29
30 /**
31 * The <code>RequestHandler</code> implements a proxy service optionally
32 * modifying output.
33 * The following configuration parameters are used to initialize this
34 * <code>Handler</code>:
35 * <dl class=props>
36 *
37 * <dt>prefix, suffix, glob, match
38 * <dd>Specify the URL that triggers this handler. (See {@link MatchString}).
39 * <dt>auth
40 * <dd>The value of the proxy-authenticate header (if any) sent to the upstream
41 * proxy
42 * <dt>proxyHost
43 * <dd>If specified, the name of the upstream proxy
44 * <dt>proxyPort
45 * <dd>The upstream proxy port, if a proxyHost is specified (defaults to 80)
46 * <dt>proxylog
47 * <dd>If set all http headers will be logged to the console. This is for
48 * debugging.
49 *
50 * </dl>
51 *
52 * A sample set of configuration parameters illustrating how to use this
53 * handler follows:
54 *
55 * <pre>
56 * handler=adblock
57 * adblock.class=org.adblockplus.brazil.RequestHandler
58 * </pre>
59 *
60 * See the description under {@link sunlabs.brazil.server.Handler#respond
61 * respond} for a more detailed explanation.
62 */
63
64 public class RequestHandler extends BaseRequestHandler
65 {
66 private AdblockPlus application;
67 private String via;
68 private static Pattern RE_HTTP = Pattern.compile("^https?:");
69
70 private boolean shouldLogHeaders;
71
72 @Override
73 public boolean init(Server server, String prefix)
74 {
75 super.init(server, prefix);
76
77 application = AdblockPlus.getApplication();
78 shouldLogHeaders = (server.props.getProperty(prefix + "proxylog") != null);
79 via = " " + server.hostName + ":" + server.listen.getLocalPort() + " (" + se rver.name + ")";
80
81 return true;
82 }
83
84 @Override
85 public boolean respond(Request request) throws IOException
86 {
87 boolean block = false;
88 String reqHost = null;
89 String refHost = null;
90
91 String referrer = request.getRequestHeader("referer");
92
93 try
94 {
95 reqHost = (new URL(request.url)).getHost();
96 if (referrer != null)
97 refHost = (new URL(referrer)).getHost();
98 }
99 catch (MalformedURLException e)
100 {
101 // We are transparent, it's not our deal if it's malformed.
102 }
103
104 try
105 {
106 if (referrer != null)
107 block = application.matches(request.url, request.query, reqHost, refHost , request.getRequestHeader("accept"));
108 }
109 catch (Exception e)
110 {
111 Log.e(prefix, "Filter error", e);
112 }
113
114 request.log(Server.LOG_LOG, prefix, block + ": " + request.url + " ("+ refHo st +")");
115
116 int count = request.server.requestCount;
117 if (shouldLogHeaders)
118 {
119 System.err.println(dumpHeaders(count, request, request.headers, true));
120 }
121
122 if (block)
123 {
124 request.sendHeaders(204, null, 0);
125 return true;
126 }
127
128 // Do not further process non-http requests
129 if (!RE_HTTP.matcher(request.url).find())
130 {
131 return false;
132 }
133
134 String url = request.url;
135
136 if ((request.query != null) && (request.query.length() > 0))
137 {
138 url += "?" + request.query;
139 }
140
141 /*
142 * "Proxy-Connection" may be used (instead of just "Connection")
143 * to keep alive a connection between a client and this proxy.
144 */
145 String pc = request.headers.get("Proxy-Connection");
146 if (pc != null)
147 {
148 request.connectionHeader = "Proxy-Connection";
149 request.keepAlive = pc.equalsIgnoreCase("Keep-Alive");
150 }
151
152 HttpRequest.removePointToPointHeaders(request.headers, false);
153
154 HttpRequest target = new HttpRequest(url);
155 try
156 {
157 target.setMethod(request.method);
158 request.headers.copyTo(target.requestHeaders);
159
160 if (proxyHost != null)
161 {
162 target.setProxy(proxyHost, proxyPort);
163 if (auth != null)
164 {
165 target.requestHeaders.add("Proxy-Authorization", auth);
166 }
167 }
168
169 if (request.postData != null)
170 {
171 OutputStream out = target.getOutputStream();
172 out.write(request.postData);
173 out.close();
174 }
175
176 target.connect();
177
178 if (shouldLogHeaders)
179 {
180 System.err.println(" " + target.status + "\n" + dumpHeaders(count, request, target.responseHeaders, false));
181 }
182 HttpRequest.removePointToPointHeaders(target.responseHeaders, true);
183
184 request.setStatus(target.getResponseCode());
185 target.responseHeaders.copyTo(request.responseHeaders);
186 try
187 {
188 request.responseHeaders.add("Via", target.status.substring(0, 8) + via);
189 }
190 catch (StringIndexOutOfBoundsException e)
191 {
192 request.responseHeaders.add("Via", via);
193 }
194
195 // Detect if we need to add ElemHide filters
196 String type = request.responseHeaders.get("Content-Type");
197
198 String selectors = null;
199 if (type != null && type.toLowerCase().startsWith("text/html"))
200 {
201 selectors = application.getSelectorsForDomain(reqHost);
202 }
203 // If no filters are applicable just pass through the response
204 if (selectors == null || target.getResponseCode() != 200)
205 {
206 int contentLength = target.getContentLength();
207 if (contentLength == 0)
208 {
209 // we do not use request.sendResponse to avoid arbitrary
210 // 200 -> 204 response code conversion
211 request.sendHeaders(-1, null, -1);
212 }
213 else
214 {
215 request.sendResponse(target.getInputStream(), contentLength, null, -1) ;
216 }
217 }
218 // Insert filters otherwise
219 else
220 {
221 HttpInputStream his = target.getInputStream();
222 int size = target.getContentLength();
223 if (size < 0)
224 {
225 size = Integer.MAX_VALUE;
226 }
227
228 FilterInputStream in = null;
229 FilterOutputStream out = null;
230
231 // Detect if content needs decoding
232 String encodingHeader = request.responseHeaders.get("Content-Encoding");
233 if (encodingHeader != null)
234 {
235 encodingHeader = encodingHeader.toLowerCase();
236 if (encodingHeader.equals("gzip") || encodingHeader.equals("x-gzip"))
237 {
238 in = new GZIPInputStream(his);
239 }
240 else if (encodingHeader.equals("compress") || encodingHeader.equals("x -compress"))
241 {
242 in = new InflaterInputStream(his);
243 }
244 else
245 {
246 // Unsupported encoding, proxy content as-is
247 in = his;
248 out = request.out;
249 selectors = null;
250 }
251 }
252 else
253 {
254 in = his;
255 }
256 // Use chunked encoding when injecting filters in page
257 if (out == null)
258 {
259 request.responseHeaders.remove("Content-Length");
260 request.responseHeaders.remove("Content-Encoding");
261 out = new ChunkedOutputStream(request.out);
262 request.responseHeaders.add("Transfer-Encoding", "chunked");
263 size = Integer.MAX_VALUE;
264 }
265
266 request.sendHeaders(-1, null, -1);
267
268 byte[] buf = new byte[Math.min(4096, size)];
269
270 boolean sent = selectors == null;
271 // TODO Do we need to set encoding here?
272 BoyerMoore matcher = new BoyerMoore("<html".getBytes());
273
274 while (size > 0)
275 {
276 out.flush();
277
278 count = in.read(buf, 0, Math.min(buf.length, size));
279 if (count < 0)
280 {
281 break;
282 }
283 size -= count;
284 try
285 {
286 // Search for <html> tag
287 if (!sent && count > 0)
288 {
289 List<Integer> matches = matcher.match(buf, 0, count);
290 if (!matches.isEmpty())
291 {
292 // TODO Do we need to set encoding here?
293 byte[] addon = selectors.getBytes();
294 // Add filters right before match
295 int m = matches.get(0);
296 out.write(buf, 0, m);
297 out.write(addon);
298 out.write(buf, m, count - m);
299 sent = true;
300 continue;
301 }
302 }
303 out.write(buf, 0, count);
304 }
305 catch (IOException e)
306 {
307 break;
308 }
309 }
310 // The correct way would be to close ChunkedOutputStream
311 // but we can not do it because underlying output stream is
312 // used later in caller code. So we use this ugly hack:
313 if (out instanceof ChunkedOutputStream)
314 ((ChunkedOutputStream) out).writeFinalChunk();
315 }
316 }
317 catch (InterruptedIOException e)
318 {
319 /*
320 * Read timeout while reading from the remote side. We use a
321 * read timeout in case the target never responds.
322 */
323 request.sendError(408, "Timeout / No response");
324 }
325 catch (EOFException e)
326 {
327 request.sendError(500, "No response");
328 }
329 catch (UnknownHostException e)
330 {
331 request.sendError(500, "Unknown host");
332 }
333 catch (ConnectException e)
334 {
335 request.sendError(500, "Connection refused");
336 }
337 catch (IOException e)
338 {
339 /*
340 * An IOException will happen if we can't communicate with the
341 * target or the client. Rather than attempting to discriminate,
342 * just send an error message to the client, and let the send
343 * fail if the client was the one that was in error.
344 */
345
346 String msg = "Error from proxy";
347 if (e.getMessage() != null)
348 {
349 msg += ": " + e.getMessage();
350 }
351 request.sendError(500, msg);
352 Log.e(prefix, msg, e);
353 }
354 finally
355 {
356 target.close();
357 }
358 return true;
359 }
360
361 /**
362 * Dump the headers on stderr
363 */
364 public static String dumpHeaders(int count, Request request, MimeHeaders heade rs, boolean sent)
365 {
366 String prompt;
367 StringBuffer sb = new StringBuffer();
368 String label = " " + count;
369 label = label.substring(label.length() - 4);
370 if (sent)
371 {
372 prompt = label + "> ";
373 sb.append(prompt).append(request.toString()).append("\n");
374 }
375 else
376 {
377 prompt = label + "< ";
378 }
379
380 for (int i = 0; i < headers.size(); i++)
381 {
382 sb.append(prompt).append(headers.getKey(i));
383 sb.append(": ").append(headers.get(i)).append("\n");
384 }
385 return (sb.toString());
386 }
387 }
OLDNEW

Powered by Google App Engine
This is Rietveld