remove whitelist, add some more info to filter_
[adBlock.git] / src / de / ub0r / android / adBlock / Proxy.java
blobf7dc33302446fda09828a5b79e608ccbf402fd39
1 /*
2 * Copyright (C) 2010 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 {
52 /** Tag for output. */
53 private static final String TAG = "AdBlock.Proxy";
55 /** Preferences: Port. */
56 static final String PREFS_PORT = "port";
57 /** Preferences: Filter. */
58 static final String PREFS_FILTER = "filter";
60 /** HTTP Response: blocked. */
61 private static final String HTTP_BLOCK = "HTTP/1.1 500 blocked by AdBlock";
62 /** HTTP Response: error. */
63 private static final String HTTP_ERROR = "HTTP/1.1 500 error by AdBlock";
64 /** HTTP Response: connected. */
65 private static final String HTTP_CONNECTED = "HTTP/1.1 200 connected";
66 /** HTTP Response: flush. */
67 private static final String HTTP_RESPONSE = "\n\n";
69 /** Default Port for HTTP. */
70 private static final int PORT_HTTP = 80;
71 /** Default Port for HTTPS. */
72 private static final int PORT_HTTPS = 443;
74 /** Proxy. */
75 private Thread proxy = null;
76 /** Proxy's port. */
77 private int port = -1;
78 /** Proxy's filter. */
79 ArrayList<String> filter = new ArrayList<String>();
80 /** Stop proxy? */
81 private boolean stop = false;
83 /**
84 * Connection handles a single HTTP Connection. Run this as a Thread.
86 * @author Felix Bechstein
88 private class Connection implements Runnable {
90 // cache object.refs
91 // 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;
99 /** State: normal. */
100 private static final short STATE_NORMAL = 0;
101 /** State: closed by local side. */
102 private static final short STATE_CLOSED_IN = 1;
103 /** State: closed by remote side. */
104 private static final short STATE_CLOSED_OUT = 2;
105 /** Connections state. */
106 private short state = STATE_NORMAL;
109 * CopyStream reads one stream and writes it's data into an other
110 * stream. Run this as a Thread.
112 * @author Felix Bechstein
114 private class CopyStream implements Runnable {
115 /** Reader. */
116 private final InputStream reader;
117 /** Writer. */
118 private final OutputStream writer;
120 /** Size of buffer. */
121 private static final int BUFFSIZE = 32768;
122 /** Size of header buffer. */
123 private static final int HEADERBUFFSIZE = 1024;
126 * Constructor.
128 * @param r
129 * reader
130 * @param w
131 * writer
133 public CopyStream(final InputStream r, final OutputStream w) {
134 this.reader = new BufferedInputStream(r, BUFFSIZE);
135 this.writer = w;
139 * Run by Thread.start().
141 @Override
142 public void run() {
143 byte[] buf = new byte[BUFFSIZE];
144 int read = 0;
145 final InputStream r = this.reader;
146 final OutputStream w = this.writer;
147 try {
148 while (true) {
149 read = r.available();
150 if (read < 1 || read > BUFFSIZE) {
151 read = BUFFSIZE;
153 read = r.read(buf, 0, read);
154 if (read < 0) {
155 break;
157 w.write(buf, 0, read);
158 if (r.available() < 1) {
159 w.flush();
162 Connection.this.close(Connection.STATE_CLOSED_OUT);
163 // this.writer.close();
164 } catch (IOException e) {
165 // FIXME: java.net.SocketException: Broken pipe
166 // no idea, what causes this :/
167 // Connection c = Connection.this;
168 // String s = new String(buf, 0, read);
169 Log.e(TAG, null, e);
175 * Constructor.
177 * @param socket
178 * local Socket
180 public Connection(final Socket socket) {
181 this.local = socket;
185 * Check if URL is blocked.
187 * @param url
188 * URL
189 * @return if URL is blocked?
191 private boolean checkURL(final String url) {
192 final ArrayList<String> f = Proxy.this.filter;
193 final int s = f.size();
194 for (int i = 0; i < s; i++) {
195 if (url.indexOf(f.get(i)) >= 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 // socket should be closed allready..
403 Log.d(TAG, "close dead remote");
404 if (connectSSL) {
405 this.local.close();
407 tHost = null;
408 rInStream = null;
409 rOutStream = null;
410 rThread = null;
412 if (url != null) {
413 block = this.checkURL(url.toString());
414 Log.d(TAG, "new url: " + url.toString());
415 if (!block) {
416 // new connection needed?
417 int p = url.getPort();
418 if (p < 0) {
419 p = PORT_HTTP;
421 if (tHost == null || !tHost.equals(url.getHost())
422 || tPort != p) {
423 // create new connection
424 Log.d(TAG, "shutdown old remote");
425 this.close(STATE_CLOSED_IN);
426 if (rThread != null) {
427 rThread.join();
428 rThread = null;
431 tHost = url.getHost();
432 tPort = p;
433 Log.d(TAG, "new socket: " + url.toString());
434 this.state = STATE_NORMAL;
435 this.remote = new Socket();
436 this.remote.connect(new InetSocketAddress(
437 tHost, tPort));
438 rInStream = this.remote.getInputStream();
439 rOutStream = this.remote.getOutputStream();
440 rThread = new Thread(new CopyStream(rInStream,
441 lOutStream));
442 rThread.start();
443 if (url.getProtocol().startsWith("https")) {
444 connectSSL = true;
445 lWriter.write(HTTP_CONNECTED
446 + HTTP_RESPONSE);
447 lWriter.flush();
448 // copy local to remote by blocks
449 Thread t2 = new Thread(new CopyStream(
450 lInStream, rOutStream));
452 t2.start();
453 remoteWriter = null;
454 break; // copy in separate thread. break
455 // while here
456 } else {
457 remoteWriter = new BufferedWriter(
458 new OutputStreamWriter(rOutStream),
459 CopyStream.BUFFSIZE);
464 // push data to remote if not blocked
465 if (block) {
466 lWriter.append(HTTP_BLOCK + HTTP_RESPONSE
467 + "BLOCKED by AdBlock!");
468 lWriter.flush();
469 } else {
470 Socket mSocket = this.remote;
471 if (mSocket != null && mSocket.isConnected()
472 && remoteWriter != null) {
473 try {
474 remoteWriter.append(buffer);
475 remoteWriter.flush();
476 } catch (IOException e) {
477 Log.d(TAG, buffer.toString(), e);
482 if (rThread != null && rThread.isAlive()) {
483 rThread.join();
485 } catch (InterruptedException e) {
486 Log.e(TAG, null, e);
487 } catch (IOException e) {
488 Log.e(TAG, null, e);
489 try {
490 lWriter.append(HTTP_ERROR + " - " + e.toString()
491 + HTTP_RESPONSE + e.toString());
492 lWriter.flush();
493 lWriter.close();
494 this.local.close();
495 } catch (IOException e1) {
496 Log.e(TAG, null, e1);
499 Log.d(TAG, "close connection");
504 * {@inheritDoc}
506 @Override
507 public final IBinder onBind(final Intent intent) {
508 return null;
512 * {@inheritDoc}
514 @Override
515 public final void onStart(final Intent intent, final int startId) {
516 super.onStart(intent, startId);
518 HelperAPI5 helperAPI5 = null;
519 try {
520 helperAPI5 = new HelperAPI5();
521 if (!helperAPI5.isAvailable()) {
522 helperAPI5 = null;
524 } catch (VerifyError e) {
525 helperAPI5 = null;
526 Log.i(TAG, "no api 5");
529 // Don't kill me!
530 if (helperAPI5 == null) {
531 this.setForeground(true);
532 } else {
533 final Notification notification = new Notification(
534 R.drawable.stat_notify_proxy, "", System
535 .currentTimeMillis());
536 final PendingIntent contentIntent = PendingIntent.getActivity(this,
537 0, new Intent(this, AdBlock.class), 0);
538 notification.setLatestEventInfo(this, this
539 .getString(R.string.notify_proxy), "", contentIntent);
540 notification.defaults |= Notification.FLAG_NO_CLEAR;
543 SharedPreferences preferences = PreferenceManager
544 .getDefaultSharedPreferences(this);
545 int p = Integer.parseInt(preferences.getString(PREFS_PORT, "8080"));
546 boolean portChanged = p != this.port;
547 this.port = p;
549 String f = preferences.getString(PREFS_FILTER, "");
550 final ArrayList<String> fl = this.filter;
551 fl.clear();
552 for (String s : f.split("\n")) {
553 if (s.length() > 0) {
554 fl.add(s);
557 if (this.proxy == null) {
558 // Toast.makeText(this, "starting proxy on port: " + this.port,
559 // Toast.LENGTH_SHORT).show();
560 final Thread pr = new Thread(this);
561 pr.start();
562 this.proxy = pr;
563 } else {
564 Toast.makeText(this,
565 this.getString(R.string.proxy_running) + " " + this.port,
566 Toast.LENGTH_SHORT).show();
567 if (portChanged) {
568 Thread pr = this.proxy;
569 pr.interrupt();
570 pr = new Thread(this);
571 pr.start();
572 this.proxy = pr;
578 * {@inheritDoc}
580 @Override
581 public final void onDestroy() {
582 super.onDestroy();
583 Toast.makeText(this, R.string.proxy_stopped, Toast.LENGTH_LONG).show();
584 this.stop = true;
585 ((NotificationManager) this
586 .getSystemService(Context.NOTIFICATION_SERVICE)).cancelAll();
590 * {@inheritDoc}
592 @Override
593 public final void run() {
594 try {
595 int p = this.port;
596 ServerSocket sock = new ServerSocket(p);
597 Socket client;
598 while (!this.stop && p == this.port) {
599 if (p != this.port) {
600 break;
602 client = sock.accept();
603 if (client != null) {
604 Log.d(TAG, "new client");
605 Thread t = new Thread(new Connection(client));
606 t.start();
609 sock.close();
610 } catch (IOException e) {
611 Log.e(TAG, null, e);