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
.BufferedReader
;
23 import java
.io
.BufferedWriter
;
24 import java
.io
.IOException
;
25 import java
.io
.InputStream
;
26 import java
.io
.InputStreamReader
;
27 import java
.io
.OutputStream
;
28 import java
.io
.OutputStreamWriter
;
29 import java
.net
.InetSocketAddress
;
30 import java
.net
.ServerSocket
;
31 import java
.net
.Socket
;
33 import java
.util
.ArrayList
;
35 import android
.app
.Service
;
36 import android
.content
.Intent
;
37 import android
.content
.SharedPreferences
;
38 import android
.os
.IBinder
;
39 import android
.preference
.PreferenceManager
;
40 import android
.util
.Log
;
41 import android
.widget
.Toast
;
44 * This ad blocking Proxy Service will work as an ordinary HTTP proxy. Set APN's
45 * proxy preferences to proxy's connection parameters.
47 * @author Felix Bechstein
49 public class Proxy
extends Service
implements Runnable
{
51 /** Preferences: Port. */
52 static final String PREFS_PORT
= "port";
53 /** Preferences: Filter. */
54 static final String PREFS_FILTER
= "filter";
56 /** HTTP Response: blocked. */
57 private static final String HTTP_BLOCK
= "HTTP/1.1 500 blocked by AdBlock";
58 /** HTTP Response: error. */
59 private static final String HTTP_ERROR
= "HTTP/1.1 500 error by AdBlock";
60 /** HTTP Response: connected. */
61 private static final String HTTP_CONNECTED
= "HTTP/1.1 200 connected";
62 /** HTTP Response: flush. */
63 private static final String HTTP_RESPONSE
= "\n\n";
65 /** Default Port for HTTP. */
66 private static final int PORT_HTTP
= 80;
67 /** Default Port for HTTPS. */
68 private static final int PORT_HTTPS
= 443;
71 private Thread proxy
= null;
73 private int port
= -1;
74 /** Proxy's filter. */
75 private ArrayList
<String
> filter
= new ArrayList
<String
>();
77 private boolean stop
= false;
79 /** Tag for output. */
80 private static final String TAG
= "AdBlock.Proxy";
83 * Connection handles a single HTTP Connection. Run this as a Thread.
85 * @author Felix Bechstein
87 private class Connection
implements Runnable
{
89 // TODO: cache object.refs
90 // TODO: no private object.refs accessed by inner classes
91 // TODO: reduce object creation
94 private final Socket local
;
96 private Socket remote
;
99 private static final short STATE_NORMAL
= 0;
100 /** State: closed by local side. */
101 private static final short STATE_CLOSED_IN
= 1;
102 /** State: closed by remote side. */
103 private static final short STATE_CLOSED_OUT
= 2;
104 /** Connections state. */
105 private short state
= STATE_NORMAL
;
108 * CopyStream reads one stream and writes it's data into an other
109 * stream. Run this as a Thread.
111 * @author Felix Bechstein
113 private class CopyStream
implements Runnable
{
115 private final InputStream reader
;
117 private final OutputStream writer
;
119 /** Size of buffer. */
120 private static final short BUFFSIZE
= 512;
130 public CopyStream(final InputStream r
, final OutputStream w
) {
131 this.reader
= new BufferedInputStream(r
, BUFFSIZE
);
136 * Run by Thread.start().
140 byte[] buf
= new byte[BUFFSIZE
];
144 read
= this.reader
.available();
145 if (read
< 1 || read
> BUFFSIZE
) {
148 read
= this.reader
.read(buf
, 0, read
);
152 this.writer
.write(buf
, 0, read
);
153 if (this.reader
.available() < 1) {
157 Connection
.this.close(Connection
.STATE_CLOSED_OUT
);
158 // this.writer.close();
159 } catch (IOException e
) {
160 // FIXME: java.net.SocketException: Broken pipe
161 // no idea, what causes this :/
162 Connection c
= Connection
.this;
164 // Log.d(TAG, new String(buf, 0, read));
175 public Connection(final Socket socket
) {
180 * Check if URL is blocked.
184 * @return if URL is blocked?
186 private boolean checkURL(final String url
) {
187 if (url
.indexOf("admob") >= 0 || url
.indexOf("google") >= 0) {
190 for (String f
: Proxy
.this.filter
) {
191 if (url
.indexOf(f
) >= 0) {
199 * Read in HTTP Header. Parse for URL to connect to.
202 * buffer reader from which we read the header
204 * buffer into which the header is written
205 * @return URL to which we should connect, port other than 80 is given
207 * @throws IOException
210 private URL
readHeader(final BufferedReader reader
,
211 final StringBuilder buffer
) throws IOException
{
215 if (this.state
== STATE_CLOSED_OUT
) {
218 String line
= reader
.readLine();
222 buffer
.append(line
+ "\r\n");
223 strings
= line
.split(" ");
224 if (strings
.length
> 1) {
225 if (strings
[0].equals("CONNECT")) {
226 String targetHost
= strings
[1];
227 int targetPort
= PORT_HTTPS
;
228 strings
= targetHost
.split(":");
229 if (strings
.length
> 1) {
230 targetPort
= Integer
.parseInt(strings
[1]);
231 targetHost
= strings
[0];
233 ret
= new URL("https://" + targetHost
+ ":" + targetPort
);
234 } else if (strings
[0].equals("GET")
235 || strings
[0].equals("POST")) {
237 if (strings
[1].startsWith("http://")) {
238 ret
= new URL(strings
[1]);
239 path
= ret
.getPath();
245 line
= reader
.readLine();
246 buffer
.append(line
+ "\r\n");
247 if (line
.length() == 0) {
250 if (line
.startsWith("Host:")) {
251 strings
= line
.split(" ");
252 if (strings
.length
> 1) {
253 ret
= new URL("http://" + strings
[1] + path
);
259 Log
.d(TAG
, "unknown method: " + strings
[0]);
264 // copy rest of reader's buffer
265 while (reader
.ready()) {
266 // FIXME this read line breaks everything!
267 // data behind header does not need a read line..
268 // we should read from InputStream directly!
269 buffer
.append(reader
.readLine() + "\r\n");
275 * Close local and remote socket.
280 * @throws IOException
283 private synchronized short close(final short nextState
)
285 Log
.d(TAG
, "close(" + nextState
+ ")");
286 short mState
= this.state
;
287 if (mState
== STATE_NORMAL
|| nextState
== STATE_NORMAL
) {
291 if (mState
!= STATE_NORMAL
) {
292 // close remote socket
293 mSocket
= this.remote
;
294 if (mSocket
!= null && mSocket
.isConnected()) {
296 mSocket
.shutdownInput();
297 mSocket
.shutdownOutput();
298 } catch (IOException e
) {
299 // Log.e(TAG, null, e);
305 if (mState
== STATE_CLOSED_OUT
) {
306 // close local socket
307 mSocket
= this.local
;
308 if (mSocket
.isConnected()) {
310 mSocket
.shutdownOutput();
311 mSocket
.shutdownInput();
312 } catch (IOException e
) {
313 // Log.e(TAG, null, e);
323 * Run by Thread.start().
327 InputStream lInStream
;
328 OutputStream lOutStream
;
329 BufferedReader lReader
;
330 BufferedWriter lWriter
;
332 lInStream
= this.local
.getInputStream();
333 lOutStream
= this.local
.getOutputStream();
334 lReader
= new BufferedReader(new InputStreamReader(lInStream
),
335 CopyStream
.BUFFSIZE
);
336 lWriter
= new BufferedWriter(
337 new OutputStreamWriter(lOutStream
), CopyStream
.BUFFSIZE
);
338 } catch (IOException e
) {
343 InputStream rInStream
= null;
344 OutputStream rOutStream
= null;
345 BufferedWriter remoteWriter
= null;
346 Thread rThread
= null;
347 StringBuilder buffer
= new StringBuilder();
348 boolean block
= false;
352 boolean connectSSL
= false;
353 while (this.local
.isConnected()) {
354 buffer
= new StringBuilder();
355 url
= this.readHeader(lReader
, buffer
);
356 if (buffer
.length() == 0) {
359 if (this.local
.isConnected() && rThread
!= null
360 && !rThread
.isAlive()) {
361 // TODO: is this a dead branch? if rThread is dead,
362 // socket should be closed allready..
363 Log
.d(TAG
, "close dead remote");
373 block
= this.checkURL(url
.toString());
374 Log
.d(TAG
, "new url: " + url
.toString());
376 // new connection needed?
377 int p
= url
.getPort();
381 if (tHost
== null || !tHost
.equals(url
.getHost())
383 // create new connection
384 Log
.d(TAG
, "shutdown old remote");
385 this.close(STATE_CLOSED_IN
);
386 if (rThread
!= null) {
391 tHost
= url
.getHost();
392 tPort
= url
.getPort();
396 Log
.d(TAG
, "new socket: " + url
.toString());
397 this.state
= STATE_NORMAL
;
398 this.remote
= new Socket();
399 this.remote
.connect(new InetSocketAddress(
401 rInStream
= this.remote
.getInputStream();
402 rOutStream
= this.remote
.getOutputStream();
403 rThread
= new Thread(new CopyStream(rInStream
,
406 if (url
.getProtocol().startsWith("https")) {
408 lWriter
.write(HTTP_CONNECTED
411 // copy local to remote by blocks
412 Thread t2
= new Thread(new CopyStream(
413 lInStream
, rOutStream
));
417 break; // copy in separate thread. break
420 remoteWriter
= new BufferedWriter(
421 new OutputStreamWriter(rOutStream
),
422 CopyStream
.BUFFSIZE
);
427 // push data to remote if not blocked
429 lWriter
.append(HTTP_BLOCK
+ HTTP_RESPONSE
430 + "BLOCKED by AdBlock!");
433 Socket mSocket
= this.remote
;
434 if (mSocket
!= null && mSocket
.isConnected()
435 && remoteWriter
!= null) {
437 remoteWriter
.append(buffer
);
438 remoteWriter
.flush();
439 } catch (IOException e
) {
440 Log
.d(TAG
, buffer
.toString(), e
);
442 // FIXME: exceptions here!
443 // sync does not fix anything
447 if (rThread
!= null && rThread
.isAlive()) {
450 } catch (InterruptedException e
) {
452 } catch (IOException e
) {
455 lWriter
.append(HTTP_ERROR
+ " - " + e
.toString()
456 + HTTP_RESPONSE
+ e
.toString());
460 } catch (IOException e1
) {
461 Log
.e(TAG
, null, e1
);
464 Log
.d(TAG
, "close connection");
469 * Default Implementation.
474 * @see android.app.Service#onBind(android.content.Intent)
477 public final IBinder
onBind(final Intent intent
) {
490 public final void onStart(final Intent intent
, final int startId
) {
491 super.onStart(intent
, startId
);
494 this.setForeground(true);
496 SharedPreferences preferences
= PreferenceManager
497 .getDefaultSharedPreferences(this);
498 int p
= Integer
.parseInt(preferences
.getString(PREFS_PORT
, "8080"));
499 boolean portChanged
= p
!= this.port
;
502 String f
= preferences
.getString(PREFS_FILTER
, "");
504 for (String s
: f
.split("\n")) {
505 if (s
.length() > 0) {
509 if (this.proxy
== null) {
510 // Toast.makeText(this, "starting proxy on port: " + this.port,
511 // Toast.LENGTH_SHORT).show();
512 this.proxy
= new Thread(this);
515 Toast
.makeText(this, "proxy running on port " + this.port
,
516 Toast
.LENGTH_SHORT
).show();
518 this.proxy
.interrupt();
519 this.proxy
= new Thread(this);
529 public final void onDestroy() {
531 Toast
.makeText(this, "stopping proxy..", Toast
.LENGTH_LONG
).show();
536 * Run by Thread.start().
539 public final void run() {
542 ServerSocket sock
= new ServerSocket(p
);
544 while (!this.stop
&& p
== this.port
) {
545 if (p
!= this.port
) {
548 client
= sock
.accept();
549 if (client
!= null) {
550 Log
.d(TAG
, "new client");
551 Thread t
= new Thread(new Connection(client
));
556 } catch (IOException e
) {