11 changed files with 1006 additions and 72 deletions
@ -0,0 +1,147 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2021 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.web.reactive.socket.adapter; |
||||||
|
|
||||||
|
import java.nio.ByteBuffer; |
||||||
|
import java.nio.charset.StandardCharsets; |
||||||
|
import java.util.function.Function; |
||||||
|
|
||||||
|
import org.eclipse.jetty.websocket.api.Session; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.WebSocket; |
||||||
|
import org.eclipse.jetty.websocket.api.extensions.Frame; |
||||||
|
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.web.reactive.socket.CloseStatus; |
||||||
|
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||||
|
import org.springframework.web.reactive.socket.WebSocketMessage; |
||||||
|
import org.springframework.web.reactive.socket.WebSocketMessage.Type; |
||||||
|
import org.springframework.web.reactive.socket.WebSocketSession; |
||||||
|
|
||||||
|
/** |
||||||
|
* Identical to {@link JettyWebSocketHandlerAdapter}, only excluding the |
||||||
|
* {@code onWebSocketFrame} method, since the {@link Frame} argument has moved |
||||||
|
* to a different package in Jetty 10. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.3.4 |
||||||
|
*/ |
||||||
|
@WebSocket |
||||||
|
public class Jetty10WebSocketHandlerAdapter { |
||||||
|
|
||||||
|
private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); |
||||||
|
|
||||||
|
|
||||||
|
private final WebSocketHandler delegateHandler; |
||||||
|
|
||||||
|
private final Function<Session, JettyWebSocketSession> sessionFactory; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private JettyWebSocketSession delegateSession; |
||||||
|
|
||||||
|
|
||||||
|
public Jetty10WebSocketHandlerAdapter(WebSocketHandler handler, |
||||||
|
Function<Session, JettyWebSocketSession> sessionFactory) { |
||||||
|
|
||||||
|
Assert.notNull(handler, "WebSocketHandler is required"); |
||||||
|
Assert.notNull(sessionFactory, "'sessionFactory' is required"); |
||||||
|
this.delegateHandler = handler; |
||||||
|
this.sessionFactory = sessionFactory; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@OnWebSocketConnect |
||||||
|
public void onWebSocketConnect(Session session) { |
||||||
|
this.delegateSession = this.sessionFactory.apply(session); |
||||||
|
this.delegateHandler.handle(this.delegateSession) |
||||||
|
.checkpoint(session.getUpgradeRequest().getRequestURI() + " [JettyWebSocketHandlerAdapter]") |
||||||
|
.subscribe(this.delegateSession); |
||||||
|
} |
||||||
|
|
||||||
|
@OnWebSocketMessage |
||||||
|
public void onWebSocketText(String message) { |
||||||
|
if (this.delegateSession != null) { |
||||||
|
WebSocketMessage webSocketMessage = toMessage(Type.TEXT, message); |
||||||
|
this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@OnWebSocketMessage |
||||||
|
public void onWebSocketBinary(byte[] message, int offset, int length) { |
||||||
|
if (this.delegateSession != null) { |
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(message, offset, length); |
||||||
|
WebSocketMessage webSocketMessage = toMessage(Type.BINARY, buffer); |
||||||
|
this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: onWebSocketFrame can't be declared without compiling against Jetty 10
|
||||||
|
// Jetty 10: org.eclipse.jetty.websocket.api.Frame
|
||||||
|
// Jetty 9: org.eclipse.jetty.websocket.api.extensions.Frame
|
||||||
|
|
||||||
|
// @OnWebSocketFrame
|
||||||
|
// public void onWebSocketFrame(Frame frame) {
|
||||||
|
// if (this.delegateSession != null) {
|
||||||
|
// if (OpCode.PONG == frame.getOpCode()) {
|
||||||
|
// ByteBuffer buffer = (frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD);
|
||||||
|
// WebSocketMessage webSocketMessage = toMessage(Type.PONG, buffer);
|
||||||
|
// this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
private <T> WebSocketMessage toMessage(Type type, T message) { |
||||||
|
WebSocketSession session = this.delegateSession; |
||||||
|
Assert.state(session != null, "Cannot create message without a session"); |
||||||
|
if (Type.TEXT.equals(type)) { |
||||||
|
byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); |
||||||
|
DataBuffer buffer = session.bufferFactory().wrap(bytes); |
||||||
|
return new WebSocketMessage(Type.TEXT, buffer); |
||||||
|
} |
||||||
|
else if (Type.BINARY.equals(type)) { |
||||||
|
DataBuffer buffer = session.bufferFactory().wrap((ByteBuffer) message); |
||||||
|
return new WebSocketMessage(Type.BINARY, buffer); |
||||||
|
} |
||||||
|
else if (Type.PONG.equals(type)) { |
||||||
|
DataBuffer buffer = session.bufferFactory().wrap((ByteBuffer) message); |
||||||
|
return new WebSocketMessage(Type.PONG, buffer); |
||||||
|
} |
||||||
|
else { |
||||||
|
throw new IllegalArgumentException("Unexpected message type: " + message); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@OnWebSocketClose |
||||||
|
public void onWebSocketClose(int statusCode, String reason) { |
||||||
|
if (this.delegateSession != null) { |
||||||
|
this.delegateSession.handleClose(CloseStatus.create(statusCode, reason)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@OnWebSocketError |
||||||
|
public void onWebSocketError(Throwable cause) { |
||||||
|
if (this.delegateSession != null) { |
||||||
|
this.delegateSession.handleError(cause); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,154 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2021 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.reactive.socket.server.upgrade; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.function.Supplier; |
||||||
|
|
||||||
|
import javax.servlet.ServletContext; |
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import javax.servlet.http.HttpServletResponse; |
||||||
|
|
||||||
|
import org.aopalliance.intercept.MethodInterceptor; |
||||||
|
import org.aopalliance.intercept.MethodInvocation; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.aop.framework.ProxyFactory; |
||||||
|
import org.springframework.aop.target.EmptyTargetSource; |
||||||
|
import org.springframework.core.io.buffer.DataBufferFactory; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequestDecorator; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponseDecorator; |
||||||
|
import org.springframework.lang.NonNull; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
import org.springframework.web.reactive.socket.HandshakeInfo; |
||||||
|
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||||
|
import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; |
||||||
|
import org.springframework.web.reactive.socket.adapter.Jetty10WebSocketHandlerAdapter; |
||||||
|
import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; |
||||||
|
import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* A {@link RequestUpgradeStrategy} for use with Jetty 10. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.3.4 |
||||||
|
*/ |
||||||
|
public class Jetty10RequestUpgradeStrategy implements RequestUpgradeStrategy { |
||||||
|
|
||||||
|
private static final Class<?> webSocketCreatorClass; |
||||||
|
|
||||||
|
private static final Method getContainerMethod; |
||||||
|
|
||||||
|
private static final Method upgradeMethod; |
||||||
|
|
||||||
|
private static final Method setAcceptedSubProtocol; |
||||||
|
|
||||||
|
static { |
||||||
|
ClassLoader loader = Jetty10RequestUpgradeStrategy.class.getClassLoader(); |
||||||
|
try { |
||||||
|
webSocketCreatorClass = loader.loadClass("org.eclipse.jetty.websocket.server.JettyWebSocketCreator"); |
||||||
|
|
||||||
|
Class<?> type = loader.loadClass("org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer"); |
||||||
|
getContainerMethod = type.getMethod("getContainer", ServletContext.class); |
||||||
|
upgradeMethod = ReflectionUtils.findMethod(type, "upgrade", (Class<?>[]) null); |
||||||
|
|
||||||
|
type = loader.loadClass("org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse"); |
||||||
|
setAcceptedSubProtocol = type.getMethod("setAcceptedSubProtocol", String.class); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
throw new IllegalStateException("No compatible Jetty version found", ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> upgrade( |
||||||
|
ServerWebExchange exchange, WebSocketHandler handler, |
||||||
|
@Nullable String subProtocol, Supplier<HandshakeInfo> handshakeInfoFactory) { |
||||||
|
|
||||||
|
ServerHttpRequest request = exchange.getRequest(); |
||||||
|
ServerHttpResponse response = exchange.getResponse(); |
||||||
|
|
||||||
|
HttpServletRequest servletRequest = ServerHttpRequestDecorator.getNativeRequest(request); |
||||||
|
HttpServletResponse servletResponse = ServerHttpResponseDecorator.getNativeResponse(response); |
||||||
|
ServletContext servletContext = servletRequest.getServletContext(); |
||||||
|
|
||||||
|
HandshakeInfo handshakeInfo = handshakeInfoFactory.get(); |
||||||
|
DataBufferFactory factory = response.bufferFactory(); |
||||||
|
|
||||||
|
// Trigger WebFlux preCommit actions and upgrade
|
||||||
|
return exchange.getResponse().setComplete() |
||||||
|
.then(Mono.deferContextual(contextView -> { |
||||||
|
Jetty10WebSocketHandlerAdapter adapter = new Jetty10WebSocketHandlerAdapter( |
||||||
|
ContextWebSocketHandler.decorate(handler, contextView), |
||||||
|
session -> new JettyWebSocketSession(session, handshakeInfo, factory)); |
||||||
|
|
||||||
|
try { |
||||||
|
Object creator = createJettyWebSocketCreator(adapter, subProtocol); |
||||||
|
Object container = ReflectionUtils.invokeMethod(getContainerMethod, null, servletContext); |
||||||
|
ReflectionUtils.invokeMethod(upgradeMethod, container, creator, servletRequest, servletResponse); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
return Mono.error(ex); |
||||||
|
} |
||||||
|
return Mono.empty(); |
||||||
|
})); |
||||||
|
} |
||||||
|
|
||||||
|
private static Object createJettyWebSocketCreator( |
||||||
|
Jetty10WebSocketHandlerAdapter adapter, @Nullable String protocol) { |
||||||
|
|
||||||
|
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); |
||||||
|
factory.addInterface(webSocketCreatorClass); |
||||||
|
factory.addAdvice(new WebSocketCreatorInterceptor(adapter, protocol)); |
||||||
|
return factory.getProxy(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Proxy for a JettyWebSocketCreator to supply the WebSocket handler and set the sub-protocol. |
||||||
|
*/ |
||||||
|
private static class WebSocketCreatorInterceptor implements MethodInterceptor { |
||||||
|
|
||||||
|
private final Jetty10WebSocketHandlerAdapter adapter; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final String protocol; |
||||||
|
|
||||||
|
|
||||||
|
public WebSocketCreatorInterceptor( |
||||||
|
Jetty10WebSocketHandlerAdapter adapter, @Nullable String protocol) { |
||||||
|
|
||||||
|
this.adapter = adapter; |
||||||
|
this.protocol = protocol; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Object invoke(@NonNull MethodInvocation invocation) { |
||||||
|
if (this.protocol != null) { |
||||||
|
ReflectionUtils.invokeMethod( |
||||||
|
setAcceptedSubProtocol, invocation.getArguments()[2], this.protocol); |
||||||
|
} |
||||||
|
return this.adapter; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,137 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2021 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.web.socket.adapter.jetty; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
import org.eclipse.jetty.websocket.api.Session; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; |
||||||
|
import org.eclipse.jetty.websocket.api.annotations.WebSocket; |
||||||
|
import org.eclipse.jetty.websocket.api.extensions.Frame; |
||||||
|
|
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.web.socket.BinaryMessage; |
||||||
|
import org.springframework.web.socket.CloseStatus; |
||||||
|
import org.springframework.web.socket.TextMessage; |
||||||
|
import org.springframework.web.socket.WebSocketHandler; |
||||||
|
import org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator; |
||||||
|
|
||||||
|
/** |
||||||
|
* Identical to {@link JettyWebSocketHandlerAdapter}, only excluding the |
||||||
|
* {@code onWebSocketFrame} method, since the {@link Frame} argument has moved |
||||||
|
* to a different package in Jetty 10. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.3.4 |
||||||
|
*/ |
||||||
|
@WebSocket |
||||||
|
public class Jetty10WebSocketHandlerAdapter { |
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(Jetty10WebSocketHandlerAdapter.class); |
||||||
|
|
||||||
|
|
||||||
|
private final WebSocketHandler webSocketHandler; |
||||||
|
|
||||||
|
private final JettyWebSocketSession wsSession; |
||||||
|
|
||||||
|
|
||||||
|
public Jetty10WebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebSocketSession wsSession) { |
||||||
|
Assert.notNull(webSocketHandler, "WebSocketHandler must not be null"); |
||||||
|
Assert.notNull(wsSession, "WebSocketSession must not be null"); |
||||||
|
this.webSocketHandler = webSocketHandler; |
||||||
|
this.wsSession = wsSession; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@OnWebSocketConnect |
||||||
|
public void onWebSocketConnect(Session session) { |
||||||
|
try { |
||||||
|
this.wsSession.initializeNativeSession(session); |
||||||
|
this.webSocketHandler.afterConnectionEstablished(this.wsSession); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@OnWebSocketMessage |
||||||
|
public void onWebSocketText(String payload) { |
||||||
|
TextMessage message = new TextMessage(payload); |
||||||
|
try { |
||||||
|
this.webSocketHandler.handleMessage(this.wsSession, message); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@OnWebSocketMessage |
||||||
|
public void onWebSocketBinary(byte[] payload, int offset, int length) { |
||||||
|
BinaryMessage message = new BinaryMessage(payload, offset, length, true); |
||||||
|
try { |
||||||
|
this.webSocketHandler.handleMessage(this.wsSession, message); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: onWebSocketFrame can't be declared without compiling against Jetty 10
|
||||||
|
// Jetty 10: org.eclipse.jetty.websocket.api.Frame
|
||||||
|
// Jetty 9: org.eclipse.jetty.websocket.api.extensions.Frame
|
||||||
|
|
||||||
|
// @OnWebSocketFrame
|
||||||
|
// public void onWebSocketFrame(Frame frame) {
|
||||||
|
// if (OpCode.PONG == frame.getOpCode()) {
|
||||||
|
// ByteBuffer payload = frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD;
|
||||||
|
// PongMessage message = new PongMessage(payload);
|
||||||
|
// try {
|
||||||
|
// this.webSocketHandler.handleMessage(this.wsSession, message);
|
||||||
|
// }
|
||||||
|
// catch (Exception ex) {
|
||||||
|
// ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
@OnWebSocketClose |
||||||
|
public void onWebSocketClose(int statusCode, String reason) { |
||||||
|
CloseStatus closeStatus = new CloseStatus(statusCode, reason); |
||||||
|
try { |
||||||
|
this.webSocketHandler.afterConnectionClosed(this.wsSession, closeStatus); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
if (logger.isWarnEnabled()) { |
||||||
|
logger.warn("Unhandled exception after connection closed for " + this, ex); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@OnWebSocketError |
||||||
|
public void onWebSocketError(Throwable cause) { |
||||||
|
try { |
||||||
|
this.webSocketHandler.handleTransportError(this.wsSession, cause); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,165 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2021 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.web.socket.server.jetty; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.lang.reflect.UndeclaredThrowableException; |
||||||
|
import java.security.Principal; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import javax.servlet.ServletContext; |
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import javax.servlet.http.HttpServletResponse; |
||||||
|
|
||||||
|
import org.aopalliance.intercept.MethodInterceptor; |
||||||
|
import org.aopalliance.intercept.MethodInvocation; |
||||||
|
import org.eclipse.jetty.websocket.server.HandshakeRFC6455; |
||||||
|
|
||||||
|
import org.springframework.aop.framework.ProxyFactory; |
||||||
|
import org.springframework.aop.target.EmptyTargetSource; |
||||||
|
import org.springframework.http.server.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServerHttpResponse; |
||||||
|
import org.springframework.http.server.ServletServerHttpRequest; |
||||||
|
import org.springframework.http.server.ServletServerHttpResponse; |
||||||
|
import org.springframework.lang.NonNull; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
import org.springframework.web.socket.WebSocketExtension; |
||||||
|
import org.springframework.web.socket.WebSocketHandler; |
||||||
|
import org.springframework.web.socket.adapter.jetty.Jetty10WebSocketHandlerAdapter; |
||||||
|
import org.springframework.web.socket.adapter.jetty.JettyWebSocketSession; |
||||||
|
import org.springframework.web.socket.server.HandshakeFailureException; |
||||||
|
import org.springframework.web.socket.server.RequestUpgradeStrategy; |
||||||
|
|
||||||
|
/** |
||||||
|
* A {@link RequestUpgradeStrategy} for Jetty 10. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.3.4 |
||||||
|
*/ |
||||||
|
public class Jetty10RequestUpgradeStrategy implements RequestUpgradeStrategy { |
||||||
|
|
||||||
|
private static final String[] SUPPORTED_VERSIONS = new String[] { String.valueOf(HandshakeRFC6455.VERSION) }; |
||||||
|
|
||||||
|
private static final Class<?> webSocketCreatorClass; |
||||||
|
|
||||||
|
private static final Method getContainerMethod; |
||||||
|
|
||||||
|
private static final Method upgradeMethod; |
||||||
|
|
||||||
|
private static final Method setAcceptedSubProtocol; |
||||||
|
|
||||||
|
static { |
||||||
|
ClassLoader loader = Jetty10RequestUpgradeStrategy.class.getClassLoader(); |
||||||
|
try { |
||||||
|
webSocketCreatorClass = loader.loadClass("org.eclipse.jetty.websocket.server.JettyWebSocketCreator"); |
||||||
|
|
||||||
|
Class<?> type = loader.loadClass("org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer"); |
||||||
|
getContainerMethod = type.getMethod("getContainer", ServletContext.class); |
||||||
|
upgradeMethod = ReflectionUtils.findMethod(type, "upgrade", (Class<?>[]) null); |
||||||
|
|
||||||
|
type = loader.loadClass("org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse"); |
||||||
|
setAcceptedSubProtocol = type.getMethod("setAcceptedSubProtocol", String.class); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
throw new IllegalStateException("No compatible Jetty version found", ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public String[] getSupportedVersions() { |
||||||
|
return SUPPORTED_VERSIONS; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<WebSocketExtension> getSupportedExtensions(ServerHttpRequest request) { |
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void upgrade(ServerHttpRequest request, ServerHttpResponse response, |
||||||
|
@Nullable String selectedProtocol, List<WebSocketExtension> selectedExtensions, |
||||||
|
@Nullable Principal user, WebSocketHandler handler, Map<String, Object> attributes) |
||||||
|
throws HandshakeFailureException { |
||||||
|
|
||||||
|
Assert.isInstanceOf(ServletServerHttpRequest.class, request, "ServletServerHttpRequest required"); |
||||||
|
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); |
||||||
|
ServletContext servletContext = servletRequest.getServletContext(); |
||||||
|
|
||||||
|
Assert.isInstanceOf(ServletServerHttpResponse.class, response, "ServletServerHttpResponse required"); |
||||||
|
HttpServletResponse servletResponse = ((ServletServerHttpResponse) response).getServletResponse(); |
||||||
|
|
||||||
|
JettyWebSocketSession session = new JettyWebSocketSession(attributes, user); |
||||||
|
Jetty10WebSocketHandlerAdapter handlerAdapter = new Jetty10WebSocketHandlerAdapter(handler, session); |
||||||
|
|
||||||
|
try { |
||||||
|
Object creator = createJettyWebSocketCreator(handlerAdapter, selectedProtocol); |
||||||
|
Object container = ReflectionUtils.invokeMethod(getContainerMethod, null, servletContext); |
||||||
|
ReflectionUtils.invokeMethod(upgradeMethod, container, creator, servletRequest, servletResponse); |
||||||
|
} |
||||||
|
catch (UndeclaredThrowableException ex) { |
||||||
|
throw new HandshakeFailureException("Failed to upgrade", ex.getUndeclaredThrowable()); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
throw new HandshakeFailureException("Failed to upgrade", ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static Object createJettyWebSocketCreator( |
||||||
|
Jetty10WebSocketHandlerAdapter adapter, @Nullable String protocol) { |
||||||
|
|
||||||
|
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); |
||||||
|
factory.addInterface(webSocketCreatorClass); |
||||||
|
factory.addAdvice(new WebSocketCreatorInterceptor(adapter, protocol)); |
||||||
|
return factory.getProxy(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Proxy for a JettyWebSocketCreator to supply the WebSocket handler and set the sub-protocol. |
||||||
|
*/ |
||||||
|
private static class WebSocketCreatorInterceptor implements MethodInterceptor { |
||||||
|
|
||||||
|
private final Jetty10WebSocketHandlerAdapter adapter; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final String protocol; |
||||||
|
|
||||||
|
|
||||||
|
public WebSocketCreatorInterceptor( |
||||||
|
Jetty10WebSocketHandlerAdapter adapter, @Nullable String protocol) { |
||||||
|
|
||||||
|
this.adapter = adapter; |
||||||
|
this.protocol = protocol; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Object invoke(@NonNull MethodInvocation invocation) { |
||||||
|
if (this.protocol != null) { |
||||||
|
ReflectionUtils.invokeMethod( |
||||||
|
setAcceptedSubProtocol, invocation.getArguments()[2], this.protocol); |
||||||
|
} |
||||||
|
return this.adapter; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue