/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 gobblin.tunnel;
import org.apache.commons.io.IOUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Due to the lack of a suitable embeddable proxy server (the Jetty version here is too old and MockServer's Proxy
* expects SSL traffic and breaks for arbitrary bytes) we had to write our own mini CONNECT proxy. This simply gets
* an HTTP CONNECT request over a socket, opens another socket to the specified remote server and relays bytes between
* the two connections.
*
* @author kkandekar@linkedin.com
*/
class ConnectProxyServer extends MockServer {
private final boolean mixServerAndProxyResponse;
private final boolean largeResponse;
Pattern hostPortPattern;
int nBytesToCloseSocketAfter;
/**
* @param mixServerAndProxyResponse Force proxy to send 200 OK and server response in single write such that both
* responses reach the tunnel in the same read. This can happen for a multitude of
* reasons, e.g. the proxy GC's, or the network hiccups, or the tunnel GC's.
* @param largeResponse Force proxy to send a large response
* @param nBytesToCloseSocketAfter
*/
public ConnectProxyServer(boolean mixServerAndProxyResponse, boolean largeResponse, int nBytesToCloseSocketAfter) {
this.mixServerAndProxyResponse = mixServerAndProxyResponse;
this.largeResponse = largeResponse;
this.nBytesToCloseSocketAfter = nBytesToCloseSocketAfter;
hostPortPattern = Pattern.compile("Host: (.*):([0-9]+)");
}
@Override
void handleClientSocket(Socket clientSocket) throws IOException {
final InputStream clientToProxyIn = clientSocket.getInputStream();
BufferedReader clientToProxyReader = new BufferedReader(new InputStreamReader(clientToProxyIn));
final OutputStream clientToProxyOut = clientSocket.getOutputStream();
String line = clientToProxyReader.readLine();
String connectRequest = "";
while (line != null && isServerRunning()) {
connectRequest += line + "\r\n";
if (connectRequest.endsWith("\r\n\r\n")) {
break;
}
line = clientToProxyReader.readLine();
}
// connect to given host:port
Matcher matcher = hostPortPattern.matcher(connectRequest);
if (!matcher.find()) {
try {
sendConnectResponse("400 Bad Request", clientToProxyOut, null, 0);
} finally {
clientSocket.close();
stopServer();
}
return;
}
String host = matcher.group(1);
int port = Integer.decode(matcher.group(2));
// connect to server
Socket serverSocket = new Socket();
try {
serverSocket.connect(new InetSocketAddress(host, port));
addSocket(serverSocket);
byte [] initialServerResponse = null;
int nbytes = 0;
if (mixServerAndProxyResponse) {
// we want to mix the initial server response with the 200 OK
initialServerResponse = new byte[64];
nbytes = serverSocket.getInputStream().read(initialServerResponse);
}
sendConnectResponse("200 OK", clientToProxyOut, initialServerResponse, nbytes);
} catch (IOException e) {
try {
sendConnectResponse("404 Not Found", clientToProxyOut, null, 0);
} finally {
clientSocket.close();
stopServer();
}
return;
}
final InputStream proxyToServerIn = serverSocket.getInputStream();
final OutputStream proxyToServerOut = serverSocket.getOutputStream();
_threads.add(new EasyThread() {
@Override
void runQuietly() throws Exception {
try {
IOUtils.copy(clientToProxyIn, proxyToServerOut);
} catch (IOException e) {
LOG.warn("Exception " + e.getMessage() + " on " + getServerSocketPort());
}
}
}.startThread());
try {
if (nBytesToCloseSocketAfter > 0) {
// Simulate proxy abruptly closing connection
int leftToRead = nBytesToCloseSocketAfter;
byte [] buffer = new byte[leftToRead+ 256];
while (true) {
int numRead = proxyToServerIn.read(buffer, 0, leftToRead);
if (numRead < 0) {
break;
}
clientToProxyOut.write(buffer, 0, numRead);
clientToProxyOut.flush();
leftToRead -= numRead;
if (leftToRead <= 0) {
LOG.warn("Cutting connection after " + nBytesToCloseSocketAfter + " bytes");
break;
}
}
} else {
IOUtils.copy(proxyToServerIn, clientToProxyOut);
}
} catch (IOException e) {
LOG.warn("Exception " + e.getMessage() + " on " + getServerSocketPort());
}
clientSocket.close();
serverSocket.close();
}
private void sendConnectResponse(String statusMessage, OutputStream out,
byte[] initialServerResponse, int initialServerResponseSize)
throws IOException {
String extraHeader = "";
if (largeResponse) {
// this is to force multiple reads while draining the proxy CONNECT response in Tunnel. Normal proxy responses
// won't be this big (well, unless you annoy squid proxy, which happens sometimes), but a select() call
// waking up for multiple reads before a buffer is full is normal
for (int i = 0; i < 260; i++) {
extraHeader += "a";
}
}
byte [] httpResponse = ("HTTP/1.1 " + statusMessage + "\r\nContent-Length: 0\r\nServer: MockProxy" + extraHeader + "\r\n\r\n").getBytes();
if (initialServerResponse != null) {
byte [] mixedResponse = new byte[httpResponse.length + initialServerResponseSize];
System.arraycopy(httpResponse, 0, mixedResponse, 0, httpResponse.length);
System.arraycopy(initialServerResponse, 0, mixedResponse, httpResponse.length, initialServerResponseSize);
out.write(mixedResponse);
} else {
out.write(httpResponse);
}
out.flush();
}
}