add note
[adBlock.git] / src / de / ub0r / android / adBlock / Proxy.java
blob95b05e42221b2a1b4693773c6cafac53cb833a66
1 /*
2 * Copyright (C) 2009 Felix Bechstein
3 *
4 * This file is part of AdBlock.
5 *
6 * This program is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License as published by the Free Software
8 * Foundation; either version 3 of the License, or (at your option) any later
9 * version.
11 * This program is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 * details.
16 * You should have received a copy of the GNU General Public License along with
17 * this program; If not, see <http://www.gnu.org/licenses/>.
19 package de.ub0r.android.adBlock;
21 import java.io.BufferedInputStream;
22 import java.io.BufferedReader;
23 import java.io.BufferedWriter;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.InputStreamReader;
27 import java.io.OutputStream;
28 import java.io.OutputStreamWriter;
29 import java.net.InetSocketAddress;
30 import java.net.ServerSocket;
31 import java.net.Socket;
32 import java.net.URL;
33 import java.util.ArrayList;
35 import android.app.Service;
36 import android.content.Intent;
37 import android.content.SharedPreferences;
38 import android.os.IBinder;
39 import android.preference.PreferenceManager;
40 import android.util.Log;
41 import android.widget.Toast;
43 /**
44 * This ad blocking Proxy Service will work as an ordinary HTTP proxy. Set APN's
45 * proxy preferences to proxy's connection parameters.
47 * @author Felix Bechstein
49 public class Proxy extends Service implements Runnable {
51 /** Preferences: Port. */
52 static final String PREFS_PORT = "port";
53 /** Preferences: Filter. */
54 static final String PREFS_FILTER = "filter";
56 /** HTTP Response: blocked. */
57 private static final String HTTP_BLOCK = "HTTP/1.1 500 blocked by AdBlock";
58 /** HTTP Response: error. */
59 private static final String HTTP_ERROR = "HTTP/1.1 500 error by AdBlock";
60 /** HTTP Response: connected. */
61 private static final String HTTP_CONNECTED = "HTTP/1.1 200 connected";
62 /** HTTP Response: flush. */
63 private static final String HTTP_RESPONSE = "\n\n";
65 /** Default Port for HTTP. */
66 private static final int PORT_HTTP = 80;
67 /** Default Port for HTTPS. */
68 private static final int PORT_HTTPS = 443;
70 /** Proxy. */
71 private Thread proxy = null;
72 /** Proxy's port. */
73 private int port = -1;
74 /** Proxy's filter. */
75 private ArrayList<String> filter = new ArrayList<String>();
76 /** Stop proxy? */
77 private boolean stop = false;
79 /** Tag for output. */
80 private static final String TAG = "AdBlock.Proxy";
82 /**
83 * Connection handles a single HTTP Connection. Run this as a Thread.
85 * @author Felix Bechstein
87 private class Connection implements Runnable {
89 // TODO: cache object.refs
90 // TODO: no private object.refs accessed by inner classes
91 // TODO: reduce object creation
93 /** Local Socket. */
94 private final Socket local;
95 /** Remote Socket. */
96 private Socket remote;
98 /** State: normal. */
99 private static final short STATE_NORMAL = 0;
100 /** State: closed by local side. */
101 private static final short STATE_CLOSED_IN = 1;
102 /** State: closed by remote side. */
103 private static final short STATE_CLOSED_OUT = 2;
104 /** Connections state. */
105 private short state = STATE_NORMAL;
108 * CopyStream reads one stream and writes it's data into an other
109 * stream. Run this as a Thread.
111 * @author Felix Bechstein
113 private class CopyStream implements Runnable {
114 /** Reader. */
115 private final InputStream reader;
116 /** Writer. */
117 private final OutputStream writer;
119 /** Size of buffer. */
120 private static final short BUFFSIZE = 512;
123 * Constructor.
125 * @param r
126 * reader
127 * @param w
128 * writer
130 public CopyStream(final InputStream r, final OutputStream w) {
131 this.reader = new BufferedInputStream(r, BUFFSIZE);
132 this.writer = w;
136 * Run by Thread.start().
138 @Override
139 public void run() {
140 byte[] buf = new byte[BUFFSIZE];
141 int read = 0;
142 try {
143 while (true) {
144 read = this.reader.available();
145 if (read < 1 || read > BUFFSIZE) {
146 read = BUFFSIZE;
148 read = this.reader.read(buf, 0, read);
149 if (read < 0) {
150 break;
152 this.writer.write(buf, 0, read);
153 if (this.reader.available() < 1) {
154 this.writer.flush();
157 Connection.this.close(Connection.STATE_CLOSED_OUT);
158 // this.writer.close();
159 } catch (IOException e) {
160 // FIXME: java.net.SocketException: Broken pipe
161 // no idea, what causes this :/
162 Connection c = Connection.this;
163 Log.e(TAG, null, e);
164 // Log.d(TAG, new String(buf, 0, read));
170 * Constructor.
172 * @param socket
173 * local Socket
175 public Connection(final Socket socket) {
176 this.local = socket;
180 * Check if URL is blocked.
182 * @param url
183 * URL
184 * @return if URL is blocked?
186 private boolean checkURL(final String url) {
187 if (url.indexOf("admob") >= 0 || url.indexOf("google") >= 0) {
188 return false;
190 for (String f : Proxy.this.filter) {
191 if (url.indexOf(f) >= 0) {
192 return true;
195 return false;
199 * Read in HTTP Header. Parse for URL to connect to.
201 * @param reader
202 * buffer reader from which we read the header
203 * @param buffer
204 * buffer into which the header is written
205 * @return URL to which we should connect, port other than 80 is given
206 * explicitly
207 * @throws IOException
208 * inner IOException
210 private URL readHeader(final BufferedReader reader,
211 final StringBuilder buffer) throws IOException {
212 URL ret = null;
213 String[] strings;
214 // read first line
215 if (this.state == STATE_CLOSED_OUT) {
216 return null;
218 String line = reader.readLine();
219 if (line == null) {
220 return null;
222 buffer.append(line + "\r\n");
223 strings = line.split(" ");
224 if (strings.length > 1) {
225 if (strings[0].equals("CONNECT")) {
226 String targetHost = strings[1];
227 int targetPort = PORT_HTTPS;
228 strings = targetHost.split(":");
229 if (strings.length > 1) {
230 targetPort = Integer.parseInt(strings[1]);
231 targetHost = strings[0];
233 ret = new URL("https://" + targetHost + ":" + targetPort);
234 } else if (strings[0].equals("GET")
235 || strings[0].equals("POST")) {
236 String path = null;
237 if (strings[1].startsWith("http://")) {
238 ret = new URL(strings[1]);
239 path = ret.getPath();
240 } else {
241 path = strings[1];
243 // read header
244 while (true) {
245 line = reader.readLine();
246 buffer.append(line + "\r\n");
247 if (line.length() == 0) {
248 break;
250 if (line.startsWith("Host:")) {
251 strings = line.split(" ");
252 if (strings.length > 1) {
253 ret = new URL("http://" + strings[1] + path);
255 break;
258 } else {
259 Log.d(TAG, "unknown method: " + strings[0]);
262 strings = null;
264 // copy rest of reader's buffer
265 while (reader.ready()) {
266 // FIXME this read line breaks everything!
267 // data behind header does not need a read line..
268 // we should read from InputStream directly!
269 buffer.append(reader.readLine() + "\r\n");
271 return ret;
275 * Close local and remote socket.
277 * @param nextState
278 * state to go to
279 * @return new state
280 * @throws IOException
281 * IOException
283 private synchronized short close(final short nextState)
284 throws IOException {
285 Log.d(TAG, "close(" + nextState + ")");
286 short mState = this.state;
287 if (mState == STATE_NORMAL || nextState == STATE_NORMAL) {
288 mState = nextState;
290 Socket mSocket;
291 if (mState != STATE_NORMAL) {
292 // close remote socket
293 mSocket = this.remote;
294 if (mSocket != null && mSocket.isConnected()) {
295 try {
296 mSocket.shutdownInput();
297 mSocket.shutdownOutput();
298 } catch (IOException e) {
299 // Log.e(TAG, null, e);
301 mSocket.close();
303 this.remote = null;
305 if (mState == STATE_CLOSED_OUT) {
306 // close local socket
307 mSocket = this.local;
308 if (mSocket.isConnected()) {
309 try {
310 mSocket.shutdownOutput();
311 mSocket.shutdownInput();
312 } catch (IOException e) {
313 // Log.e(TAG, null, e);
315 mSocket.close();
318 this.state = mState;
319 return mState;
323 * Run by Thread.start().
325 @Override
326 public void run() {
327 InputStream lInStream;
328 OutputStream lOutStream;
329 BufferedReader lReader;
330 BufferedWriter lWriter;
331 try {
332 lInStream = this.local.getInputStream();
333 lOutStream = this.local.getOutputStream();
334 lReader = new BufferedReader(new InputStreamReader(lInStream),
335 CopyStream.BUFFSIZE);
336 lWriter = new BufferedWriter(
337 new OutputStreamWriter(lOutStream), CopyStream.BUFFSIZE);
338 } catch (IOException e) {
339 Log.e(TAG, null, e);
340 return;
342 try {
343 InputStream rInStream = null;
344 OutputStream rOutStream = null;
345 BufferedWriter remoteWriter = null;
346 Thread rThread = null;
347 StringBuilder buffer = new StringBuilder();
348 boolean block = false;
349 String tHost = null;
350 int tPort = -1;
351 URL url;
352 boolean connectSSL = false;
353 while (this.local.isConnected()) {
354 buffer = new StringBuilder();
355 url = this.readHeader(lReader, buffer);
356 if (buffer.length() == 0) {
357 break;
359 if (this.local.isConnected() && rThread != null
360 && !rThread.isAlive()) {
361 // TODO: is this a dead branch? if rThread is dead,
362 // socket should be closed allready..
363 Log.d(TAG, "close dead remote");
364 if (connectSSL) {
365 this.local.close();
367 tHost = null;
368 rInStream = null;
369 rOutStream = null;
370 rThread = null;
372 if (url != null) {
373 block = this.checkURL(url.toString());
374 Log.d(TAG, "new url: " + url.toString());
375 if (!block) {
376 // new connection needed?
377 int p = url.getPort();
378 if (p < 0) {
379 p = PORT_HTTP;
381 if (tHost == null || !tHost.equals(url.getHost())
382 || tPort != p) {
383 // create new connection
384 Log.d(TAG, "shutdown old remote");
385 this.close(STATE_CLOSED_IN);
386 if (rThread != null) {
387 rThread.join();
388 rThread = null;
391 tHost = url.getHost();
392 tPort = url.getPort();
393 if (tPort < 0) {
394 tPort = PORT_HTTP;
396 Log.d(TAG, "new socket: " + url.toString());
397 this.state = STATE_NORMAL;
398 this.remote = new Socket();
399 this.remote.connect(new InetSocketAddress(
400 tHost, tPort));
401 rInStream = this.remote.getInputStream();
402 rOutStream = this.remote.getOutputStream();
403 rThread = new Thread(new CopyStream(rInStream,
404 lOutStream));
405 rThread.start();
406 if (url.getProtocol().startsWith("https")) {
407 connectSSL = true;
408 lWriter.write(HTTP_CONNECTED
409 + HTTP_RESPONSE);
410 lWriter.flush();
411 // copy local to remote by blocks
412 Thread t2 = new Thread(new CopyStream(
413 lInStream, rOutStream));
415 t2.start();
416 remoteWriter = null;
417 break; // copy in separate thread. break
418 // while here
419 } else {
420 remoteWriter = new BufferedWriter(
421 new OutputStreamWriter(rOutStream),
422 CopyStream.BUFFSIZE);
427 // push data to remote if not blocked
428 if (block) {
429 lWriter.append(HTTP_BLOCK + HTTP_RESPONSE
430 + "BLOCKED by AdBlock!");
431 lWriter.flush();
432 } else {
433 Socket mSocket = this.remote;
434 if (mSocket != null && mSocket.isConnected()
435 && remoteWriter != null) {
436 try {
437 remoteWriter.append(buffer);
438 remoteWriter.flush();
439 } catch (IOException e) {
440 Log.d(TAG, buffer.toString(), e);
442 // FIXME: exceptions here!
443 // sync does not fix anything
447 if (rThread != null && rThread.isAlive()) {
448 rThread.join();
450 } catch (InterruptedException e) {
451 Log.e(TAG, null, e);
452 } catch (IOException e) {
453 Log.e(TAG, null, e);
454 try {
455 lWriter.append(HTTP_ERROR + " - " + e.toString()
456 + HTTP_RESPONSE + e.toString());
457 lWriter.flush();
458 lWriter.close();
459 this.local.close();
460 } catch (IOException e1) {
461 Log.e(TAG, null, e1);
464 Log.d(TAG, "close connection");
469 * Default Implementation.
471 * @param intent
472 * called Intent
473 * @return IBinder
474 * @see android.app.Service#onBind(android.content.Intent)
476 @Override
477 public final IBinder onBind(final Intent intent) {
478 return null;
482 * Called on start.
484 * @param intent
485 * Intent called
486 * @param startId
487 * start ID
489 @Override
490 public final void onStart(final Intent intent, final int startId) {
491 super.onStart(intent, startId);
493 // Don't kill me!
494 this.setForeground(true);
496 SharedPreferences preferences = PreferenceManager
497 .getDefaultSharedPreferences(this);
498 int p = Integer.parseInt(preferences.getString(PREFS_PORT, "8080"));
499 boolean portChanged = p != this.port;
500 this.port = p;
502 String f = preferences.getString(PREFS_FILTER, "");
503 this.filter.clear();
504 for (String s : f.split("\n")) {
505 if (s.length() > 0) {
506 this.filter.add(s);
509 if (this.proxy == null) {
510 // Toast.makeText(this, "starting proxy on port: " + this.port,
511 // Toast.LENGTH_SHORT).show();
512 this.proxy = new Thread(this);
513 this.proxy.start();
514 } else {
515 Toast.makeText(this, "proxy running on port " + this.port,
516 Toast.LENGTH_SHORT).show();
517 if (portChanged) {
518 this.proxy.interrupt();
519 this.proxy = new Thread(this);
520 this.proxy.start();
526 * Called on destroy.
528 @Override
529 public final void onDestroy() {
530 super.onDestroy();
531 Toast.makeText(this, "stopping proxy..", Toast.LENGTH_LONG).show();
532 this.stop = true;
536 * Run by Thread.start().
538 @Override
539 public final void run() {
540 try {
541 int p = this.port;
542 ServerSocket sock = new ServerSocket(p);
543 Socket client;
544 while (!this.stop && p == this.port) {
545 if (p != this.port) {
546 break;
548 client = sock.accept();
549 if (client != null) {
550 Log.d(TAG, "new client");
551 Thread t = new Thread(new Connection(client));
552 t.start();
555 sock.close();
556 } catch (IOException e) {
557 Log.e(TAG, null, e);