/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.glassfish.jersey.jdk.connector;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.Application;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.internal.util.Base64;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.grizzly.http.server.HttpHandler;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.grizzly.http.server.Response;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* @author Petr Janouch (petr.janouch at oracle.com)
*/
public class ProxyTest extends JerseyTest {
private static final Charset CHARACTER_SET = Charset.forName("iso-8859-1");
private static final String PROXY_HOST = "localhost";
private static final int PROXY_PORT = 8321;
private static final String PROXY_USER_NAME = "petr";
private static final String PROXY_PASSWORD = "my secret password";
@Test
public void testConnect() throws IOException {
doTest(Proxy.Authentication.NONE);
}
@Test
public void testBasicAuthentication() throws IOException {
doTest(Proxy.Authentication.BASIC);
}
@Test
public void testDigestAuthentication() throws IOException {
doTest(Proxy.Authentication.DIGEST);
}
private void doTest(Proxy.Authentication authentication) throws IOException {
Proxy proxy = new Proxy(authentication);
try {
proxy.start();
javax.ws.rs.core.Response response = target("resource").request().get();
assertEquals(200, response.getStatus());
assertEquals("OK", response.readEntity(String.class));
assertTrue(proxy.getProxyHit());
} finally {
proxy.stop();
}
}
@Test
public void authenticationFailTest() throws IOException {
Proxy proxy = new Proxy(Proxy.Authentication.BASIC);
try {
proxy.start();
proxy.setAuthernticationFail(true);
try {
target("resource").request().get();
fail();
} catch (Exception e) {
assertEquals(ProxyAuthenticationException.class, e.getCause().getCause().getClass());
}
assertTrue(proxy.getProxyHit());
} finally {
proxy.stop();
}
}
@Override
protected Application configure() {
return new ResourceConfig(Resource.class);
}
@Override
protected void configureClient(final ClientConfig config) {
config.property(JdkConnectorProvider.MAX_CONNECTIONS_PER_DESTINATION, 1);
config.property(ClientProperties.PROXY_URI, "http://" + PROXY_HOST + ":" + PROXY_PORT);
config.property(ClientProperties.PROXY_USERNAME, PROXY_USER_NAME);
config.property(ClientProperties.PROXY_PASSWORD, PROXY_PASSWORD);
config.connectorProvider(new JdkConnectorProvider());
}
@Path("/resource")
public static class Resource {
@GET
public String get() {
return "OK";
}
}
private static class Proxy {
private final HttpServer server = HttpServer.createSimpleServer("/", PROXY_HOST, PROXY_PORT);
private volatile String destinationUri = null;
private final Authentication authentication;
private volatile boolean proxyHit = false;
private volatile boolean authenticationFail = false;
Proxy(Authentication authentication) {
this.authentication = authentication;
}
boolean getProxyHit() {
return proxyHit;
}
void setAuthernticationFail(boolean authenticationFail) {
this.authenticationFail = authenticationFail;
}
void start() throws IOException {
server.getServerConfiguration().addHttpHandler(new HttpHandler() {
public void service(Request request, Response response) throws Exception {
if (request.getMethod().getMethodString().equals("CONNECT")) {
proxyHit = true;
String authorizationHeader = request.getHeader("Proxy-Authorization");
if (authentication != Authentication.NONE && authorizationHeader == null) {
// if we need authentication and receive CONNECT with no Proxy-authorization header, send 407
send407(request, response);
return;
}
if (authenticationFail) {
send407(request, response);
return;
}
if (authentication == Authentication.BASIC) {
if (!verifyBasicAuthorizationHeader(response, authorizationHeader)) {
return;
}
// if success continue
} else if (authentication == Authentication.DIGEST) {
if (!verifyDigestAuthorizationHeader(response, authorizationHeader)) {
return;
}
// if success continue
}
// check that both Host header and URI contain host:port
String requestURI = request.getRequestURI();
String host = request.getHeader("Host");
if (!requestURI.equals(host)) {
response.setStatus(400);
System.out.println("Request URI: " + requestURI);
System.out.println("Host header: " + host);
return;
}
// save the destination where a normal proxy would open a connection
destinationUri = "http://" + requestURI;
response.setStatus(200);
hackGrizzlyConnect(request, response);
return;
}
handleTrafficAfterConnect(request, response);
}
});
server.start();
}
private void send407(Request request, Response response) {
response.setStatus(407);
if (authentication == Authentication.BASIC) {
response.setHeader("Proxy-Authenticate", "Basic");
} else {
response.setHeader("Proxy-Authenticate", "Digest realm=\"my-realm\", domain=\"\", "
+ "nonce=\"n9iv3MeSNkEfM3uJt2gnBUaWUbKAljxp\", algorithm=MD5, \"\n"
+ " + \"qop=\"auth\", stale=false");
}
hackGrizzlyConnect(request, response);
}
private boolean verifyBasicAuthorizationHeader(Response response, String authorizationHeader) {
if (!authorizationHeader.startsWith("Basic")) {
System.out.println(
"Authorization header during Basic authentication does not start with \"Basic\"");
response.setStatus(400);
return false;
}
String decoded = new String(Base64.decode(authorizationHeader.substring(6).getBytes()),
CHARACTER_SET);
final String[] split = decoded.split(":");
final String username = split[0];
final String password = split[1];
if (!username.equals(PROXY_USER_NAME)) {
response.setStatus(400);
System.out.println("Found unexpected username: " + username);
return false;
}
if (!password.equals(PROXY_PASSWORD)) {
response.setStatus(400);
System.out.println("Found unexpected password: " + username);
return false;
}
return true;
}
private boolean verifyDigestAuthorizationHeader(Response response, String authorizationHeader) {
if (!authorizationHeader.startsWith("Digest")) {
System.out.println(
"Authorization header during Digest authentication does not start with \"Digest\"");
response.setStatus(400);
return false;
}
final Matcher match = Pattern.compile("username=\"([^\"]+)\"").matcher(authorizationHeader);
if (!match.find()) {
return false;
}
final String username = match.group(1);
if (!username.equals(PROXY_USER_NAME)) {
response.setStatus(400);
System.out.println("Found unexpected username: " + username);
return false;
}
return true;
}
private void hackGrizzlyConnect(Request request, Response response) {
// Grizzly does not like CONNECT method and sets keep alive to false
// This hacks Grizzly, so it will keep the connection open
response.getResponse().getProcessingState().setKeepAlive(true);
response.getResponse().setContentLength(0);
request.setMethod("GET");
}
private void handleTrafficAfterConnect(Request request, Response response) throws IOException {
if (destinationUri == null) {
// It seems that CONNECT has not been called
System.out.println("Received non-CONNECT without receiving CONNECT first");
response.setStatus(400);
return;
}
// create a client and relay the request to the final destination
ClientConfig clientConfig = new ClientConfig();
clientConfig.connectorProvider(new JdkConnectorProvider());
Client client = ClientBuilder.newClient(clientConfig);
Invocation.Builder destinationRequest = client.target(destinationUri).path(request.getRequestURI()).request();
for (String headerName : request.getHeaderNames()) {
destinationRequest.header(headerName, request.getHeader(headerName));
}
javax.ws.rs.core.Response destinationResponse = destinationRequest
.method(request.getMethod().getMethodString());
// translate the received response into the proxy response
response.setStatus(destinationResponse.getStatus());
OutputStream outputStream = response.getOutputStream();
String body = destinationResponse.readEntity(String.class);
outputStream.write(body.getBytes());
client.close();
}
void stop() {
server.shutdown();
}
private enum Authentication {
NONE,
BASIC,
DIGEST
}
}
}