add state
[adBlock.git] / src / de / ub0r / android / adBlock / Proxy.java
blob4f6f27259015555b561ee3f965ac932ff1e7b296
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.BufferedOutputStream;
23 import java.io.BufferedReader;
24 import java.io.BufferedWriter;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.InputStreamReader;
28 import java.io.OutputStream;
29 import java.io.OutputStreamWriter;
30 import java.net.InetSocketAddress;
31 import java.net.ServerSocket;
32 import java.net.Socket;
33 import java.net.URL;
34 import java.util.ArrayList;
36 import android.app.Service;
37 import android.content.Intent;
38 import android.content.SharedPreferences;
39 import android.os.IBinder;
40 import android.preference.PreferenceManager;
41 import android.util.Log;
42 import android.widget.Toast;
44 /**
45 * This ad blocking Proxy Service will work as an ordinary HTTP proxy. Set APN's
46 * proxy preferences to proxy's connection parameters.
48 * @author Felix Bechstein
50 public class Proxy extends Service implements Runnable {
52 /** Preferences: Port. */
53 static final String PREFS_PORT = "port";
54 /** Preferences: Filter. */
55 static final String PREFS_FILTER = "filter";
57 /** HTTP Response: blocked. */
58 private static final String HTTP_BLOCK = "HTTP/1.1 500 blocked by AdBlock";
59 /** HTTP Response: error. */
60 private static final String HTTP_ERROR = "HTTP/1.1 500 error by AdBlock";
61 /** HTTP Response: connected. */
62 private static final String HTTP_CONNECTED = "HTTP/1.1 200 connected";
63 /** HTTP Response: flush. */
64 private static final String HTTP_RESPONSE = "\n\n";
66 /** Default Port for HTTP. */
67 private static final int PORT_HTTP = 80;
68 /** Default Port for HTTPS. */
69 private static final int PORT_HTTPS = 443;
71 /** Proxy. */
72 private Thread proxy = null;
73 /** Proxy's port. */
74 private int port = -1;
75 /** Proxy's filter. */
76 private ArrayList<String> filter = new ArrayList<String>();
77 /** Stop proxy? */
78 private boolean stop = false;
80 /** Tag for output. */
81 private static final String TAG = "AdBlock.Proxy";
83 /**
84 * Connection handles a single HTTP Connection. Run this as a Thread.
86 * @author Felix Bechstein
88 private class Connection implements Runnable {
90 // TODO: cache object.refs
91 // TODO: no private object.refs accessed by inner classes
92 // TODO: reduce object creation
94 /** Local Socket. */
95 private final Socket local;
96 /** Remote Socket. */
97 private Socket remote;
98 /** Time to wait for new input. */
99 private static final long SLEEP = 100;
100 /** Is this connection still running? */
101 private boolean running = true;
102 /** First run. */
103 private boolean firstRun = true;
105 /** State: normal. */
106 private static final short STATE_NORMAL = 0;
107 /** State: closed by local side. */
108 private static final short STATE_CLOSED_IN = 1;
109 /** State: closed by remote side. */
110 private static final short STATE_CLOSED_OUT = 2;
111 /** Connections state. */
112 private short state = STATE_NORMAL;
113 /** Sync state. */
114 private Object syncState = new Object();
117 * CopyStream reads one stream and writes it's data into an other
118 * stream. Run this as a Thread.
120 * @author Felix Bechstein
122 private class CopyStream implements Runnable {
123 /** Reader. */
124 private final InputStream reader;
125 /** Writer. */
126 private final OutputStream writer;
128 /** Size of buffer. */
129 private static final short BUFFSIZE = 512;
132 * Constructor.
134 * @param r
135 * reader
136 * @param w
137 * writer
139 public CopyStream(final InputStream r, final OutputStream w) {
140 this.reader = new BufferedInputStream(r, BUFFSIZE);
141 this.writer = new BufferedOutputStream(w, BUFFSIZE);
145 * Run by Thread.start().
147 @Override
148 public void run() {
149 byte[] buf = new byte[BUFFSIZE];
150 int read = 0;
151 try {
152 while (true) {
153 read = this.reader.available();
154 if (read < 1 || read > BUFFSIZE) {
155 read = BUFFSIZE;
157 read = this.reader.read(buf, 0, read);
158 if (read < 0) {
159 break;
161 this.writer.write(buf, 0, read);
162 if (this.reader.available() < 1) {
163 this.writer.flush();
166 synchronized (Connection.this.syncState) {
167 // only change state if old state was normal
168 if (Connection.this.state == Connection.STATE_NORMAL) {
169 Connection.this.state = Connection.STATE_CLOSED_OUT;
172 this.writer.close();
173 Connection.this.close();
174 } catch (IOException e) {
175 Log.e(TAG, null, e);
176 Log.d(TAG, new String(buf, 0, read));
182 * Constructor.
184 * @param socket
185 * local Socket
187 public Connection(final Socket socket) {
188 this.local = socket;
192 * Check if URL is blocked.
194 * @param url
195 * URL
196 * @return if URL is blocked?
198 private boolean checkURL(final String url) {
199 if (url.indexOf("admob") >= 0 || url.indexOf("google") >= 0) {
200 return false;
202 for (String f : Proxy.this.filter) {
203 if (url.indexOf(f) >= 0) {
204 return true;
207 return false;
211 * Read in HTTP Header. Parse for URL to connect to.
213 * @param reader
214 * buffer reader from which we read the header
215 * @param buffer
216 * buffer into which the header is written
217 * @return URL to which we should connect, port other than 80 is given
218 * explicitly
219 * @throws IOException
220 * inner IOException
222 private URL readHeader(final BufferedReader reader,
223 final StringBuilder buffer) throws IOException {
224 URL ret = null;
225 String[] strings;
226 // read first line
227 while (!reader.ready() && this.state != STATE_CLOSED_OUT) {
228 try { // isConnected does not work :/
229 Thread.sleep(SLEEP);
230 } catch (InterruptedException e) {
231 Log.e(TAG, null, e);
234 this.firstRun = false;
235 if (this.state == STATE_CLOSED_OUT) {
236 return null;
238 String line = reader.readLine();
239 if (line == null) {
240 return null;
242 buffer.append(line + "\n");
243 strings = line.split(" ");
244 if (strings.length > 1) {
245 if (strings[0].equals("CONNECT")) {
246 String targetHost = strings[1];
247 int targetPort = PORT_HTTPS;
248 strings = targetHost.split(":");
249 if (strings.length > 1) {
250 targetPort = Integer.parseInt(strings[1]);
251 targetHost = strings[0];
253 ret = new URL("https://" + targetHost + ":" + targetPort);
254 } else if (strings[0].equals("GET")
255 || strings[1].equals("POST")) {
256 String path = null;
257 if (strings[1].startsWith("http://")) {
258 ret = new URL(strings[1]);
259 path = ret.getPath();
260 } else {
261 path = strings[1];
263 // read header
264 while (true) {
265 line = reader.readLine();
266 buffer.append(line + "\n");
267 if (line.length() == 0) {
268 break;
270 if (line.startsWith("Host:")) {
271 strings = line.split(" ");
272 if (strings.length > 1) {
273 ret = new URL("http://" + strings[1] + path);
275 break;
280 strings = null;
282 // copy rest of reader's buffer
283 while (reader.ready()) {
284 buffer.append(reader.readLine() + "\n");
286 return ret;
290 * Close local and remote socket.
292 * @throws IOException
293 * IOException
295 private void close() throws IOException {
296 Log.d(TAG, "close()");
297 // this.running = false;
298 if (this.remote != null) {
299 synchronized (this.remote) {
300 if (this.remote.isConnected()) {
301 try {
302 this.remote.shutdownInput();
303 this.remote.shutdownOutput();
304 } catch (IOException e) {
305 Log.e(TAG, null, e);
307 this.remote.close();
310 this.remote = null;
312 if (this.local != null && this.state == STATE_CLOSED_OUT) {
313 synchronized (this.local) {
314 if (this.local.isConnected()) {
315 try {
316 this.local.shutdownOutput();
317 this.local.shutdownInput();
318 } catch (IOException e) {
319 Log.e(TAG, null, e);
321 this.local.close();
328 * Run by Thread.start().
330 @Override
331 public void run() {
332 InputStream lInStream;
333 OutputStream lOutStream;
334 BufferedReader lReader;
335 BufferedWriter lWriter;
336 try {
337 lInStream = this.local.getInputStream();
338 lOutStream = this.local.getOutputStream();
339 lReader = new BufferedReader(new InputStreamReader(lInStream),
340 CopyStream.BUFFSIZE);
341 lWriter = new BufferedWriter(
342 new OutputStreamWriter(lOutStream), CopyStream.BUFFSIZE);
343 } catch (IOException e) {
344 Log.e(TAG, null, e);
345 return;
347 try {
348 InputStream rInStream = null;
349 OutputStream rOutStream = null;
350 BufferedWriter remoteWriter = null;
351 Thread rThread = null;
352 StringBuilder buffer = new StringBuilder();
353 boolean block = false;
354 String tHost = null;
355 int tPort = -1;
356 URL url;
357 boolean connectSSL = false;
358 while (this.running && this.local.isConnected()) {
359 buffer = new StringBuilder();
360 url = this.readHeader(lReader, buffer);
361 if (buffer.length() == 0) {
362 break;
364 if (this.local.isConnected() && rThread != null
365 && !rThread.isAlive()) {
366 rThread.join();
367 tHost = null;
368 if (connectSSL) {
369 this.local.close();
371 if (remote != null) {
372 synchronized (this.remote) {
373 Socket mSocket = this.remote;
374 Log.d(TAG, "close dead remote");
375 mSocket.shutdownInput();
376 mSocket.shutdownOutput();
377 mSocket.close();
378 this.remote = null;
381 rInStream = null;
382 rOutStream = null;
383 rThread = null;
385 if (url != null) {
386 block = this.checkURL(url.toString());
387 Log.d(TAG, "new url: " + url.toString());
388 if (!block) {
389 // new connection needed?
390 int p = url.getPort();
391 if (p < 0) {
392 p = PORT_HTTP;
394 if (tHost == null || !tHost.equals(url.getHost())
395 || tPort != p) {
396 // create new connection
397 if (this.remote != null) {
398 this.state = STATE_CLOSED_IN;
399 synchronized (this.remote) {
400 Socket mSocket = this.remote;
401 Log.d(TAG, "shutdown old remote");
402 mSocket.shutdownInput();
403 mSocket.shutdownOutput();
404 mSocket.close();
405 this.remote = null;
407 rThread.join();
409 tHost = url.getHost();
410 tPort = url.getPort();
411 if (tPort < 0) {
412 tPort = PORT_HTTP;
414 Log.d(TAG, "new socket: " + url.toString());
415 this.state = STATE_NORMAL;
416 this.remote = new Socket();
417 this.remote.connect(new InetSocketAddress(
418 tHost, tPort));
419 rInStream = this.remote.getInputStream();
420 rOutStream = this.remote.getOutputStream();
421 rThread = new Thread(new CopyStream(rInStream,
422 lOutStream));
423 rThread.start();
424 if (url.getProtocol().startsWith("https")) {
425 connectSSL = true;
426 lWriter.write(HTTP_CONNECTED
427 + HTTP_RESPONSE);
428 lWriter.flush();
429 // copy local to remote by blocks
430 Thread t2 = new Thread(new CopyStream(
431 lInStream, rOutStream));
433 t2.start();
434 remoteWriter = null;
435 break; // copy in separate thread. break
436 // while here
437 } else {
438 remoteWriter = new BufferedWriter(
439 new OutputStreamWriter(rOutStream),
440 CopyStream.BUFFSIZE);
445 // push data to remote if not blocked
446 if (block) {
447 lWriter.append(HTTP_BLOCK + HTTP_RESPONSE
448 + "BLOCKED by AdBlock!");
449 lWriter.flush();
450 } else if (this.remote != null && this.remote.isConnected()
451 && remoteWriter != null) {
452 remoteWriter.append(buffer);
453 remoteWriter.flush();
456 if (rThread != null && rThread.isAlive()) {
457 rThread.join();
459 } catch (InterruptedException e) {
460 Log.e(TAG, null, e);
461 } catch (IOException e) {
462 Log.e(TAG, null, e);
463 try {
464 lWriter.append(HTTP_ERROR + " - " + e.toString()
465 + HTTP_RESPONSE + e.toString());
466 lWriter.flush();
467 lWriter.close();
468 this.local.close();
469 } catch (IOException e1) {
470 Log.e(TAG, null, e1);
473 Log.d(TAG, "close connection");
478 * Default Implementation.
480 * @param intent
481 * called Intent
482 * @return IBinder
483 * @see android.app.Service#onBind(android.content.Intent)
485 @Override
486 public final IBinder onBind(final Intent intent) {
487 return null;
491 * Called on start.
493 * @param intent
494 * Intent called
495 * @param startId
496 * start ID
498 @Override
499 public final void onStart(final Intent intent, final int startId) {
500 super.onStart(intent, startId);
502 // Don't kill me!
503 this.setForeground(true);
505 SharedPreferences preferences = PreferenceManager
506 .getDefaultSharedPreferences(this);
507 int p = Integer.parseInt(preferences.getString(PREFS_PORT, "8080"));
508 boolean portChanged = p != this.port;
509 this.port = p;
511 String f = preferences.getString(PREFS_FILTER, "");
512 this.filter.clear();
513 for (String s : f.split("\n")) {
514 if (s.length() > 0) {
515 this.filter.add(s);
518 if (this.proxy == null) {
519 // Toast.makeText(this, "starting proxy on port: " + this.port,
520 // Toast.LENGTH_SHORT).show();
521 this.proxy = new Thread(this);
522 this.proxy.start();
523 } else {
524 Toast.makeText(this, "proxy running on port " + this.port,
525 Toast.LENGTH_SHORT).show();
526 if (portChanged) {
527 this.proxy.interrupt();
528 this.proxy = new Thread(this);
529 this.proxy.start();
535 * Called on destroy.
537 @Override
538 public final void onDestroy() {
539 super.onDestroy();
540 Toast.makeText(this, "stopping proxy..", Toast.LENGTH_LONG).show();
541 this.stop = true;
545 * Run by Thread.start().
547 @Override
548 public final void run() {
549 try {
550 int p = this.port;
551 ServerSocket sock = new ServerSocket(p);
552 Socket client;
553 while (!this.stop && p == this.port) {
554 if (p != this.port) {
555 break;
557 client = sock.accept();
558 if (client != null) {
559 Log.d(TAG, "new client");
560 Thread t = new Thread(new Connection(client));
561 t.start();
564 sock.close();
565 } catch (IOException e) {
566 Log.e(TAG, null, e);