-private to inner classes
[adBlock.git] / src / de / ub0r / android / adBlock / Proxy.java
blob50e634d0cd7add6b729e7a3e6ffe8b961c7cd316
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.BufferedWriter;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.io.OutputStreamWriter;
27 import java.net.InetSocketAddress;
28 import java.net.ServerSocket;
29 import java.net.Socket;
30 import java.net.URL;
31 import java.util.ArrayList;
33 import android.app.Notification;
34 import android.app.NotificationManager;
35 import android.app.PendingIntent;
36 import android.app.Service;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.SharedPreferences;
40 import android.os.IBinder;
41 import android.preference.PreferenceManager;
42 import android.util.Log;
43 import android.widget.Toast;
45 /**
46 * This ad blocking Proxy Service will work as an ordinary HTTP proxy. Set APN's
47 * proxy preferences to proxy's connection parameters.
49 * @author Felix Bechstein
51 public class Proxy extends Service implements Runnable {
53 /** Preferences: Port. */
54 static final String PREFS_PORT = "port";
55 /** Preferences: Filter. */
56 static final String PREFS_FILTER = "filter";
58 /** HTTP Response: blocked. */
59 private static final String HTTP_BLOCK = "HTTP/1.1 500 blocked by AdBlock";
60 /** HTTP Response: error. */
61 private static final String HTTP_ERROR = "HTTP/1.1 500 error by AdBlock";
62 /** HTTP Response: connected. */
63 private static final String HTTP_CONNECTED = "HTTP/1.1 200 connected";
64 /** HTTP Response: flush. */
65 private static final String HTTP_RESPONSE = "\n\n";
67 /** Default Port for HTTP. */
68 private static final int PORT_HTTP = 80;
69 /** Default Port for HTTPS. */
70 private static final int PORT_HTTPS = 443;
72 /** Proxy. */
73 private Thread proxy = null;
74 /** Proxy's port. */
75 private int port = -1;
76 /** Proxy's filter. */
77 private ArrayList<String> filter = new ArrayList<String>();
78 /** Stop proxy? */
79 private boolean stop = false;
81 /** Tag for output. */
82 private static final String TAG = "AdBlock.Proxy";
84 /**
85 * Connection handles a single HTTP Connection. Run this as a Thread.
87 * @author Felix Bechstein
89 class Connection implements Runnable {
91 // TODO: cache object.refs
92 // TODO: no private object.refs accessed by inner classes
93 // TODO: reduce object creation
95 /** Local Socket. */
96 private final Socket local;
97 /** Remote Socket. */
98 private Socket remote;
100 /** State: normal. */
101 private static final short STATE_NORMAL = 0;
102 /** State: closed by local side. */
103 private static final short STATE_CLOSED_IN = 1;
104 /** State: closed by remote side. */
105 private static final short STATE_CLOSED_OUT = 2;
106 /** Connections state. */
107 private short state = STATE_NORMAL;
110 * CopyStream reads one stream and writes it's data into an other
111 * stream. Run this as a Thread.
113 * @author Felix Bechstein
115 private class CopyStream implements Runnable {
116 /** Reader. */
117 private final InputStream reader;
118 /** Writer. */
119 private final OutputStream writer;
121 /** Size of buffer. */
122 private static final int BUFFSIZE = 32768;
123 /** Size of header buffer. */
124 private static final int HEADERBUFFSIZE = 1024;
127 * Constructor.
129 * @param r
130 * reader
131 * @param w
132 * writer
134 public CopyStream(final InputStream r, final OutputStream w) {
135 this.reader = new BufferedInputStream(r, BUFFSIZE);
136 this.writer = w;
140 * Run by Thread.start().
142 @Override
143 public void run() {
144 byte[] buf = new byte[BUFFSIZE];
145 int read = 0;
146 try {
147 while (true) {
148 read = this.reader.available();
149 if (read < 1 || read > BUFFSIZE) {
150 read = BUFFSIZE;
152 read = this.reader.read(buf, 0, read);
153 if (read < 0) {
154 break;
156 this.writer.write(buf, 0, read);
157 if (this.reader.available() < 1) {
158 this.writer.flush();
161 Connection.this.close(Connection.STATE_CLOSED_OUT);
162 // this.writer.close();
163 } catch (IOException e) {
164 // FIXME: java.net.SocketException: Broken pipe
165 // no idea, what causes this :/
166 // Connection c = Connection.this;
167 // String s = new String(buf, 0, read);
168 Log.e(TAG, null, e);
174 * Constructor.
176 * @param socket
177 * local Socket
179 public Connection(final Socket socket) {
180 this.local = socket;
184 * Check if URL is blocked.
186 * @param url
187 * URL
188 * @return if URL is blocked?
190 private boolean checkURL(final String url) {
191 if (url.indexOf("admob") >= 0 || url.indexOf("google") >= 0) {
192 return false;
194 for (String f : Proxy.this.filter) {
195 if (url.indexOf(f) >= 0) {
196 return true;
199 return false;
203 * Read in HTTP Header. Parse for URL to connect to.
205 * @param reader
206 * buffer reader from which we read the header
207 * @param buffer
208 * buffer into which the header is written
209 * @return URL to which we should connect, port other than 80 is given
210 * explicitly
211 * @throws IOException
212 * inner IOException
214 private URL readHeader(final BufferedInputStream reader,
215 final StringBuilder buffer) throws IOException {
216 URL ret = null;
217 String[] strings;
218 int avail;
219 byte[] buf = new byte[CopyStream.HEADERBUFFSIZE];
220 // read first line
221 if (this.state == STATE_CLOSED_OUT) {
222 return null;
224 avail = reader.available();
225 if (avail > CopyStream.HEADERBUFFSIZE) {
226 avail = CopyStream.HEADERBUFFSIZE;
227 } else if (avail == 0) {
228 avail = CopyStream.HEADERBUFFSIZE;
230 avail = reader.read(buf, 0, avail);
231 if (avail < 1) {
232 return null;
234 String line = new String(buf, 0, avail);
235 String testLine = line;
236 int i = line.indexOf(" http://");
237 if (i > 0) {
238 // remove "http://host:port" from line
239 int j = line.indexOf('/', i + 9);
240 if (j > i) {
241 testLine = line.substring(0, i + 1) + line.substring(j);
244 buffer.append(testLine);
245 strings = line.split(" ");
246 if (strings.length > 1) {
247 if (strings[0].equals("CONNECT")) {
248 String targetHost = strings[1];
249 int targetPort = PORT_HTTPS;
250 strings = targetHost.split(":");
251 if (strings.length > 1) {
252 targetPort = Integer.parseInt(strings[1]);
253 targetHost = strings[0];
255 ret = new URL("https://" + targetHost + ":" + targetPort);
256 } else if (strings[0].equals("GET")
257 || strings[0].equals("POST")) {
258 String path = null;
259 if (strings[1].startsWith("http://")) {
260 ret = new URL(strings[1]);
261 path = ret.getPath();
262 } else {
263 path = strings[1];
265 // read header
266 String lastLine = line;
267 do {
268 testLine = lastLine + line;
269 i = testLine.indexOf("\nHost: ");
270 if (i >= 0) {
271 int j = testLine.indexOf("\n", i + 6);
272 if (j > 0) {
273 String tHost = testLine.substring(i + 6, j)
274 .trim();
275 ret = new URL("http://" + tHost + path);
276 break;
277 } else {
278 // test for "Host:" again with longer buffer
279 line = lastLine + line;
282 if (line.indexOf("\r\n\r\n") >= 0) {
283 break;
285 lastLine = line;
286 avail = reader.available();
287 if (avail > 0) {
288 if (avail > CopyStream.HEADERBUFFSIZE) {
289 avail = CopyStream.HEADERBUFFSIZE;
291 avail = reader.read(buf, 0, avail);
292 // FIXME: this may break
293 line = new String(buf, 0, avail);
294 buffer.append(line);
296 } while (avail > 0);
297 } else {
298 Log.d(TAG, "unknown method: " + strings[0]);
301 strings = null;
303 // copy rest of reader's buffer
304 avail = reader.available();
305 while (avail > 0) {
306 if (avail > CopyStream.HEADERBUFFSIZE) {
307 avail = CopyStream.HEADERBUFFSIZE;
309 avail = reader.read(buf, 0, avail);
310 // FIXME: this may break!
311 buffer.append(new String(buf, 0, avail));
312 avail = reader.available();
314 return ret;
318 * Close local and remote socket.
320 * @param nextState
321 * state to go to
322 * @return new state
323 * @throws IOException
324 * IOException
326 private synchronized short close(final short nextState)
327 throws IOException {
328 Log.d(TAG, "close(" + nextState + ")");
329 short mState = this.state;
330 if (mState == STATE_NORMAL || nextState == STATE_NORMAL) {
331 mState = nextState;
333 Socket mSocket;
334 if (mState != STATE_NORMAL) {
335 // close remote socket
336 mSocket = this.remote;
337 if (mSocket != null && mSocket.isConnected()) {
338 try {
339 mSocket.shutdownInput();
340 mSocket.shutdownOutput();
341 } catch (IOException e) {
342 // Log.e(TAG, null, e);
344 mSocket.close();
346 this.remote = null;
348 if (mState == STATE_CLOSED_OUT) {
349 // close local socket
350 mSocket = this.local;
351 if (mSocket.isConnected()) {
352 try {
353 mSocket.shutdownOutput();
354 mSocket.shutdownInput();
355 } catch (IOException e) {
356 // Log.e(TAG, null, e);
358 mSocket.close();
361 this.state = mState;
362 return mState;
366 * {@inheritDoc}
368 @Override
369 public void run() {
370 BufferedInputStream lInStream;
371 OutputStream lOutStream;
372 BufferedWriter lWriter;
373 try {
374 lInStream = new BufferedInputStream(
375 this.local.getInputStream(), CopyStream.BUFFSIZE);
376 lOutStream = this.local.getOutputStream();
377 lWriter = new BufferedWriter(
378 new OutputStreamWriter(lOutStream), CopyStream.BUFFSIZE);
379 } catch (IOException e) {
380 Log.e(TAG, null, e);
381 return;
383 try {
384 InputStream rInStream = null;
385 OutputStream rOutStream = null;
386 BufferedWriter remoteWriter = null;
387 Thread rThread = null;
388 StringBuilder buffer = new StringBuilder();
389 boolean block = false;
390 String tHost = null;
391 int tPort = -1;
392 URL url;
393 boolean connectSSL = false;
394 while (this.local.isConnected()) {
395 buffer = new StringBuilder();
396 url = this.readHeader(lInStream, buffer);
397 if (buffer.length() == 0) {
398 break;
400 if (this.local.isConnected() && rThread != null
401 && !rThread.isAlive()) {
402 // TODO: is this a dead branch? if rThread is dead,
403 // socket should be closed allready..
404 Log.d(TAG, "close dead remote");
405 if (connectSSL) {
406 this.local.close();
408 tHost = null;
409 rInStream = null;
410 rOutStream = null;
411 rThread = null;
413 if (url != null) {
414 block = this.checkURL(url.toString());
415 Log.d(TAG, "new url: " + url.toString());
416 if (!block) {
417 // new connection needed?
418 int p = url.getPort();
419 if (p < 0) {
420 p = PORT_HTTP;
422 if (tHost == null || !tHost.equals(url.getHost())
423 || tPort != p) {
424 // create new connection
425 Log.d(TAG, "shutdown old remote");
426 this.close(STATE_CLOSED_IN);
427 if (rThread != null) {
428 rThread.join();
429 rThread = null;
432 tHost = url.getHost();
433 tPort = p;
434 Log.d(TAG, "new socket: " + url.toString());
435 this.state = STATE_NORMAL;
436 this.remote = new Socket();
437 this.remote.connect(new InetSocketAddress(
438 tHost, tPort));
439 rInStream = this.remote.getInputStream();
440 rOutStream = this.remote.getOutputStream();
441 rThread = new Thread(new CopyStream(rInStream,
442 lOutStream));
443 rThread.start();
444 if (url.getProtocol().startsWith("https")) {
445 connectSSL = true;
446 lWriter.write(HTTP_CONNECTED
447 + HTTP_RESPONSE);
448 lWriter.flush();
449 // copy local to remote by blocks
450 Thread t2 = new Thread(new CopyStream(
451 lInStream, rOutStream));
453 t2.start();
454 remoteWriter = null;
455 break; // copy in separate thread. break
456 // while here
457 } else {
458 remoteWriter = new BufferedWriter(
459 new OutputStreamWriter(rOutStream),
460 CopyStream.BUFFSIZE);
465 // push data to remote if not blocked
466 if (block) {
467 lWriter.append(HTTP_BLOCK + HTTP_RESPONSE
468 + "BLOCKED by AdBlock!");
469 lWriter.flush();
470 } else {
471 Socket mSocket = this.remote;
472 if (mSocket != null && mSocket.isConnected()
473 && remoteWriter != null) {
474 try {
475 remoteWriter.append(buffer);
476 remoteWriter.flush();
477 } catch (IOException e) {
478 Log.d(TAG, buffer.toString(), e);
480 // FIXME: exceptions here!
481 // sync does not fix anything
485 if (rThread != null && rThread.isAlive()) {
486 rThread.join();
488 } catch (InterruptedException e) {
489 Log.e(TAG, null, e);
490 } catch (IOException e) {
491 Log.e(TAG, null, e);
492 try {
493 lWriter.append(HTTP_ERROR + " - " + e.toString()
494 + HTTP_RESPONSE + e.toString());
495 lWriter.flush();
496 lWriter.close();
497 this.local.close();
498 } catch (IOException e1) {
499 Log.e(TAG, null, e1);
502 Log.d(TAG, "close connection");
507 * {@inheritDoc}
509 @Override
510 public final IBinder onBind(final Intent intent) {
511 return null;
515 * {@inheritDoc}
517 @Override
518 public final void onStart(final Intent intent, final int startId) {
519 super.onStart(intent, startId);
521 // Don't kill me!
522 this.setForeground(true);
523 final Notification notification = new Notification(
524 R.drawable.stat_notify_proxy, "", System.currentTimeMillis());
525 final PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
526 new Intent(this, AdBlock.class), 0);
527 notification.setLatestEventInfo(this, this
528 .getString(R.string.notify_proxy), "", contentIntent);
529 notification.defaults |= Notification.FLAG_NO_CLEAR;
530 try {
531 new HelperAPI5().startForeground(this, 0, notification);
532 } catch (VerifyError e) {
533 Log.i(TAG, "no api 5");
536 SharedPreferences preferences = PreferenceManager
537 .getDefaultSharedPreferences(this);
538 int p = Integer.parseInt(preferences.getString(PREFS_PORT, "8080"));
539 boolean portChanged = p != this.port;
540 this.port = p;
542 String f = preferences.getString(PREFS_FILTER, "");
543 this.filter.clear();
544 for (String s : f.split("\n")) {
545 if (s.length() > 0) {
546 this.filter.add(s);
549 if (this.proxy == null) {
550 // Toast.makeText(this, "starting proxy on port: " + this.port,
551 // Toast.LENGTH_SHORT).show();
552 this.proxy = new Thread(this);
553 this.proxy.start();
554 } else {
555 Toast.makeText(this,
556 this.getString(R.string.proxy_running) + " " + this.port,
557 Toast.LENGTH_SHORT).show();
558 if (portChanged) {
559 this.proxy.interrupt();
560 this.proxy = new Thread(this);
561 this.proxy.start();
567 * {@inheritDoc}
569 @Override
570 public final void onDestroy() {
571 super.onDestroy();
572 Toast.makeText(this, R.string.proxy_stopped, Toast.LENGTH_LONG).show();
573 this.stop = true;
574 ((NotificationManager) this
575 .getSystemService(Context.NOTIFICATION_SERVICE)).cancelAll();
579 * {@inheritDoc}
581 @Override
582 public final void run() {
583 try {
584 int p = this.port;
585 ServerSocket sock = new ServerSocket(p);
586 Socket client;
587 while (!this.stop && p == this.port) {
588 if (p != this.port) {
589 break;
591 client = sock.accept();
592 if (client != null) {
593 Log.d(TAG, "new client");
594 Thread t = new Thread(new Connection(client));
595 t.start();
598 sock.close();
599 } catch (IOException e) {
600 Log.e(TAG, null, e);