/*
* 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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.HttpForward;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
/**
* Tests for HTTP traffic through Tunnel via an HTTP proxy.
*
* @author navteniev@linkedin.com
*/
@Test(groups = { "gobblin.tunnel", "disabledOnTravis" })
public class TunnelTest {
private ClientAndServer mockServer;
int PORT = 0;
@BeforeClass
public void startProxy()
throws IOException {
mockServer = ClientAndServer.startClientAndServer(0);
PORT = mockServer.getPort();
}
@AfterClass
public void stopProxy() {
mockServer.stop();
}
@AfterMethod
public void reset(){
mockServer.reset();
}
@Test
public void mustBuildTunnelAndStartAcceptingConnections()
throws Exception {
Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);
try {
int tunnelPort = tunnel.getPort();
assertTrue(tunnelPort > 0);
} finally {
tunnel.close();
}
}
@Test
public void mustHandleClientDisconnectingWithoutClosingTunnel()
throws Exception {
mockExample();
Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);
try {
int tunnelPort = tunnel.getPort();
SocketChannel client = SocketChannel.open();
client.connect(new InetSocketAddress("localhost", tunnelPort));
client.write(ByteBuffer.wrap("GET / HTTP/1.1%nUser-Agent: GobblinTunnel%nConnection:keep - alive %n%n".getBytes()));
client.close();
assertNotNull(fetchContent(tunnelPort));
} finally {
tunnel.close();
}
}
@Test
public void mustHandleConnectionToExternalResource()
throws Exception {
mockExample();
Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);
try {
String content = fetchContent(tunnel.getPort());
assertNotNull(content);
} finally {
tunnel.close();
}
}
@Test
public void mustHandleMultipleConnections()
throws Exception {
mockExample();
Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);
int clients = 5;
final CountDownLatch startSignal = new CountDownLatch(1);
final CountDownLatch doneSignal = new CountDownLatch(clients);
ExecutorService executor = Executors.newFixedThreadPool(clients);
try {
final int tunnelPort = tunnel.getPort();
List<Future<String>> results = new ArrayList<Future<String>>();
for (int i = 0; i < clients; i++) {
Future<String> result = executor.submit(new Callable<String>() {
@Override
public String call()
throws Exception {
startSignal.await();
try {
return fetchContent(tunnelPort);
} finally {
doneSignal.countDown();
}
}
});
results.add(result);
}
startSignal.countDown();
doneSignal.await();
for (Future<String> result : results) {
assertNotNull(result.get());
}
} finally {
tunnel.close();
}
}
@Test(expectedExceptions = SocketException.class)
public void mustRefuseConnectionWhenProxyIsUnreachable()
throws Exception {
Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", 1);
try {
int tunnelPort = tunnel.getPort();
fetchContent(tunnelPort);
} finally {
tunnel.close();
}
}
@Test(expectedExceptions = SocketException.class)
public void mustRefuseConnectionWhenProxyRefuses() throws Exception{
mockServer.when(HttpRequest.request().withMethod("CONNECT").withPath("www.us.apache.org:80"))
.respond(HttpResponse.response().withStatusCode(403));
Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);
try {
int tunnelPort = tunnel.getPort();
fetchContent(tunnelPort);
} finally {
tunnel.close();
}
}
@Test(expectedExceptions = SocketException.class)
public void mustRefuseConnectionWhenProxyTimesOut() throws Exception{
mockServer.when(HttpRequest.request().withMethod("CONNECT").withPath("www.us.apache.org:80"))
.respond(HttpResponse.response().withDelay(TimeUnit.SECONDS,2).withStatusCode(200));
Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);
try {
int tunnelPort = tunnel.getPort();
fetchContent(tunnelPort);
} finally {
tunnel.close();
}
}
@Test(enabled = false)
public void mustDownloadLargeFiles()
throws Exception {
mockServer.when(HttpRequest.request().withMethod("CONNECT").withPath("www.us.apache.org:80"))
.respond(HttpResponse.response().withStatusCode(200));
mockServer.when(HttpRequest.request().withMethod("GET")
.withPath("/dist//httpcomponents/httpclient/binary/httpcomponents-client-4.5.1-bin.tar.gz"))
.forward(HttpForward.forward().withHost("www.us.apache.org").withPort(80));
Tunnel tunnel = Tunnel.build("www.us.apache.org", 80, "localhost", PORT);
try {
IOUtils.copyLarge((InputStream) new URL("http://localhost:" + tunnel.getPort()
+ "/dist//httpcomponents/httpclient/binary/httpcomponents-client-4.5.1-bin.tar.gz")
.getContent(new Class[]{InputStream.class}),
new FileOutputStream(File.createTempFile("httpcomponents-client-4.5.1-bin", "tar.gz")));
} finally {
tunnel.close();
}
}
private String fetchContent(int tunnelPort)
throws IOException {
InputStream content = (InputStream) new URL(String.format("http://localhost:%s/", tunnelPort)).openConnection()
.getContent(new Class[]{InputStream.class});
return IOUtils.toString(content);
}
private void mockExample()
throws IOException {
mockServer.when(HttpRequest.request().withMethod("CONNECT").withPath("example.org:80"))
.respond(HttpResponse.response().withStatusCode(200));
mockServer.when(HttpRequest.request().withMethod("GET").withPath("/"))
.respond(HttpResponse.response(IOUtils.toString(getClass().getResourceAsStream("/example.org.html"))));
}
}