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