| /** |
| * 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<Proxy> 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 |
| * <code>http://google.com:80/execute?query=cat#top</code> |
| * will result in <code>/execute?query=cat#top</code>. |
| * |
| * @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; |
| } |
| } |