Home | History | Annotate | Download | only in proxyhandler
      1 /**
      2  * Copyright (c) 2013, The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.proxyhandler;
     17 
     18 import android.os.RemoteException;
     19 import android.util.Log;
     20 
     21 import com.android.net.IProxyPortListener;
     22 import com.google.android.collect.Lists;
     23 import com.google.android.collect.Sets;
     24 
     25 import java.io.IOException;
     26 import java.io.InputStream;
     27 import java.io.OutputStream;
     28 import java.net.InetSocketAddress;
     29 import java.net.Proxy;
     30 import java.net.ProxySelector;
     31 import java.net.ServerSocket;
     32 import java.net.Socket;
     33 import java.net.SocketException;
     34 import java.net.URI;
     35 import java.net.URISyntaxException;
     36 import java.util.List;
     37 import java.util.Set;
     38 import java.util.concurrent.ExecutorService;
     39 import java.util.concurrent.Executors;
     40 
     41 /**
     42  * @hide
     43  */
     44 public class ProxyServer extends Thread {
     45 
     46     private static final String CONNECT = "CONNECT";
     47     private static final String HTTP_OK = "HTTP/1.1 200 OK\n";
     48 
     49     private static final String TAG = "ProxyServer";
     50 
     51     // HTTP Headers
     52     private static final String HEADER_CONNECTION = "connection";
     53     private static final String HEADER_PROXY_CONNECTION = "proxy-connection";
     54 
     55     private ExecutorService threadExecutor;
     56 
     57     public boolean mIsRunning = false;
     58 
     59     private ServerSocket serverSocket;
     60     private int mPort;
     61     private IProxyPortListener mCallback;
     62 
     63     private class ProxyConnection implements Runnable {
     64         private Socket connection;
     65 
     66         private ProxyConnection(Socket connection) {
     67             this.connection = connection;
     68         }
     69 
     70         @Override
     71         public void run() {
     72             try {
     73                 String requestLine = getLine(connection.getInputStream());
     74                 String[] splitLine = requestLine.split(" ");
     75                 if (splitLine.length < 3) {
     76                     connection.close();
     77                     return;
     78                 }
     79                 String requestType = splitLine[0];
     80                 String urlString = splitLine[1];
     81                 String httpVersion = splitLine[2];
     82 
     83                 URI url = null;
     84                 String host;
     85                 int port;
     86 
     87                 if (requestType.equals(CONNECT)) {
     88                     String[] hostPortSplit = urlString.split(":");
     89                     host = hostPortSplit[0];
     90                     // Use default SSL port if not specified. Parse it otherwise
     91                     if (hostPortSplit.length < 2) {
     92                         port = 443;
     93                     } else {
     94                         try {
     95                             port = Integer.parseInt(hostPortSplit[1]);
     96                         } catch (NumberFormatException nfe) {
     97                             connection.close();
     98                             return;
     99                         }
    100                     }
    101                     urlString = "Https://" + host + ":" + port;
    102                 } else {
    103                     try {
    104                         url = new URI(urlString);
    105                         host = url.getHost();
    106                         port = url.getPort();
    107                         if (port < 0) {
    108                             port = 80;
    109                         }
    110                     } catch (URISyntaxException e) {
    111                         connection.close();
    112                         return;
    113                     }
    114                 }
    115 
    116                 List<Proxy> list = Lists.newArrayList();
    117                 try {
    118                     list = ProxySelector.getDefault().select(new URI(urlString));
    119                 } catch (URISyntaxException e) {
    120                     e.printStackTrace();
    121                 }
    122                 Socket server = null;
    123                 for (Proxy proxy : list) {
    124                     try {
    125                         if (!proxy.equals(Proxy.NO_PROXY)) {
    126                             // Only Inets created by PacProxySelector.
    127                             InetSocketAddress inetSocketAddress =
    128                                     (InetSocketAddress)proxy.address();
    129                             server = new Socket(inetSocketAddress.getHostName(),
    130                                     inetSocketAddress.getPort());
    131                             sendLine(server, requestLine);
    132                         } else {
    133                             server = new Socket(host, port);
    134                             if (requestType.equals(CONNECT)) {
    135                                 skipToRequestBody(connection);
    136                                 // No proxy to respond so we must.
    137                                 sendLine(connection, HTTP_OK);
    138                             } else {
    139                                 // Proxying the request directly to the origin server.
    140                                 sendAugmentedRequestToHost(connection, server,
    141                                         requestType, url, httpVersion);
    142                             }
    143                         }
    144                     } catch (IOException ioe) {
    145                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    146                             Log.v(TAG, "Unable to connect to proxy " + proxy, ioe);
    147                         }
    148                     }
    149                     if (server != null) {
    150                         break;
    151                     }
    152                 }
    153                 if (list.isEmpty()) {
    154                     server = new Socket(host, port);
    155                     if (requestType.equals(CONNECT)) {
    156                         skipToRequestBody(connection);
    157                         // No proxy to respond so we must.
    158                         sendLine(connection, HTTP_OK);
    159                     } else {
    160                         // Proxying the request directly to the origin server.
    161                         sendAugmentedRequestToHost(connection, server,
    162                                 requestType, url, httpVersion);
    163                     }
    164                 }
    165                 // Pass data back and forth until complete.
    166                 if (server != null) {
    167                     SocketConnect.connect(connection, server);
    168                 }
    169             } catch (Exception e) {
    170                 Log.d(TAG, "Problem Proxying", e);
    171             }
    172             try {
    173                 connection.close();
    174             } catch (IOException ioe) {
    175                 // Do nothing
    176             }
    177         }
    178 
    179         /**
    180          * Sends HTTP request-line (i.e. the first line in the request)
    181          * that contains absolute path of a given absolute URI.
    182          *
    183          * @param server server to send the request to.
    184          * @param requestType type of the request, a.k.a. HTTP method.
    185          * @param absoluteUri absolute URI which absolute path should be extracted.
    186          * @param httpVersion version of HTTP, e.g. HTTP/1.1.
    187          * @throws IOException if the request-line cannot be sent.
    188          */
    189         private void sendRequestLineWithPath(Socket server, String requestType,
    190                 URI absoluteUri, String httpVersion) throws IOException {
    191 
    192             String absolutePath = getAbsolutePathFromAbsoluteURI(absoluteUri);
    193             String outgoingRequestLine = String.format("%s %s %s",
    194                     requestType, absolutePath, httpVersion);
    195             sendLine(server, outgoingRequestLine);
    196         }
    197 
    198         /**
    199          * Extracts absolute path form a given URI. E.g., passing
    200          * <code>http://google.com:80/execute?query=cat#top</code>
    201          * will result in <code>/execute?query=cat#top</code>.
    202          *
    203          * @param uri URI which absolute path has to be extracted,
    204          * @return the absolute path of the URI,
    205          */
    206         private String getAbsolutePathFromAbsoluteURI(URI uri) {
    207             String rawPath = uri.getRawPath();
    208             String rawQuery = uri.getRawQuery();
    209             String rawFragment = uri.getRawFragment();
    210             StringBuilder absolutePath = new StringBuilder();
    211 
    212             if (rawPath != null) {
    213                 absolutePath.append(rawPath);
    214             } else {
    215                 absolutePath.append("/");
    216             }
    217             if (rawQuery != null) {
    218                 absolutePath.append("?").append(rawQuery);
    219             }
    220             if (rawFragment != null) {
    221                 absolutePath.append("#").append(rawFragment);
    222             }
    223             return absolutePath.toString();
    224         }
    225 
    226         private String getLine(InputStream inputStream) throws IOException {
    227             StringBuilder buffer = new StringBuilder();
    228             int byteBuffer = inputStream.read();
    229             if (byteBuffer < 0) return "";
    230             do {
    231                 if (byteBuffer != '\r') {
    232                     buffer.append((char)byteBuffer);
    233                 }
    234                 byteBuffer = inputStream.read();
    235             } while ((byteBuffer != '\n') && (byteBuffer >= 0));
    236 
    237             return buffer.toString();
    238         }
    239 
    240         private void sendLine(Socket socket, String line) throws IOException {
    241             OutputStream os = socket.getOutputStream();
    242             os.write(line.getBytes());
    243             os.write('\r');
    244             os.write('\n');
    245             os.flush();
    246         }
    247 
    248         /**
    249          * Reads from socket until an empty line is read which indicates the end of HTTP headers.
    250          *
    251          * @param socket socket to read from.
    252          * @throws IOException if an exception took place during the socket read.
    253          */
    254         private void skipToRequestBody(Socket socket) throws IOException {
    255             while (getLine(socket.getInputStream()).length() != 0);
    256         }
    257 
    258         /**
    259          * Sends an augmented request to the final host (DIRECT connection).
    260          *
    261          * @param src socket to read HTTP headers from.The socket current position should point
    262          *            to the beginning of the HTTP header section.
    263          * @param dst socket to write the augmented request to.
    264          * @param httpMethod original request http method.
    265          * @param uri original request absolute URI.
    266          * @param httpVersion original request http version.
    267          * @throws IOException if an exception took place during socket reads or writes.
    268          */
    269         private void sendAugmentedRequestToHost(Socket src, Socket dst,
    270                 String httpMethod, URI uri, String httpVersion) throws IOException {
    271 
    272             sendRequestLineWithPath(dst, httpMethod, uri, httpVersion);
    273             filterAndForwardRequestHeaders(src, dst);
    274 
    275             // Currently the proxy does not support keep-alive connections; therefore,
    276             // the proxy has to request the destination server to close the connection
    277             // after the destination server sent the response.
    278             sendLine(dst, "Connection: close");
    279 
    280             // Sends and empty line that indicates termination of the header section.
    281             sendLine(dst, "");
    282         }
    283 
    284         /**
    285          * Forwards original request headers filtering out the ones that have to be removed.
    286          *
    287          * @param src source socket that contains original request headers.
    288          * @param dst destination socket to send the filtered headers to.
    289          * @throws IOException if the data cannot be read from or written to the sockets.
    290          */
    291         private void filterAndForwardRequestHeaders(Socket src, Socket dst) throws IOException {
    292             String line;
    293             do {
    294                 line = getLine(src.getInputStream());
    295                 if (line.length() > 0 && !shouldRemoveHeaderLine(line)) {
    296                     sendLine(dst, line);
    297                 }
    298             } while (line.length() > 0);
    299         }
    300 
    301         /**
    302          * Returns true if a given header line has to be removed from the original request.
    303          *
    304          * @param line header line that should be analysed.
    305          * @return true if the header line should be removed and not forwarded to the destination.
    306          */
    307         private boolean shouldRemoveHeaderLine(String line) {
    308             int colIndex = line.indexOf(":");
    309             if (colIndex != -1) {
    310                 String headerName = line.substring(0, colIndex).trim();
    311                 if (headerName.regionMatches(true, 0, HEADER_CONNECTION, 0,
    312                                                       HEADER_CONNECTION.length())
    313                         || headerName.regionMatches(true, 0, HEADER_PROXY_CONNECTION,
    314                                                           0, HEADER_PROXY_CONNECTION.length())) {
    315                     return true;
    316                 }
    317             }
    318             return false;
    319         }
    320     }
    321 
    322     public ProxyServer() {
    323         threadExecutor = Executors.newCachedThreadPool();
    324         mPort = -1;
    325         mCallback = null;
    326     }
    327 
    328     @Override
    329     public void run() {
    330         try {
    331             serverSocket = new ServerSocket(0);
    332 
    333             setPort(serverSocket.getLocalPort());
    334 
    335             while (mIsRunning) {
    336                 try {
    337                     Socket socket = serverSocket.accept();
    338                     // Only receive local connections.
    339                     if (socket.getInetAddress().isLoopbackAddress()) {
    340                         ProxyConnection parser = new ProxyConnection(socket);
    341 
    342                         threadExecutor.execute(parser);
    343                     } else {
    344                         socket.close();
    345                     }
    346                 } catch (IOException e) {
    347                     e.printStackTrace();
    348                 }
    349             }
    350         } catch (SocketException e) {
    351             Log.e(TAG, "Failed to start proxy server", e);
    352         } catch (IOException e1) {
    353             Log.e(TAG, "Failed to start proxy server", e1);
    354         }
    355 
    356         mIsRunning = false;
    357     }
    358 
    359     public synchronized void setPort(int port) {
    360         if (mCallback != null) {
    361             try {
    362                 mCallback.setProxyPort(port);
    363             } catch (RemoteException e) {
    364                 Log.w(TAG, "Proxy failed to report port to PacManager", e);
    365             }
    366         }
    367         mPort = port;
    368     }
    369 
    370     public synchronized void setCallback(IProxyPortListener callback) {
    371         if (mPort != -1) {
    372             try {
    373                 callback.setProxyPort(mPort);
    374             } catch (RemoteException e) {
    375                 Log.w(TAG, "Proxy failed to report port to PacManager", e);
    376             }
    377         }
    378         mCallback = callback;
    379     }
    380 
    381     public synchronized void startServer() {
    382         mIsRunning = true;
    383         start();
    384     }
    385 
    386     public synchronized void stopServer() {
    387         mIsRunning = false;
    388         if (serverSocket != null) {
    389             try {
    390                 serverSocket.close();
    391                 serverSocket = null;
    392             } catch (IOException e) {
    393                 e.printStackTrace();
    394             }
    395         }
    396     }
    397 
    398     public boolean isBound() {
    399         return (mPort != -1);
    400     }
    401 
    402     public int getPort() {
    403         return mPort;
    404     }
    405 }
    406