/*
* Copyright © 2015, 2016 IBM Corp. All rights reserved.
*
* 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.cloudant.tests;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import com.cloudant.client.api.ClientBuilder;
import com.cloudant.client.api.CloudantClient;
import com.cloudant.http.Http;
import com.cloudant.tests.util.MockWebServerResources;
import com.cloudant.tests.util.HttpFactoryParameterizedTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.Parameterized;
import org.littleshoot.proxy.HttpProxyServer;
import org.littleshoot.proxy.HttpProxyServerBootstrap;
import org.littleshoot.proxy.ProxyAuthenticator;
import org.littleshoot.proxy.SslEngineSource;
import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLEngine;
public class HttpProxyTest extends HttpFactoryParameterizedTest {
@Parameterized.Parameters(name = "okhttp: {0}; secure proxy: {1}; https server: {2}; proxy " +
"auth: {3}")
public static List<Object[]> combinations() {
boolean[] tf = new boolean[]{true, false};
List<Object[]> combos = new ArrayList<Object[]>();
for (boolean okUsable : tf) {
for (boolean secureProxy : new boolean[]{false}) {
// AFAICT there is no way to instruct HttpURLConnection to connect via SSL to a
// proxy server - so for now we just test an unencrypted proxy.
// Note this is independent of the SSL tunnelling to an https server and influences
// only requests between client and proxy. With an https server client requests are
// tunnelled directly to the https server, other than the original HTTP CONNECT
// request. The reason for using a SSL proxy would be to encrypt proxy auth creds
// but it appears this scenario is not readily supported.
for (boolean httpsServer : tf) {
for (boolean proxyAuth : tf) {
combos.add(new Object[]{okUsable, secureProxy, httpsServer, proxyAuth});
}
}
}
}
return combos;
}
// Note Parameter(0) okUsable is inherited
@Parameterized.Parameter(1)
public boolean secureProxy;
@Parameterized.Parameter(2)
public boolean useHttpsServer;
@Parameterized.Parameter(3)
public boolean useProxyAuth;
@Rule
public MockWebServer server = new MockWebServer();
HttpProxyServer proxy;
String mockProxyUser = "alpha";
String mockProxyPass = "alphaPass";
// Unfortunately getting the System property jdk.http.auth.tunneling.disabledSchemes doesn't
// actually give us the default value (it returns null so the property being unset enables some
// default behaviour). It is not possible to unset the value after we have set it so the best we
// can do is set it back to a value we think is appropirate. According to release notes for the
// fix for CVE-2016-5597 the Basic scheme is disabled so we'll reset to that value.
private final String defaultDisabledList = "Basic";
/**
* Enables https on the mock web server receiving our requests if useHttpsServer is true.
*
* @throws Exception
*/
@Before
public void setupMockServerSSLIfNeeded() throws Exception {
if (useHttpsServer) {
server.useHttps(MockWebServerResources.getSSLSocketFactory(), false);
}
}
/**
* Starts a littleproxy instance that will proxy the requests. Applies appropriate configuration
* options to the proxy based on the test parameters.
*
* @throws Exception
*/
@Before
public void setupAndStartProxy() throws Exception {
HttpProxyServerBootstrap proxyBoostrap = DefaultHttpProxyServer.bootstrap()
.withAllowLocalOnly(true) // only run on localhost
.withAuthenticateSslClients(false); // we aren't checking client certs
if (useProxyAuth) {
// check the proxy user and password
ProxyAuthenticator pa = new ProxyAuthenticator() {
@Override
public boolean authenticate(String userName, String password) {
return (mockProxyUser.equals(userName) && mockProxyPass.equals(password));
}
@Override
public String getRealm() {
return null;
}
};
proxyBoostrap.withProxyAuthenticator(pa);
}
if (secureProxy) {
proxyBoostrap.withSslEngineSource(new SslEngineSource() {
@Override
public SSLEngine newSslEngine() {
return MockWebServerResources.getSSLContext().createSSLEngine();
}
@Override
public SSLEngine newSslEngine(String peerHost, int peerPort) {
return MockWebServerResources.getSSLContext().createSSLEngine(peerHost,
peerPort);
}
});
}
// Start the proxy server
proxy = proxyBoostrap.start();
}
/**
* Shutdown the proxy server at the end of the test.
*
* @throws Exception
*/
@After
public void shutdownProxy() throws Exception {
proxy.stop();
}
/**
* The Proxy-Authorization header that we add to requests gets encrypted in the case of a SSL
* tunnel connection to a HTTPS server. The default HttpURLConnection does not add the header
* to the CONNECT request so in that case we require an Authenticator to provide credentials
* to the proxy server. The client code does not set an Authenticator automatically because
* it is a global default so it must be set by the application developer or system
* administrators in accordance with their environment. For the purposes of this test we can
* add and remove the Authenticator before and after testing.
*/
@Before
public void setAuthenticatorIfNeeded() {
// If we are not using okhttp and we have an https server and a proxy that needs auth then
// we need to set the default Authenticator
if (useProxyAuth && useHttpsServer && !okUsable) {
// Allow https tunnelling through http proxy for the duration of the test
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (getRequestorType() == RequestorType.PROXY) {
return new PasswordAuthentication(mockProxyUser, mockProxyPass.toCharArray());
} else {
return null;
}
}
});
}
}
/**
* Reset the Authenticator after the test.
*
* @see #setAuthenticatorIfNeeded()
*/
@After
public void resetAuthenticator() {
// If we are not using okhttp and we have an https server and a proxy that needs auth then
// we need to set the default Authenticator
if (useProxyAuth && useHttpsServer && !okUsable) {
Authenticator.setDefault(null);
// Reset the disabled schemes property
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", defaultDisabledList);
}
}
/**
* This test validates that a request can successfully traverse a proxy to our mock server.
*/
@Test
public void proxiedRequest() throws Exception {
//mock a 200 OK
server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
return new MockResponse();
}
});
InetSocketAddress address = proxy.getListenAddress();
URL proxyUrl = new URL((secureProxy) ? "https" : "http", address.getHostName(), address
.getPort(), "/");
ClientBuilder builder = CloudantClientHelper.newMockWebServerClientBuilder(server)
.proxyURL(proxyUrl);
if (useProxyAuth) {
builder.proxyUser(mockProxyUser).proxyPassword(mockProxyPass);
}
// We don't use SSL authentication for this test
CloudantClient client = builder.disableSSLAuthentication().build();
String response = client.executeRequest(Http.GET(client.getBaseUri())).responseAsString();
assertTrue("There should be no response body on the mock response", response.isEmpty());
//if it wasn't a 20x then an exception should have been thrown by now
RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS);
assertNotNull(request);
}
}