2 * Copyright (C) 2009 Felix Bechstein
4 * This file is part of AdBlock.
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
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
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
;
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
;
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;
75 private Thread proxy
= null;
77 private int port
= -1;
78 /** Proxy's filter. */
79 private ArrayList
<String
> filter
= new ArrayList
<String
>();
81 private boolean stop
= false;
84 * Connection handles a single HTTP Connection. Run this as a Thread.
86 * @author Felix Bechstein
88 class Connection
implements Runnable
{
90 // TODO: cache object.refs
91 // TODO: no private object.refs accessed by inner classes
92 // TODO: reduce object creation
95 private final Socket local
;
97 private Socket remote
;
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
{
116 private final InputStream reader
;
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;
133 public CopyStream(final InputStream r
, final OutputStream w
) {
134 this.reader
= new BufferedInputStream(r
, BUFFSIZE
);
139 * Run by Thread.start().
143 byte[] buf
= new byte[BUFFSIZE
];
147 read
= this.reader
.available();
148 if (read
< 1 || read
> BUFFSIZE
) {
151 read
= this.reader
.read(buf
, 0, read
);
155 this.writer
.write(buf
, 0, read
);
156 if (this.reader
.available() < 1) {
160 Connection
.this.close(Connection
.STATE_CLOSED_OUT
);
161 // this.writer.close();
162 } catch (IOException e
) {
163 // FIXME: java.net.SocketException: Broken pipe
164 // no idea, what causes this :/
165 // Connection c = Connection.this;
166 // String s = new String(buf, 0, read);
178 public Connection(final Socket socket
) {
183 * Check if URL is blocked.
187 * @return if URL is blocked?
189 private boolean checkURL(final String url
) {
190 if (url
.indexOf("admob") >= 0 || url
.indexOf("google") >= 0) {
193 for (String f
: Proxy
.this.filter
) {
194 if (url
.indexOf(f
) >= 0) {
202 * Read in HTTP Header. Parse for URL to connect to.
205 * buffer reader from which we read the header
207 * buffer into which the header is written
208 * @return URL to which we should connect, port other than 80 is given
210 * @throws IOException
213 private URL
readHeader(final BufferedInputStream reader
,
214 final StringBuilder buffer
) throws IOException
{
218 byte[] buf
= new byte[CopyStream
.HEADERBUFFSIZE
];
220 if (this.state
== STATE_CLOSED_OUT
) {
223 avail
= reader
.available();
224 if (avail
> CopyStream
.HEADERBUFFSIZE
) {
225 avail
= CopyStream
.HEADERBUFFSIZE
;
226 } else if (avail
== 0) {
227 avail
= CopyStream
.HEADERBUFFSIZE
;
229 avail
= reader
.read(buf
, 0, avail
);
233 String line
= new String(buf
, 0, avail
);
234 String testLine
= line
;
235 int i
= line
.indexOf(" http://");
237 // remove "http://host:port" from line
238 int j
= line
.indexOf('/', i
+ 9);
240 testLine
= line
.substring(0, i
+ 1) + line
.substring(j
);
243 buffer
.append(testLine
);
244 strings
= line
.split(" ");
245 if (strings
.length
> 1) {
246 if (strings
[0].equals("CONNECT")) {
247 String targetHost
= strings
[1];
248 int targetPort
= PORT_HTTPS
;
249 strings
= targetHost
.split(":");
250 if (strings
.length
> 1) {
251 targetPort
= Integer
.parseInt(strings
[1]);
252 targetHost
= strings
[0];
254 ret
= new URL("https://" + targetHost
+ ":" + targetPort
);
255 } else if (strings
[0].equals("GET")
256 || strings
[0].equals("POST")) {
258 if (strings
[1].startsWith("http://")) {
259 ret
= new URL(strings
[1]);
260 path
= ret
.getPath();
265 String lastLine
= line
;
267 testLine
= lastLine
+ line
;
268 i
= testLine
.indexOf("\nHost: ");
270 int j
= testLine
.indexOf("\n", i
+ 6);
272 String tHost
= testLine
.substring(i
+ 6, j
)
274 ret
= new URL("http://" + tHost
+ path
);
277 // test for "Host:" again with longer buffer
278 line
= lastLine
+ line
;
281 if (line
.indexOf("\r\n\r\n") >= 0) {
285 avail
= reader
.available();
287 if (avail
> CopyStream
.HEADERBUFFSIZE
) {
288 avail
= CopyStream
.HEADERBUFFSIZE
;
290 avail
= reader
.read(buf
, 0, avail
);
291 // FIXME: this may break
292 line
= new String(buf
, 0, avail
);
297 Log
.d(TAG
, "unknown method: " + strings
[0]);
302 // copy rest of reader's buffer
303 avail
= reader
.available();
305 if (avail
> CopyStream
.HEADERBUFFSIZE
) {
306 avail
= CopyStream
.HEADERBUFFSIZE
;
308 avail
= reader
.read(buf
, 0, avail
);
309 // FIXME: this may break!
310 buffer
.append(new String(buf
, 0, avail
));
311 avail
= reader
.available();
317 * Close local and remote socket.
322 * @throws IOException
325 private synchronized short close(final short nextState
)
327 Log
.d(TAG
, "close(" + nextState
+ ")");
328 short mState
= this.state
;
329 if (mState
== STATE_NORMAL
|| nextState
== STATE_NORMAL
) {
333 if (mState
!= STATE_NORMAL
) {
334 // close remote socket
335 mSocket
= this.remote
;
336 if (mSocket
!= null && mSocket
.isConnected()) {
338 mSocket
.shutdownInput();
339 mSocket
.shutdownOutput();
340 } catch (IOException e
) {
341 // Log.e(TAG, null, e);
347 if (mState
== STATE_CLOSED_OUT
) {
348 // close local socket
349 mSocket
= this.local
;
350 if (mSocket
.isConnected()) {
352 mSocket
.shutdownOutput();
353 mSocket
.shutdownInput();
354 } catch (IOException e
) {
355 // Log.e(TAG, null, e);
369 BufferedInputStream lInStream
;
370 OutputStream lOutStream
;
371 BufferedWriter lWriter
;
373 lInStream
= new BufferedInputStream(
374 this.local
.getInputStream(), CopyStream
.BUFFSIZE
);
375 lOutStream
= this.local
.getOutputStream();
376 lWriter
= new BufferedWriter(
377 new OutputStreamWriter(lOutStream
), CopyStream
.BUFFSIZE
);
378 } catch (IOException e
) {
383 InputStream rInStream
= null;
384 OutputStream rOutStream
= null;
385 BufferedWriter remoteWriter
= null;
386 Thread rThread
= null;
387 StringBuilder buffer
= new StringBuilder();
388 boolean block
= false;
392 boolean connectSSL
= false;
393 while (this.local
.isConnected()) {
394 buffer
= new StringBuilder();
395 url
= this.readHeader(lInStream
, buffer
);
396 if (buffer
.length() == 0) {
399 if (this.local
.isConnected() && rThread
!= null
400 && !rThread
.isAlive()) {
401 // TODO: is this a dead branch? if rThread is dead,
402 // socket should be closed allready..
403 Log
.d(TAG
, "close dead remote");
413 block
= this.checkURL(url
.toString());
414 Log
.d(TAG
, "new url: " + url
.toString());
416 // new connection needed?
417 int p
= url
.getPort();
421 if (tHost
== null || !tHost
.equals(url
.getHost())
423 // create new connection
424 Log
.d(TAG
, "shutdown old remote");
425 this.close(STATE_CLOSED_IN
);
426 if (rThread
!= null) {
431 tHost
= url
.getHost();
433 Log
.d(TAG
, "new socket: " + url
.toString());
434 this.state
= STATE_NORMAL
;
435 this.remote
= new Socket();
436 this.remote
.connect(new InetSocketAddress(
438 rInStream
= this.remote
.getInputStream();
439 rOutStream
= this.remote
.getOutputStream();
440 rThread
= new Thread(new CopyStream(rInStream
,
443 if (url
.getProtocol().startsWith("https")) {
445 lWriter
.write(HTTP_CONNECTED
448 // copy local to remote by blocks
449 Thread t2
= new Thread(new CopyStream(
450 lInStream
, rOutStream
));
454 break; // copy in separate thread. break
457 remoteWriter
= new BufferedWriter(
458 new OutputStreamWriter(rOutStream
),
459 CopyStream
.BUFFSIZE
);
464 // push data to remote if not blocked
466 lWriter
.append(HTTP_BLOCK
+ HTTP_RESPONSE
467 + "BLOCKED by AdBlock!");
470 Socket mSocket
= this.remote
;
471 if (mSocket
!= null && mSocket
.isConnected()
472 && remoteWriter
!= null) {
474 remoteWriter
.append(buffer
);
475 remoteWriter
.flush();
476 } catch (IOException e
) {
477 Log
.d(TAG
, buffer
.toString(), e
);
479 // FIXME: exceptions here!
480 // sync does not fix anything
484 if (rThread
!= null && rThread
.isAlive()) {
487 } catch (InterruptedException e
) {
489 } catch (IOException e
) {
492 lWriter
.append(HTTP_ERROR
+ " - " + e
.toString()
493 + HTTP_RESPONSE
+ e
.toString());
497 } catch (IOException e1
) {
498 Log
.e(TAG
, null, e1
);
501 Log
.d(TAG
, "close connection");
509 public final IBinder
onBind(final Intent intent
) {
517 public final void onStart(final Intent intent
, final int startId
) {
518 super.onStart(intent
, startId
);
520 HelperAPI5 helperAPI5
= null;
522 helperAPI5
= new HelperAPI5();
523 } catch (VerifyError e
) {
524 Log
.i(TAG
, "no api 5");
528 if (helperAPI5
== null) {
529 this.setForeground(true);
531 final Notification notification
= new Notification(
532 R
.drawable
.stat_notify_proxy
, "", System
533 .currentTimeMillis());
534 final PendingIntent contentIntent
= PendingIntent
.getActivity(this,
535 0, new Intent(this, AdBlock
.class), 0);
536 notification
.setLatestEventInfo(this, this
537 .getString(R
.string
.notify_proxy
), "", contentIntent
);
538 notification
.defaults
|= Notification
.FLAG_NO_CLEAR
;
540 helperAPI5
.startForeground(this, 0, notification
);
541 } catch (NoSuchMethodError e
) {
542 this.setForeground(true);
546 SharedPreferences preferences
= PreferenceManager
547 .getDefaultSharedPreferences(this);
548 int p
= Integer
.parseInt(preferences
.getString(PREFS_PORT
, "8080"));
549 boolean portChanged
= p
!= this.port
;
552 String f
= preferences
.getString(PREFS_FILTER
, "");
554 for (String s
: f
.split("\n")) {
555 if (s
.length() > 0) {
559 if (this.proxy
== null) {
560 // Toast.makeText(this, "starting proxy on port: " + this.port,
561 // Toast.LENGTH_SHORT).show();
562 this.proxy
= new Thread(this);
566 this.getString(R
.string
.proxy_running
) + " " + this.port
,
567 Toast
.LENGTH_SHORT
).show();
569 this.proxy
.interrupt();
570 this.proxy
= new Thread(this);
580 public final void onDestroy() {
582 Toast
.makeText(this, R
.string
.proxy_stopped
, Toast
.LENGTH_LONG
).show();
584 ((NotificationManager
) this
585 .getSystemService(Context
.NOTIFICATION_SERVICE
)).cancelAll();
592 public final void run() {
595 ServerSocket sock
= new ServerSocket(p
);
597 while (!this.stop
&& p
== this.port
) {
598 if (p
!= this.port
) {
601 client
= sock
.accept();
602 if (client
!= null) {
603 Log
.d(TAG
, "new client");
604 Thread t
= new Thread(new Connection(client
));
609 } catch (IOException e
) {