e4995be33c
Motivation: In many places Netty uses Unpooled.buffer(0) while should use EMPTY_BUFFER. We can't change this due to back compatibility in the constructors but can use Unpooled.EMPTY_BUFFER in some cases to ensure we not allocate at all. In others we can directly use the allocator either from the Channel / ChannelHandlerContext or the request / response. Modification: - Use Unpooled.EMPTY_BUFFER where possible - Use allocator where possible Result: Fixes #9345 for websockets and http package
304 lines
12 KiB
Java
304 lines
12 KiB
Java
/*
|
|
* Copyright 2012 The Netty Project
|
|
*
|
|
* 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:
|
|
*
|
|
* http://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 io.netty.handler.codec.http.websocketx;
|
|
|
|
import io.netty.buffer.ByteBuf;
|
|
import io.netty.buffer.Unpooled;
|
|
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
|
import io.netty.handler.codec.http.FullHttpRequest;
|
|
import io.netty.handler.codec.http.FullHttpResponse;
|
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
|
import io.netty.handler.codec.http.HttpHeaderValues;
|
|
import io.netty.handler.codec.http.HttpHeaders;
|
|
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.AsciiString;
|
|
|
|
import java.net.URI;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.concurrent.ThreadLocalRandom;
|
|
|
|
/**
|
|
* <p>
|
|
* Performs client side opening and closing handshakes for web socket specification version <a
|
|
* href="http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00" >draft-ietf-hybi-thewebsocketprotocol-
|
|
* 00</a>
|
|
* </p>
|
|
* <p>
|
|
* A very large portion of this code was taken from the Netty 3.2 HTTP example.
|
|
* </p>
|
|
*/
|
|
public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
|
|
|
|
private static final AsciiString WEBSOCKET = AsciiString.cached("WebSocket");
|
|
|
|
private ByteBuf expectedChallengeResponseBytes;
|
|
|
|
/**
|
|
* Creates a new instance with the specified destination WebSocket location and version to initiate.
|
|
*
|
|
* @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 customHeaders
|
|
* Map of custom headers to add to the client request
|
|
* @param maxFramePayloadLength
|
|
* Maximum length of a frame's payload
|
|
*/
|
|
public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
|
|
HttpHeaders customHeaders, int maxFramePayloadLength) {
|
|
this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength,
|
|
DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance with the specified destination WebSocket location and version to initiate.
|
|
*
|
|
* @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 customHeaders
|
|
* Map of custom headers to add to the client request
|
|
* @param maxFramePayloadLength
|
|
* Maximum length of a frame's payload
|
|
* @param forceCloseTimeoutMillis
|
|
* Close the connection if it was not closed by the server after timeout specified
|
|
*/
|
|
public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
|
|
HttpHeaders customHeaders, int maxFramePayloadLength,
|
|
long forceCloseTimeoutMillis) {
|
|
this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, false);
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance with the specified destination WebSocket location and version to initiate.
|
|
*
|
|
* @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 customHeaders
|
|
* Map of custom headers to add to the client request
|
|
* @param maxFramePayloadLength
|
|
* Maximum length of a frame's payload
|
|
* @param forceCloseTimeoutMillis
|
|
* Close the connection if it was not closed by the server after timeout specified
|
|
* @param absoluteUpgradeUrl
|
|
* Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
|
|
* clear HTTP
|
|
*/
|
|
WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
|
|
HttpHeaders customHeaders, int maxFramePayloadLength,
|
|
long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) {
|
|
super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
|
|
absoluteUpgradeUrl);
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Sends the opening request to the server:
|
|
* </p>
|
|
*
|
|
* <pre>
|
|
* GET /demo HTTP/1.1
|
|
* Upgrade: WebSocket
|
|
* Connection: Upgrade
|
|
* Host: example.com
|
|
* Origin: http://example.com
|
|
* Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
|
|
* Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
|
|
*
|
|
* ^n:ds[4U
|
|
* </pre>
|
|
*
|
|
*/
|
|
@Override
|
|
protected FullHttpRequest newHandshakeRequest() {
|
|
// Make keys
|
|
int spaces1 = ThreadLocalRandom.current().nextInt(1, 13);
|
|
int spaces2 = ThreadLocalRandom.current().nextInt(1, 13);
|
|
|
|
int max1 = Integer.MAX_VALUE / spaces1;
|
|
int max2 = Integer.MAX_VALUE / spaces2;
|
|
|
|
int number1 = ThreadLocalRandom.current().nextInt(0, max1);
|
|
int number2 = ThreadLocalRandom.current().nextInt(0, max2);
|
|
|
|
int product1 = number1 * spaces1;
|
|
int product2 = number2 * spaces2;
|
|
|
|
String key1 = Integer.toString(product1);
|
|
String key2 = Integer.toString(product2);
|
|
|
|
key1 = insertRandomCharacters(key1);
|
|
key2 = insertRandomCharacters(key2);
|
|
|
|
key1 = insertSpaces(key1, spaces1);
|
|
key2 = insertSpaces(key2, spaces2);
|
|
|
|
byte[] key3 = WebSocketUtil.randomBytes(8);
|
|
|
|
ByteBuffer buffer = ByteBuffer.allocate(4);
|
|
buffer.putInt(number1);
|
|
byte[] number1Array = buffer.array();
|
|
buffer = ByteBuffer.allocate(4);
|
|
buffer.putInt(number2);
|
|
byte[] number2Array = buffer.array();
|
|
|
|
byte[] challenge = new byte[16];
|
|
System.arraycopy(number1Array, 0, challenge, 0, 4);
|
|
System.arraycopy(number2Array, 0, challenge, 4, 4);
|
|
System.arraycopy(key3, 0, challenge, 8, 8);
|
|
expectedChallengeResponseBytes = Unpooled.wrappedBuffer(WebSocketUtil.md5(challenge));
|
|
|
|
URI wsURL = uri();
|
|
|
|
// Format request
|
|
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
|
|
Unpooled.wrappedBuffer(key3));
|
|
HttpHeaders headers = request.headers();
|
|
|
|
if (customHeaders != null) {
|
|
headers.add(customHeaders);
|
|
}
|
|
|
|
headers.set(HttpHeaderNames.UPGRADE, WEBSOCKET)
|
|
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
|
|
.set(HttpHeaderNames.HOST, websocketHostValue(wsURL))
|
|
.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL))
|
|
.set(HttpHeaderNames.SEC_WEBSOCKET_KEY1, key1)
|
|
.set(HttpHeaderNames.SEC_WEBSOCKET_KEY2, key2);
|
|
|
|
String expectedSubprotocol = expectedSubprotocol();
|
|
if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
|
|
headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
|
|
}
|
|
|
|
// Set Content-Length to workaround some known defect.
|
|
// See also: http://www.ietf.org/mail-archive/web/hybi/current/msg02149.html
|
|
headers.set(HttpHeaderNames.CONTENT_LENGTH, key3.length);
|
|
return request;
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Process server response:
|
|
* </p>
|
|
*
|
|
* <pre>
|
|
* HTTP/1.1 101 WebSocket Protocol Handshake
|
|
* Upgrade: WebSocket
|
|
* Connection: Upgrade
|
|
* Sec-WebSocket-Origin: http://example.com
|
|
* Sec-WebSocket-Location: ws://example.com/demo
|
|
* Sec-WebSocket-Protocol: sample
|
|
*
|
|
* 8jKS'y:G*Co,Wxa-
|
|
* </pre>
|
|
*
|
|
* @param response
|
|
* HTTP response returned from the server for the request sent by beginOpeningHandshake00().
|
|
* @throws WebSocketHandshakeException
|
|
*/
|
|
@Override
|
|
protected void verify(FullHttpResponse response) {
|
|
if (!response.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS)) {
|
|
throw new WebSocketHandshakeException("Invalid handshake response getStatus: " + response.status());
|
|
}
|
|
|
|
HttpHeaders headers = response.headers();
|
|
|
|
CharSequence upgrade = headers.get(HttpHeaderNames.UPGRADE);
|
|
if (!WEBSOCKET.contentEqualsIgnoreCase(upgrade)) {
|
|
throw new WebSocketHandshakeException("Invalid handshake response upgrade: "
|
|
+ upgrade);
|
|
}
|
|
|
|
if (!headers.containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)) {
|
|
throw new WebSocketHandshakeException("Invalid handshake response connection: "
|
|
+ headers.get(HttpHeaderNames.CONNECTION));
|
|
}
|
|
|
|
ByteBuf challenge = response.content();
|
|
if (!challenge.equals(expectedChallengeResponseBytes)) {
|
|
throw new WebSocketHandshakeException("Invalid challenge");
|
|
}
|
|
}
|
|
|
|
private static String insertRandomCharacters(String key) {
|
|
int count = ThreadLocalRandom.current().nextInt(1, 13);
|
|
|
|
char[] randomChars = new char[count];
|
|
int randCount = 0;
|
|
while (randCount < count) {
|
|
int rand = ThreadLocalRandom.current().nextInt(0x22, 0x7e);
|
|
if (rand < 0x2f || 0x3a < rand) {
|
|
randomChars[randCount] = (char) rand;
|
|
randCount += 1;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
int split = ThreadLocalRandom.current().nextInt(0, key.length() + 1);
|
|
String part1 = key.substring(0, split);
|
|
String part2 = key.substring(split);
|
|
key = part1 + randomChars[i] + part2;
|
|
}
|
|
|
|
return key;
|
|
}
|
|
|
|
private static String insertSpaces(String key, int spaces) {
|
|
for (int i = 0; i < spaces; i++) {
|
|
int split = ThreadLocalRandom.current().nextInt(1, key.length());
|
|
String part1 = key.substring(0, split);
|
|
String part2 = key.substring(split);
|
|
key = part1 + ' ' + part2;
|
|
}
|
|
|
|
return key;
|
|
}
|
|
|
|
@Override
|
|
protected WebSocketFrameDecoder newWebsocketDecoder() {
|
|
return new WebSocket00FrameDecoder(maxFramePayloadLength());
|
|
}
|
|
|
|
@Override
|
|
protected WebSocketFrameEncoder newWebSocketEncoder() {
|
|
return new WebSocket00FrameEncoder();
|
|
}
|
|
|
|
@Override
|
|
public WebSocketClientHandshaker00 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
|
|
super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
|
|
return this;
|
|
}
|
|
|
|
}
|