happy new year
[adBlock.git] / src / de / ub0r / android / adBlock / Proxy.java
blobb960e3ff84a542a7e1377ed42e869f30cf4485f6
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 if (url.indexOf("admob") >= 0 || url.indexOf("google") >= 0) {
193 return false;
195 final ArrayList<String> f = Proxy.this.filter;
196 final int s = f.size();
197 for (int i = 0; i < s; i++) {
198 if (url.indexOf(f.get(i)) >= 0) {
199 return true;
202 return false;
206 * Read in HTTP Header. Parse for URL to connect to.
208 * @param reader
209 * buffer reader from which we read the header
210 * @param buffer
211 * buffer into which the header is written
212 * @return URL to which we should connect, port other than 80 is given
213 * explicitly
214 * @throws IOException
215 * inner IOException
217 private URL readHeader(final BufferedInputStream reader,
218 final StringBuilder buffer) throws IOException {
219 URL ret = null;
220 String[] strings;
221 int avail;
222 byte[] buf = new byte[CopyStream.HEADERBUFFSIZE];
223 // read first line
224 if (this.state == STATE_CLOSED_OUT) {
225 return null;
227 avail = reader.available();
228 if (avail > CopyStream.HEADERBUFFSIZE) {
229 avail = CopyStream.HEADERBUFFSIZE;
230 } else if (avail == 0) {
231 avail = CopyStream.HEADERBUFFSIZE;
233 avail = reader.read(buf, 0, avail);
234 if (avail < 1) {
235 return null;
237 String line = new String(buf, 0, avail);
238 String testLine = line;
239 int i = line.indexOf(" http://");
240 if (i > 0) {
241 // remove "http://host:port" from line
242 int j = line.indexOf('/', i + 9);
243 if (j > i) {
244 testLine = line.substring(0, i + 1) + line.substring(j);
247 buffer.append(testLine);
248 strings = line.split(" ");
249 if (strings.length > 1) {
250 if (strings[0].equals("CONNECT")) {
251 String targetHost = strings[1];
252 int targetPort = PORT_HTTPS;
253 strings = targetHost.split(":");
254 if (strings.length > 1) {
255 targetPort = Integer.parseInt(strings[1]);
256 targetHost = strings[0];
258 ret = new URL("https://" + targetHost + ":" + targetPort);
259 } else if (strings[0].equals("GET")
260 || strings[0].equals("POST")) {
261 String path = null;
262 if (strings[1].startsWith("http://")) {
263 ret = new URL(strings[1]);
264 path = ret.getPath();
265 } else {
266 path = strings[1];
268 // read header
269 String lastLine = line;
270 do {
271 testLine = lastLine + line;
272 i = testLine.indexOf("\nHost: ");
273 if (i >= 0) {
274 int j = testLine.indexOf("\n", i + 6);
275 if (j > 0) {
276 String tHost = testLine.substring(i + 6, j)
277 .trim();
278 ret = new URL("http://" + tHost + path);
279 break;
280 } else {
281 // test for "Host:" again with longer buffer
282 line = lastLine + line;
285 if (line.indexOf("\r\n\r\n") >= 0) {
286 break;
288 lastLine = line;
289 avail = reader.available();
290 if (avail > 0) {
291 if (avail > CopyStream.HEADERBUFFSIZE) {
292 avail = CopyStream.HEADERBUFFSIZE;
294 avail = reader.read(buf, 0, avail);
295 // FIXME: this may break
296 line = new String(buf, 0, avail);
297 buffer.append(line);
299 } while (avail > 0);
300 } else {
301 Log.d(TAG, "unknown method: " + strings[0]);
304 strings = null;
306 // copy rest of reader's buffer
307 avail = reader.available();
308 while (avail > 0) {
309 if (avail > CopyStream.HEADERBUFFSIZE) {
310 avail = CopyStream.HEADERBUFFSIZE;
312 avail = reader.read(buf, 0, avail);
313 // FIXME: this may break!
314 buffer.append(new String(buf, 0, avail));
315 avail = reader.available();
317 return ret;
321 * Close local and remote socket.
323 * @param nextState
324 * state to go to
325 * @return new state
326 * @throws IOException
327 * IOException
329 private synchronized short close(final short nextState)
330 throws IOException {
331 Log.d(TAG, "close(" + nextState + ")");
332 short mState = this.state;
333 if (mState == STATE_NORMAL || nextState == STATE_NORMAL) {
334 mState = nextState;
336 Socket mSocket;
337 if (mState != STATE_NORMAL) {
338 // close remote socket
339 mSocket = this.remote;
340 if (mSocket != null && mSocket.isConnected()) {
341 try {
342 mSocket.shutdownInput();
343 mSocket.shutdownOutput();
344 } catch (IOException e) {
345 // Log.e(TAG, null, e);
347 mSocket.close();
349 this.remote = null;
351 if (mState == STATE_CLOSED_OUT) {
352 // close local socket
353 mSocket = this.local;
354 if (mSocket.isConnected()) {
355 try {
356 mSocket.shutdownOutput();
357 mSocket.shutdownInput();
358 } catch (IOException e) {
359 // Log.e(TAG, null, e);
361 mSocket.close();
364 this.state = mState;
365 return mState;
369 * {@inheritDoc}
371 @Override
372 public void run() {
373 BufferedInputStream lInStream;
374 OutputStream lOutStream;
375 BufferedWriter lWriter;
376 try {
377 lInStream = new BufferedInputStream(
378 this.local.getInputStream(), CopyStream.BUFFSIZE);
379 lOutStream = this.local.getOutputStream();
380 lWriter = new BufferedWriter(
381 new OutputStreamWriter(lOutStream), CopyStream.BUFFSIZE);
382 } catch (IOException e) {
383 Log.e(TAG, null, e);
384 return;
386 try {
387 InputStream rInStream = null;
388 OutputStream rOutStream = null;
389 BufferedWriter remoteWriter = null;
390 Thread rThread = null;
391 StringBuilder buffer = new StringBuilder();
392 boolean block = false;
393 String tHost = null;
394 int tPort = -1;
395 URL url;
396 boolean connectSSL = false;
397 while (this.local.isConnected()) {
398 buffer = new StringBuilder();
399 url = this.readHeader(lInStream, buffer);
400 if (buffer.length() == 0) {
401 break;
403 if (this.local.isConnected() && rThread != null
404 && !rThread.isAlive()) {
405 // socket should be closed allready..
406 Log.d(TAG, "close dead remote");
407 if (connectSSL) {
408 this.local.close();
410 tHost = null;
411 rInStream = null;
412 rOutStream = null;
413 rThread = null;
415 if (url != null) {
416 block = this.checkURL(url.toString());
417 Log.d(TAG, "new url: " + url.toString());
418 if (!block) {
419 // new connection needed?
420 int p = url.getPort();
421 if (p < 0) {
422 p = PORT_HTTP;
424 if (tHost == null || !tHost.equals(url.getHost())
425 || tPort != p) {
426 // create new connection
427 Log.d(TAG, "shutdown old remote");
428 this.close(STATE_CLOSED_IN);
429 if (rThread != null) {
430 rThread.join();
431 rThread = null;
434 tHost = url.getHost();
435 tPort = p;
436 Log.d(TAG, "new socket: " + url.toString());
437 this.state = STATE_NORMAL;
438 this.remote = new Socket();
439 this.remote.connect(new InetSocketAddress(
440 tHost, tPort));
441 rInStream = this.remote.getInputStream();
442 rOutStream = this.remote.getOutputStream();
443 rThread = new Thread(new CopyStream(rInStream,
444 lOutStream));
445 rThread.start();
446 if (url.getProtocol().startsWith("https")) {
447 connectSSL = true;
448 lWriter.write(HTTP_CONNECTED
449 + HTTP_RESPONSE);
450 lWriter.flush();
451 // copy local to remote by blocks
452 Thread t2 = new Thread(new CopyStream(
453 lInStream, rOutStream));
455 t2.start();
456 remoteWriter = null;
457 break; // copy in separate thread. break
458 // while here
459 } else {
460 remoteWriter = new BufferedWriter(
461 new OutputStreamWriter(rOutStream),
462 CopyStream.BUFFSIZE);
467 // push data to remote if not blocked
468 if (block) {
469 lWriter.append(HTTP_BLOCK + HTTP_RESPONSE
470 + "BLOCKED by AdBlock!");
471 lWriter.flush();
472 } else {
473 Socket mSocket = this.remote;
474 if (mSocket != null && mSocket.isConnected()
475 && remoteWriter != null) {
476 try {
477 remoteWriter.append(buffer);
478 remoteWriter.flush();
479 } catch (IOException e) {
480 Log.d(TAG, buffer.toString(), e);
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 HelperAPI5 helperAPI5 = null;
522 try {
523 helperAPI5 = new HelperAPI5();
524 if (!helperAPI5.isAvailable()) {
525 helperAPI5 = null;
527 } catch (VerifyError e) {
528 helperAPI5 = null;
529 Log.i(TAG, "no api 5");
532 // Don't kill me!
533 if (helperAPI5 == null) {
534 this.setForeground(true);
535 } else {
536 final Notification notification = new Notification(
537 R.drawable.stat_notify_proxy, "", System
538 .currentTimeMillis());
539 final PendingIntent contentIntent = PendingIntent.getActivity(this,
540 0, new Intent(this, AdBlock.class), 0);
541 notification.setLatestEventInfo(this, this
542 .getString(R.string.notify_proxy), "", contentIntent);
543 notification.defaults |= Notification.FLAG_NO_CLEAR;
546 SharedPreferences preferences = PreferenceManager
547 .getDefaultSharedPreferences(this);
548 int p = Integer.parseInt(preferences.getString(PREFS_PORT, "8080"));
549 boolean portChanged = p != this.port;
550 this.port = p;
552 String f = preferences.getString(PREFS_FILTER, "");
553 final ArrayList<String> fl = this.filter;
554 fl.clear();
555 for (String s : f.split("\n")) {
556 if (s.length() > 0) {
557 fl.add(s);
560 if (this.proxy == null) {
561 // Toast.makeText(this, "starting proxy on port: " + this.port,
562 // Toast.LENGTH_SHORT).show();
563 final Thread pr = new Thread(this);
564 pr.start();
565 this.proxy = pr;
566 } else {
567 Toast.makeText(this,
568 this.getString(R.string.proxy_running) + " " + this.port,
569 Toast.LENGTH_SHORT).show();
570 if (portChanged) {
571 Thread pr = this.proxy;
572 pr.interrupt();
573 pr = new Thread(this);
574 pr.start();
575 this.proxy = pr;
581 * {@inheritDoc}
583 @Override
584 public final void onDestroy() {
585 super.onDestroy();
586 Toast.makeText(this, R.string.proxy_stopped, Toast.LENGTH_LONG).show();
587 this.stop = true;
588 ((NotificationManager) this
589 .getSystemService(Context.NOTIFICATION_SERVICE)).cancelAll();
593 * {@inheritDoc}
595 @Override
596 public final void run() {
597 try {
598 int p = this.port;
599 ServerSocket sock = new ServerSocket(p);
600 Socket client;
601 while (!this.stop && p == this.port) {
602 if (p != this.port) {
603 break;
605 client = sock.accept();
606 if (client != null) {
607 Log.d(TAG, "new client");
608 Thread t = new Thread(new Connection(client));
609 t.start();
612 sock.close();
613 } catch (IOException e) {
614 Log.e(TAG, null, e);