All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.websocket.client.WebSocketUpgradeRequest Maven / Gradle / Ivy

There is a newer version: 9.4.56.v20240826
Show newest version
//
//  ========================================================================
//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.websocket.client;

import java.net.HttpCookie;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeoutException;

import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpConversation;
import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Response.CompleteListener;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.http.HttpConnectionOverHTTP;
import org.eclipse.jetty.client.http.HttpConnectionUpgrader;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.UpgradeException;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.WebSocketConstants;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
import org.eclipse.jetty.websocket.client.io.UpgradeListener;
import org.eclipse.jetty.websocket.client.io.WebSocketClientConnection;
import org.eclipse.jetty.websocket.common.AcceptHash;
import org.eclipse.jetty.websocket.common.SessionFactory;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.events.EventDriver;
import org.eclipse.jetty.websocket.common.extensions.ExtensionStack;

public class WebSocketUpgradeRequest extends HttpRequest implements CompleteListener, HttpConnectionUpgrader
{
    private static final Logger LOG = Log.getLogger(WebSocketUpgradeRequest.class);

    private class ClientUpgradeRequestFacade implements UpgradeRequest
    {
        private List extensions;
        private List subProtocols;
        private Object session;

        public ClientUpgradeRequestFacade()
        {
            this.extensions = new ArrayList<>();
            this.subProtocols = new ArrayList<>();
        }

        public void init(ClientUpgradeRequest request)
        {
            this.extensions = new ArrayList<>(request.getExtensions());
            this.subProtocols = new ArrayList<>(request.getSubProtocols());

            request.getHeaders().forEach((name, values) ->
                values.forEach((value) -> header(name, value))
            );

            for (HttpCookie cookie : request.getCookies())
            {
                cookie(cookie);
            }
        }

        @Override
        public List getExtensions()
        {
            return extensions;
        }

        @Override
        public List getSubProtocols()
        {
            return subProtocols;
        }

        @Override
        public void addExtensions(ExtensionConfig... configs)
        {
            for (ExtensionConfig config : configs)
            {
                this.extensions.add(config);
            }
            updateExtensionHeader();
        }

        @Override
        public void addExtensions(String... configs)
        {
            this.extensions.addAll(ExtensionConfig.parseList(configs));
            updateExtensionHeader();
        }

        @Override
        public void clearHeaders()
        {
            throw new UnsupportedOperationException("Clearing all headers breaks WebSocket upgrade");
        }

        @Override
        public String getHeader(String name)
        {
            return getHttpFields().get(name);
        }

        @Override
        public int getHeaderInt(String name)
        {
            String value = getHttpFields().get(name);
            if (value == null)
            {
                return -1;
            }
            return Integer.parseInt(value);
        }

        @Override
        public List getHeaders(String name)
        {
            return getHttpFields().getValuesList(name);
        }

        @Override
        public String getHttpVersion()
        {
            return getVersion().asString();
        }

        @Override
        public String getOrigin()
        {
            return getHttpFields().get(HttpHeader.ORIGIN);
        }

        @Override
        public Map> getParameterMap()
        {
            Map> paramMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

            String query = getQueryString();
            MultiMap multimap = new MultiMap<>();
            UrlEncoded.decodeTo(query, multimap, StandardCharsets.UTF_8);

            paramMap.putAll(multimap);

            return paramMap;
        }

        @Override
        public String getProtocolVersion()
        {
            String ver = getHttpFields().get(HttpHeader.SEC_WEBSOCKET_VERSION);
            if (ver == null)
            {
                return Integer.toString(WebSocketConstants.SPEC_VERSION);
            }
            return ver;
        }

        @Override
        public String getQueryString()
        {
            return getURI().getQuery();
        }

        @Override
        public URI getRequestURI()
        {
            return getURI();
        }

        @Override
        public Object getSession()
        {
            return this.session;
        }

        @Override
        public Principal getUserPrincipal()
        {
            // HttpClient doesn't use Principal concepts
            return null;
        }

        @Override
        public boolean hasSubProtocol(String test)
        {
            return getSubProtocols().contains(test);
        }

        @Override
        public boolean isOrigin(String test)
        {
            return test.equalsIgnoreCase(getOrigin());
        }

        @Override
        public boolean isSecure()
        {
            // TODO: need to obtain information from actual request to know of SSL was used?
            return "wss".equalsIgnoreCase(getURI().getScheme());
        }

        @Override
        public void setCookies(List cookies)
        {
            for (HttpCookie cookie : cookies)
            {
                cookie(cookie);
            }
        }

        @Override
        public void setExtensions(List configs)
        {
            this.extensions = configs;
            updateExtensionHeader();
        }

        private void updateExtensionHeader()
        {
            HttpFields headers = getHttpFields();
            headers.remove(HttpHeader.SEC_WEBSOCKET_EXTENSIONS);
            for (ExtensionConfig config : extensions)
            {
                headers.add(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, config.getParameterizedName());
            }
        }

        @Override
        public void setHeader(String name, List values)
        {
            getHttpFields().put(name, values);
        }

