diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/Negotiation.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/Negotiation.java index 7db266a0d24..5ad5283d924 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/Negotiation.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/Negotiation.java @@ -36,146 +36,25 @@ import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.internal.ExtensionStack; -public class Negotiation +public abstract class Negotiation { private final Request baseRequest; private final HttpServletRequest request; private final HttpServletResponse response; - private final List offeredExtensions; - private final List offeredSubprotocols; private final WebSocketComponents components; - private final String version; - private final Boolean upgrade; - private final String key; - + private String version; + private List offeredExtensions; private List negotiatedExtensions; - private String subprotocol; + private List offeredProtocols; private ExtensionStack extensionStack; + private String protocol; - /** - * @throws BadMessageException if there is any errors parsing the upgrade request - */ - public Negotiation( - Request baseRequest, - HttpServletRequest request, - HttpServletResponse response, - WebSocketComponents components) throws BadMessageException + public Negotiation(Request baseRequest, HttpServletRequest request, HttpServletResponse response, WebSocketComponents webSocketComponents) { this.baseRequest = baseRequest; this.request = request; this.response = response; - this.components = components; - - Boolean upgrade = null; - String key = null; - String version = null; - QuotedCSV connectionCSVs = null; - QuotedCSV extensions = null; - QuotedCSV subprotocols = null; - - try - { - for (HttpField field : baseRequest.getHttpFields()) - { - if (field.getHeader() != null) - { - switch (field.getHeader()) - { - case UPGRADE: - if (upgrade == null && "websocket".equalsIgnoreCase(field.getValue())) - upgrade = Boolean.TRUE; - break; - - case CONNECTION: - if (connectionCSVs == null) - connectionCSVs = new QuotedCSV(); - connectionCSVs.addValue(field.getValue()); - break; - - case SEC_WEBSOCKET_KEY: - key = field.getValue(); - break; - - case SEC_WEBSOCKET_VERSION: - version = field.getValue(); - break; - - case SEC_WEBSOCKET_EXTENSIONS: - if (extensions == null) - extensions = new QuotedCSV(field.getValue()); - else - extensions.addValue(field.getValue()); - break; - - case SEC_WEBSOCKET_SUBPROTOCOL: - if (subprotocols == null) - subprotocols = new QuotedCSV(field.getValue()); - else - subprotocols.addValue(field.getValue()); - break; - - default: - } - } - } - - this.version = version; - this.key = key; - this.upgrade = upgrade != null && connectionCSVs != null && connectionCSVs.getValues().stream().anyMatch(s -> s.equalsIgnoreCase("Upgrade")); - - Set available = components.getExtensionRegistry().getAvailableExtensionNames(); - offeredExtensions = extensions == null - ? Collections.emptyList() - : extensions.getValues().stream() - .map(ExtensionConfig::parse) - .filter(ec -> available.contains(ec.getName().toLowerCase()) && !ec.getName().startsWith("@")) - .collect(Collectors.toList()); - - offeredSubprotocols = subprotocols == null - ? Collections.emptyList() - : subprotocols.getValues(); - - negotiatedExtensions = new ArrayList<>(); - for (ExtensionConfig config : offeredExtensions) - { - long matches = negotiatedExtensions.stream() - .filter(negotiatedConfig -> negotiatedConfig.getName().equals(config.getName())).count(); - if (matches == 0) - negotiatedExtensions.add(config); - } - } - catch (Throwable t) - { - throw new BadMessageException("Invalid Handshake Request", t); - } - } - - public String getKey() - { - return key; - } - - public List getOfferedExtensions() - { - return offeredExtensions; - } - - public void setNegotiatedExtensions(List extensions) - { - if (extensions == offeredExtensions) - return; - negotiatedExtensions = extensions == null ? null : new ArrayList<>(extensions); - extensionStack = null; - } - - public List getNegotiatedExtensions() - { - return negotiatedExtensions; - } - - public List getOfferedSubprotocols() - { - return offeredSubprotocols; + this.components = webSocketComponents; } public Request getBaseRequest() @@ -193,25 +72,113 @@ public class Negotiation return response; } - public void setSubprotocol(String subprotocol) + public void negotiate() throws BadMessageException { - this.subprotocol = subprotocol; - response.setHeader(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL.asString(), subprotocol); + try + { + negotiateHeaders(getBaseRequest()); + } + catch (Throwable x) + { + throw new BadMessageException("Invalid upgrade request", x); + } } - public String getSubprotocol() + protected void negotiateHeaders(Request baseRequest) { - return subprotocol; + QuotedCSV extensions = null; + QuotedCSV protocols = null; + for (HttpField field : baseRequest.getHttpFields()) + { + if (field.getHeader() != null) + { + switch (field.getHeader()) + { + case SEC_WEBSOCKET_VERSION: + version = field.getValue(); + break; + + case SEC_WEBSOCKET_EXTENSIONS: + if (extensions == null) + extensions = new QuotedCSV(field.getValue()); + else + extensions.addValue(field.getValue()); + break; + + case SEC_WEBSOCKET_SUBPROTOCOL: + if (protocols == null) + protocols = new QuotedCSV(field.getValue()); + else + protocols.addValue(field.getValue()); + break; + + default: + break; + } + } + } + + Set available = components.getExtensionRegistry().getAvailableExtensionNames(); + offeredExtensions = extensions == null + ? Collections.emptyList() + : extensions.getValues().stream() + .map(ExtensionConfig::parse) + .filter(ec -> available.contains(ec.getName().toLowerCase()) && !ec.getName().startsWith("@")) + .collect(Collectors.toList()); + + offeredProtocols = protocols == null + ? Collections.emptyList() + : protocols.getValues(); + + negotiatedExtensions = new ArrayList<>(); + for (ExtensionConfig config : offeredExtensions) + { + long matches = negotiatedExtensions.stream() + .filter(negotiatedConfig -> negotiatedConfig.getName().equals(config.getName())).count(); + if (matches == 0) + negotiatedExtensions.add(config); + } } + public abstract boolean isSuccessful(); + public String getVersion() { return version; } - public boolean isUpgrade() + public String getSubprotocol() { - return upgrade; + return protocol; + } + + public void setSubprotocol(String protocol) + { + this.protocol = protocol; + response.setHeader(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL.asString(), protocol); + } + + public List getOfferedSubprotocols() + { + return offeredProtocols; + } + + public List getOfferedExtensions() + { + return offeredExtensions; + } + + public List getNegotiatedExtensions() + { + return negotiatedExtensions; + } + + public void setNegotiatedExtensions(List extensions) + { + if (extensions == offeredExtensions) + return; + negotiatedExtensions = extensions; + extensionStack = null; } public ExtensionStack getExtensionStack() @@ -229,14 +196,14 @@ public class Negotiation else baseRequest.getResponse().setHeader(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, null); } - return extensionStack; } @Override public String toString() { - return String.format("Negotiation@%x{uri=%s,oe=%s,op=%s}", + return String.format("%s@%x{uri=%s,oe=%s,op=%s}", + getClass().getSimpleName(), hashCode(), getRequest().getRequestURI(), getOfferedExtensions(), diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/AbstractHandshaker.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/AbstractHandshaker.java new file mode 100644 index 00000000000..b11080af73e --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/AbstractHandshaker.java @@ -0,0 +1,211 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// 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.core.server.internal; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.Executor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.PreEncodedHttpField; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpTransport; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Scheduler; +import org.eclipse.jetty.websocket.core.Behavior; +import org.eclipse.jetty.websocket.core.ExtensionConfig; +import org.eclipse.jetty.websocket.core.FrameHandler; +import org.eclipse.jetty.websocket.core.WebSocketComponents; +import org.eclipse.jetty.websocket.core.WebSocketConstants; +import org.eclipse.jetty.websocket.core.WebSocketException; +import org.eclipse.jetty.websocket.core.internal.ExtensionStack; +import org.eclipse.jetty.websocket.core.internal.Negotiated; +import org.eclipse.jetty.websocket.core.internal.WebSocketConnection; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; +import org.eclipse.jetty.websocket.core.server.Handshaker; +import org.eclipse.jetty.websocket.core.server.Negotiation; +import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; + +public abstract class AbstractHandshaker implements Handshaker +{ + protected static final Logger LOG = Log.getLogger(RFC8441Handshaker.class); + private static final HttpField SERVER_VERSION = new PreEncodedHttpField(HttpHeader.SERVER, HttpConfiguration.SERVER_VERSION); + + @Override + public boolean upgradeRequest(WebSocketNegotiator negotiator, HttpServletRequest request, HttpServletResponse response, FrameHandler.Customizer defaultCustomizer) throws IOException + { + if (!validateRequest(request)) + return false; + + Negotiation negotiation = newNegotiation(request, response, new WebSocketComponents()); + if (LOG.isDebugEnabled()) + LOG.debug("negotiation {}", negotiation); + negotiation.negotiate(); + + if (!validateNegotiation(negotiation)) + return false; + + // Negotiate the FrameHandler + FrameHandler handler = negotiator.negotiate(negotiation); + if (handler == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("not upgraded: no frame handler provided {}", request); + return false; + } + + // Handle error responses + Request baseRequest = negotiation.getBaseRequest(); + if (response.isCommitted()) + { + if (LOG.isDebugEnabled()) + LOG.debug("not upgraded: response committed {}", request); + baseRequest.setHandled(true); + return false; + } + int httpStatus = response.getStatus(); + if (httpStatus > 200) + { + if (LOG.isDebugEnabled()) + LOG.debug("not upgraded: invalid http code {} {}", httpStatus, request); + response.flushBuffer(); + baseRequest.setHandled(true); + return false; + } + + + // Validate negotiated protocol + String protocol = negotiation.getSubprotocol(); + List offeredProtocols = negotiation.getOfferedSubprotocols(); + if (protocol != null) + { + if (!offeredProtocols.contains(protocol)) + throw new WebSocketException("not upgraded: selected a protocol not present in offered protocols"); + } + else + { + if (!offeredProtocols.isEmpty()) + throw new WebSocketException("not upgraded: no protocol selected from offered protocols"); + } + + // validate negotiated extensions + for (ExtensionConfig config : negotiation.getNegotiatedExtensions()) + { + if (config.getName().startsWith("@")) + continue; + + long matches = negotiation.getOfferedExtensions().stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); + if (matches < 1) + throw new WebSocketException("Upgrade failed: negotiated extension not requested"); + + matches = negotiation.getNegotiatedExtensions().stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); + if (matches > 1) + throw new WebSocketException("Upgrade failed: multiple negotiated extensions of the same name"); + } + + // Create and Negotiate the ExtensionStack + ExtensionStack extensionStack = negotiation.getExtensionStack(); + + Negotiated negotiated = new Negotiated(baseRequest.getHttpURI().toURI(), protocol, baseRequest.isSecure(), extensionStack, WebSocketConstants.SPEC_VERSION_STRING); + + // Create the Session + WebSocketCoreSession coreSession = newWebSocketCoreSession(handler, negotiated); + if (defaultCustomizer != null) + defaultCustomizer.customize(coreSession); + negotiator.customize(coreSession); + + if (LOG.isDebugEnabled()) + LOG.debug("session {}", coreSession); + + WebSocketConnection connection = createWebSocketConnection(baseRequest, coreSession); + if (LOG.isDebugEnabled()) + LOG.debug("connection {}", connection); + if (connection == null) + throw new WebSocketException("not upgraded: no connection"); + + HttpChannel httpChannel = baseRequest.getHttpChannel(); + HttpConfiguration httpConfig = httpChannel.getHttpConfiguration(); + connection.setUseInputDirectByteBuffers(httpConfig.isUseInputDirectByteBuffers()); + connection.setUseOutputDirectByteBuffers(httpChannel.isUseOutputDirectByteBuffers()); + + httpChannel.getConnector().getEventListeners().forEach(connection::addEventListener); + + coreSession.setWebSocketConnection(connection); + + Response baseResponse = baseRequest.getResponse(); + prepareResponse(baseResponse, negotiation); + if (httpConfig.getSendServerVersion()) + baseResponse.getHttpFields().put(SERVER_VERSION); + baseResponse.flushBuffer(); + baseRequest.setHandled(true); + + baseRequest.setAttribute(HttpTransport.UPGRADE_CONNECTION_ATTRIBUTE, connection); + + if (LOG.isDebugEnabled()) + LOG.debug("upgrade connection={} session={} framehandler={}", connection, coreSession, handler); + + return true; + } + + protected abstract boolean validateRequest(HttpServletRequest request); + + protected abstract Negotiation newNegotiation(HttpServletRequest request, HttpServletResponse response, WebSocketComponents webSocketComponents); + + protected boolean validateNegotiation(Negotiation negotiation) + { + if (!negotiation.isSuccessful()) + { + if (LOG.isDebugEnabled()) + LOG.debug("not upgraded: no upgrade header or connection upgrade", negotiation.getBaseRequest()); + return false; + } + + if (!WebSocketConstants.SPEC_VERSION_STRING.equals(negotiation.getVersion())) + { + if (LOG.isDebugEnabled()) + LOG.debug("not upgraded: unsupported version {} {}", negotiation.getVersion(), negotiation.getBaseRequest()); + return false; + } + + return true; + } + + protected WebSocketCoreSession newWebSocketCoreSession(FrameHandler handler, Negotiated negotiated) + { + return new WebSocketCoreSession(handler, Behavior.SERVER, negotiated); + } + + protected abstract WebSocketConnection createWebSocketConnection(Request baseRequest, WebSocketCoreSession coreSession); + + protected WebSocketConnection newWebSocketConnection(EndPoint endPoint, Executor executor, Scheduler scheduler, ByteBufferPool byteBufferPool, WebSocketCoreSession coreSession) + { + return new WebSocketConnection(endPoint, executor, scheduler, byteBufferPool, coreSession); + } + + protected abstract void prepareResponse(Response response, Negotiation negotiation); +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java index 314c4394dfc..f5398e0e491 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java @@ -18,54 +18,33 @@ package org.eclipse.jetty.websocket.core.server.internal; -import java.io.IOException; -import java.util.concurrent.Executor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.BadMessageException; 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.HttpVersion; import org.eclipse.jetty.http.PreEncodedHttpField; -import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpChannel; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.HttpTransport; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Scheduler; -import org.eclipse.jetty.websocket.core.Behavior; -import org.eclipse.jetty.websocket.core.ExtensionConfig; -import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.WebSocketComponents; -import org.eclipse.jetty.websocket.core.WebSocketConstants; -import org.eclipse.jetty.websocket.core.WebSocketException; -import org.eclipse.jetty.websocket.core.internal.ExtensionStack; -import org.eclipse.jetty.websocket.core.internal.Negotiated; import org.eclipse.jetty.websocket.core.internal.WebSocketConnection; import org.eclipse.jetty.websocket.core.internal.WebSocketCore; import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; -import org.eclipse.jetty.websocket.core.server.Handshaker; import org.eclipse.jetty.websocket.core.server.Negotiation; -import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; -public final class RFC6455Handshaker implements Handshaker +public final class RFC6455Handshaker extends AbstractHandshaker { - static final Logger LOG = Log.getLogger(RFC6455Handshaker.class); private static final HttpField UPGRADE_WEBSOCKET = new PreEncodedHttpField(HttpHeader.UPGRADE, "WebSocket"); private static final HttpField CONNECTION_UPGRADE = new PreEncodedHttpField(HttpHeader.CONNECTION, HttpHeader.UPGRADE.asString()); - private static final HttpField SERVER_VERSION = new PreEncodedHttpField(HttpHeader.SERVER, HttpConfiguration.SERVER_VERSION); - public boolean upgradeRequest(WebSocketNegotiator negotiator, HttpServletRequest request, HttpServletResponse response, - FrameHandler.Customizer defaultCustomizer) throws IOException + @Override + protected boolean validateRequest(HttpServletRequest request) { if (!HttpMethod.GET.is(request.getMethod())) { @@ -74,176 +53,48 @@ public final class RFC6455Handshaker implements Handshaker return false; } - final Request baseRequest = Request.getBaseRequest(request); - if (!HttpVersion.HTTP_1_1.equals(baseRequest.getHttpVersion())) + if (!HttpVersion.HTTP_1_1.is(request.getProtocol())) { if (LOG.isDebugEnabled()) - LOG.debug("not upgraded version!=1.1 {}", baseRequest); + LOG.debug("not upgraded version!=1.1 {}", request); return false; } - Negotiation negotiation = new Negotiation(baseRequest, request, response, new WebSocketComponents()); - if (LOG.isDebugEnabled()) - LOG.debug("negotiation {}", negotiation); - - if (!negotiation.isUpgrade()) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: no upgrade header or connection upgrade", baseRequest); - return false; - } - - if (!WebSocketConstants.SPEC_VERSION_STRING.equals(negotiation.getVersion())) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: unsupported version {} {}", negotiation.getVersion(), baseRequest); - return false; - } - - if (negotiation.getKey() == null) - throw new BadMessageException("Missing request header 'Sec-WebSocket-Key'"); - - // Negotiate the FrameHandler - FrameHandler handler = negotiator.negotiate(negotiation); - if (LOG.isDebugEnabled()) - LOG.debug("negotiated handler {}", handler); - - // Handle error responses - if (response.isCommitted()) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: response committed {}", baseRequest); - baseRequest.setHandled(true); - return false; - } - if (response.getStatus() > 200) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: error sent {} {}", response.getStatus(), baseRequest); - response.flushBuffer(); - baseRequest.setHandled(true); - return false; - } - - // Check for handler - if (handler == null) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: no frame handler provided {}", baseRequest); - return false; - } - - // validate negotiated subprotocol - String subprotocol = negotiation.getSubprotocol(); - if (subprotocol != null) - { - if (!negotiation.getOfferedSubprotocols().contains(subprotocol)) - throw new WebSocketException("not upgraded: selected a subprotocol not present in offered subprotocols"); - } - else - { - if (!negotiation.getOfferedSubprotocols().isEmpty()) - throw new WebSocketException("not upgraded: no subprotocol selected from offered subprotocols"); - } - - // validate negotiated extensions - for (ExtensionConfig config : negotiation.getNegotiatedExtensions()) - { - if (config.getName().startsWith("@")) - continue; - - long matches = negotiation.getOfferedExtensions().stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); - if (matches < 1) - throw new WebSocketException("Upgrade failed: negotiated extension not requested"); - - matches = negotiation.getNegotiatedExtensions().stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); - if (matches > 1) - throw new WebSocketException("Upgrade failed: multiple negotiated extensions of the same name"); - } - - // Create and Negotiate the ExtensionStack - ExtensionStack extensionStack = negotiation.getExtensionStack(); - - Negotiated negotiated = new Negotiated( - baseRequest.getHttpURI().toURI(), - subprotocol, - baseRequest.isSecure(), - extensionStack, - WebSocketConstants.SPEC_VERSION_STRING); - - // Create the Session - WebSocketCoreSession coreSession = newWebSocketCoreSession(handler, negotiated); - if (defaultCustomizer != null) - defaultCustomizer.customize(coreSession); - negotiator.customize(coreSession); - - if (LOG.isDebugEnabled()) - LOG.debug("session {}", coreSession); - - // Create a connection - HttpChannel httpChannel = baseRequest.getHttpChannel(); - Connector connector = httpChannel.getConnector(); - WebSocketConnection connection = newWebSocketConnection(httpChannel.getEndPoint(), connector.getExecutor(), connector.getScheduler(), connector.getByteBufferPool(), coreSession); - // TODO: perhaps use of direct buffers should be WebSocket specific - // rather than inheriting the setting from HttpConfiguration. - HttpConfiguration httpConfig = httpChannel.getHttpConfiguration(); - connection.setUseInputDirectByteBuffers(httpConfig.isUseInputDirectByteBuffers()); - connection.setUseOutputDirectByteBuffers(httpChannel.isUseOutputDirectByteBuffers()); - if (LOG.isDebugEnabled()) - LOG.debug("connection {}", connection); - if (connection == null) - throw new WebSocketException("not upgraded: no connection"); - - connector.getEventListeners().forEach(connection::addEventListener); - - coreSession.setWebSocketConnection(connection); - - // send upgrade response - Response baseResponse = baseRequest.getResponse(); - baseResponse.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); - baseResponse.getHttpFields().put(UPGRADE_WEBSOCKET); - baseResponse.getHttpFields().put(CONNECTION_UPGRADE); - baseResponse.getHttpFields().put(HttpHeader.SEC_WEBSOCKET_ACCEPT, WebSocketCore.hashKey(negotiation.getKey())); - - // See bugs.eclipse.org/485969 - if (getSendServerVersion(connector)) - { - baseResponse.getHttpFields().put(SERVER_VERSION); - } - - baseResponse.flushBuffer(); - baseRequest.setHandled(true); - - // upgrade - if (LOG.isDebugEnabled()) - LOG.debug("upgrade connection={} session={}", connection, coreSession); - - baseRequest.setAttribute(HttpTransport.UPGRADE_CONNECTION_ATTRIBUTE, connection); return true; } - protected WebSocketCoreSession newWebSocketCoreSession(FrameHandler handler, Negotiated negotiated) + @Override + protected Negotiation newNegotiation(HttpServletRequest request, HttpServletResponse response, WebSocketComponents webSocketComponents) { - return new WebSocketCoreSession(handler, Behavior.SERVER, negotiated); + return new RFC6544Negotiation(Request.getBaseRequest(request), request, response, webSocketComponents); } - protected WebSocketConnection newWebSocketConnection(EndPoint endPoint, Executor executor, Scheduler scheduler, ByteBufferPool byteBufferPool, WebSocketCoreSession coreSession) + @Override + protected boolean validateNegotiation(Negotiation negotiation) { - return new WebSocketConnection(endPoint, executor, scheduler, byteBufferPool, coreSession); - } - - private boolean getSendServerVersion(Connector connector) - { - ConnectionFactory connFactory = connector.getConnectionFactory(HttpVersion.HTTP_1_1.asString()); - if (connFactory == null) + boolean result = super.validateNegotiation(negotiation); + if (!result) return false; + if (((RFC6544Negotiation)negotiation).getKey() == null) + throw new BadMessageException("Missing request header 'Sec-WebSocket-Key'"); + return true; + } - if (connFactory instanceof HttpConnectionFactory) - { - HttpConfiguration httpConf = ((HttpConnectionFactory)connFactory).getHttpConfiguration(); - if (httpConf != null) - return httpConf.getSendServerVersion(); - } - return false; + @Override + protected WebSocketConnection createWebSocketConnection(Request baseRequest, WebSocketCoreSession coreSession) + { + HttpChannel httpChannel = baseRequest.getHttpChannel(); + Connector connector = httpChannel.getConnector(); + return newWebSocketConnection(httpChannel.getEndPoint(), connector.getExecutor(), connector.getScheduler(), connector.getByteBufferPool(), coreSession); + } + + @Override + protected void prepareResponse(Response response, Negotiation negotiation) + { + response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + HttpFields responseFields = response.getHttpFields(); + responseFields.put(UPGRADE_WEBSOCKET); + responseFields.put(CONNECTION_UPGRADE); + responseFields.put(HttpHeader.SEC_WEBSOCKET_ACCEPT, WebSocketCore.hashKey(((RFC6544Negotiation)negotiation).getKey())); } } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6544Negotiation.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6544Negotiation.java new file mode 100644 index 00000000000..f836c9b7986 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6544Negotiation.java @@ -0,0 +1,90 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// 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.core.server.internal; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.QuotedCSV; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.websocket.core.WebSocketComponents; +import org.eclipse.jetty.websocket.core.server.Negotiation; + +public class RFC6544Negotiation extends Negotiation +{ + private boolean successful; + private String key; + + public RFC6544Negotiation(Request baseRequest, HttpServletRequest request, HttpServletResponse response, WebSocketComponents components) throws BadMessageException + { + super(baseRequest, request, response, components); + } + + @Override + protected void negotiateHeaders(Request baseRequest) + { + super.negotiateHeaders(baseRequest); + + boolean upgrade = false; + QuotedCSV connectionCSVs = null; + for (HttpField field : baseRequest.getHttpFields()) + { + HttpHeader header = field.getHeader(); + if (header != null) + { + switch (header) + { + case UPGRADE: + upgrade = "websocket".equalsIgnoreCase(field.getValue()); + break; + + case CONNECTION: + if (connectionCSVs == null) + connectionCSVs = new QuotedCSV(); + connectionCSVs.addValue(field.getValue()); + break; + + case SEC_WEBSOCKET_KEY: + key = field.getValue(); + break; + + default: + break; + } + } + } + + successful = upgrade && connectionCSVs != null && + connectionCSVs.getValues().stream().anyMatch(s -> s.equalsIgnoreCase("upgrade")); + } + + @Override + public boolean isSuccessful() + { + return successful; + } + + public String getKey() + { + return key; + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC8441Handshaker.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC8441Handshaker.java index 9c75b53a0ce..ae9c19364c1 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC8441Handshaker.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC8441Handshaker.java @@ -18,220 +18,62 @@ package org.eclipse.jetty.websocket.core.server.internal; -import java.io.IOException; -import java.util.concurrent.Executor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.HttpField; -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.http.PreEncodedHttpField; -import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpChannel; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.HttpTransport; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Scheduler; -import org.eclipse.jetty.websocket.core.Behavior; -import org.eclipse.jetty.websocket.core.ExtensionConfig; -import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.WebSocketComponents; -import org.eclipse.jetty.websocket.core.WebSocketConstants; -import org.eclipse.jetty.websocket.core.WebSocketException; -import org.eclipse.jetty.websocket.core.internal.ExtensionStack; -import org.eclipse.jetty.websocket.core.internal.Negotiated; import org.eclipse.jetty.websocket.core.internal.WebSocketConnection; import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; -import org.eclipse.jetty.websocket.core.server.Handshaker; import org.eclipse.jetty.websocket.core.server.Negotiation; -import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; -public class RFC8441Handshaker implements Handshaker +public class RFC8441Handshaker extends AbstractHandshaker { - static final Logger LOG = Log.getLogger(RFC8441Handshaker.class); - private static final HttpField SERVER_VERSION = new PreEncodedHttpField(HttpHeader.SERVER, HttpConfiguration.SERVER_VERSION); - @Override - public boolean upgradeRequest(WebSocketNegotiator negotiator, HttpServletRequest request, HttpServletResponse response, FrameHandler.Customizer defaultCustomizer) throws IOException + protected boolean validateRequest(HttpServletRequest request) { if (!HttpMethod.CONNECT.is(request.getMethod())) { if (LOG.isDebugEnabled()) - LOG.debug("not upgraded method!=GET {}", request.toString()); + LOG.debug("not upgraded method!=GET {}", request); return false; } - Request baseRequest = Request.getBaseRequest(request); - if (!HttpVersion.HTTP_2.equals(baseRequest.getHttpVersion())) + if (!HttpVersion.HTTP_2.is(request.getProtocol())) { if (LOG.isDebugEnabled()) - LOG.debug("not upgraded HttpVersion!=2 {}", baseRequest); + LOG.debug("not upgraded HttpVersion!=2 {}", request); return false; } - Negotiation negotiation = new RFC8441Negotiation(baseRequest, request, response, new WebSocketComponents()); - if (LOG.isDebugEnabled()) - LOG.debug("negotiation {}", negotiation); - - if (!negotiation.isUpgrade()) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: no upgrade header or connection upgrade", baseRequest); - return false; - } - - if (!WebSocketConstants.SPEC_VERSION_STRING.equals(negotiation.getVersion())) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: unsupported version {} {}", negotiation.getVersion(), baseRequest); - return false; - } - - // Negotiate the FrameHandler - FrameHandler handler = negotiator.negotiate(negotiation); - if (LOG.isDebugEnabled()) - LOG.debug("negotiated handler {}", handler); - - // Handle error responses - if (response.isCommitted()) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: response committed {}", baseRequest); - baseRequest.setHandled(true); - return false; - } - if (response.getStatus() > 200) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: error sent {} {}", response.getStatus(), baseRequest); - response.flushBuffer(); - baseRequest.setHandled(true); - return false; - } - - // Check for handler - if (handler == null) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded: no frame handler provided {}", baseRequest); - return false; - } - - // validate negotiated subprotocol - String subprotocol = negotiation.getSubprotocol(); - if (subprotocol != null) - { - if (!negotiation.getOfferedSubprotocols().contains(subprotocol)) - throw new WebSocketException("not upgraded: selected a subprotocol not present in offered subprotocols"); - } - else - { - if (!negotiation.getOfferedSubprotocols().isEmpty()) - throw new WebSocketException("not upgraded: no subprotocol selected from offered subprotocols"); - } - - // validate negotiated extensions - for (ExtensionConfig config : negotiation.getNegotiatedExtensions()) - { - if (config.getName().startsWith("@")) - continue; - - long matches = negotiation.getOfferedExtensions().stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); - if (matches < 1) - throw new WebSocketException("Upgrade failed: negotiated extension not requested"); - - matches = negotiation.getNegotiatedExtensions().stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); - if (matches > 1) - throw new WebSocketException("Upgrade failed: multiple negotiated extensions of the same name"); - } - - // Create and Negotiate the ExtensionStack - ExtensionStack extensionStack = negotiation.getExtensionStack(); - - Negotiated negotiated = new Negotiated( - baseRequest.getHttpURI().toURI(), - subprotocol, - baseRequest.isSecure(), - extensionStack, - WebSocketConstants.SPEC_VERSION_STRING); - - // Create the Channel - WebSocketCoreSession coreSession = newWebSocketCoreSession(handler, negotiated); - if (defaultCustomizer != null) - defaultCustomizer.customize(coreSession); - negotiator.customize(coreSession); - - if (LOG.isDebugEnabled()) - LOG.debug("coreSession {}", coreSession); - - // Create the Connection - HttpChannel httpChannel = baseRequest.getHttpChannel(); - Connector connector = httpChannel.getConnector(); - EndPoint endPoint = httpChannel.getTunnellingEndPoint(); - WebSocketConnection connection = newWebSocketConnection(endPoint, connector.getExecutor(), connector.getScheduler(), connector.getByteBufferPool(), coreSession); - if (LOG.isDebugEnabled()) - LOG.debug("connection {}", connection); - if (connection == null) - throw new WebSocketException("not upgraded: no connection"); - - for (Connection.Listener listener : connector.getBeans(Connection.Listener.class)) - { - connection.addEventListener(listener); - } - - coreSession.setWebSocketConnection(connection); - - // send upgrade response - Response baseResponse = baseRequest.getResponse(); - baseResponse.setStatus(HttpStatus.OK_200); - - // See bugs.eclipse.org/485969 - if (getSendServerVersion(connector)) - baseResponse.getHttpFields().put(SERVER_VERSION); - - baseRequest.setHandled(true); - - // upgrade - if (LOG.isDebugEnabled()) - LOG.debug("upgrade connection={} session={}", connection, coreSession); - - baseRequest.setAttribute(HttpTransport.UPGRADE_CONNECTION_ATTRIBUTE, connection); return true; } - protected WebSocketCoreSession newWebSocketCoreSession(FrameHandler handler, Negotiated negotiated) + @Override + protected Negotiation newNegotiation(HttpServletRequest request, HttpServletResponse response, WebSocketComponents webSocketComponents) { - return new WebSocketCoreSession(handler, Behavior.SERVER, negotiated); + return new RFC8441Negotiation(Request.getBaseRequest(request), request, response, webSocketComponents); } - protected WebSocketConnection newWebSocketConnection(EndPoint endPoint, Executor executor, Scheduler scheduler, ByteBufferPool byteBufferPool, WebSocketCoreSession coreSession) + @Override + protected WebSocketConnection createWebSocketConnection(Request baseRequest, WebSocketCoreSession coreSession) { - return new WebSocketConnection(endPoint, executor, scheduler, byteBufferPool, coreSession); + HttpChannel httpChannel = baseRequest.getHttpChannel(); + Connector connector = httpChannel.getConnector(); + EndPoint endPoint = httpChannel.getTunnellingEndPoint(); + return newWebSocketConnection(endPoint, connector.getExecutor(), connector.getScheduler(), connector.getByteBufferPool(), coreSession); } - private boolean getSendServerVersion(Connector connector) + @Override + protected void prepareResponse(Response response, Negotiation negotiation) { - ConnectionFactory connFactory = connector.getConnectionFactory(HttpVersion.HTTP_2.asString()); - if (connFactory == null) - return false; - - if (connFactory instanceof HttpConnectionFactory) - { - HttpConfiguration httpConf = ((HttpConnectionFactory)connFactory).getHttpConfiguration(); - if (httpConf != null) - return httpConf.getSendServerVersion(); - } - return false; + response.setStatus(HttpStatus.OK_200); } } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC8441Negotiation.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC8441Negotiation.java index ac3f01b8ccb..eaee84d6437 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC8441Negotiation.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC8441Negotiation.java @@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.server.Negotiation; @@ -34,11 +35,11 @@ public class RFC8441Negotiation extends Negotiation } @Override - public boolean isUpgrade() + public boolean isSuccessful() { - if (!getBaseRequest().hasMetaData()) + MetaData.Request metaData = getBaseRequest().getMetaData(); + if (metaData == null) return false; - - return "websocket".equals(getBaseRequest().getMetaData().getProtocol()); + return "websocket".equals(metaData.getProtocol()); } } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java index ce78b1cf86f..9e4e3e0cce6 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java @@ -97,7 +97,7 @@ public class WebSocketNegotiationTest extends WebSocketTester break; case "testNotAcceptingExtensions": - negotiation.setNegotiatedExtensions(Collections.EMPTY_LIST); + negotiation.setNegotiatedExtensions(Collections.emptyList()); break; case "testNoSubProtocolSelected": @@ -353,4 +353,4 @@ public class WebSocketNegotiationTest extends WebSocketTester assertThat(response, containsString("400 Bad Request")); } -} \ No newline at end of file +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketServer.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketServer.java index 478a57fb013..955fe846794 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketServer.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketServer.java @@ -26,15 +26,12 @@ import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.core.server.Negotiation; import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; import org.eclipse.jetty.websocket.core.server.WebSocketUpgradeHandler; public class WebSocketServer { - private static Logger LOG = Log.getLogger(WebSocketServer.class); private final Server server; private URI serverUri; @@ -59,12 +56,12 @@ public class WebSocketServer return server; } - public WebSocketServer(FrameHandler frameHandler) throws Exception + public WebSocketServer(FrameHandler frameHandler) { this(new DefaultNegotiator(frameHandler)); } - public WebSocketServer(WebSocketNegotiator negotiator) throws Exception + public WebSocketServer(WebSocketNegotiator negotiator) { server = new Server(); ServerConnector connector = new ServerConnector(server); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/chat/ChatWebSocketServer.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/chat/ChatWebSocketServer.java index 8b9e87911c0..c12c300b788 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/chat/ChatWebSocketServer.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/chat/ChatWebSocketServer.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -34,8 +33,6 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.core.CloseStatus; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.MessageHandler; @@ -47,8 +44,6 @@ import static org.eclipse.jetty.util.Callback.NOOP; public class ChatWebSocketServer { - private static Logger LOG = Log.getLogger(ChatWebSocketServer.class); - private Set members = new HashSet<>(); private FrameHandler negotiate(Negotiation negotiation) @@ -77,7 +72,7 @@ public class ChatWebSocketServer { members.add(this); callback.succeeded(); - }, x -> callback.failed(x))); + }, callback::failed)); } @Override