mock-server / mockserver

MockServer enables easy mocking of any system you integrate with via HTTP or HTTPS with clients written in Java, JavaScript and Ruby. MockServer also includes a proxy that introspects all proxied traffic including encrypted SSL traffic and supports Port Forwarding, Web Proxying (i.e. HTTP proxy), HTTPS Tunneling Proxying (using HTTP CONNECT) and SOCKS Proxying (i.e. dynamic port forwarding).
http://mock-server.com
Apache License 2.0
4.61k stars 1.08k forks source link

Authentication Proxy seems to not work with HttpURLConnection #1142

Closed tiboun closed 2 years ago

tiboun commented 2 years ago

Describe the issue Setting up a proxy with required authentication seems to be ignored when using HttpURLConnection. I should probably missing something.

What you are trying to do Trying to test corporate proxy with authentication requirement in order to have a good integration test.

MockServer version 5.11.1

To Reproduce

  @Test
  public void shouldNotConnectToSecurePortRequiringAuthenticationWithHttpURLConnection() throws Exception {
    int mockServerPort = new MockServer().getLocalPort();
    String existingUsername = ConfigurationProperties.proxyAuthenticationUsername();
    String existingPassword = ConfigurationProperties.proxyAuthenticationPassword();
    ClientAndServer mockServer = ClientAndServer.startClientAndServer();
    try {
      String username = UUIDService.getUUID();
      String password = UUIDService.getUUID();
      ConfigurationProperties.proxyAuthenticationUsername(username);
      ConfigurationProperties.proxyAuthenticationPassword(password);

      try (Socket socket = new Socket("127.0.0.1", mockServerPort)) {
        // given
        OutputStream output = socket.getOutputStream();

        // when
        output.write(("" +
                "CONNECT 127.0.0.1:443 HTTP/1.1\r\n" +
                "Host: 127.0.0.1:" + mockServer.getPort() + "\r\n" +
                "\r\n"
        ).getBytes(UTF_8));
        output.flush();

        // then
        System.out.println(IOStreamUtils.readInputStreamToString(socket));
        mockServer.when(
                request().withPath("/subjects")
        ).respond(
                response().withHeaders(
                        new Header(CONTENT_TYPE.toString(), MediaType.APPLICATION_JSON.toString())
                ).withBody(
                        "[\"abc\"]"
                )
        );
        System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
        System.setProperty("jdk.http.auth.proxying.disabledSchemes", "");
        Authenticator.setDefault(new Authenticator() {
          @Override
          protected PasswordAuthentication getPasswordAuthentication() {
            System.out.println(getRequestorType().toString());
            if (getRequestorType() == RequestorType.PROXY) {
              System.out.println("return password authentication");
              String randomPassword = UUIDService.getUUID();
                return new PasswordAuthentication(username, randomPassword.toCharArray());
            }
            return null;
          }
        });
        URL url = new URL("http://localhost:" + mockServer.getPort() + "/subjects");
        HttpURLConnection con = (HttpURLConnection) url.openConnection(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", mockServerPort)));
        System.out.println(con.getResponseCode());
      }
    } finally {
      ConfigurationProperties.proxyAuthenticationUsername(existingUsername);
      ConfigurationProperties.proxyAuthenticationPassword(existingPassword);
    }
  }

Expected behaviour I expect the request to use the Authenticator defined.

jamesdbloom commented 2 years ago

The problem is that you are sending a request using HTTP and not HTTPS so proxy authentication doesn't kick in. When you send a request using HTTP the JDK client just re-writes the target socket and does nothing else. In effect the proxying is done client side and so there is no way for MockServer to authenticate that. As soon as you switch the protocol to HTTPS as shown below the authentication works as expected. Also the initial CONNECT request shows the expected response you get back from MockServer for a CONNECT request. As soon as the protocol is switched to HTTPS that the Java JDK sends the CONNECT request and gets the expected authentication error i.e. 407.

@Test
public void shouldNotConnectToSecurePortRequiringAuthenticationWithHttpURLConnection() throws Exception {
    int mockServerPort = new MockServer().getLocalPort();
    String existingUsername = ConfigurationProperties.proxyAuthenticationUsername();
    String existingPassword = ConfigurationProperties.proxyAuthenticationPassword();
    ClientAndServer mockServer = ClientAndServer.startClientAndServer();
    try {
        String username = UUIDService.getUUID();
        String password = UUIDService.getUUID();
        ConfigurationProperties.proxyAuthenticationUsername(username);
        ConfigurationProperties.proxyAuthenticationPassword(password);

        try (Socket socket = new Socket("127.0.0.1", mockServerPort)) {
            // given
            OutputStream output = socket.getOutputStream();

            // when
            output.write(("" +
                "CONNECT 127.0.0.1:443 HTTP/1.1\r\n" +
                "Host: 127.0.0.1:" + mockServer.getPort() + "\r\n" +
                "\r\n"
            ).getBytes(UTF_8));
            output.flush();

            // then
            System.out.println(IOStreamUtils.readInputStreamToString(socket));
            mockServer.when(
                request().withPath("/subjects")
            ).respond(
                response().withHeaders(
                    new Header(CONTENT_TYPE.toString(), MediaType.APPLICATION_JSON.toString())
                ).withBody(
                    "[\"abc\"]"
                )
            );
            System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
            System.setProperty("jdk.http.auth.proxying.disabledSchemes", "");
            Authenticator.setDefault(new Authenticator() {
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    System.out.println(getRequestorType().toString());
                    if (getRequestorType() == RequestorType.PROXY) {
                        System.out.println("return password authentication");
                        String randomPassword = UUIDService.getUUID();
                        return new PasswordAuthentication(username, randomPassword.toCharArray());
                    }
                    return null;
                }
            });
            URL url = new URL("https://localhost:" + mockServer.getPort() + "/subjects");
            HttpURLConnection con = (HttpURLConnection) url.openConnection(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", mockServerPort)));
            System.out.println(con.getResponseCode());
        }
    } finally {
        ConfigurationProperties.proxyAuthenticationUsername(existingUsername);
        ConfigurationProperties.proxyAuthenticationPassword(existingPassword);
    }
}

The only change I made was to update the protocol as follows, changing:

new URL("http://localhost:" + mockServer.getPort() + "/subjects");

to:

new URL("https://localhost:" + mockServer.getPort() + "/subjects");
tiboun commented 2 years ago

Hi jamesdbloom,

Thanks for you answer. Do you mean that proxy authentication is only available for https or did I misunderstood ?

As far as I know, we can require authentication on proxy wether it's http or https.

jamesdbloom commented 2 years ago

Yes MockServer only supports authentication for HTTPS or SOCKS, the main issue being how would MockServer know that a request was being proxied if it is HTTP only because that request could equally be directed at MockServer as a target server not just a proxy.

I am however considering adding authentication in general to MockServer, it also might be a good idea to provide a configuration setting to make MockServer assume first it is a proxy by proxying any request that doesn't match an expectation (instead of returning a 404) in that case the proxy authentication could be enabled.

I'm going to re-open this ticket to add that configuration property which would mean when enabled it was possible to authenticate HTTP proxied requests.

jamesdbloom commented 2 years ago

I have released it is possible to detect accurately when MockServer is proxying an HTTP request so I have added support for authentication which will be shortly pushed to close this ticket.

tiboun commented 2 years ago

Thank you very much jamesdbloom ! You're awesome !

kavithareddyedula commented 6 months ago

0

this code snippet worked for me .. thank you for the help

import java.net.*

System.setProperty("http.proxyUser", "userid"); System.setProperty("http.proxyPassword", "mmmmm"); System.setProperty("jdk.http.auth.tunneling.disabledSchemes", ""); val url = URL(uploadUrl) val proxyInet = InetSocketAddress(proxySettings.url, Utility.getPort()) val proxy = Proxy(Proxy.Type.HTTP, proxyInet) var uname_pwd = "userid"+":"+"mmmmm" val authString:String = "Basic " +Base64.getEncoder().encodeToString(uname_pwd.toByteArray()) val authenticator = object : Authenticator() { override fun getPasswordAuthentication(): PasswordAuthentication { return PasswordAuthentication("userid", "mmmmmmm".toCharArray()) } } Authenticator.setDefault(authenticator) val connection = url.openConnection(proxy) as HttpsURLConnection