diff options
13 files changed, 1407 insertions, 0 deletions
diff --git a/Userland/Libraries/CMakeLists.txt b/Userland/Libraries/CMakeLists.txt index a78e05bee1..c0fa49770b 100644 --- a/Userland/Libraries/CMakeLists.txt +++ b/Userland/Libraries/CMakeLists.txt @@ -37,4 +37,5 @@ add_subdirectory(LibTLS) add_subdirectory(LibTTF) add_subdirectory(LibVT) add_subdirectory(LibWeb) +add_subdirectory(LibWebSocket) add_subdirectory(LibX86) diff --git a/Userland/Libraries/LibWebSocket/CMakeLists.txt b/Userland/Libraries/LibWebSocket/CMakeLists.txt new file mode 100644 index 0000000000..b238fa6a69 --- /dev/null +++ b/Userland/Libraries/LibWebSocket/CMakeLists.txt @@ -0,0 +1,10 @@ +set(SOURCES + ConnectionInfo.cpp + Impl/AbstractWebSocketImpl.cpp + Impl/TCPWebSocketConnectionImpl.cpp + Impl/TLSv12WebSocketConnectionImpl.cpp + WebSocket.cpp +) + +serenity_lib(LibWebSocket websocket) +target_link_libraries(LibWebSocket LibCore LibCrypto LibTLS) diff --git a/Userland/Libraries/LibWebSocket/ConnectionInfo.cpp b/Userland/Libraries/LibWebSocket/ConnectionInfo.cpp new file mode 100644 index 0000000000..d140a9c591 --- /dev/null +++ b/Userland/Libraries/LibWebSocket/ConnectionInfo.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibWebSocket/ConnectionInfo.h> + +namespace WebSocket { + +ConnectionInfo::ConnectionInfo(URL url) + : m_url(move(url)) +{ +} + +bool ConnectionInfo::is_secure() const +{ + // RFC 6455 Section 3 : + // The URI is called "secure" if the scheme component matches "wss" case-insensitively. + return m_url.protocol().equals_ignoring_case("wss"sv); +} + +String ConnectionInfo::resource_name() const +{ + // RFC 6455 Section 3 : + // The "resource-name" can be constructed by concatenating the following: + StringBuilder builder; + // "/" if the path component is empty + if (m_url.path().is_empty()) + builder.append("/"); + // The path component + builder.append(m_url.path()); + // "?" if the query component is non-empty + if (!m_url.query().is_empty()) + builder.append("?"); + // the query component + builder.append(m_url.query()); + return builder.to_string(); +} + +} diff --git a/Userland/Libraries/LibWebSocket/ConnectionInfo.h b/Userland/Libraries/LibWebSocket/ConnectionInfo.h new file mode 100644 index 0000000000..feadf8a20b --- /dev/null +++ b/Userland/Libraries/LibWebSocket/ConnectionInfo.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/URL.h> +#include <AK/Vector.h> +#include <LibCore/Object.h> +#include <LibCore/TCPSocket.h> +#include <LibTLS/TLSv12.h> +#include <LibWebSocket/Message.h> + +namespace WebSocket { + +class ConnectionInfo final { +public: + ConnectionInfo(URL); + + URL const& url() const { return m_url; } + + String const& origin() const { return m_origin; } + void set_origin(String origin) { m_origin = move(origin); } + + Vector<String> const& protocols() const { return m_protocols; } + void set_protocols(Vector<String> protocols) { m_protocols = move(protocols); } + + Vector<String> const& extensions() const { return m_extensions; } + void set_extensions(Vector<String> extensions) { m_extensions = move(extensions); } + + struct Header { + String name; + String value; + }; + Vector<Header> const& headers() const { return m_headers; } + void set_headers(Vector<Header> headers) { m_headers = move(headers); } + + // secure flag - defined in RFC 6455 Section 3 + bool is_secure() const; + + // "resource-name" or "/resource name/" - defined in RFC 6455 Section 3 + String resource_name() const; + +private: + URL m_url; + String m_origin; + Vector<String> m_protocols {}; + Vector<String> m_extensions {}; + Vector<Header> m_headers {}; +}; + +} diff --git a/Userland/Libraries/LibWebSocket/Impl/AbstractWebSocketImpl.cpp b/Userland/Libraries/LibWebSocket/Impl/AbstractWebSocketImpl.cpp new file mode 100644 index 0000000000..acd8a254e8 --- /dev/null +++ b/Userland/Libraries/LibWebSocket/Impl/AbstractWebSocketImpl.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibWebSocket/Impl/AbstractWebSocketImpl.h> + +namespace WebSocket { + +AbstractWebSocketImpl::AbstractWebSocketImpl(Core::Object* parent) + : Object(parent) +{ +} + +AbstractWebSocketImpl::~AbstractWebSocketImpl() +{ +} + +} diff --git a/Userland/Libraries/LibWebSocket/Impl/AbstractWebSocketImpl.h b/Userland/Libraries/LibWebSocket/Impl/AbstractWebSocketImpl.h new file mode 100644 index 0000000000..24445d2e58 --- /dev/null +++ b/Userland/Libraries/LibWebSocket/Impl/AbstractWebSocketImpl.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/ByteBuffer.h> +#include <AK/Span.h> +#include <AK/String.h> +#include <LibCore/Object.h> +#include <LibWebSocket/ConnectionInfo.h> + +namespace WebSocket { + +class AbstractWebSocketImpl : public Core::Object { + C_OBJECT_ABSTRACT(AbstractWebSocketImpl); + +public: + virtual ~AbstractWebSocketImpl() override; + explicit AbstractWebSocketImpl(Core::Object* parent = nullptr); + + virtual void connect(ConnectionInfo const&) = 0; + + virtual bool can_read_line() = 0; + virtual String read_line(size_t size) = 0; + + virtual bool can_read() = 0; + virtual ByteBuffer read(int max_size) = 0; + + virtual bool send(ReadonlyBytes) = 0; + + virtual bool eof() = 0; + + virtual void discard_connection() = 0; + + Function<void()> on_connected; + Function<void()> on_connection_error; + Function<void()> on_ready_to_read; +}; + +} diff --git a/Userland/Libraries/LibWebSocket/Impl/TCPWebSocketConnectionImpl.cpp b/Userland/Libraries/LibWebSocket/Impl/TCPWebSocketConnectionImpl.cpp new file mode 100644 index 0000000000..ec4268ee6c --- /dev/null +++ b/Userland/Libraries/LibWebSocket/Impl/TCPWebSocketConnectionImpl.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibWebSocket/Impl/TCPWebSocketConnectionImpl.h> + +namespace WebSocket { + +TCPWebSocketConnectionImpl::TCPWebSocketConnectionImpl(Core::Object* parent) + : AbstractWebSocketImpl(parent) +{ +} + +TCPWebSocketConnectionImpl::~TCPWebSocketConnectionImpl() +{ + discard_connection(); +} + +void TCPWebSocketConnectionImpl::connect(ConnectionInfo const& connection) +{ + VERIFY(!m_socket); + VERIFY(on_connected); + VERIFY(on_connection_error); + VERIFY(on_ready_to_read); + m_socket = Core::TCPSocket::construct(this); + + m_notifier = Core::Notifier::construct(m_socket->fd(), Core::Notifier::Read); + m_notifier->on_ready_to_read = [this] { + on_ready_to_read(); + }; + + m_socket->on_connected = [this] { + on_connected(); + }; + bool success = m_socket->connect(connection.url().host(), connection.url().port()); + if (!success) { + deferred_invoke([this](auto&) { + on_connection_error(); + }); + } +} + +bool TCPWebSocketConnectionImpl::send(ReadonlyBytes data) +{ + return m_socket->write(data); +} + +bool TCPWebSocketConnectionImpl::can_read_line() +{ + return m_socket->can_read_line(); +} + +String TCPWebSocketConnectionImpl::read_line(size_t size) +{ + return m_socket->read_line(size); +} + +bool TCPWebSocketConnectionImpl::can_read() +{ + return m_socket->can_read(); +} + +ByteBuffer TCPWebSocketConnectionImpl::read(int max_size) +{ + return m_socket->read(max_size); +} + +bool TCPWebSocketConnectionImpl::eof() +{ + return m_socket->eof(); +} + +void TCPWebSocketConnectionImpl::discard_connection() +{ + if (!m_socket) + return; + m_socket->on_ready_to_read = nullptr; + remove_child(*m_socket); + m_socket = nullptr; +} + +} diff --git a/Userland/Libraries/LibWebSocket/Impl/TCPWebSocketConnectionImpl.h b/Userland/Libraries/LibWebSocket/Impl/TCPWebSocketConnectionImpl.h new file mode 100644 index 0000000000..cbdabe0e9f --- /dev/null +++ b/Userland/Libraries/LibWebSocket/Impl/TCPWebSocketConnectionImpl.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/ByteBuffer.h> +#include <AK/Span.h> +#include <AK/String.h> +#include <LibCore/Notifier.h> +#include <LibCore/Object.h> +#include <LibCore/TCPSocket.h> +#include <LibWebSocket/ConnectionInfo.h> +#include <LibWebSocket/Impl/AbstractWebSocketImpl.h> + +namespace WebSocket { + +class TCPWebSocketConnectionImpl final : public AbstractWebSocketImpl { + C_OBJECT(TCPWebSocketConnectionImpl); + +public: + virtual ~TCPWebSocketConnectionImpl() override; + explicit TCPWebSocketConnectionImpl(Core::Object* parent = nullptr); + + virtual void connect(ConnectionInfo const& connection) override; + + virtual bool can_read_line() override; + virtual String read_line(size_t size) override; + + virtual bool can_read() override; + virtual ByteBuffer read(int max_size) override; + + virtual bool send(ReadonlyBytes data) override; + + virtual bool eof() override; + + virtual void discard_connection() override; + +private: + RefPtr<Core::Notifier> m_notifier; + RefPtr<Core::TCPSocket> m_socket; +}; + +} diff --git a/Userland/Libraries/LibWebSocket/Impl/TLSv12WebSocketConnectionImpl.cpp b/Userland/Libraries/LibWebSocket/Impl/TLSv12WebSocketConnectionImpl.cpp new file mode 100644 index 0000000000..20dc337c63 --- /dev/null +++ b/Userland/Libraries/LibWebSocket/Impl/TLSv12WebSocketConnectionImpl.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibWebSocket/Impl/TLSv12WebSocketConnectionImpl.h> + +namespace WebSocket { + +TLSv12WebSocketConnectionImpl::TLSv12WebSocketConnectionImpl(Core::Object* parent) + : AbstractWebSocketImpl(parent) +{ +} + +TLSv12WebSocketConnectionImpl::~TLSv12WebSocketConnectionImpl() +{ + discard_connection(); +} + +void TLSv12WebSocketConnectionImpl::connect(ConnectionInfo const& connection) +{ + VERIFY(!m_socket); + VERIFY(on_connected); + VERIFY(on_connection_error); + VERIFY(on_ready_to_read); + m_socket = TLS::TLSv12::construct(this); + + m_notifier = Core::Notifier::construct(m_socket->fd(), Core::Notifier::Read); + m_notifier->on_ready_to_read = [this] { + on_ready_to_read(); + }; + + m_socket->set_root_certificates(DefaultRootCACertificates::the().certificates()); + m_socket->on_tls_error = [this](TLS::AlertDescription) { + on_connection_error(); + }; + m_socket->on_tls_ready_to_write = [this] { + on_connected(); + }; + m_socket->on_tls_finished = [this] { + on_connection_error(); + }; + m_socket->on_tls_certificate_request = [this](auto&) { + // FIXME : Once we handle TLS certificate requests, handle it here as well. + }; + bool success = m_socket->connect(connection.url().host(), connection.url().port()); + if (!success) { + deferred_invoke([this](auto&) { + on_connection_error(); + }); + } +} + +bool TLSv12WebSocketConnectionImpl::send(ReadonlyBytes data) +{ + return m_socket->write(data); +} + +bool TLSv12WebSocketConnectionImpl::can_read_line() +{ + return m_socket->can_read_line(); +} + +String TLSv12WebSocketConnectionImpl::read_line(size_t size) +{ + return m_socket->read_line(size); +} + +bool TLSv12WebSocketConnectionImpl::can_read() +{ + return m_socket->can_read(); +} + +ByteBuffer TLSv12WebSocketConnectionImpl::read(int max_size) +{ + return m_socket->read(max_size); +} + +bool TLSv12WebSocketConnectionImpl::eof() +{ + return m_socket->eof(); +} + +void TLSv12WebSocketConnectionImpl::discard_connection() +{ + if (!m_socket) + return; + m_socket->on_tls_connected = nullptr; + m_socket->on_tls_error = nullptr; + m_socket->on_tls_finished = nullptr; + m_socket->on_tls_certificate_request = nullptr; + m_socket->on_ready_to_read = nullptr; + remove_child(*m_socket); + m_socket = nullptr; +} + +} diff --git a/Userland/Libraries/LibWebSocket/Impl/TLSv12WebSocketConnectionImpl.h b/Userland/Libraries/LibWebSocket/Impl/TLSv12WebSocketConnectionImpl.h new file mode 100644 index 0000000000..a429d58b22 --- /dev/null +++ b/Userland/Libraries/LibWebSocket/Impl/TLSv12WebSocketConnectionImpl.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/ByteBuffer.h> +#include <AK/Span.h> +#include <AK/String.h> +#include <LibCore/Object.h> +#include <LibTLS/TLSv12.h> +#include <LibWebSocket/ConnectionInfo.h> +#include <LibWebSocket/Impl/AbstractWebSocketImpl.h> + +namespace WebSocket { + +class TLSv12WebSocketConnectionImpl final : public AbstractWebSocketImpl { + C_OBJECT(TLSv12WebSocketConnectionImpl); + +public: + virtual ~TLSv12WebSocketConnectionImpl() override; + explicit TLSv12WebSocketConnectionImpl(Core::Object* parent = nullptr); + + void connect(ConnectionInfo const& connection) override; + + virtual bool can_read_line() override; + virtual String read_line(size_t size) override; + + virtual bool can_read() override; + virtual ByteBuffer read(int max_size) override; + + virtual bool send(ReadonlyBytes data) override; + + virtual bool eof() override; + + virtual void discard_connection() override; + +private: + RefPtr<Core::Notifier> m_notifier; + RefPtr<TLS::TLSv12> m_socket; +}; + +} diff --git a/Userland/Libraries/LibWebSocket/Message.h b/Userland/Libraries/LibWebSocket/Message.h new file mode 100644 index 0000000000..020eabff4b --- /dev/null +++ b/Userland/Libraries/LibWebSocket/Message.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/ByteBuffer.h> +#include <AK/Optional.h> + +namespace WebSocket { + +class Message { +public: + explicit Message(String const& data) + : m_is_text(true) + , m_data(ByteBuffer::copy(data.bytes())) + { + } + + explicit Message(ByteBuffer data, bool is_text) + : m_is_text(is_text) + , m_data(move(data)) + { + } + + explicit Message(ByteBuffer const&& data, bool is_text) + : m_is_text(is_text) + , m_data(move(data)) + { + } + + bool is_text() const { return m_is_text; } + ByteBuffer const& data() const { return m_data; } + +private: + bool m_is_text { false }; + ByteBuffer m_data; +}; + +} diff --git a/Userland/Libraries/LibWebSocket/WebSocket.cpp b/Userland/Libraries/LibWebSocket/WebSocket.cpp new file mode 100644 index 0000000000..16da2ac99c --- /dev/null +++ b/Userland/Libraries/LibWebSocket/WebSocket.cpp @@ -0,0 +1,613 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Base64.h> +#include <AK/Debug.h> +#include <AK/Random.h> +#include <LibCrypto/Hash/HashManager.h> +#include <LibWebSocket/Impl/TCPWebSocketConnectionImpl.h> +#include <LibWebSocket/Impl/TLSv12WebSocketConnectionImpl.h> +#include <LibWebSocket/WebSocket.h> +#include <stdio.h> +#include <unistd.h> + +namespace WebSocket { + +// Note : The websocket protocol is defined by RFC 6455, found at https://tools.ietf.org/html/rfc6455 +// In this file, section numbers will refer to the RFC 6455 + +NonnullRefPtr<WebSocket> WebSocket::create(ConnectionInfo connection) +{ + return adopt(*new WebSocket(connection)); +} + +WebSocket::WebSocket(ConnectionInfo connection) + : m_connection(connection) +{ +} + +WebSocket::~WebSocket() +{ +} + +void WebSocket::start() +{ + VERIFY(m_state == WebSocket::InternalState::NotStarted); + VERIFY(!m_impl); + if (m_connection.is_secure()) + m_impl = TLSv12WebSocketConnectionImpl::construct(); + else + m_impl = TCPWebSocketConnectionImpl::construct(); + + m_impl->on_connection_error = [this] { + dbgln("WebSocket: Connection error (underlying socket)"); + fatal_error(WebSocket::Error::CouldNotEstablishConnection); + }; + m_impl->on_connected = [this] { + if (m_state != WebSocket::InternalState::EstablishingProtocolConnection) + return; + m_state = WebSocket::InternalState::SendingClientHandshake; + send_client_handshake(); + drain_read(); + }; + m_impl->on_ready_to_read = [this] { + drain_read(); + }; + m_state = WebSocket::InternalState::EstablishingProtocolConnection; + m_impl->connect(m_connection); +} + +ReadyState WebSocket::ready_state() +{ + switch (m_state) { + case WebSocket::InternalState::NotStarted: + case WebSocket::InternalState::EstablishingProtocolConnection: + case WebSocket::InternalState::SendingClientHandshake: + case WebSocket::InternalState::WaitingForServerHandshake: + return ReadyState::Connecting; + case WebSocket::InternalState::Open: + return ReadyState::Open; + case WebSocket::InternalState::Closing: + return ReadyState::Closing; + case WebSocket::InternalState::Closed: + case WebSocket::InternalState::Errored: + return ReadyState::Closed; + default: + VERIFY_NOT_REACHED(); + return ReadyState::Closed; + } +} + +void WebSocket::send(Message message) +{ + // Calling send on a socket that is not opened is not allowed + VERIFY(m_state == WebSocket::InternalState::Open); + VERIFY(m_impl); + if (message.is_text()) + send_frame(WebSocket::OpCode::Text, message.data(), true); + else + send_frame(WebSocket::OpCode::Binary, message.data(), true); +} + +void WebSocket::close(u16 code, String message) +{ + // Calling close on a socket that is not opened is not allowed + VERIFY(m_state == WebSocket::InternalState::Open); + VERIFY(m_impl); + auto message_bytes = message.bytes(); + auto close_payload = ByteBuffer::create_uninitialized(message_bytes.size() + 2); + close_payload.overwrite(0, (u8*)&code, 2); + close_payload.overwrite(2, message_bytes.data(), message_bytes.size()); + send_frame(WebSocket::OpCode::ConnectionClose, close_payload, true); +} + +void WebSocket::drain_read() +{ + if (m_impl->eof()) { + // The connection got closed by the server + m_state = WebSocket::InternalState::Closed; + notify_close(m_last_close_code, m_last_close_message, true); + discard_connection(); + return; + } + + while (m_impl->can_read()) { + if (m_state == WebSocket::InternalState::WaitingForServerHandshake) { + read_server_handshake(); + return; + } + if (m_state == WebSocket::InternalState::Open) { + read_frame(); + return; + } + if (m_state == WebSocket::InternalState::Closing) { + read_frame(); + return; + } + } +} + +// The client handshake message is defined in the second list of section 4.1 +void WebSocket::send_client_handshake() +{ + VERIFY(m_impl); + VERIFY(m_state == WebSocket::InternalState::SendingClientHandshake); + StringBuilder builder; + + // 2. and 3. GET /resource name/ HTTP 1.1 + builder.appendff("GET {} HTTP/1.1\r\n", m_connection.resource_name()); + + // 4. Host + auto url = m_connection.url(); + builder.appendff("Host: {}", url.host()); + if (!m_connection.is_secure() && url.port() != 80) + builder.appendff(":{}", url.port()); + else if (m_connection.is_secure() && url.port() != 443) + builder.appendff(":{}", url.port()); + builder.append("\r\n"); + + // 5. and 6. Connection Upgrade + builder.append("Upgrade: websocket\r\n"); + builder.append("Connection: Upgrade\r\n"); + + // 7. 16-byte nonce encoded as Base64 + u8 nonce_data[16]; + fill_with_random(nonce_data, 16); + m_websocket_key = encode_base64(ReadonlyBytes(nonce_data, 16)); + builder.appendff("Sec-WebSocket-Key: {}\r\n", m_websocket_key); + + // 8. Origin (optional field) + if (!m_connection.origin().is_empty()) { + builder.appendff("Origin: {}\r\n", m_connection.origin()); + } + + // 9. Websocket version + builder.append("Sec-WebSocket-Version: 13\r\n"); + + // 10. Websocket protocol (optional field) + if (!m_connection.protocols().is_empty()) { + builder.append("Sec-WebSocket-Protocol: "); + builder.join(",", m_connection.protocols()); + builder.append("\r\n"); + } + + // 11. Websocket extensions (optional field) + if (!m_connection.extensions().is_empty()) { + builder.append("Sec-WebSocket-Extensions: "); + builder.join(",", m_connection.extensions()); + builder.append("\r\n"); + } + + // 12. Additional headers + for (auto& header : m_connection.headers()) { + builder.appendff("{}: {}\r\n", header.name, header.value); + } + + builder.append("\r\n"); + + m_state = WebSocket::InternalState::WaitingForServerHandshake; + auto success = m_impl->send(builder.to_string().bytes()); + VERIFY(success); +} + +// The server handshake message is defined in the third list of section 4.1 +void WebSocket::read_server_handshake() +{ + VERIFY(m_impl); + VERIFY(m_state == WebSocket::InternalState::WaitingForServerHandshake); + // Read the server handshake + if (!m_impl->can_read_line()) + return; + + if (!m_has_read_server_handshake_first_line) { + auto header = m_impl->read_line(PAGE_SIZE); + auto parts = header.split(' '); + if (parts.size() < 2) { + dbgln("WebSocket: Server HTTP Handshake contained HTTP header was malformed"); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + discard_connection(); + return; + } + if (parts[0] != "HTTP/1.1") { + dbgln("WebSocket: Server HTTP Handshake contained HTTP header {} which isn't supported", parts[0]); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + discard_connection(); + return; + } + if (parts[1] != "101") { + // 1. If the status code is not 101, handle as per HTTP procedures. + // FIXME : This could be a redirect or a 401 authentification request, which we do not handle. + dbgln("WebSocket: Server HTTP Handshake return status {} which isn't supported", parts[1]); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + return; + } + m_has_read_server_handshake_first_line = true; + } + + // Read the rest of the reply until we find an empty line + while (m_impl->can_read_line()) { + auto line = m_impl->read_line(PAGE_SIZE); + if (line.is_whitespace()) { + // We're done with the HTTP headers. + // Fail the connection if we're missing any of the following: + if (!m_has_read_server_handshake_upgrade) { + // 2. |Upgrade| should be present + dbgln("WebSocket: Server HTTP Handshake didn't contain an |Upgrade| header"); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + return; + } + if (!m_has_read_server_handshake_connection) { + // 2. |Connection| should be present + dbgln("WebSocket: Server HTTP Handshake didn't contain a |Connection| header"); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + return; + } + if (!m_has_read_server_handshake_accept) { + // 2. |Sec-WebSocket-Accept| should be present + dbgln("WebSocket: Server HTTP Handshake didn't contain a |Sec-WebSocket-Accept| header"); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + return; + } + + m_state = WebSocket::InternalState::Open; + notify_open(); + return; + } + + auto parts = line.split(':'); + if (parts.size() < 2) { + // The header field is not valid + dbgln("WebSocket: Got invalid header line {} in the Server HTTP handshake", line); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + return; + } + + auto header_name = parts[0]; + + if (header_name.equals_ignoring_case("Upgrade")) { + // 2. |Upgrade| should be case-insensitive "websocket" + if (!parts[1].trim_whitespace().equals_ignoring_case("websocket")) { + dbgln("WebSocket: Server HTTP Handshake Header |Upgrade| should be 'websocket', got '{}'. Failing connection.", parts[1]); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + return; + } + + m_has_read_server_handshake_upgrade = true; + continue; + } + + if (header_name.equals_ignoring_case("Connection")) { + // 3. |Connection| should be case-insensitive "Upgrade" + if (!parts[1].trim_whitespace().equals_ignoring_case("Upgrade")) { + dbgln("WebSocket: Server HTTP Handshake Header |Connection| should be 'Upgrade', got '{}'. Failing connection.", parts[1]); + return; + } + + m_has_read_server_handshake_connection = true; + continue; + } + + if (header_name.equals_ignoring_case("Sec-WebSocket-Accept")) { + // 4. |Sec-WebSocket-Accept| should be base64(SHA1(|Sec-WebSocket-Key| + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) + auto expected_content = String::formatted("{}258EAFA5-E914-47DA-95CA-C5AB0DC85B11", m_websocket_key); + + Crypto::Hash::Manager hash; + hash.initialize(Crypto::Hash::HashKind::SHA1); + hash.update(expected_content); + auto expected_sha1 = hash.digest(); + auto expected_sha1_string = encode_base64(ReadonlyBytes(expected_sha1.immutable_data(), expected_sha1.data_length())); + if (!parts[1].trim_whitespace().equals_ignoring_case(expected_sha1_string)) { + dbgln("WebSocket: Server HTTP Handshake Header |Sec-Websocket-Accept| should be '{}', got '{}'. Failing connection.", expected_sha1_string, parts[1]); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + return; + } + + m_has_read_server_handshake_accept = true; + continue; + } + + if (header_name.equals_ignoring_case("Sec-WebSocket-Extensions")) { + // 5. |Sec-WebSocket-Extensions| should not contain an extension that doesn't appear in m_connection->extensions() + auto server_extensions = parts[1].split(','); + for (auto extension : server_extensions) { + auto trimmed_extension = extension.trim_whitespace(); + bool found_extension = false; + for (auto supported_extension : m_connection.extensions()) { + if (trimmed_extension.equals_ignoring_case(supported_extension)) { + found_extension = true; + } + } + if (!found_extension) { + dbgln("WebSocket: Server HTTP Handshake Header |Sec-WebSocket-Extensions| contains '{}', which is not supported by the client. Failing connection.", trimmed_extension); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + return; + } + } + continue; + } + + if (header_name.equals_ignoring_case("Sec-WebSocket-Protocol")) { + // 6. |Sec-WebSocket-Protocol| should not contain an extension that doesn't appear in m_connection->protocols() + auto server_protocols = parts[1].split(','); + for (auto protocol : server_protocols) { + auto trimmed_protocol = protocol.trim_whitespace(); + bool found_protocol = false; + for (auto supported_protocol : m_connection.protocols()) { + if (trimmed_protocol.equals_ignoring_case(supported_protocol)) { + found_protocol = true; + } + } + if (!found_protocol) { + dbgln("WebSocket: Server HTTP Handshake Header |Sec-WebSocket-Protocol| contains '{}', which is not supported by the client. Failing connection.", trimmed_protocol); + fatal_error(WebSocket::Error::ConnectionUpgradeFailed); + return; + } + } + continue; + } + } + + // If needed, we will keep reading the header on the next drain_read call +} + +void WebSocket::read_frame() +{ + VERIFY(m_impl); + VERIFY(m_state == WebSocket::InternalState::Open || m_state == WebSocket::InternalState::Closing); + + auto head_bytes = m_impl->read(2); + if (head_bytes.size() == 0) { + // The connection got closed. + m_state = WebSocket::InternalState::Closed; + notify_close(m_last_close_code, m_last_close_message, true); + discard_connection(); + return; + } + VERIFY(head_bytes.size() == 2); + + bool is_final_frame = head_bytes[0] & 0x80; + if (!is_final_frame) { + // FIXME: Support fragmented frames + TODO(); + } + + auto op_code = (WebSocket::OpCode)(head_bytes[0] & 0x0f); + bool is_masked = head_bytes[1] & 0x80; + + // Parse the payload length. + size_t payload_length; + auto payload_length_bits = head_bytes[1] & 0x7f; + if (payload_length_bits == 127) { + // A code of 127 means that the next 8 bytes contains the payload length + auto actual_bytes = m_impl->read(8); + VERIFY(actual_bytes.size() == 8); + u64 full_payload_length = (u64)((u64)(actual_bytes[0] & 0xff) << 56) + | (u64)((u64)(actual_bytes[1] & 0xff) << 48) + | (u64)((u64)(actual_bytes[2] & 0xff) << 40) + | (u64)((u64)(actual_bytes[3] & 0xff) << 32) + | (u64)((u64)(actual_bytes[4] & 0xff) << 24) + | (u64)((u64)(actual_bytes[5] & 0xff) << 16) + | (u64)((u64)(actual_bytes[6] & 0xff) << 8) + | (u64)((u64)(actual_bytes[7] & 0xff) << 0); + VERIFY(full_payload_length <= NumericLimits<size_t>::max()); + payload_length = (size_t)full_payload_length; + } else if (payload_length_bits == 126) { + // A code of 126 means that the next 2 bytes contains the payload length + auto actual_bytes = m_impl->read(2); + VERIFY(actual_bytes.size() == 2); + payload_length = (size_t)((size_t)(actual_bytes[0] & 0xff) << 8) + | (size_t)((size_t)(actual_bytes[1] & 0xff) << 0); + } else { + payload_length = (size_t)payload_length_bits; + } + + // Parse the mask, if it exists. + // Note : this is technically non-conformant with Section 5.1 : + // > A server MUST NOT mask any frames that it sends to the client. + // > A client MUST close a connection if it detects a masked frame. + // > (These rules might be relaxed in a future specification.) + // But because it doesn't cost much, we can support receiving masked frames anyways. + u8 masking_key[4]; + if (is_masked) { + auto masking_key_data = m_impl->read(4); + VERIFY(masking_key_data.size() == 4); + masking_key[0] = masking_key_data[0]; + masking_key[1] = masking_key_data[1]; + masking_key[2] = masking_key_data[2]; + masking_key[3] = masking_key_data[3]; + } + + auto payload = ByteBuffer::create_uninitialized(payload_length); + u64 read_length = 0; + while (read_length < payload_length) { + auto payload_part = m_impl->read(payload_length - read_length); + if (payload_part.size() == 0) { + // We got disconnected, somehow. + dbgln("Websocket: Server disconnected while sending payload ({} bytes read out of {})", read_length, payload_length); + fatal_error(WebSocket::Error::ServerClosedSocket); + return; + } + // We read at most "actual_length - read" bytes, so this is safe to do. + payload.overwrite(read_length, payload_part.data(), payload_part.size()); + read_length -= payload_part.size(); + } + + if (is_masked) { + // Unmask the payload + for (size_t i = 0; i < payload.size(); ++i) { + payload[i] = payload[i] ^ (masking_key[i % 4]); + } + } + + if (op_code == WebSocket::OpCode::ConnectionClose) { + if (payload.size() > 1) { + m_last_close_code = (((u16)(payload[0] & 0xff) << 8) | ((u16)(payload[1] & 0xff))); + m_last_close_message = String(ReadonlyBytes(payload.offset_pointer(2), payload.size() - 2)); + } + m_state = WebSocket::InternalState::Closing; + return; + } + if (op_code == WebSocket::OpCode::Ping) { + // Immediately send a pong frame as a reply, with the given payload. + send_frame(WebSocket::OpCode::Pong, payload, true); + return; + } + if (op_code == WebSocket::OpCode::Pong) { + // We can safely ignore the pong + return; + } + if (op_code == WebSocket::OpCode::Continuation) { + // FIXME: Support fragmented frames + TODO(); + return; + } + if (op_code == WebSocket::OpCode::Text) { + notify_message(Message(payload, true)); + return; + } + if (op_code == WebSocket::OpCode::Binary) { + notify_message(Message(payload, false)); + return; + } + dbgln("Websocket: Found unknown opcode {}", (u8)op_code); +} + +void WebSocket::send_frame(WebSocket::OpCode op_code, ReadonlyBytes payload, bool is_final) +{ + VERIFY(m_impl); + VERIFY(m_state == WebSocket::InternalState::Open); + u8 frame_head[1] = { (u8)((is_final ? 0x80 : 0x00) | ((u8)(op_code)&0xf)) }; + m_impl->send(ReadonlyBytes(frame_head, 1)); + // Section 5.1 : a client MUST mask all frames that it sends to the server + bool has_mask = true; + if (payload.size() > NumericLimits<u64>::max()) { + // FIXME: We can technically stream this via non-final packets. + TODO(); + } else if (payload.size() > NumericLimits<u16>::max()) { + // Send (the 'mask' flag + 127) + the 8-byte payload length + if constexpr (sizeof(size_t) >= 64) { + u8 payload_length[9] = { + (u8)((has_mask ? 0x80 : 0x00) | 127), + (u8)((payload.size() >> 56) & 0xff), + (u8)((payload.size() >> 48) & 0xff), + (u8)((payload.size() >> 40) & 0xff), + (u8)((payload.size() >> 32) & 0xff), + (u8)((payload.size() >> 24) & 0xff), + (u8)((payload.size() >> 16) & 0xff), + (u8)((payload.size() >> 8) & 0xff), + (u8)((payload.size() >> 0) & 0xff), + }; + m_impl->send(ReadonlyBytes(payload_length, 9)); + } else { + u8 payload_length[9] = { + (u8)((has_mask ? 0x80 : 0x00) | 127), + 0, + 0, + 0, + 0, + (u8)((payload.size() >> 24) & 0xff), + (u8)((payload.size() >> 16) & 0xff), + (u8)((payload.size() >> 8) & 0xff), + (u8)((payload.size() >> 0) & 0xff), + }; + m_impl->send(ReadonlyBytes(payload_length, 9)); + } + } else if (payload.size() >= 126) { + // Send (the 'mask' flag + 126) + the 2-byte payload length + u8 payload_length[3] = { + (u8)((has_mask ? 0x80 : 0x00) | 126), + (u8)((payload.size() >> 8) & 0xff), + (u8)((payload.size() >> 0) & 0xff), + }; + m_impl->send(ReadonlyBytes(payload_length, 3)); + } else { + // Send the mask flag + the payload in a single byte + u8 payload_length[1] = { + (u8)((has_mask ? 0x80 : 0x00) | (u8)(payload.size() & 0x7f)), + }; + m_impl->send(ReadonlyBytes(payload_length, 1)); + } + if (has_mask) { + // Section 10.3 : + // > Clients MUST choose a new masking key for each frame, using an algorithm + // > that cannot be predicted by end applications that provide data + u8 masking_key[4]; + fill_with_random(masking_key, 4); + m_impl->send(ReadonlyBytes(masking_key, 4)); + // Mask the payload + auto masked_payload = ByteBuffer::create_uninitialized(payload.size()); + for (size_t i = 0; i < payload.size(); ++i) { + masked_payload[i] = payload[i] ^ (masking_key[i % 4]); + } + m_impl->send(masked_payload); + } else { + m_impl->send(payload); + } +} + +void WebSocket::fatal_error(WebSocket::Error error) +{ + m_state = WebSocket::InternalState::Errored; + notify_error(error); + discard_connection(); +} + +void WebSocket::discard_connection() +{ + VERIFY(m_impl); + m_impl->discard_connection(); + m_impl = nullptr; +} + +void WebSocket::notify_open() +{ + if (!on_open) + return; + on_open(); +} + +void WebSocket::notify_close(u16 code, String reason, bool was_clean) +{ + if (!on_close) + return; + on_close(code, reason, was_clean); +} + +void WebSocket::notify_error(WebSocket::Error error) +{ + if (!on_error) + return; + on_error(error); +} + +void WebSocket::notify_message(Message message) +{ + if (!on_message) + return; + on_message(message); +} + +} diff --git a/Userland/Libraries/LibWebSocket/WebSocket.h b/Userland/Libraries/LibWebSocket/WebSocket.h new file mode 100644 index 0000000000..4db9af113b --- /dev/null +++ b/Userland/Libraries/LibWebSocket/WebSocket.h @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2021, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Span.h> +#include <LibCore/Object.h> +#include <LibWebSocket/ConnectionInfo.h> +#include <LibWebSocket/Impl/AbstractWebSocketImpl.h> +#include <LibWebSocket/Message.h> + +namespace WebSocket { + +enum class ReadyState { + Connecting = 0, + Open = 1, + Closing = 2, + Closed = 3, +}; + +class WebSocket final : public Core::Object { + C_OBJECT(WebSocket) +public: + static NonnullRefPtr<WebSocket> create(ConnectionInfo); + virtual ~WebSocket() override; + + URL const& url() const { return m_connection.url(); } + + ReadyState ready_state(); + + // Call this to start the WebSocket connection. + void start(); + + // This can only be used if the `ready_state` is `ReadyState::Open` + void send(Message); + + // This can only be used if the `ready_state` is `ReadyState::Open` + void close(u16 code = 1005, String reason = {}); + + Function<void()> on_open; + Function<void(u16 code, String reason, bool was_clean)> on_close; + Function<void(Message message)> on_message; + + enum class Error { + CouldNotEstablishConnection, + ConnectionUpgradeFailed, + ServerClosedSocket, + }; + + Function<void(Error)> on_error; + +private: + explicit WebSocket(ConnectionInfo); + + // As defined in section 5.2 + enum class OpCode : u8 { + Continuation = 0x0, + Text = 0x1, + Binary = 0x2, + ConnectionClose = 0x8, + Ping = 0x9, + Pong = 0xA, + }; + + void drain_read(); + + void send_client_handshake(); + void read_server_handshake(); + + void read_frame(); + void send_frame(OpCode, ReadonlyBytes, bool is_final); + + void notify_open(); + void notify_close(u16 code, String reason, bool was_clean); + void notify_error(Error); + void notify_message(Message); + + void fatal_error(Error); + void discard_connection(); + + enum class InternalState { + NotStarted, + EstablishingProtocolConnection, + SendingClientHandshake, + WaitingForServerHandshake, + Open, + Closing, + Closed, + Errored, + }; + + InternalState m_state { InternalState::NotStarted }; + + String m_websocket_key; + bool m_has_read_server_handshake_first_line { false }; + bool m_has_read_server_handshake_upgrade { false }; + bool m_has_read_server_handshake_connection { false }; + bool m_has_read_server_handshake_accept { false }; + + u16 m_last_close_code { 1005 }; + String m_last_close_message; + + ConnectionInfo m_connection; + RefPtr<AbstractWebSocketImpl> m_impl; +}; + +} |