        @Override
        public void setHeader(String name, String value)
        {
            getHttpFields().put(name, value);
        }

        @Override
        public void setHeaders(Map> headers)
        {
            for (Map.Entry> entry : headers.entrySet())
            {
                getHttpFields().put(entry.getKey(), entry.getValue());
            }
        }

        @Override
        public void setHttpVersion(String httpVersion)
        {
            version(HttpVersion.fromString(httpVersion));
        }

        @Override
        public void setMethod(String method)
        {
            method(method);
        }

        @Override
        public void setRequestURI(URI uri)
        {
            throw new UnsupportedOperationException("Cannot reset/change RequestURI");
        }

        @Override
        public void setSession(Object session)
        {
            this.session = session;
        }

        @Override
        public void setSubProtocols(List protocols)
        {
            this.subProtocols = protocols;
        }

        @Override
        public void setSubProtocols(String... protocols)
        {
            this.subProtocols.clear();
            this.subProtocols.addAll(Arrays.asList(protocols));
        }

        @Override
        public List getCookies()
        {
            return WebSocketUpgradeRequest.this.getCookies();
        }

        @Override
        public Map> getHeaders()
        {
            Map> headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            HttpFields fields = getHttpFields();
            for (String name : fields.getFieldNamesCollection())
            {
                headersMap.put(name, fields.getValuesList(name));
            }
            return headersMap;
        }

        @Override
        public String getHost()
        {
            return WebSocketUpgradeRequest.this.getHost();
        }

        @Override
        public String getMethod()
        {
            return WebSocketUpgradeRequest.this.getMethod();
        }
    }

    private final WebSocketClient wsClient;
    private final EventDriver localEndpoint;
    private final CompletableFuture fut;
    /**
     * WebSocket API UpgradeRequest Facade to HttpClient HttpRequest
     */
    private final ClientUpgradeRequestFacade apiRequestFacade;
    private UpgradeListener upgradeListener;

