/************************************************************************************ * * D++, A Lightweight C++ library for Discord * * SPDX-License-Identifier: Apache-2.0 * Copyright 2021 Craig Edwards and D++ contributors * (https://github.com/brainboxdotcc/DPP/graphs/contributors) * * 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. * ************************************************************************************/ #pragma once #include #include #include #include namespace dpp { /** * @brief Websocket protocol types available on Discord */ enum websocket_protocol_t : uint8_t { /** * @brief JSON data, text, UTF-8 character set */ ws_json = 0, /** * @brief Erlang Term Format (ETF) binary protocol */ ws_etf = 1 }; /** * @brief Websocket connection status */ enum ws_state : uint8_t { /** * @brief Sending/receiving HTTP headers, acting as a standard HTTP connection. * This is the state prior to receiving "HTTP/1.1 101 Switching Protocols" from the * server side. */ HTTP_HEADERS, /** * @brief Connected as a websocket, and "upgraded". Now talking using binary frames. */ CONNECTED }; /** * @brief Low-level websocket opcodes for frames */ enum ws_opcode : uint8_t { /** * @brief Continuation. */ OP_CONTINUATION = 0x00, /** * @brief Text frame. */ OP_TEXT = 0x01, /** * @brief Binary frame. */ OP_BINARY = 0x02, /** * @brief Close notification with close code. */ OP_CLOSE = 0x08, /** * @brief Low level ping. */ OP_PING = 0x09, /** * @brief Low level pong. */ OP_PONG = 0x0a, /** * @brief Automatic selection of type */ OP_AUTO = 0xff, }; /** * @brief Implements a websocket client based on the SSL client */ class DPP_EXPORT websocket_client : public ssl_connection { /** * @brief Connection key used in the HTTP headers */ std::string key; /** * @brief Current websocket state */ ws_state state; /** * @brief Path part of URL for websocket */ std::string path; /** * @brief Data opcode, represents the type of frames we send */ ws_opcode data_opcode; /** * @brief HTTP headers received on connecting/upgrading */ std::map http_headers; /** * @brief Parse headers for a websocket frame from the buffer. * @param buffer The buffer to operate on. Will modify the string removing completed items from the head of the queue * @return true if a complete header has been received */ bool parseheader(std::string& buffer); /** * @brief Fill a header for outbound messages * @param outbuf The raw frame to fill * @param sendlength The size of the data to encapsulate * @param opcode the ws_opcode to send in the header * @return size of filled header */ size_t fill_header(unsigned char* outbuf, size_t sendlength, ws_opcode opcode); /** * @brief Handle ping requests. * @param payload The ping payload, to be returned as-is for a pong */ void handle_ping(const std::string& payload); protected: /** * @brief Connect to websocket server */ virtual void connect() override; /** * @brief Get websocket state * @return websocket state */ [[nodiscard]] ws_state get_state() const; /** * @brief If true the connection timed out while waiting, * when waiting for SSL negotiation, TCP connect(), or HTTP. */ bool timed_out; /** * @brief Time at which the connection should be abandoned, * if we are still connecting or negotiating with a HTTP server */ time_t timeout; public: /** * @brief Connect to a specific websocket server. * @param creator Creating cluster * @param hostname Hostname to connect to * @param port Port to connect to * @param urlpath The URL path components of the HTTP request to send * @param opcode The encoding type to use, either OP_BINARY or OP_TEXT * @note This just indicates the default for frames sent. Certain sockets, * such as voice websockets, may send a combination of OP_TEXT and OP_BINARY * frames, whereas shard websockets will only ever send OP_BINARY for ETF and * OP_TEXT for JSON. */ websocket_client(cluster* creator, const std::string& hostname, const std::string& port = "443", const std::string& urlpath = "", ws_opcode opcode = OP_BINARY); /** * @brief Destroy the websocket client object */ virtual ~websocket_client() = default; /** * @brief Write to websocket. Encapsulates data in frames if the status is CONNECTED. * @param data The data to send. * @param _opcode The opcode of the data to send, either binary or text. The default * is to use the socket's opcode as set in the constructor. */ virtual void write(const std::string_view data, ws_opcode _opcode = OP_AUTO); /** * @brief Processes incoming frames from the SSL socket input buffer. * @param buffer The buffer contents. Can modify this value removing the head elements when processed. */ virtual bool handle_buffer(std::string& buffer) override; /** * @brief Close websocket */ virtual void close() override; /** * @brief Receives raw frame content only without headers * * @param buffer The buffer contents * @param opcode Frame type, e.g. OP_TEXT, OP_BINARY * @return True if the frame was successfully handled. False if no valid frame is in the buffer. */ virtual bool handle_frame(const std::string& buffer, ws_opcode opcode); /** * @brief Called upon error frame. * * @param errorcode The error code from the websocket server */ virtual void error(uint32_t errorcode); /** * @brief Fires every second from the underlying socket I/O loop, used for sending websocket pings */ virtual void one_second_timer() override; /** * @brief Send OP_CLOSE error code 1000 to the other side of the connection. * This indicates graceful close. * @note This informs Discord to invalidate the session, you cannot resume if you send this */ void send_close_packet(); /** * @brief Called on HTTP socket closure */ virtual void on_disconnect(); }; }