2011-12-15 06:09:09 +01:00
|
|
|
/*
|
2012-06-04 22:31:44 +02:00
|
|
|
* Copyright 2012 The Netty Project
|
2011-12-15 06:09:09 +01:00
|
|
|
*
|
|
|
|
* The Netty Project licenses this file to you 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:
|
|
|
|
*
|
2012-06-04 22:31:44 +02:00
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
2011-12-15 06:09:09 +01:00
|
|
|
*
|
|
|
|
* 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 io.netty.handler.codec.http.websocketx;
|
|
|
|
|
2013-03-07 10:57:27 +01:00
|
|
|
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
|
|
|
import io.netty.handler.codec.http.FullHttpRequest;
|
2013-01-16 05:22:50 +01:00
|
|
|
import io.netty.handler.codec.http.FullHttpResponse;
|
2014-10-31 08:48:28 +01:00
|
|
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
|
|
|
import io.netty.handler.codec.http.HttpHeaderValues;
|
2013-01-16 16:33:40 +01:00
|
|
|
import io.netty.handler.codec.http.HttpHeaders;
|
2011-12-15 06:09:09 +01:00
|
|
|
import io.netty.handler.codec.http.HttpMethod;
|
|
|
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
|
|
|
import io.netty.handler.codec.http.HttpVersion;
|
|
|
|
import io.netty.util.CharsetUtil;
|
2013-02-26 23:54:25 +01:00
|
|
|
import io.netty.util.internal.logging.InternalLogger;
|
|
|
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
2011-12-15 06:09:09 +01:00
|
|
|
|
2012-05-31 00:57:39 +02:00
|
|
|
import java.net.URI;
|
|
|
|
|
2011-12-15 06:09:09 +01:00
|
|
|
/**
|
|
|
|
* <p>
|
|
|
|
* Performs client side opening and closing handshakes for web socket specification version <a
|
|
|
|
* href="http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17" >draft-ietf-hybi-thewebsocketprotocol-
|
|
|
|
* 17</a>
|
|
|
|
* </p>
|
|
|
|
*/
|
|
|
|
public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
|
|
|
|
|
2011-12-15 12:25:40 +01:00
|
|
|
private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketClientHandshaker13.class);
|
|
|
|
|
|
|
|
public static final String MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
|
|
|
2012-01-11 12:16:14 +01:00
|
|
|
private String expectedChallengeResponseString;
|
2011-12-15 12:25:40 +01:00
|
|
|
|
2012-01-19 05:12:45 +01:00
|
|
|
private final boolean allowExtensions;
|
2014-10-22 21:59:45 +02:00
|
|
|
private final boolean performMasking;
|
|
|
|
private final boolean allowMaskMismatch;
|
2011-12-15 12:25:40 +01:00
|
|
|
|
|
|
|
/**
|
2012-05-31 02:13:00 +02:00
|
|
|
* Creates a new instance.
|
2012-05-31 00:57:39 +02:00
|
|
|
*
|
2011-12-15 12:25:40 +01:00
|
|
|
* @param webSocketURL
|
|
|
|
* URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
|
|
|
|
* sent to this URL.
|
|
|
|
* @param version
|
|
|
|
* Version of web socket specification to use to connect to the server
|
2012-01-19 05:12:45 +01:00
|
|
|
* @param subprotocol
|
2011-12-15 12:25:40 +01:00
|
|
|
* Sub protocol request sent to the server.
|
|
|
|
* @param allowExtensions
|
|
|
|
* Allow extensions to be used in the reserved bits of the web socket frame
|
|
|
|
* @param customHeaders
|
|
|
|
* Map of custom headers to add to the client request
|
2012-05-31 02:13:00 +02:00
|
|
|
* @param maxFramePayloadLength
|
|
|
|
* Maximum length of a frame's payload
|
2011-12-15 12:25:40 +01:00
|
|
|
*/
|
2012-01-19 05:12:45 +01:00
|
|
|
public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
|
2014-10-22 21:59:45 +02:00
|
|
|
boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
|
|
|
|
this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new instance.
|
|
|
|
*
|
|
|
|
* @param webSocketURL
|
|
|
|
* URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
|
|
|
|
* sent to this URL.
|
|
|
|
* @param version
|
|
|
|
* Version of web socket specification to use to connect to the server
|
|
|
|
* @param subprotocol
|
|
|
|
* Sub protocol request sent to the server.
|
|
|
|
* @param allowExtensions
|
|
|
|
* Allow extensions to be used in the reserved bits of the web socket frame
|
|
|
|
* @param customHeaders
|
|
|
|
* Map of custom headers to add to the client request
|
|
|
|
* @param maxFramePayloadLength
|
|
|
|
* Maximum length of a frame's payload
|
|
|
|
* @param performMasking
|
|
|
|
* Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
|
|
|
|
* with the websocket specifications. Client applications that communicate with a non-standard server
|
|
|
|
* which doesn't require masking might set this to false to achieve a higher performance.
|
|
|
|
* @param allowMaskMismatch
|
2017-02-15 20:03:59 +01:00
|
|
|
* When set to true, frames which are not masked properly according to the standard will still be
|
|
|
|
* accepted.
|
2014-10-22 21:59:45 +02:00
|
|
|
*/
|
|
|
|
public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
|
|
|
|
boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
|
|
|
|
boolean performMasking, boolean allowMaskMismatch) {
|
2012-05-31 02:13:00 +02:00
|
|
|
super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
|
2011-12-15 12:25:40 +01:00
|
|
|
this.allowExtensions = allowExtensions;
|
2014-10-22 21:59:45 +02:00
|
|
|
this.performMasking = performMasking;
|
|
|
|
this.allowMaskMismatch = allowMaskMismatch;
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* /**
|
|
|
|
* <p>
|
|
|
|
* Sends the opening request to the server:
|
|
|
|
* </p>
|
2012-05-31 00:57:39 +02:00
|
|
|
*
|
2011-12-15 12:25:40 +01:00
|
|
|
* <pre>
|
|
|
|
* GET /chat HTTP/1.1
|
|
|
|
* Host: server.example.com
|
|
|
|
* Upgrade: websocket
|
|
|
|
* Connection: Upgrade
|
|
|
|
* Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
|
|
|
|
* Sec-WebSocket-Origin: http://example.com
|
|
|
|
* Sec-WebSocket-Protocol: chat, superchat
|
|
|
|
* Sec-WebSocket-Version: 13
|
|
|
|
* </pre>
|
2012-05-31 00:57:39 +02:00
|
|
|
*
|
2011-12-15 12:25:40 +01:00
|
|
|
*/
|
|
|
|
@Override
|
2013-03-07 10:57:27 +01:00
|
|
|
protected FullHttpRequest newHandshakeRequest() {
|
2011-12-15 12:25:40 +01:00
|
|
|
// Get path
|
2013-01-17 07:06:46 +01:00
|
|
|
URI wsURL = uri();
|
2015-11-26 22:29:28 +01:00
|
|
|
String path = rawPath(wsURL);
|
2012-11-12 15:29:02 +01:00
|
|
|
|
2011-12-15 12:25:40 +01:00
|
|
|
// Get 16 bit nonce and base 64 encode it
|
2012-01-19 05:12:45 +01:00
|
|
|
byte[] nonce = WebSocketUtil.randomBytes(16);
|
|
|
|
String key = WebSocketUtil.base64(nonce);
|
2011-12-15 12:25:40 +01:00
|
|
|
|
|
|
|
String acceptSeed = key + MAGIC_GUID;
|
2012-01-19 05:12:45 +01:00
|
|
|
byte[] sha1 = WebSocketUtil.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
|
|
|
|
expectedChallengeResponseString = WebSocketUtil.base64(sha1);
|
2011-12-15 12:25:40 +01:00
|
|
|
|
|
|
|
if (logger.isDebugEnabled()) {
|
2014-02-14 04:31:17 +01:00
|
|
|
logger.debug(
|
|
|
|
"WebSocket version 13 client handshake key: {}, expected response: {}",
|
|
|
|
key, expectedChallengeResponseString);
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Format request
|
2013-03-07 10:57:27 +01:00
|
|
|
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
|
2013-01-16 16:33:40 +01:00
|
|
|
HttpHeaders headers = request.headers();
|
|
|
|
|
2014-10-31 08:48:28 +01:00
|
|
|
headers.add(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
|
|
|
|
.add(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
|
|
|
|
.add(HttpHeaderNames.SEC_WEBSOCKET_KEY, key)
|
2017-02-27 17:56:42 +01:00
|
|
|
.add(HttpHeaderNames.HOST, websocketHostValue(wsURL))
|
2017-10-22 18:39:36 +02:00
|
|
|
.add(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL));
|
2012-05-31 00:57:39 +02:00
|
|
|
|
2013-01-17 07:06:46 +01:00
|
|
|
String expectedSubprotocol = expectedSubprotocol();
|
2012-11-10 00:03:52 +01:00
|
|
|
if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
|
2014-10-31 08:48:28 +01:00
|
|
|
headers.add(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
2012-05-12 13:05:15 +02:00
|
|
|
|
2014-10-31 08:48:28 +01:00
|
|
|
headers.add(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "13");
|
2011-12-15 12:25:40 +01:00
|
|
|
|
|
|
|
if (customHeaders != null) {
|
2013-01-16 16:33:40 +01:00
|
|
|
headers.add(customHeaders);
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
2013-03-07 10:57:27 +01:00
|
|
|
return request;
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* <p>
|
|
|
|
* Process server response:
|
|
|
|
* </p>
|
2012-05-31 00:57:39 +02:00
|
|
|
*
|
2011-12-15 12:25:40 +01:00
|
|
|
* <pre>
|
|
|
|
* HTTP/1.1 101 Switching Protocols
|
|
|
|
* Upgrade: websocket
|
|
|
|
* Connection: Upgrade
|
|
|
|
* Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
|
|
|
|
* Sec-WebSocket-Protocol: chat
|
|
|
|
* </pre>
|
2012-05-31 00:57:39 +02:00
|
|
|
*
|
2011-12-15 12:25:40 +01:00
|
|
|
* @param response
|
|
|
|
* HTTP response returned from the server for the request sent by beginOpeningHandshake00().
|
|
|
|
* @throws WebSocketHandshakeException
|
|
|
|
*/
|
|
|
|
@Override
|
2013-03-07 10:57:27 +01:00
|
|
|
protected void verify(FullHttpResponse response) {
|
2012-01-19 05:12:45 +01:00
|
|
|
final HttpResponseStatus status = HttpResponseStatus.SWITCHING_PROTOCOLS;
|
2013-01-16 16:33:40 +01:00
|
|
|
final HttpHeaders headers = response.headers();
|
2011-12-15 12:25:40 +01:00
|
|
|
|
2014-06-24 10:39:46 +02:00
|
|
|
if (!response.status().equals(status)) {
|
|
|
|
throw new WebSocketHandshakeException("Invalid handshake response getStatus: " + response.status());
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
|
|
|
|
2014-10-31 08:48:28 +01:00
|
|
|
CharSequence upgrade = headers.get(HttpHeaderNames.UPGRADE);
|
2015-08-13 04:05:37 +02:00
|
|
|
if (!HttpHeaderValues.WEBSOCKET.contentEqualsIgnoreCase(upgrade)) {
|
2013-01-16 16:33:40 +01:00
|
|
|
throw new WebSocketHandshakeException("Invalid handshake response upgrade: " + upgrade);
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
|
|
|
|
2016-01-27 14:26:33 +01:00
|
|
|
if (!headers.containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)) {
|
|
|
|
throw new WebSocketHandshakeException("Invalid handshake response connection: "
|
|
|
|
+ headers.get(HttpHeaderNames.CONNECTION));
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
|
|
|
|
2014-10-31 08:48:28 +01:00
|
|
|
CharSequence accept = headers.get(HttpHeaderNames.SEC_WEBSOCKET_ACCEPT);
|
2012-01-19 05:12:45 +01:00
|
|
|
if (accept == null || !accept.equals(expectedChallengeResponseString)) {
|
2013-01-16 16:33:40 +01:00
|
|
|
throw new WebSocketHandshakeException(String.format(
|
|
|
|
"Invalid challenge. Actual: %s. Expected: %s", accept, expectedChallengeResponseString));
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
2013-03-07 10:57:27 +01:00
|
|
|
}
|
2011-12-15 12:25:40 +01:00
|
|
|
|
2013-03-07 10:57:27 +01:00
|
|
|
@Override
|
2013-07-04 06:41:22 +02:00
|
|
|
protected WebSocketFrameDecoder newWebsocketDecoder() {
|
2014-10-22 21:59:45 +02:00
|
|
|
return new WebSocket13FrameDecoder(false, allowExtensions, maxFramePayloadLength(), allowMaskMismatch);
|
2013-03-07 10:57:27 +01:00
|
|
|
}
|
2012-05-31 11:02:02 +02:00
|
|
|
|
2013-03-07 10:57:27 +01:00
|
|
|
@Override
|
2013-07-04 06:41:22 +02:00
|
|
|
protected WebSocketFrameEncoder newWebSocketEncoder() {
|
2014-10-22 21:59:45 +02:00
|
|
|
return new WebSocket13FrameEncoder(performMasking);
|
2011-12-15 12:25:40 +01:00
|
|
|
}
|
2011-12-15 06:09:09 +01:00
|
|
|
}
|