    /**
     * Exists for internal use of HttpClient by WebSocketClient.
     * 

* Maintained for Backward compatibility and also for JSR356 WebSocket ClientContainer use. * * @param wsClient the WebSocketClient that this request uses * @param httpClient the HttpClient that this request uses * @param request the ClientUpgradeRequest (backward compat) to base this request from */ protected WebSocketUpgradeRequest(WebSocketClient wsClient, HttpClient httpClient, ClientUpgradeRequest request) { this(wsClient, httpClient, request.getRequestURI(), request.getLocalEndpoint()); apiRequestFacade.init(request); } /** * Initiating a WebSocket Upgrade using HTTP/1.1 * * @param wsClient the WebSocketClient that this request uses * @param httpClient the HttpClient that this request uses * @param localEndpoint the local endpoint (following Jetty WebSocket Client API rules) to use for incoming * WebSocket events * @param wsURI the WebSocket URI to connect to */ public WebSocketUpgradeRequest(WebSocketClient wsClient, HttpClient httpClient, URI wsURI, Object localEndpoint) { super(httpClient, new HttpConversation(), wsURI); apiRequestFacade = new ClientUpgradeRequestFacade(); if (!wsURI.isAbsolute()) { throw new IllegalArgumentException("WebSocket URI must be an absolute URI: " + wsURI); } String scheme = wsURI.getScheme(); if (scheme == null || !(scheme.equalsIgnoreCase("ws") || scheme.equalsIgnoreCase("wss"))) { throw new IllegalArgumentException("WebSocket URI must use 'ws' or 'wss' scheme: " + wsURI); } this.wsClient = wsClient; try { if (!this.wsClient.isRunning()) { this.wsClient.start(); } } catch (Exception e) { throw new IllegalStateException("Unable to start WebSocketClient", e); } this.localEndpoint = this.wsClient.getEventDriverFactory().wrap(localEndpoint); this.fut = new CompletableFuture<>(); this.fut.whenComplete((session, throwable) -> { if (throwable != null) abort(throwable); }); getConversation().setAttribute(HttpConnectionUpgrader.class.getName(), this); } private String genRandomKey() { byte[] bytes = new byte[16]; ThreadLocalRandom.current().nextBytes(bytes); return Base64.getEncoder().encodeToString(bytes); } private ExtensionFactory getExtensionFactory() { return this.wsClient.getExtensionFactory(); } private SessionFactory getSessionFactory() { return this.wsClient.getSessionFactory(); } private void initWebSocketHeaders() { method(HttpMethod.GET); version(HttpVersion.HTTP_1_1); // The Upgrade Headers header(HttpHeader.UPGRADE, "websocket"); header(HttpHeader.CONNECTION, "Upgrade"); // The WebSocket Headers header(HttpHeader.SEC_WEBSOCKET_KEY, genRandomKey()); header(HttpHeader.SEC_WEBSOCKET_VERSION, "13"); // (Per the hybi list): Add no-cache headers to avoid compatibility issue. // There are some proxies that rewrite "Connection: upgrade" // to "Connection: close" in the response if a request doesn't contain // these headers. header(HttpHeader.PRAGMA, "no-cache"); header(HttpHeader.CACHE_CONTROL, "no-cache"); // handle "Sec-WebSocket-Extensions" if (!apiRequestFacade.getExtensions().isEmpty()) { for (ExtensionConfig ext : apiRequestFacade.getExtensions()) { header(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, ext.getParameterizedName()); } } // handle "Sec-WebSocket-Protocol" if (!apiRequestFacade.getSubProtocols().isEmpty()) { for (String protocol : apiRequestFacade.getSubProtocols()) { header(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, protocol); } } if (upgradeListener != null) { upgradeListener.onHandshakeRequest(apiRequestFacade); } } @Override public void onComplete(Result result) { if (LOG.isDebugEnabled()) { LOG.debug("onComplete() - {}", result); } URI requestURI = result.getRequest().getURI(); Response response = result.getResponse(); int responseStatusCode = response.getStatus(); String responseLine = responseStatusCode + " " + response.getReason(); if (result.isFailed()) { if (LOG.isDebugEnabled()) { if (result.getFailure() != null) LOG.debug("General Failure", result.getFailure()); if (result.getRequestFailure() != null) LOG.debug("Request Failure", result.getRequestFailure()); if (result.getResponseFailure() != null) LOG.debug("Response Failure", result.getResponseFailure()); } Throwable failure = result.getFailure(); if ((failure instanceof java.io.IOException) || (failure instanceof UpgradeException)) { // handle as-is handleException(failure); } else { // wrap in UpgradeException handleException(new UpgradeException(requestURI, responseStatusCode, responseLine, failure)); } return; } if (responseStatusCode != HttpStatus.SWITCHING_PROTOCOLS_101) { // Failed to upgrade (other reason) handleException(new UpgradeException(requestURI, responseStatusCode, "Failed to upgrade to websocket: Unexpected HTTP Response Status Code: " + responseLine)); } } private void handleException(Throwable failure) { try { localEndpoint.onError(failure); } catch (Throwable t) { LOG.warn("Exception while notifying onError", t); } fut.completeExceptionally(failure); } @Override public ContentResponse send() throws InterruptedException, TimeoutException, ExecutionException { throw new RuntimeException("Working with raw ContentResponse is invalid for WebSocket"); } @Override public void send(final CompleteListener listener) { initWebSocketHeaders(); super.send(listener); } public CompletableFuture sendAsync() { send(this); return fut; } @Override public void upgrade(HttpResponse response, HttpConnectionOverHTTP oldConn) { if (!this.getHeaders().get(HttpHeader.UPGRADE).equalsIgnoreCase("websocket")) { // Not my upgrade throw new HttpResponseException("Not WebSocket Upgrade", response); } // Check the Accept hash String reqKey = this.getHeaders().get(HttpHeader.SEC_WEBSOCKET_KEY); String expectedHash = AcceptHash.hashKey(reqKey); String respHash = response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_ACCEPT); if (!expectedHash.equalsIgnoreCase(respHash)) { throw new HttpResponseException("Invalid Sec-WebSocket-Accept hash", response); } // We can upgrade EndPoint endp = oldConn.getEndPoint(); WebSocketClientConnection connection = new WebSocketClientConnection(endp, wsClient.getExecutor(), wsClient.getScheduler(), localEndpoint.getPolicy(), wsClient.getBufferPool()); Collection connectionListeners = wsClient.getBeans(Connection.Listener.class); if (connectionListeners != null) { connectionListeners.forEach((listener) -> { if (!(listener instanceof WebSocketSession)) connection.addListener(listener); }); } URI requestURI = this.getURI(); ClientUpgradeResponse upgradeResponse = new ClientUpgradeResponse(response); WebSocketSession session = getSessionFactory().createSession(requestURI, localEndpoint, connection); session.setUpgradeRequest(new ClientUpgradeRequest(this)); session.setUpgradeResponse(upgradeResponse); connection.addListener(session); List extensions = new ArrayList<>(); HttpField extField = response.getHeaders().getField(HttpHeader.SEC_WEBSOCKET_EXTENSIONS); if (extField != null) { String[] extValues = extField.getValues(); if (extValues != null) { for (String extVal : extValues) { QuotedStringTokenizer tok = new QuotedStringTokenizer(extVal, ","); while (tok.hasMoreTokens()) { extensions.add(ExtensionConfig.parse(tok.nextToken())); } } } } ExtensionStack extensionStack = new ExtensionStack(getExtensionFactory()); extensionStack.negotiate(extensions); extensionStack.configure(connection.getParser()); extensionStack.configure(connection.getGenerator()); // Setup Incoming Routing connection.setNextIncomingFrames(extensionStack); extensionStack.setNextIncoming(session); // Setup Outgoing Routing session.setOutgoingHandler(extensionStack); extensionStack.setNextOutgoing(connection); session.addManaged(extensionStack); session.setFuture(fut); if (upgradeListener != null) { upgradeListener.onHandshakeResponse(upgradeResponse); } // Now swap out the connection endp.upgrade(connection); } public void setUpgradeListener(UpgradeListener upgradeListener) { this.upgradeListener = upgradeListener; } private HttpFields getHttpFields() { return super.getHeaders(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy