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 */ 16package com.android.proxyhandler; 17 18import android.os.RemoteException; 19import android.util.Log; 20 21import com.android.net.IProxyPortListener; 22import com.google.android.collect.Lists; 23import com.google.android.collect.Sets; 24 25import java.io.IOException; 26import java.io.InputStream; 27import java.io.OutputStream; 28import java.net.InetSocketAddress; 29import java.net.Proxy; 30import java.net.ProxySelector; 31import java.net.ServerSocket; 32import java.net.Socket; 33import java.net.SocketException; 34import java.net.URI; 35import java.net.URISyntaxException; 36import java.util.List; 37import java.util.Set; 38import java.util.concurrent.ExecutorService; 39import java.util.concurrent.Executors; 40 41/** 42 * @hide 43 */ 44public 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