Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8217429

WebSocket over authenticating proxy fails to send Upgrade headers

    XMLWordPrintable

Details

    • b06
    • generic
    • generic

    Backports

      Description

        A DESCRIPTION OF THE PROBLEM :
        HTTP Client with a custom provided Authenticator for a websocket connection will connect against a basic auth secured endpoint. The first unauthorized request will contain the header "Connection: upgrade".The endpoint will return HTTP code 401, to trigger an authorization on the client.

        The client will calculate the "Authorization" header and redo the first request, but without the "Connection: upgrade" header.

        The reason for this is that, when the client works the response (401) in AuthenticationFilter response(...) (237) the new request (req) will be created from the old one with:
        req = HttpRequestImpl.newInstanceForAuthentication(req);
        addBasicCredentials(req, proxy, pw);
        return req;

        The newInstanceForAuthentication() calls:
        private HttpRequestImpl(URI uri, String method, HttpRequestImpl other) which does not copy system headers.

        Instead of the desired behavior seen in:
        public HttpRequestImpl(HttpRequest request, ProxySelector ps) which for Websocket connections copy the systemHeaders.

        REGRESSION : Last worked in version 11.0.1

        STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
        websocket server with Basic Auth
        HTTP Client trying to connect to that server with Authenticator configured

        EXPECTED VERSUS ACTUAL BEHAVIOR :
        EXPECTED -
        Both request headers (without and with Authorization) contain "Connection: Upgrade" and the connection can be successfully set up.
        ACTUAL -
        Second client request header does not contain "Connection: Upgrade" after first request.

        ---------- BEGIN SOURCE ----------
        import java.io.IOException;
        import java.net.Authenticator;
        import java.net.PasswordAuthentication;
        import java.net.URI;
        import java.net.http.HttpClient;
        import java.net.http.HttpConnectTimeoutException;
        import java.net.http.WebSocket;
        import java.nio.ByteBuffer;
        import java.util.Base64;
        import java.util.Collection;
        import java.util.List;
        import java.util.concurrent.CompletionStage;
        import java.util.concurrent.Executor;
        import java.util.concurrent.Future;

        import javax.servlet.ServletException;
        import javax.servlet.http.HttpServlet;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;

        import org.bouncycastle.util.encoders.Base64Encoder;
        import org.eclipse.jetty.io.ByteBufferPool;
        import org.eclipse.jetty.io.EndPoint;
        import org.eclipse.jetty.security.ConstraintMapping;
        import org.eclipse.jetty.security.ConstraintSecurityHandler;
        import org.eclipse.jetty.security.HashLoginService;
        import org.eclipse.jetty.security.RoleInfo;
        import org.eclipse.jetty.security.SecurityHandler;
        import org.eclipse.jetty.security.UserStore;
        import org.eclipse.jetty.security.authentication.BasicAuthenticator;
        import org.eclipse.jetty.server.ConnectionFactory;
        import org.eclipse.jetty.server.Connector;
        import org.eclipse.jetty.server.HttpConfiguration;
        import org.eclipse.jetty.server.Request;
        import org.eclipse.jetty.server.Response;
        import org.eclipse.jetty.server.Server;
        import org.eclipse.jetty.server.ServerConnector;
        import org.eclipse.jetty.server.UserIdentity;
        import org.eclipse.jetty.servlet.ServletContextHandler;
        import org.eclipse.jetty.servlet.ServletHolder;
        import org.eclipse.jetty.util.component.Container;
        import org.eclipse.jetty.util.component.LifeCycle;
        import org.eclipse.jetty.util.log.Log;
        import org.eclipse.jetty.util.log.StdErrLog;
        import org.eclipse.jetty.util.security.Constraint;
        import org.eclipse.jetty.util.security.Credential;
        import org.eclipse.jetty.util.thread.Scheduler;
        import org.eclipse.jetty.websocket.api.Session;
        import org.eclipse.jetty.websocket.api.WebSocketAdapter;
        import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
        import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
        import org.junit.After;
        import org.junit.Before;
        import org.junit.Test;

        public class WebSocketAuthenticationTest extends WebSocketAdapter {
            public class TestServlet extends WebSocketServlet {
                @Override
                public void configure(WebSocketServletFactory factory) {
                    factory.register(WebSocketAuthenticationTest.class);
                }
            }

            @Override
            public void onWebSocketConnect(Session sess) {
                super.onWebSocketConnect(sess);
            }

            private String username = "MyUserName";
            private String password = "MyPassword";

            private Server server;

            private WebSocket.Listener websocketListener = new WebSocket.Listener() {
                @Override
                public void onOpen(WebSocket webSocket) {
                }

                @Override
                public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
                    return null;
                }

                @Override
                public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
                    return null;
                }

                @Override
                public CompletionStage<?> onPing(WebSocket webSocket, ByteBuffer message) {
                    return null;
                }

                @Override
                public CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message) {
                    return null;
                }

                @Override
                public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
                    return null;
                }

                @Override
                public void onError(WebSocket webSocket, Throwable error) {
                }
            };

            @Before
            public void setUp() throws Exception {
                StdErrLog logger = new StdErrLog();
                logger.setDebugEnabled(true);
                Log.setLog(logger);

                server = new Server();
                ServerConnector connector = new ServerConnector(server);
                connector.setPort(8080);
                server.addConnector(connector);
                ServletContextHandler ctx = new ServletContextHandler(ServletContextHandler.SESSIONS);
                ctx.setContextPath("/");
                ctx.addServlet(new ServletHolder("ws-events", new TestServlet()), "/endpoint/*");
                ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
                HashLoginService loginService = new HashLoginService();
                UserStore us = new UserStore();
                us.addUser(username, Credential.getCredential(password), new String[]{"user"});
                Constraint constraint = new Constraint();
                constraint.setName(Constraint.__BASIC_AUTH);
                constraint.setRoles(new String[]{"user"});
                constraint.setAuthenticate(true);
                loginService.setUserStore(us);
                securityHandler.setLoginService(loginService);
                ConstraintMapping constraintMapping = new ConstraintMapping();
                constraintMapping.setConstraint(constraint);
                constraintMapping.setPathSpec("/endpoint/*");
                securityHandler.addConstraintMapping(constraintMapping);
                ctx.setSecurityHandler(securityHandler);
                server.setHandler(ctx);
                server.start();
            }

            @After
            public void cleanUp() throws Exception {
                server.stop();
            }

            @Test
            public void connectToWithAuthenticatorServer() throws Exception {
                final HttpClient httpClient = HttpClient.newBuilder().authenticator(new Authenticator() {
                    @Override
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication(username, password.toCharArray());
                    }
                }).build();
                httpClient.newWebSocketBuilder().buildAsync(new URI("ws://localhost:8080/endpoint/"), websocketListener).get();
            }

            @Test
            public void connectToWithHeaderServer() throws Exception {
                final HttpClient httpClient = HttpClient.newBuilder().build();
                httpClient.newWebSocketBuilder().header("Authorization", "Basic " + Base64.getEncoder().encodeToString((username+ ":" + password).getBytes()))
                        .buildAsync(new URI("ws://localhost:8080/endpoint/"), websocketListener).get();
            }
        }

        ---------- END SOURCE ----------

        CUSTOMER SUBMITTED WORKAROUND :
        Set the Authorization header explicitly before doing the request.

        FREQUENCY : always


        Attachments

          1. JI-9059016.zip
            5.56 MB
          2. out1.log
            217 kB

          Issue Links

            Activity

              People

                chegar Chris Hegarty
                webbuggrp Webbug Group
                Votes:
                0 Vote for this issue
                Watchers:
                5 Start watching this issue

                Dates

                  Created:
                  Updated:
                  Resolved: