blob: ff31c2693b01b60bc9cf366b1ceb16b3b0737b80 [file] [log] [blame]
/*
* Copyright (C) 2015 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.managedprovisioning.comm;
import com.android.managedprovisioning.comm.Bluetooth.NetworkData;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* The connection between a channel and a socket to a web server. This connection handles most
* compliant proxy clients. It expects an initial CONNECT request
* {@see http://tools.ietf.org/html/rfc2817#section-5.2}. Additionally, it can handle clients which
* omit the CONNECT request, as long as they specify an absoluteURI in their request line.
* {@see http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html}.
*
* If a client does not send a CONNECT request, and attempts to make a request using an
* abs_path {@see http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html} in the request line, the
* connection will be rejected.
*/
public class ProxyConnection extends Thread {
private static final String CONNECT = "CONNECT";
private static final String HTTPS = "https";
private static final String RESPONSE_OK = "HTTP/1.1 200 OK\n\n";
private NetToBtThread mNetToBt;
private BtToNetThread mBtToNet;
private volatile boolean mNetRunning;
private Socket mNetSocket;
private final PipedInputStream mHttpInput;
private final OutputStream mHttpOutput;
private final int mConnId;
private final Channel mChannel;
/**
* Used to create network data response packets. The device Id can be empty because this is
* called from the programmer device.
*/
private final PacketUtil mPacketUtil = new PacketUtil("");
public ProxyConnection(Channel channel, int connId) {
mChannel = channel;
mConnId = connId;
mHttpInput = new PipedInputStream();
mHttpOutput = new PipedOutputStream();
try {
mHttpInput.connect((PipedOutputStream) mHttpOutput);
} catch (IOException e) {
// The streams were just created so this shouldn't happen.
ProvisionCommLogger.loge(e);
}
mNetRunning = true;
}
public boolean isRunning() {
return mNetRunning;
}
public void shutdown() {
ProvisionCommLogger.logd("Shutting down ConnectionProcessor");
try {
mHttpOutput.close();
} catch (IOException io) {
ProvisionCommLogger.logd(io);
}
endConnection();
}
@Override
public void run() {
ProvisionCommLogger.logd("Creating a new socket.");
processConnect();
}
private void endConnection() {
try {
if (mChannel != null) {
mChannel.write(mPacketUtil.createEndPacket(mConnId));
} else {
ProvisionCommLogger.logd(
"Attempted to write end of connection with null connection");
}
} catch (IOException io) {
ProvisionCommLogger.logd("Could not write closing packet.", io);
}
try {
if (mNetSocket != null) {
mNetSocket.close();
}
} catch (IOException io) {
ProvisionCommLogger.logd("Attempted to close socket when already closed.", io);
}
ProvisionCommLogger.logd("Ended connection");
}
private class NetToBtThread extends Thread {
@Override
public void run() {
final byte[] buffer = new byte[16384];
InputStream input = null;
try {
input = mNetSocket.getInputStream();
while (mNetSocket.isConnected()) {
int readBytes = input.read(buffer);
if (readBytes < 0) {
ProvisionCommLogger.logd("Passing " + readBytes + " bytes");
mChannel.write(mPacketUtil.createEndPacket(mConnId));
break;
}
ProvisionCommLogger.logd("Passing " + readBytes + " bytes");
mChannel.write(mPacketUtil.createDataPacket(mConnId, NetworkData.OK, buffer,
readBytes));
}
} catch (IOException io) {
ProvisionCommLogger.logd("Server socket input stream is closed.");
} finally {
if (input != null) {
try {
input.close();
} catch (IOException ex) {
ProvisionCommLogger.logw(
"Failed to close connection", ex);
}
}
}
ProvisionCommLogger.logd("SocketReader is ending.");
mNetRunning = false;
}
}
private class BtToNetThread extends Thread {
@Override
public void run() {
final byte[] buffer = new byte[16384];
try {
while (true) {
int readBytes = mHttpInput.read(buffer);
if (readBytes < 0) {
break;
}
if (mNetSocket == null) {
break;
} else {
mNetSocket.getOutputStream().write(buffer, 0, readBytes);
}
}
} catch (IOException io) {
ProvisionCommLogger.logd("Bluetooth input stream for this connection is closed.");
} finally {
try {
mHttpInput.close();
} catch (IOException ex) {
ProvisionCommLogger.logw("Failed to close connection", ex);
}
}
ProvisionCommLogger.logd("SocketWriter is ending.");
}
}
private String getLine() throws IOException {
ProvisionCommLogger.logi("getLine");
StringBuilder buffer = new StringBuilder();
int ch;
while ((ch = mHttpInput.read()) != -1) {
if (ch == '\r')
continue;
if (ch == '\n')
break;
buffer.append((char) ch);
}
ProvisionCommLogger.logi("Proxy reading: " + buffer);
return buffer.toString();
}
@VisibleForTesting
protected static class RequestLineInfo {
String method;
URI uri;
public RequestLineInfo(String method, URI uri) {
this.method = method;
this.uri = uri;
}
}
/**
* Parse a request line. Supports CONNECT requests, as well as other requests using absoluteURI
* {@see http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html}
* {@see http://tools.ietf.org/html/rfc2817#section-5.2}
* {@see https://www.ietf.org/rfc/rfc2396.txt}
* @param requestLine The requestLine from the HTTP request
* @return A struct containing the parsed request if parsing was successful, otherwise null.
*/
@VisibleForTesting
protected static RequestLineInfo parseRequestLine(String requestLine) {
String[] split = requestLine.split(" ");
if (split.length < 2) {
ProvisionCommLogger.loge("Could not parse request line: " + requestLine);
return null;
}
String method = split[0];
String uriString = split[1];
if (CONNECT.equals(method)) {
// CONNECT request lines come through as an 'authority' element (see RFC 2396), which do
// not contain a scheme. We don't need the scheme to open the socket, but we do need it
// for the ProxySelector - so force it to HTTPS.
if (!uriString.contains("://")) {
uriString = HTTPS + "://" + uriString;
}
}
URI uri;
try {
// parse with URL first - this is more restrictive, and catches the case where a GET
// request comes through with an abs_path, but no host, in the request line. This
// situation is unsupported by this proxy (but should never happen with a compliant
// client - we should always see a CONNECT request first).
URL url = new URL(uriString);
uri = url.toURI();
} catch (MalformedURLException|URISyntaxException e) {
ProvisionCommLogger.loge(
"Invalid or unsupported URI in request line: " + requestLine, e);
return null;
}
if (uri.getPort() < 0) {
// If no port was specified, choose a default
int newPort = 80;
if (CONNECT.equals(method) || HTTPS.equals(uri.getScheme().toLowerCase())) {
newPort = 443;
}
try {
// sadly this is the only way to "mutate" a URI
uri = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), newPort,
uri.getPath(), uri.getQuery(), uri.getFragment());
} catch (URISyntaxException e) {
ProvisionCommLogger.loge(
"Invalid URI when trying to enforce https: " + requestLine, e);
return null;
}
}
return new RequestLineInfo(method, uri);
}
/**
* Create a socket to the requested host. Check for an applicable proxy, and use it if found.
* @param uri URI containing the host to connect to.
* @return
* @throws IOException
*/
private boolean createSocket(URI uri) throws IOException {
boolean usingProxy = false;
String host = uri.getHost();
int port = uri.getPort();
List<Proxy> list = ProxySelector.getDefault().select(uri);
for (Proxy proxy : list) {
if (proxy.equals(Proxy.NO_PROXY)) {
break; // break out and create a normal socket
} else {
if (proxy.address() instanceof InetSocketAddress) {
// Only Inets created by PacProxySelector and ProxySelectorImpl.
InetSocketAddress inetSocketAddress =
(InetSocketAddress)proxy.address();
// A proxy specified with an IP addr should only ever use that IP. This
// will ensure that the proxy only ever connects to its specified
// address. If the proxy is resolved, use the associated IP address. If
// unresolved, use the specified host name.
host = inetSocketAddress.isUnresolved() ?
inetSocketAddress.getHostName() :
inetSocketAddress.getAddress().getHostAddress();
port = inetSocketAddress.getPort();
usingProxy = true;
break;
} else {
ProvisionCommLogger.logw("Unsupported Inet type from ProxySelector, skipping:" +
proxy.address().getClass().getSimpleName());
}
}
}
mNetSocket = new Socket(host, port);
return usingProxy;
}
private void processConnect() {
try {
String requestLine = getLine() + '\r' + '\n';
RequestLineInfo info = parseRequestLine(requestLine);
if (info == null) {
mNetRunning = false;
return;
}
boolean usingProxy;
try {
usingProxy = createSocket(info.uri);
} catch (IOException e) {
ProvisionCommLogger.loge("Failed to create socket: " +
info.uri.getHost() + ":" + info.uri.getPort(), e);
mNetRunning = false;
mNetSocket = null;
return;
}
String toSend = "";
if (mNetSocket == null) {
if (CONNECT.equals(info.method) && !usingProxy) {
// If we're not talking to a proxy, and we're handling a CONNECT, we need to
// send a response
handleConnect();
} else {
mNetSocket.getOutputStream().write(toSend.getBytes());
}
}
mNetToBt = new NetToBtThread();
mNetToBt.start();
mBtToNet = new BtToNetThread();
mBtToNet.start();
} catch (Exception e) {
ProvisionCommLogger.logd(e);
mNetRunning = false;
}
}
public void closePipe() {
try {
mHttpInput.close();
} catch (IOException e) {
ProvisionCommLogger.logd(e);
}
try {
mHttpOutput.close();
} catch (IOException e) {
ProvisionCommLogger.logd(e);
}
}
private void handleConnect() throws IOException {
while (getLine().length() != 0);
// No proxy to respond so we must.
mChannel.write(mPacketUtil.createDataPacket(mConnId, NetworkData.OK,
RESPONSE_OK.getBytes(),
RESPONSE_OK.length()));
}
public OutputStream getOutput() {
return mHttpOutput;
}
}