fix wrapper for cm 4.2.5
[adBlock.git] / src / de / ub0r / android / adBlock / Proxy.java
blob869fe4d8a59162491c3051820e9280a2511620ed
1 /*
2 * Copyright (C) 2009 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 private 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 class Connection implements Runnable {
90 // TODO: cache object.refs
91 // TODO: 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 try {
146 while (true) {
147 read = this.reader.available();
148 if (read < 1 || read > BUFFSIZE) {
149 read = BUFFSIZE;
151 read = this.reader.read(buf, 0, read);
152 if (read < 0) {
153 break;
155 this.writer.write(buf, 0, read);
156 if (this.reader.available() < 1) {
157 this.writer.flush();
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);
167 Log.e(TAG, null, e);
173 * Constructor.
175 * @param socket
176 * local Socket
178 public Connection(final Socket socket) {
179 this.local = socket;
183 * Check if URL is blocked.
185 * @param url
186 * URL
187 * @return if URL is blocked?
189 private boolean checkURL(final String url) {
190 if (url.indexOf("admob") >= 0 || url.indexOf("google") >= 0) {
191 return false;
193 for (String f : Proxy.this.filter) {
194 if (url.indexOf(f) >= 0) {
195 return true;
198 return false;
202 * Read in HTTP Header. Parse for URL to connect to.
204 * @param reader
205 * buffer reader from which we read the header
206 * @param buffer
207 * buffer into which the header is written
208 * @return URL to which we should connect, port other than 80 is given
209 * explicitly
210 * @throws IOException
211 * inner IOException
213 private URL readHeader(final BufferedInputStream reader,
214 final StringBuilder buffer) throws IOException {
215 URL ret = null;
216 String[] strings;
217 int avail;
218 byte[] buf = new byte[CopyStream.HEADERBUFFSIZE];
219 // read first line
220 if (this.state == STATE_CLOSED_OUT) {
221 return null;
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);
230 if (avail < 1) {
231 return null;
233 String line = new String(buf, 0, avail);
234 String testLine = line;
235 int i = line.indexOf(" http://");
236 if (i > 0) {
237 // remove "http://host:port" from line
238 int j = line.indexOf('/', i + 9);
239 if (j > i) {
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")) {
257 String path = null;
258 if (strings[1].startsWith("http://")) {
259 ret = new URL(strings[1]);
260 path = ret.getPath();
261 } else {
262 path = strings[1];
264 // read header
265 String lastLine = line;
266 do {
267 testLine = lastLine + line;
268 i = testLine.indexOf("\nHost: ");
269 if (i >= 0) {
270 int j = testLine.indexOf("\n", i + 6);
271 if (j > 0) {
272 String tHost = testLine.substring(i + 6, j)
273 .trim();
274 ret = new URL("http://" + tHost + path);
275 break;
276 } else {
277 // test for "Host:" again with longer buffer
278 line = lastLine + line;
281 if (line.indexOf("\r\n\r\n") >= 0) {
282 break;
284 lastLine = line;
285 avail = reader.available();
286 if (avail > 0) {
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);
293 buffer.append(line);
295 } while (avail > 0);
296 } else {
297 Log.d(TAG, "unknown method: " + strings[0]);
300 strings = null;
302 // copy rest of reader's buffer
303 avail = reader.available();
304 while (avail > 0) {
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();
313 return ret;
317 * Close local and remote socket.
319 * @param nextState
320 * state to go to
321 * @return new state
322 * @throws IOException
323 * IOException
325 private synchronized short close(final short nextState)
326 throws IOException {
327 Log.d(TAG, "close(" + nextState + ")");
328 short mState = this.state;
329 if (mState == STATE_NORMAL || nextState == STATE_NORMAL) {
330 mState = nextState;
332 Socket mSocket;
333 if (mState != STATE_NORMAL) {
334 // close remote socket
335 mSocket = this.remote;
336 if (mSocket != null && mSocket.isConnected()) {
337 try {
338 mSocket.shutdownInput();
339 mSocket.shutdownOutput();
340 } catch (IOException e) {
341 // Log.e(TAG, null, e);
343 mSocket.close();
345 this.remote = null;
347 if (mState == STATE_CLOSED_OUT) {
348 // close local socket
349 mSocket = this.local;
350 if (mSocket.isConnected()) {
351 try {
352 mSocket.shutdownOutput();
353 mSocket.shutdownInput();
354 } catch (IOException e) {
355 // Log.e(TAG, null, e);
357 mSocket.close();
360 this.state = mState;
361 return mState;
365 * {@inheritDoc}
367 @Override
368 public void run() {
369 BufferedInputStream lInStream;
370 OutputStream lOutStream;
371 BufferedWriter lWriter;
372 try {
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) {
379 Log.e(TAG, null, e);
380 return;
382 try {
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;
389 String tHost = null;
390 int tPort = -1;
391 URL url;
392 boolean connectSSL = false;
393 while (this.local.isConnected()) {
394 buffer = new StringBuilder();
395 url = this.readHeader(lInStream, buffer);
396 if (buffer.length() == 0) {
397 break;
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");
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);
479 // FIXME: exceptions here!
480 // sync does not fix anything
484 if (rThread != null && rThread.isAlive()) {
485 rThread.join();
487 } catch (InterruptedException e) {
488 Log.e(TAG, null, e);
489 } catch (IOException e) {
490 Log.e(TAG, null, e);
491 try {
492 lWriter.append(HTTP_ERROR + " - " + e.toString()
493 + HTTP_RESPONSE + e.toString());
494 lWriter.flush();
495 lWriter.close();
496 this.local.close();
497 } catch (IOException e1) {
498 Log.e(TAG, null, e1);
501 Log.d(TAG, "close connection");
506 * {@inheritDoc}
508 @Override
509 public final IBinder onBind(final Intent intent) {
510 return null;
514 * {@inheritDoc}
516 @Override
517 public final void onStart(final Intent intent, final int startId) {
518 super.onStart(intent, startId);
520 HelperAPI5 helperAPI5 = null;
521 try {
522 helperAPI5 = new HelperAPI5();
523 } catch (VerifyError e) {
524 Log.i(TAG, "no api 5");
527 // Don't kill me!
528 if (helperAPI5 == null) {
529 this.setForeground(true);
530 } else {
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;
539 try {
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;
550 this.port = p;
552 String f = preferences.getString(PREFS_FILTER, "");
553 this.filter.clear();
554 for (String s : f.split("\n")) {
555 if (s.length() > 0) {
556 this.filter.add(s);
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);
563 this.proxy.start();
564 } else {
565 Toast.makeText(this,
566 this.getString(R.string.proxy_running) + " " + this.port,
567 Toast.LENGTH_SHORT).show();
568 if (portChanged) {
569 this.proxy.interrupt();
570 this.proxy = new Thread(this);
571 this.proxy.start();
577 * {@inheritDoc}
579 @Override
580 public final void onDestroy() {
581 super.onDestroy();
582 Toast.makeText(this, R.string.proxy_stopped, Toast.LENGTH_LONG).show();
583 this.stop = true;
584 ((NotificationManager) this
585 .getSystemService(Context.NOTIFICATION_SERVICE)).cancelAll();
589 * {@inheritDoc}
591 @Override
592 public final void run() {
593 try {
594 int p = this.port;
595 ServerSocket sock = new ServerSocket(p);
596 Socket client;
597 while (!this.stop && p == this.port) {
598 if (p != this.port) {
599 break;
601 client = sock.accept();
602 if (client != null) {
603 Log.d(TAG, "new client");
604 Thread t = new Thread(new Connection(client));
605 t.start();
608 sock.close();
609 } catch (IOException e) {
610 Log.e(TAG, null, e);