com.neovisionaries.ws.client.HandshakeReader Maven / Gradle / Ivy
Show all versions of nv-websocket-client Show documentation
/*
* Copyright (C) 2016 Neo Visionaries Inc.
*
* 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
*
* 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 com.neovisionaries.ws.client;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Reader for a WebSocket opening handshake response.
*
* @since 1.19
*/
class HandshakeReader
{
private static final String ACCEPT_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
private final WebSocket mWebSocket;
public HandshakeReader(WebSocket websocket)
{
mWebSocket = websocket;
}
public Map> readHandshake(WebSocketInputStream input, String key) throws WebSocketException
{
// Read the status line.
StatusLine statusLine = readStatusLine(input);
// Read HTTP headers.
Map> headers = readHttpHeaders(input);
// Validate the status line.
validateStatusLine(statusLine, headers, input);
// Validate the value of Upgrade.
validateUpgrade(statusLine, headers);
// Validate the value of Connection.
validateConnection(statusLine, headers);
// Validate the value of Sec-WebSocket-Accept.
validateAccept(statusLine, headers, key);
// Validate the value of Sec-WebSocket-Extensions.
validateExtensions(statusLine, headers);
// Validate the value of Sec-WebSocket-Protocol.
validateProtocol(statusLine, headers);
// OK. The server has accepted the web socket request.
return headers;
}
/**
* Read a status line from an HTTP server.
*/
private StatusLine readStatusLine(WebSocketInputStream input) throws WebSocketException
{
String line;
try
{
// Read the status line.
line = input.readLine();
}
catch (IOException e)
{
// Failed to read an opening handshake response from the server.
throw new WebSocketException(
WebSocketError.OPENING_HANDSHAKE_RESPONSE_FAILURE,
"Failed to read an opening handshake response from the server: " + e.getMessage(), e);
}
if (line == null || line.length() == 0)
{
// The status line of the opening handshake response is empty.
throw new WebSocketException(
WebSocketError.STATUS_LINE_EMPTY,
"The status line of the opening handshake response is empty.");
}
try
{
// Parse the status line.
return new StatusLine(line);
}
catch (Exception e)
{
// The status line of the opening handshake response is badly formatted.
throw new WebSocketException(
WebSocketError.STATUS_LINE_BAD_FORMAT,
"The status line of the opening handshake response is badly formatted. The status line is: " + line);
}
}
private Map> readHttpHeaders(WebSocketInputStream input) throws WebSocketException
{
// Create a map of HTTP headers. Keys are case-insensitive.
Map> headers =
new TreeMap>(String.CASE_INSENSITIVE_ORDER);
StringBuilder builder = null;
String line;
while (true)
{
try
{
line = input.readLine();
}
catch (IOException e)
{
// An error occurred while HTTP header section was being read.
throw new WebSocketException(
WebSocketError.HTTP_HEADER_FAILURE,
"An error occurred while HTTP header section was being read: " + e.getMessage(), e);
}
// If the end of the header section was reached.
if (line == null || line.length() == 0)
{
if (builder != null)
{
parseHttpHeader(headers, builder.toString());
}
// The end of the header section.
break;
}
// The first line of the line.
char ch = line.charAt(0);
// If the first char is SP or HT.
if (ch == ' ' || ch == '\t')
{
if (builder == null)
{
// Weird. No preceding "field-name:field-value" line. Ignore this line.
continue;
}
// Replacing the leading 1*(SP|HT) to a single SP.
line = line.replaceAll("^[ \t]+", " ");
// Concatenate
builder.append(line);
continue;
}
if (builder != null)
{
parseHttpHeader(headers, builder.toString());
}
builder = new StringBuilder(line);
}
return headers;
}
private void parseHttpHeader(Map> headers, String header)
{
// Split 'header' to name & value.
String[] pair = header.split(":", 2);
if (pair.length < 2)
{
// Weird. Ignore this header.
return;
}
// Name. (Remove leading and trailing spaces)
String name = pair[0].trim();
// Value. (Remove leading and trailing spaces)
String value = pair[1].trim();
List list = headers.get(name);
if (list == null)
{
list = new ArrayList();
headers.put(name, list);
}
list.add(value);
}
/**
* Validate the status line. {@code "101 Switching Protocols"} is expected.
*/
private void validateStatusLine(StatusLine statusLine, Map> headers, WebSocketInputStream input) throws WebSocketException
{
// If the status code is 101 (Switching Protocols).
if (statusLine.getStatusCode() == 101)
{
// OK. The server can speak the WebSocket protocol.
return;
}
// Read the response body.
byte[] body = readBody(headers, input);
// The status code of the opening handshake response is not Switching Protocols.
throw new OpeningHandshakeException(
WebSocketError.NOT_SWITCHING_PROTOCOLS,
"The status code of the opening handshake response is not '101 Switching Protocols'. The status line is: " + statusLine,
statusLine, headers, body);
}
/**
* Read the response body
*/
private byte[] readBody(Map> headers, WebSocketInputStream input)
{
// Get the value of "Content-Length" header.
int length = getContentLength(headers);
if (length <= 0)
{
// Response body is not available.
return null;
}
try
{
// Allocate a byte array of the content length.
byte[] body = new byte[length];
// Read the response body into the byte array.
input.readBytes(body, length);
// Return the content of the response body.
return body;
}
catch (Throwable t)
{
// Response body is not available.
return null;
}
}
/**
* Get the value of "Content-Length" header.
*/
private int getContentLength(Map> headers)
{
try
{
return Integer.parseInt(headers.get("Content-Length").get(0));
}
catch (Exception e)
{
return -1;
}
}
/**
* Validate the value of {@code Upgrade} header.
*
*
* From RFC 6455, p19.
*
* If the response lacks an {@code Upgrade} header field or the {@code Upgrade}
* header field contains a value that is not an ASCII case-insensitive match for
* the value "websocket", the client MUST Fail the WebSocket Connection.
*
*
*/
private void validateUpgrade(StatusLine statusLine, Map> headers) throws WebSocketException
{
// Get the values of Upgrade.
List values = headers.get("Upgrade");
if (values == null || values.size() == 0)
{
// The opening handshake response does not contain 'Upgrade' header.
throw new OpeningHandshakeException(
WebSocketError.NO_UPGRADE_HEADER,
"The opening handshake response does not contain 'Upgrade' header.",
statusLine, headers);
}
for (String value : values)
{
// Split the value of Upgrade header into elements.
String[] elements = value.split("\\s*,\\s*");
for (String element : elements)
{
if ("websocket".equalsIgnoreCase(element))
{
// Found 'websocket' in Upgrade header.
return;
}
}
}
// 'websocket' was not found in 'Upgrade' header.
throw new OpeningHandshakeException(
WebSocketError.NO_WEBSOCKET_IN_UPGRADE_HEADER,
"'websocket' was not found in 'Upgrade' header.",
statusLine, headers);
}
/**
* Validate the value of {@code Connection} header.
*
*
* From RFC 6455, p19.
*
* If the response lacks a {@code Connection} header field or the {@code Connection}
* header field doesn't contain a token that is an ASCII case-insensitive match
* for the value "Upgrade", the client MUST Fail the WebSocket Connection.
*
*
*/
private void validateConnection(StatusLine statusLine, Map> headers) throws WebSocketException
{
// Get the values of Upgrade.
List values = headers.get("Connection");
if (values == null || values.size() == 0)
{
// The opening handshake response does not contain 'Connection' header.
throw new OpeningHandshakeException(
WebSocketError.NO_CONNECTION_HEADER,
"The opening handshake response does not contain 'Connection' header.",
statusLine, headers);
}
for (String value : values)
{
// Split the value of Connection header into elements.
String[] elements = value.split("\\s*,\\s*");
for (String element : elements)
{
if ("Upgrade".equalsIgnoreCase(element))
{
// Found 'Upgrade' in Connection header.
return;
}
}
}
// 'Upgrade' was not found in 'Connection' header.
throw new OpeningHandshakeException(
WebSocketError.NO_UPGRADE_IN_CONNECTION_HEADER,
"'Upgrade' was not found in 'Connection' header.",
statusLine, headers);
}
/**
* Validate the value of {@code Sec-WebSocket-Accept} header.
*
*
* From RFC 6455, p19.
*
* If the response lacks a {@code Sec-WebSocket-Accept} header field or the
* {@code Sec-WebSocket-Accept} contains a value other than the base64-encoded
* SHA-1 of the concatenation of the {@code Sec-WebSocket-Key} (as a string,
* not base64-decoded) with the string "{@code 258EAFA5-E914-47DA-95CA-C5AB0DC85B11}"
* but ignoring any leading and trailing whitespace, the client MUST Fail the
* WebSocket Connection.
*
*
*/
private void validateAccept(StatusLine statusLine, Map> headers, String key) throws WebSocketException
{
// Get the values of Sec-WebSocket-Accept.
List values = headers.get("Sec-WebSocket-Accept");
if (values == null)
{
// The opening handshake response does not contain 'Sec-WebSocket-Accept' header.
throw new OpeningHandshakeException(
WebSocketError.NO_SEC_WEBSOCKET_ACCEPT_HEADER,
"The opening handshake response does not contain 'Sec-WebSocket-Accept' header.",
statusLine, headers);
}
// The actual value of Sec-WebSocket-Accept.
String actual = values.get(0);
// Concatenate.
String input = key + ACCEPT_MAGIC;
// Expected value of Sec-WebSocket-Accept
String expected;
try
{
// Message digest for SHA-1.
MessageDigest md = MessageDigest.getInstance("SHA-1");
// Compute the digest value.
byte[] digest = md.digest(Misc.getBytesUTF8(input));
// Base64.
expected = Base64.encode(digest);
}
catch (Exception e)
{
// This never happens.
return;
}
if (expected.equals(actual) == false)
{
// The value of 'Sec-WebSocket-Accept' header is different from the expected one.
throw new OpeningHandshakeException(
WebSocketError.UNEXPECTED_SEC_WEBSOCKET_ACCEPT_HEADER,
"The value of 'Sec-WebSocket-Accept' header is different from the expected one.",
statusLine, headers);
}
// OK. The value of Sec-WebSocket-Accept is the same as the expected one.
}
/**
* Validate the value of {@code Sec-WebSocket-Extensions} header.
*
*
* From RFC 6455, p19.
*
* If the response includes a {@code Sec-WebSocket-Extensions} header
* field and this header field indicates the use of an extension
* that was not present in the client's handshake (the server has
* indicated an extension not requested by the client), the client
* MUST Fail the WebSocket Connection.
*
*
*/
private void validateExtensions(StatusLine statusLine, Map> headers) throws WebSocketException
{
// Get the values of Sec-WebSocket-Extensions.
List values = headers.get("Sec-WebSocket-Extensions");
if (values == null || values.size() == 0)
{
// Nothing to check.
return;
}
List extensions = new ArrayList();
for (String value : values)
{
// Split the value into elements each of which represents an extension.
String[] elements = value.split("\\s*,\\s*");
for (String element : elements)
{
// Parse the string whose format is supposed to be "{name}[; {key}[={value}]*".
WebSocketExtension extension = WebSocketExtension.parse(element);
if (extension == null)
{
// The value in 'Sec-WebSocket-Extensions' failed to be parsed.
throw new OpeningHandshakeException(
WebSocketError.EXTENSION_PARSE_ERROR,
"The value in 'Sec-WebSocket-Extensions' failed to be parsed: " + element,
statusLine, headers);
}
// The extension name.
String name = extension.getName();
// If the extension is not contained in the original request from this client.
if (mWebSocket.getHandshakeBuilder().containsExtension(name) == false)
{
// The extension contained in the Sec-WebSocket-Extensions header is not supported.
throw new OpeningHandshakeException(
WebSocketError.UNSUPPORTED_EXTENSION,
"The extension contained in the Sec-WebSocket-Extensions header is not supported: " + name,
statusLine, headers);
}
// Let the extension validate itself.
extension.validate();
// The extension has been agreed.
extensions.add(extension);
}
}
// Check if extensions conflict with each other.
validateExtensionCombination(statusLine, headers, extensions);
// Agreed extensions.
mWebSocket.setAgreedExtensions(extensions);
}
private void validateExtensionCombination(
StatusLine statusLine, Map> headers, List extensions) throws WebSocketException
{
// Currently, only duplication of per-message compression extensions is checked.
// A per-message compression extension found in the list.
WebSocketExtension pmce = null;
for (WebSocketExtension extension : extensions)
{
// If the extension is not a per-message compression extension.
if ((extension instanceof PerMessageCompressionExtension) == false)
{
continue;
}
// If the found per-message compression extension is the first one.
if (pmce == null)
{
// Found a per-message compression extension.
pmce = extension;
continue;
}
// Found the second per-message compression extension. Conflict.
String message = String.format(
"'%s' extension and '%s' extension conflict with each other.",
pmce.getName(), extension.getName());
// The extensions conflict with each other.
throw new OpeningHandshakeException(
WebSocketError.EXTENSIONS_CONFLICT, message, statusLine, headers);
}
}
/**
* Validate the value of {@code Sec-WebSocket-Protocol} header.
*
*
* From RFC 6455, p20.
*
* If the response includes a {@code Sec-WebSocket-Protocol} header field
* and this header field indicates the use of a subprotocol that was
* not present in the client's handshake (the server has indicated a
* subprotocol not requested by the client), the client MUST Fail
* the WebSocket Connection.
*
*
*/
private void validateProtocol(StatusLine statusLine, Map> headers) throws WebSocketException
{
// Get the values of Sec-WebSocket-Protocol.
List values = headers.get("Sec-WebSocket-Protocol");
if (values == null)
{
// Nothing to check.
return;
}
// Protocol
String protocol = values.get(0);
if (protocol == null || protocol.length() == 0)
{
// Ignore.
return;
}
// If the protocol is not contained in the original request
// from this client.
if (mWebSocket.getHandshakeBuilder().containsProtocol(protocol) == false)
{
// The protocol contained in the Sec-WebSocket-Protocol header is not supported.
throw new OpeningHandshakeException(
WebSocketError.UNSUPPORTED_PROTOCOL,
"The protocol contained in the Sec-WebSocket-Protocol header is not supported: " + protocol,
statusLine, headers);
}
// Agreed protocol.
mWebSocket.setAgreedProtocol(protocol);
}
}