/** * Copyright (c) 2013, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.proxyhandler; import android.os.RemoteException; import android.util.Log; import com.android.net.IProxyPortListener; import com.google.android.collect.Lists; import com.google.android.collect.Sets; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @hide */ public class ProxyServer extends Thread { private static final String CONNECT = "CONNECT"; private static final String HTTP_OK = "HTTP/1.1 200 OK\n"; private static final String TAG = "ProxyServer"; // HTTP Headers private static final String HEADER_CONNECTION = "connection"; private static final String HEADER_PROXY_CONNECTION = "proxy-connection"; private ExecutorService threadExecutor; public boolean mIsRunning = false; private ServerSocket serverSocket; private int mPort; private IProxyPortListener mCallback; private class ProxyConnection implements Runnable { private Socket connection; private ProxyConnection(Socket connection) { this.connection = connection; } @Override public void run() { try { String requestLine = getLine(connection.getInputStream()); String[] splitLine = requestLine.split(" "); if (splitLine.length < 3) { connection.close(); return; } String requestType = splitLine[0]; String urlString = splitLine[1]; String httpVersion = splitLine[2]; URI url = null; String host; int port; if (requestType.equals(CONNECT)) { String[] hostPortSplit = urlString.split(":"); host = hostPortSplit[0]; // Use default SSL port if not specified. Parse it otherwise if (hostPortSplit.length < 2) { port = 443; } else { try { port = Integer.parseInt(hostPortSplit[1]); } catch (NumberFormatException nfe) { connection.close(); return; } } urlString = "Https://" + host + ":" + port; } else { try { url = new URI(urlString); host = url.getHost(); port = url.getPort(); if (port < 0) { port = 80; } } catch (URISyntaxException e) { connection.close(); return; } } List list = Lists.newArrayList(); try { list = ProxySelector.getDefault().select(new URI(urlString)); } catch (URISyntaxException e) { e.printStackTrace(); } Socket server = null; for (Proxy proxy : list) { try { if (!proxy.equals(Proxy.NO_PROXY)) { // Only Inets created by PacProxySelector. InetSocketAddress inetSocketAddress = (InetSocketAddress)proxy.address(); server = new Socket(inetSocketAddress.getHostName(), inetSocketAddress.getPort()); sendLine(server, requestLine); } else { server = new Socket(host, port); if (requestType.equals(CONNECT)) { skipToRequestBody(connection); // No proxy to respond so we must. sendLine(connection, HTTP_OK); } else { // Proxying the request directly to the origin server. sendAugmentedRequestToHost(connection, server, requestType, url, httpVersion); } } } catch (IOException ioe) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Unable to connect to proxy " + proxy, ioe); } } if (server != null) { break; } } if (list.isEmpty()) { server = new Socket(host, port); if (requestType.equals(CONNECT)) { skipToRequestBody(connection); // No proxy to respond so we must. sendLine(connection, HTTP_OK); } else { // Proxying the request directly to the origin server. sendAugmentedRequestToHost(connection, server, requestType, url, httpVersion); } } // Pass data back and forth until complete. if (server != null) { SocketConnect.connect(connection, server); } } catch (Exception e) { Log.d(TAG, "Problem Proxying", e); } try { connection.close(); } catch (IOException ioe) { // Do nothing } } /** * Sends HTTP request-line (i.e. the first line in the request) * that contains absolute path of a given absolute URI. * * @param server server to send the request to. * @param requestType type of the request, a.k.a. HTTP method. * @param absoluteUri absolute URI which absolute path should be extracted. * @param httpVersion version of HTTP, e.g. HTTP/1.1. * @throws IOException if the request-line cannot be sent. */ private void sendRequestLineWithPath(Socket server, String requestType, URI absoluteUri, String httpVersion) throws IOException { String absolutePath = getAbsolutePathFromAbsoluteURI(absoluteUri); String outgoingRequestLine = String.format("%s %s %s", requestType, absolutePath, httpVersion); sendLine(server, outgoingRequestLine); } /** * Extracts absolute path form a given URI. E.g., passing * http://google.com:80/execute?query=cat#top * will result in /execute?query=cat#top. * * @param uri URI which absolute path has to be extracted, * @return the absolute path of the URI, */ private String getAbsolutePathFromAbsoluteURI(URI uri) { String rawPath = uri.getRawPath(); String rawQuery = uri.getRawQuery(); String rawFragment = uri.getRawFragment(); StringBuilder absolutePath = new StringBuilder(); if (rawPath != null) { absolutePath.append(rawPath); } else { absolutePath.append("/"); } if (rawQuery != null) { absolutePath.append("?").append(rawQuery); } if (rawFragment != null) { absolutePath.append("#").append(rawFragment); } return absolutePath.toString(); } private String getLine(InputStream inputStream) throws IOException { StringBuilder buffer = new StringBuilder(); int byteBuffer = inputStream.read(); if (byteBuffer < 0) return ""; do { if (byteBuffer != '\r') { buffer.append((char)byteBuffer); } byteBuffer = inputStream.read(); } while ((byteBuffer != '\n') && (byteBuffer >= 0)); return buffer.toString(); } private void sendLine(Socket socket, String line) throws IOException { OutputStream os = socket.getOutputStream(); os.write(line.getBytes()); os.write('\r'); os.write('\n'); os.flush(); } /** * Reads from socket until an empty line is read which indicates the end of HTTP headers. * * @param socket socket to read from. * @throws IOException if an exception took place during the socket read. */ private void skipToRequestBody(Socket socket) throws IOException { while (getLine(socket.getInputStream()).length() != 0); } /** * Sends an augmented request to the final host (DIRECT connection). * * @param src socket to read HTTP headers from.The socket current position should point * to the beginning of the HTTP header section. * @param dst socket to write the augmented request to. * @param httpMethod original request http method. * @param uri original request absolute URI. * @param httpVersion original request http version. * @throws IOException if an exception took place during socket reads or writes. */ private void sendAugmentedRequestToHost(Socket src, Socket dst, String httpMethod, URI uri, String httpVersion) throws IOException { sendRequestLineWithPath(dst, httpMethod, uri, httpVersion); filterAndForwardRequestHeaders(src, dst); // Currently the proxy does not support keep-alive connections; therefore, // the proxy has to request the destination server to close the connection // after the destination server sent the response. sendLine(dst, "Connection: close"); // Sends and empty line that indicates termination of the header section. sendLine(dst, ""); } /** * Forwards original request headers filtering out the ones that have to be removed. * * @param src source socket that contains original request headers. * @param dst destination socket to send the filtered headers to. * @throws IOException if the data cannot be read from or written to the sockets. */ private void filterAndForwardRequestHeaders(Socket src, Socket dst) throws IOException { String line; do { line = getLine(src.getInputStream()); if (line.length() > 0 && !shouldRemoveHeaderLine(line)) { sendLine(dst, line); } } while (line.length() > 0); } /** * Returns true if a given header line has to be removed from the original request. * * @param line header line that should be analysed. * @return true if the header line should be removed and not forwarded to the destination. */ private boolean shouldRemoveHeaderLine(String line) { int colIndex = line.indexOf(":"); if (colIndex != -1) { String headerName = line.substring(0, colIndex).trim(); if (headerName.regionMatches(true, 0, HEADER_CONNECTION, 0, HEADER_CONNECTION.length()) || headerName.regionMatches(true, 0, HEADER_PROXY_CONNECTION, 0, HEADER_PROXY_CONNECTION.length())) { return true; } } return false; } } public ProxyServer() { threadExecutor = Executors.newCachedThreadPool(); mPort = -1; mCallback = null; } @Override public void run() { try { serverSocket = new ServerSocket(0); setPort(serverSocket.getLocalPort()); while (mIsRunning) { try { Socket socket = serverSocket.accept(); // Only receive local connections. if (socket.getInetAddress().isLoopbackAddress()) { ProxyConnection parser = new ProxyConnection(socket); threadExecutor.execute(parser); } else { socket.close(); } } catch (IOException e) { e.printStackTrace(); } } } catch (SocketException e) { Log.e(TAG, "Failed to start proxy server", e); } catch (IOException e1) { Log.e(TAG, "Failed to start proxy server", e1); } mIsRunning = false; } public synchronized void setPort(int port) { if (mCallback != null) { try { mCallback.setProxyPort(port); } catch (RemoteException e) { Log.w(TAG, "Proxy failed to report port to PacManager", e); } } mPort = port; } public synchronized void setCallback(IProxyPortListener callback) { if (mPort != -1) { try { callback.setProxyPort(mPort); } catch (RemoteException e) { Log.w(TAG, "Proxy failed to report port to PacManager", e); } } mCallback = callback; } public synchronized void startServer() { mIsRunning = true; start(); } public synchronized void stopServer() { mIsRunning = false; if (serverSocket != null) { try { serverSocket.close(); serverSocket = null; } catch (IOException e) { e.printStackTrace(); } } } public boolean isBound() { return (mPort != -1); } public int getPort() { return mPort; } }