All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.neovisionaries.ws.client.HandshakeReader Maven / Gradle / Ivy

There is a newer version: 2.14
Show newest version
/*
 * 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy