ProxyTest.java

/*
 * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2019 Banco do Brasil S/A. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.tests.e2e.client.connector.proxy;

import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.HttpUrlConnectorProvider;
import org.glassfish.jersey.client.spi.ConnectorProvider;
import org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider;
import org.glassfish.jersey.jetty.connector.JettyConnectorProvider;
import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * Moved from jetty-connector
 * @author Marcelo Rubim
 */
@Suite
@SelectClasses({
        ProxyTest.ApacheConnectorProviderProxyTest.class,
        ProxyTest.Apache5ConnectorProviderProxyTest.class,
        ProxyTest.GrizzlyConnectorProviderProxyTest.class,
        ProxyTest.JettyConnectorProviderProxyTest.class,
        ProxyTest.NettyConnectorProviderProxyTest.class,
        ProxyTest.HttpUrlConnectorProviderProxyTest.class
})
public class ProxyTest {
    private static final Charset CHARACTER_SET = Charset.forName("iso-8859-1");
    private static final String PROXY_URI = "http://127.0.0.1:9997";
    private static final String PROXY_USERNAME = "proxy-user";
    private static final String PROXY_PASSWORD = "proxy-password";
    private static final String PROXY_NO_PASS = "proxy-no-pass";

    public static class ApacheConnectorProviderProxyTest extends ProxyTemplateTest {
        public ApacheConnectorProviderProxyTest()
                throws NoSuchMethodException, InvocationTargetException, InstantiationException,
                IllegalAccessException {
            super(ApacheConnectorProvider.class);
        }
    }

    public static class Apache5ConnectorProviderProxyTest extends ProxyTemplateTest {
        public Apache5ConnectorProviderProxyTest()
                throws NoSuchMethodException, InvocationTargetException, InstantiationException,
                IllegalAccessException {
            super(Apache5ConnectorProvider.class);
        }
    }

    public static class GrizzlyConnectorProviderProxyTest extends ProxyTemplateTest {
        public GrizzlyConnectorProviderProxyTest()
                throws NoSuchMethodException, InvocationTargetException, InstantiationException,
                IllegalAccessException {
            super(GrizzlyConnectorProvider.class);
        }
    }

    public static class JettyConnectorProviderProxyTest extends ProxyTemplateTest {
        public JettyConnectorProviderProxyTest()
                throws NoSuchMethodException, InvocationTargetException, InstantiationException,
                IllegalAccessException {
            super(JettyConnectorProvider.class);
        }
    }

    public static class NettyConnectorProviderProxyTest extends ProxyTemplateTest {
        public NettyConnectorProviderProxyTest()
                throws NoSuchMethodException, InvocationTargetException, InstantiationException,
                IllegalAccessException {
            super(NettyConnectorProvider.class);
        }
    }

    public static class HttpUrlConnectorProviderProxyTest extends ProxyTemplateTest {
        public HttpUrlConnectorProviderProxyTest()
                throws NoSuchMethodException, InvocationTargetException, InstantiationException,
                IllegalAccessException {
            super(HttpUrlConnectorProvider.class);
        }
    }

    public abstract static class ProxyTemplateTest {
        private final ConnectorProvider connectorProvider;

        public ProxyTemplateTest(Class<? extends ConnectorProvider> connectorProviderClass)
                throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            this.connectorProvider = connectorProviderClass.getConstructor().newInstance();
        }


        protected void configureClient(ClientConfig config) {
            config.connectorProvider(connectorProvider);
        }

        @Test
        public void testGetNoPass() {
            client().property(ClientProperties.PROXY_URI, ProxyTest.PROXY_URI);
            try (Response response = target("proxyTest").request().header(PROXY_NO_PASS, 200).get()) {
                assertEquals(200, response.getStatus());
            }
        }

        @Test
        public void testGet407() {
            // Grizzly sends (String)null password and username
            int expected = GrizzlyConnectorProvider.class.isInstance(connectorProvider) ? 400 : 407;
            client().property(ClientProperties.PROXY_URI, ProxyTest.PROXY_URI);
            try (Response response = target("proxyTest").request().get()) {
                assertEquals(expected, response.getStatus());
            } catch (ProcessingException pe) {
                Assertions.assertTrue(pe.getMessage().contains("407")); // netty
            }
        }

        @Test
        public void testGetSuccess() {
            client().property(ClientProperties.PROXY_URI, ProxyTest.PROXY_URI);
            client().property(ClientProperties.PROXY_USERNAME, ProxyTest.PROXY_USERNAME);
            client().property(ClientProperties.PROXY_PASSWORD, ProxyTest.PROXY_PASSWORD);
            Response response = target("proxyTest").request().get();
            response.bufferEntity();
            assertEquals(200, response.getStatus(), response.readEntity(String.class));
        }

        private static Server server;
        @BeforeAll
        public static void startFakeProxy() {
            server = new Server(9997);
            server.setHandler(new ProxyHandler());
            try {
                server.start();
            } catch (Exception e) {

            }
        }

        @AfterAll
        public static void tearDownProxy() {
            try {
                server.stop();
            } catch (Exception e) {

            }
        }

        private static Client client;
        @BeforeEach
        public void beforeEach() {
            ClientConfig config = new ClientConfig();
            this.configureClient(config);
            client = ClientBuilder.newClient(config);
        }

        private Client client() {
            return client;
        }

        private WebTarget target(String path) {
            return client().target("http://localhost:9998").path(path);
        }
    }

    static class ProxyHandler extends AbstractHandler {
        Set<HttpChannel> httpConnect = new HashSet<>();
        @Override
        public void handle(String target,
                           Request baseRequest,
                           HttpServletRequest request,
                           HttpServletResponse response) {
            if (request.getHeader(PROXY_NO_PASS) != null) {
                response.setStatus(Integer.parseInt(request.getHeader(PROXY_NO_PASS)));
            } else if (request.getHeader("Proxy-Authorization") != null) {
                String proxyAuthorization = request.getHeader("Proxy-Authorization");
                String decoded = new String(Base64.getDecoder().decode(proxyAuthorization.substring(6).getBytes()),
                        CHARACTER_SET);
                final String[] split = decoded.split(":");
                final String username = split[0];
                final String password = split[1];

                if (!username.equals(PROXY_USERNAME)) {
                    response.setStatus(400);
                    System.out.println("Found unexpected username: " + username);
                }

                if (!password.equals(PROXY_PASSWORD)) {
                    response.setStatus(400);
                    System.out.println("Found unexpected password: " + username);
                }

                if (response.getStatus() != 400) {
                    response.setStatus(200);
                    if ("CONNECT".equalsIgnoreCase(baseRequest.getMethod())) { // NETTY way of doing proxy
                        httpConnect.add(baseRequest.getHttpChannel());
                    }
                }
                //TODO Add redirect to requestURI
            } else {
                if (httpConnect.contains(baseRequest.getHttpChannel())) {
                    response.setStatus(200);
                } else {
                    response.setStatus(407);
                    response.addHeader("Proxy-Authenticate", "Basic");
                }
            }

            baseRequest.setHandled(true);
        }
    }
}