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
.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
;
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
;
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;
72 private Thread proxy
= null;
74 private int port
= -1;
75 /** Proxy's filter. */
76 private ArrayList
<String
> filter
= new ArrayList
<String
>();
78 private boolean stop
= false;
80 /** Tag for output. */
81 private static final String TAG
= "AdBlock.Proxy";
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
95 private final Socket local
;
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;
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
;
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
{
124 private final InputStream reader
;
126 private final OutputStream writer
;
128 /** Size of buffer. */
129 private static final short BUFFSIZE
= 512;
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().
149 byte[] buf
= new byte[BUFFSIZE
];
153 read
= this.reader
.available();
154 if (read
< 1 || read
> BUFFSIZE
) {
157 read
= this.reader
.read(buf
, 0, read
);
161 this.writer
.write(buf
, 0, read
);
162 if (this.reader
.available() < 1) {
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
;
173 Connection
.this.close();
174 } catch (IOException e
) {
176 Log
.d(TAG
, new String(buf
, 0, read
));
187 public Connection(final Socket socket
) {
192 * Check if URL is blocked.
196 * @return if URL is blocked?
198 private boolean checkURL(final String url
) {
199 if (url
.indexOf("admob") >= 0 || url
.indexOf("google") >= 0) {
202 for (String f
: Proxy
.this.filter
) {
203 if (url
.indexOf(f
) >= 0) {
211 * Read in HTTP Header. Parse for URL to connect to.
214 * buffer reader from which we read the header
216 * buffer into which the header is written
217 * @return URL to which we should connect, port other than 80 is given
219 * @throws IOException
222 private URL
readHeader(final BufferedReader reader
,
223 final StringBuilder buffer
) throws IOException
{
227 while (!reader
.ready() && this.state
!= STATE_CLOSED_OUT
) {
228 try { // isConnected does not work :/
230 } catch (InterruptedException e
) {
234 this.firstRun
= false;
235 if (this.state
== STATE_CLOSED_OUT
) {
238 String line
= reader
.readLine();
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")) {
257 if (strings
[1].startsWith("http://")) {
258 ret
= new URL(strings
[1]);
259 path
= ret
.getPath();
265 line
= reader
.readLine();
266 buffer
.append(line
+ "\n");
267 if (line
.length() == 0) {
270 if (line
.startsWith("Host:")) {
271 strings
= line
.split(" ");
272 if (strings
.length
> 1) {
273 ret
= new URL("http://" + strings
[1] + path
);
282 // copy rest of reader's buffer
283 while (reader
.ready()) {
284 buffer
.append(reader
.readLine() + "\n");
290 * Close local and remote socket.
292 * @throws 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()) {
302 this.remote
.shutdownInput();
303 this.remote
.shutdownOutput();
304 } catch (IOException e
) {
312 if (this.local
!= null && this.state
== STATE_CLOSED_OUT
) {
313 synchronized (this.local
) {
314 if (this.local
.isConnected()) {
316 this.local
.shutdownOutput();
317 this.local
.shutdownInput();
318 } catch (IOException e
) {
328 * Run by Thread.start().
332 InputStream lInStream
;
333 OutputStream lOutStream
;
334 BufferedReader lReader
;
335 BufferedWriter lWriter
;
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
) {
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;
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) {
364 if (this.local
.isConnected() && rThread
!= null
365 && !rThread
.isAlive()) {
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();
386 block
= this.checkURL(url
.toString());
387 Log
.d(TAG
, "new url: " + url
.toString());
389 // new connection needed?
390 int p
= url
.getPort();
394 if (tHost
== null || !tHost
.equals(url
.getHost())
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();
409 tHost
= url
.getHost();
410 tPort
= url
.getPort();
414 Log
.d(TAG
, "new socket: " + url
.toString());
415 this.state
= STATE_NORMAL
;
416 this.remote
= new Socket();
417 this.remote
.connect(new InetSocketAddress(
419 rInStream
= this.remote
.getInputStream();
420 rOutStream
= this.remote
.getOutputStream();
421 rThread
= new Thread(new CopyStream(rInStream
,
424 if (url
.getProtocol().startsWith("https")) {
426 lWriter
.write(HTTP_CONNECTED
429 // copy local to remote by blocks
430 Thread t2
= new Thread(new CopyStream(
431 lInStream
, rOutStream
));
435 break; // copy in separate thread. break
438 remoteWriter
= new BufferedWriter(
439 new OutputStreamWriter(rOutStream
),
440 CopyStream
.BUFFSIZE
);
445 // push data to remote if not blocked
447 lWriter
.append(HTTP_BLOCK
+ HTTP_RESPONSE
448 + "BLOCKED by AdBlock!");
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()) {
459 } catch (InterruptedException e
) {
461 } catch (IOException e
) {
464 lWriter
.append(HTTP_ERROR
+ " - " + e
.toString()
465 + HTTP_RESPONSE
+ e
.toString());
469 } catch (IOException e1
) {
470 Log
.e(TAG
, null, e1
);
473 Log
.d(TAG
, "close connection");
478 * Default Implementation.
483 * @see android.app.Service#onBind(android.content.Intent)
486 public final IBinder
onBind(final Intent intent
) {
499 public final void onStart(final Intent intent
, final int startId
) {
500 super.onStart(intent
, startId
);
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
;
511 String f
= preferences
.getString(PREFS_FILTER
, "");
513 for (String s
: f
.split("\n")) {
514 if (s
.length() > 0) {
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);
524 Toast
.makeText(this, "proxy running on port " + this.port
,
525 Toast
.LENGTH_SHORT
).show();
527 this.proxy
.interrupt();
528 this.proxy
= new Thread(this);
538 public final void onDestroy() {
540 Toast
.makeText(this, "stopping proxy..", Toast
.LENGTH_LONG
).show();
545 * Run by Thread.start().
548 public final void run() {
551 ServerSocket sock
= new ServerSocket(p
);
553 while (!this.stop
&& p
== this.port
) {
554 if (p
!= this.port
) {
557 client
= sock
.accept();
558 if (client
!= null) {
559 Log
.d(TAG
, "new client");
560 Thread t
= new Thread(new Connection(client
));
565 } catch (IOException e
) {