initial commit, 4.5 stable
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
This commit is contained in:
50
modules/websocket/SCsub
Normal file
50
modules/websocket/SCsub
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
from misc.utility.scons_hints import *
|
||||
|
||||
Import("env")
|
||||
Import("env_modules")
|
||||
|
||||
env_ws = env_modules.Clone()
|
||||
|
||||
thirdparty_obj = []
|
||||
|
||||
if env["platform"] == "web":
|
||||
# Our JavaScript/C++ interface.
|
||||
env.AddJSLibraries(["library_godot_websocket.js"])
|
||||
|
||||
elif env["builtin_wslay"]:
|
||||
# Thirdparty source files
|
||||
thirdparty_dir = "#thirdparty/wslay/"
|
||||
thirdparty_sources = [
|
||||
"wslay_net.c",
|
||||
"wslay_event.c",
|
||||
"wslay_queue.c",
|
||||
"wslay_frame.c",
|
||||
]
|
||||
thirdparty_sources = [thirdparty_dir + s for s in thirdparty_sources]
|
||||
|
||||
env_ws.Prepend(CPPEXTPATH=[thirdparty_dir])
|
||||
env_ws.Append(CPPDEFINES=["HAVE_CONFIG_H"])
|
||||
|
||||
if env["platform"] == "windows":
|
||||
env_ws.Append(CPPDEFINES=["HAVE_WINSOCK2_H"])
|
||||
else:
|
||||
env_ws.Append(CPPDEFINES=["HAVE_NETINET_IN_H"])
|
||||
|
||||
env_thirdparty = env_ws.Clone()
|
||||
env_thirdparty.disable_warnings()
|
||||
env_thirdparty.add_source_files(thirdparty_obj, thirdparty_sources)
|
||||
env.modules_sources += thirdparty_obj
|
||||
|
||||
|
||||
# Godot source files
|
||||
|
||||
module_obj = []
|
||||
|
||||
env_ws.add_source_files(module_obj, "*.cpp")
|
||||
if env.editor_build:
|
||||
env_ws.add_source_files(module_obj, "editor/*.cpp")
|
||||
env.modules_sources += module_obj
|
||||
|
||||
# Needed to force rebuilding the module files when the thirdparty library is updated.
|
||||
env.Depends(module_obj, thirdparty_obj)
|
19
modules/websocket/config.py
Normal file
19
modules/websocket/config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
def can_build(env, platform):
|
||||
return True
|
||||
|
||||
|
||||
def configure(env):
|
||||
pass
|
||||
|
||||
|
||||
def get_doc_classes():
|
||||
return [
|
||||
"WebSocketClient",
|
||||
"WebSocketMultiplayerPeer",
|
||||
"WebSocketPeer",
|
||||
"WebSocketServer",
|
||||
]
|
||||
|
||||
|
||||
def get_doc_path():
|
||||
return "doc_classes"
|
73
modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml
Normal file
73
modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<class name="WebSocketMultiplayerPeer" inherits="MultiplayerPeer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
|
||||
<brief_description>
|
||||
Base class for WebSocket server and client.
|
||||
</brief_description>
|
||||
<description>
|
||||
Base class for WebSocket server and client, allowing them to be used as multiplayer peer for the [MultiplayerAPI].
|
||||
[b]Note:[/b] When exporting to Android, make sure to enable the [code]INTERNET[/code] permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android.
|
||||
</description>
|
||||
<tutorials>
|
||||
</tutorials>
|
||||
<methods>
|
||||
<method name="create_client">
|
||||
<return type="int" enum="Error" />
|
||||
<param index="0" name="url" type="String" />
|
||||
<param index="1" name="tls_client_options" type="TLSOptions" default="null" />
|
||||
<description>
|
||||
Starts a new multiplayer client connecting to the given [param url]. TLS certificates will be verified against the hostname when connecting using the [code]wss://[/code] protocol. You can pass the optional [param tls_client_options] parameter to customize the trusted certification authorities, or disable the common name verification. See [method TLSOptions.client] and [method TLSOptions.client_unsafe].
|
||||
[b]Note:[/b] It is recommended to specify the scheme part of the URL, i.e. the [param url] should start with either [code]ws://[/code] or [code]wss://[/code].
|
||||
</description>
|
||||
</method>
|
||||
<method name="create_server">
|
||||
<return type="int" enum="Error" />
|
||||
<param index="0" name="port" type="int" />
|
||||
<param index="1" name="bind_address" type="String" default=""*"" />
|
||||
<param index="2" name="tls_server_options" type="TLSOptions" default="null" />
|
||||
<description>
|
||||
Starts a new multiplayer server listening on the given [param port]. You can optionally specify a [param bind_address], and provide valid [param tls_server_options] to use TLS. See [method TLSOptions.server].
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_peer" qualifiers="const">
|
||||
<return type="WebSocketPeer" />
|
||||
<param index="0" name="peer_id" type="int" />
|
||||
<description>
|
||||
Returns the [WebSocketPeer] associated to the given [param peer_id].
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_peer_address" qualifiers="const">
|
||||
<return type="String" />
|
||||
<param index="0" name="id" type="int" />
|
||||
<description>
|
||||
Returns the IP address of the given peer.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_peer_port" qualifiers="const">
|
||||
<return type="int" />
|
||||
<param index="0" name="id" type="int" />
|
||||
<description>
|
||||
Returns the remote port of the given peer.
|
||||
</description>
|
||||
</method>
|
||||
</methods>
|
||||
<members>
|
||||
<member name="handshake_headers" type="PackedStringArray" setter="set_handshake_headers" getter="get_handshake_headers" default="PackedStringArray()">
|
||||
The extra headers to use during handshake. See [member WebSocketPeer.handshake_headers] for more details.
|
||||
</member>
|
||||
<member name="handshake_timeout" type="float" setter="set_handshake_timeout" getter="get_handshake_timeout" default="3.0">
|
||||
The maximum time each peer can stay in a connecting state before being dropped.
|
||||
</member>
|
||||
<member name="inbound_buffer_size" type="int" setter="set_inbound_buffer_size" getter="get_inbound_buffer_size" default="65535">
|
||||
The inbound buffer size for connected peers. See [member WebSocketPeer.inbound_buffer_size] for more details.
|
||||
</member>
|
||||
<member name="max_queued_packets" type="int" setter="set_max_queued_packets" getter="get_max_queued_packets" default="4096">
|
||||
The maximum number of queued packets for connected peers. See [member WebSocketPeer.max_queued_packets] for more details.
|
||||
</member>
|
||||
<member name="outbound_buffer_size" type="int" setter="set_outbound_buffer_size" getter="get_outbound_buffer_size" default="65535">
|
||||
The outbound buffer size for connected peers. See [member WebSocketPeer.outbound_buffer_size] for more details.
|
||||
</member>
|
||||
<member name="supported_protocols" type="PackedStringArray" setter="set_supported_protocols" getter="get_supported_protocols" default="PackedStringArray()">
|
||||
The supported WebSocket sub-protocols. See [member WebSocketPeer.supported_protocols] for more details.
|
||||
</member>
|
||||
</members>
|
||||
</class>
|
196
modules/websocket/doc_classes/WebSocketPeer.xml
Normal file
196
modules/websocket/doc_classes/WebSocketPeer.xml
Normal file
@@ -0,0 +1,196 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<class name="WebSocketPeer" inherits="PacketPeer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
|
||||
<brief_description>
|
||||
A WebSocket connection.
|
||||
</brief_description>
|
||||
<description>
|
||||
This class represents WebSocket connection, and can be used as a WebSocket client (RFC 6455-compliant) or as a remote peer of a WebSocket server.
|
||||
You can send WebSocket binary frames using [method PacketPeer.put_packet], and WebSocket text frames using [method send] (prefer text frames when interacting with text-based API). You can check the frame type of the last packet via [method was_string_packet].
|
||||
To start a WebSocket client, first call [method connect_to_url], then regularly call [method poll] (e.g. during [Node] process). You can query the socket state via [method get_ready_state], get the number of pending packets using [method PacketPeer.get_available_packet_count], and retrieve them via [method PacketPeer.get_packet].
|
||||
[codeblocks]
|
||||
[gdscript]
|
||||
extends Node
|
||||
|
||||
var socket = WebSocketPeer.new()
|
||||
|
||||
func _ready():
|
||||
socket.connect_to_url("wss://example.com")
|
||||
|
||||
func _process(delta):
|
||||
socket.poll()
|
||||
var state = socket.get_ready_state()
|
||||
if state == WebSocketPeer.STATE_OPEN:
|
||||
while socket.get_available_packet_count():
|
||||
print("Packet: ", socket.get_packet())
|
||||
elif state == WebSocketPeer.STATE_CLOSING:
|
||||
# Keep polling to achieve proper close.
|
||||
pass
|
||||
elif state == WebSocketPeer.STATE_CLOSED:
|
||||
var code = socket.get_close_code()
|
||||
var reason = socket.get_close_reason()
|
||||
print("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1])
|
||||
set_process(false) # Stop processing.
|
||||
[/gdscript]
|
||||
[/codeblocks]
|
||||
To use the peer as part of a WebSocket server refer to [method accept_stream] and the online tutorial.
|
||||
</description>
|
||||
<tutorials>
|
||||
</tutorials>
|
||||
<methods>
|
||||
<method name="accept_stream">
|
||||
<return type="int" enum="Error" />
|
||||
<param index="0" name="stream" type="StreamPeer" />
|
||||
<description>
|
||||
Accepts a peer connection performing the HTTP handshake as a WebSocket server. The [param stream] must be a valid TCP stream retrieved via [method TCPServer.take_connection], or a TLS stream accepted via [method StreamPeerTLS.accept_stream].
|
||||
[b]Note:[/b] Not supported in Web exports due to browsers' restrictions.
|
||||
</description>
|
||||
</method>
|
||||
<method name="close">
|
||||
<return type="void" />
|
||||
<param index="0" name="code" type="int" default="1000" />
|
||||
<param index="1" name="reason" type="String" default="""" />
|
||||
<description>
|
||||
Closes this WebSocket connection. [param code] is the status code for the closure (see RFC 6455 section 7.4 for a list of valid status codes). [param reason] is the human readable reason for closing the connection (can be any UTF-8 string that's smaller than 123 bytes). If [param code] is negative, the connection will be closed immediately without notifying the remote peer.
|
||||
[b]Note:[/b] To achieve a clean close, you will need to keep polling until [constant STATE_CLOSED] is reached.
|
||||
[b]Note:[/b] The Web export might not support all status codes. Please refer to browser-specific documentation for more details.
|
||||
</description>
|
||||
</method>
|
||||
<method name="connect_to_url">
|
||||
<return type="int" enum="Error" />
|
||||
<param index="0" name="url" type="String" />
|
||||
<param index="1" name="tls_client_options" type="TLSOptions" default="null" />
|
||||
<description>
|
||||
Connects to the given URL. TLS certificates will be verified against the hostname when connecting using the [code]wss://[/code] protocol. You can pass the optional [param tls_client_options] parameter to customize the trusted certification authorities, or disable the common name verification. See [method TLSOptions.client] and [method TLSOptions.client_unsafe].
|
||||
[b]Note:[/b] This method is non-blocking, and will return [constant OK] before the connection is established as long as the provided parameters are valid and the peer is not in an invalid state (e.g. already connected). Regularly call [method poll] (e.g. during [Node] process) and check the result of [method get_ready_state] to know whether the connection succeeds or fails.
|
||||
[b]Note:[/b] To avoid mixed content warnings or errors in Web, you may have to use a [param url] that starts with [code]wss://[/code] (secure) instead of [code]ws://[/code]. When doing so, make sure to use the fully qualified domain name that matches the one defined in the server's TLS certificate. Do not connect directly via the IP address for [code]wss://[/code] connections, as it won't match with the TLS certificate.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_close_code" qualifiers="const">
|
||||
<return type="int" />
|
||||
<description>
|
||||
Returns the received WebSocket close frame status code, or [code]-1[/code] when the connection was not cleanly closed. Only call this method when [method get_ready_state] returns [constant STATE_CLOSED].
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_close_reason" qualifiers="const">
|
||||
<return type="String" />
|
||||
<description>
|
||||
Returns the received WebSocket close frame status reason string. Only call this method when [method get_ready_state] returns [constant STATE_CLOSED].
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_connected_host" qualifiers="const">
|
||||
<return type="String" />
|
||||
<description>
|
||||
Returns the IP address of the connected peer.
|
||||
[b]Note:[/b] Not available in the Web export.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_connected_port" qualifiers="const">
|
||||
<return type="int" />
|
||||
<description>
|
||||
Returns the remote port of the connected peer.
|
||||
[b]Note:[/b] Not available in the Web export.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_current_outbound_buffered_amount" qualifiers="const">
|
||||
<return type="int" />
|
||||
<description>
|
||||
Returns the current amount of data in the outbound websocket buffer. [b]Note:[/b] Web exports use WebSocket.bufferedAmount, while other platforms use an internal buffer.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_ready_state" qualifiers="const">
|
||||
<return type="int" enum="WebSocketPeer.State" />
|
||||
<description>
|
||||
Returns the ready state of the connection.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_requested_url" qualifiers="const">
|
||||
<return type="String" />
|
||||
<description>
|
||||
Returns the URL requested by this peer. The URL is derived from the [code]url[/code] passed to [method connect_to_url] or from the HTTP headers when acting as server (i.e. when using [method accept_stream]).
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_selected_protocol" qualifiers="const">
|
||||
<return type="String" />
|
||||
<description>
|
||||
Returns the selected WebSocket sub-protocol for this connection or an empty string if the sub-protocol has not been selected yet.
|
||||
</description>
|
||||
</method>
|
||||
<method name="poll">
|
||||
<return type="void" />
|
||||
<description>
|
||||
Updates the connection state and receive incoming packets. Call this function regularly to keep it in a clean state.
|
||||
</description>
|
||||
</method>
|
||||
<method name="send">
|
||||
<return type="int" enum="Error" />
|
||||
<param index="0" name="message" type="PackedByteArray" />
|
||||
<param index="1" name="write_mode" type="int" enum="WebSocketPeer.WriteMode" default="1" />
|
||||
<description>
|
||||
Sends the given [param message] using the desired [param write_mode]. When sending a [String], prefer using [method send_text].
|
||||
</description>
|
||||
</method>
|
||||
<method name="send_text">
|
||||
<return type="int" enum="Error" />
|
||||
<param index="0" name="message" type="String" />
|
||||
<description>
|
||||
Sends the given [param message] using WebSocket text mode. Prefer this method over [method PacketPeer.put_packet] when interacting with third-party text-based API (e.g. when using [JSON] formatted messages).
|
||||
</description>
|
||||
</method>
|
||||
<method name="set_no_delay">
|
||||
<return type="void" />
|
||||
<param index="0" name="enabled" type="bool" />
|
||||
<description>
|
||||
Disable Nagle's algorithm on the underlying TCP socket (default). See [method StreamPeerTCP.set_no_delay] for more information.
|
||||
[b]Note:[/b] Not available in the Web export.
|
||||
</description>
|
||||
</method>
|
||||
<method name="was_string_packet" qualifiers="const">
|
||||
<return type="bool" />
|
||||
<description>
|
||||
Returns [code]true[/code] if the last received packet was sent as a text payload. See [enum WriteMode].
|
||||
</description>
|
||||
</method>
|
||||
</methods>
|
||||
<members>
|
||||
<member name="handshake_headers" type="PackedStringArray" setter="set_handshake_headers" getter="get_handshake_headers" default="PackedStringArray()">
|
||||
The extra HTTP headers to be sent during the WebSocket handshake.
|
||||
[b]Note:[/b] Not supported in Web exports due to browsers' restrictions.
|
||||
</member>
|
||||
<member name="heartbeat_interval" type="float" setter="set_heartbeat_interval" getter="get_heartbeat_interval" default="0.0">
|
||||
The interval (in seconds) at which the peer will automatically send WebSocket "ping" control frames. When set to [code]0[/code], no "ping" control frames will be sent.
|
||||
[b]Note:[/b] Has no effect in Web exports due to browser restrictions.
|
||||
</member>
|
||||
<member name="inbound_buffer_size" type="int" setter="set_inbound_buffer_size" getter="get_inbound_buffer_size" default="65535">
|
||||
The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the inbound packets).
|
||||
</member>
|
||||
<member name="max_queued_packets" type="int" setter="set_max_queued_packets" getter="get_max_queued_packets" default="4096">
|
||||
The maximum amount of packets that will be allowed in the queues (both inbound and outbound).
|
||||
</member>
|
||||
<member name="outbound_buffer_size" type="int" setter="set_outbound_buffer_size" getter="get_outbound_buffer_size" default="65535">
|
||||
The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the outbound packets).
|
||||
</member>
|
||||
<member name="supported_protocols" type="PackedStringArray" setter="set_supported_protocols" getter="get_supported_protocols" default="PackedStringArray()">
|
||||
The WebSocket sub-protocols allowed during the WebSocket handshake.
|
||||
</member>
|
||||
</members>
|
||||
<constants>
|
||||
<constant name="WRITE_MODE_TEXT" value="0" enum="WriteMode">
|
||||
Specifies that WebSockets messages should be transferred as text payload (only valid UTF-8 is allowed).
|
||||
</constant>
|
||||
<constant name="WRITE_MODE_BINARY" value="1" enum="WriteMode">
|
||||
Specifies that WebSockets messages should be transferred as binary payload (any byte combination is allowed).
|
||||
</constant>
|
||||
<constant name="STATE_CONNECTING" value="0" enum="State">
|
||||
Socket has been created. The connection is not yet open.
|
||||
</constant>
|
||||
<constant name="STATE_OPEN" value="1" enum="State">
|
||||
The connection is open and ready to communicate.
|
||||
</constant>
|
||||
<constant name="STATE_CLOSING" value="2" enum="State">
|
||||
The connection is in the process of closing. This means a close request has been sent to the remote peer but confirmation has not been received.
|
||||
</constant>
|
||||
<constant name="STATE_CLOSED" value="3" enum="State">
|
||||
The connection is closed or couldn't be opened.
|
||||
</constant>
|
||||
</constants>
|
||||
</class>
|
135
modules/websocket/editor/editor_debugger_server_websocket.cpp
Normal file
135
modules/websocket/editor/editor_debugger_server_websocket.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
/**************************************************************************/
|
||||
/* editor_debugger_server_websocket.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "editor_debugger_server_websocket.h"
|
||||
|
||||
#include "../remote_debugger_peer_websocket.h"
|
||||
|
||||
#include "editor/editor_log.h"
|
||||
#include "editor/editor_node.h"
|
||||
#include "editor/settings/editor_settings.h"
|
||||
|
||||
void EditorDebuggerServerWebSocket::poll() {
|
||||
if (pending_peer.is_null() && tcp_server->is_connection_available()) {
|
||||
Ref<WebSocketPeer> peer = Ref<WebSocketPeer>(WebSocketPeer::create());
|
||||
ERR_FAIL_COND(peer.is_null()); // Bug.
|
||||
|
||||
Vector<String> ws_protocols;
|
||||
ws_protocols.push_back("binary"); // Compatibility for emscripten TCP-to-WebSocket.
|
||||
peer->set_supported_protocols(ws_protocols);
|
||||
|
||||
Error err = peer->accept_stream(tcp_server->take_connection());
|
||||
if (err == OK) {
|
||||
pending_timer = OS::get_singleton()->get_ticks_msec();
|
||||
pending_peer = peer;
|
||||
}
|
||||
}
|
||||
if (pending_peer.is_valid() && pending_peer->get_ready_state() != WebSocketPeer::STATE_OPEN) {
|
||||
pending_peer->poll();
|
||||
WebSocketPeer::State ready_state = pending_peer->get_ready_state();
|
||||
if (ready_state != WebSocketPeer::STATE_CONNECTING && ready_state != WebSocketPeer::STATE_OPEN) {
|
||||
pending_peer.unref(); // Failed.
|
||||
}
|
||||
if (ready_state == WebSocketPeer::STATE_CONNECTING && OS::get_singleton()->get_ticks_msec() - pending_timer > 3000) {
|
||||
pending_peer.unref(); // Timeout.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String EditorDebuggerServerWebSocket::get_uri() const {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
Error EditorDebuggerServerWebSocket::start(const String &p_uri) {
|
||||
// Default host and port
|
||||
String bind_host = (String)EDITOR_GET("network/debug/remote_host");
|
||||
int bind_port = (int)EDITOR_GET("network/debug/remote_port");
|
||||
|
||||
// Optionally override
|
||||
if (!p_uri.is_empty() && p_uri != "ws://") {
|
||||
String scheme, path, fragment;
|
||||
Error err = p_uri.parse_url(scheme, bind_host, bind_port, path, fragment);
|
||||
ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER);
|
||||
ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
// Try listening on ports
|
||||
const int max_attempts = 5;
|
||||
for (int attempt = 1;; ++attempt) {
|
||||
const Error err = tcp_server->listen(bind_port, bind_host);
|
||||
if (err == OK) {
|
||||
break;
|
||||
}
|
||||
if (attempt >= max_attempts) {
|
||||
EditorNode::get_log()->add_message(vformat("Cannot listen on port %d, remote debugging unavailable.", bind_port), EditorLog::MSG_TYPE_ERROR);
|
||||
return err;
|
||||
}
|
||||
int last_port = bind_port++;
|
||||
EditorNode::get_log()->add_message(vformat("Cannot listen on port %d, trying %d instead.", last_port, bind_port), EditorLog::MSG_TYPE_WARNING);
|
||||
}
|
||||
|
||||
// Endpoint that the client should connect to
|
||||
endpoint = vformat("ws://%s:%d", bind_host, bind_port);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
void EditorDebuggerServerWebSocket::stop() {
|
||||
pending_peer.unref();
|
||||
tcp_server->stop();
|
||||
}
|
||||
|
||||
bool EditorDebuggerServerWebSocket::is_active() const {
|
||||
return tcp_server->is_listening();
|
||||
}
|
||||
|
||||
bool EditorDebuggerServerWebSocket::is_connection_available() const {
|
||||
return pending_peer.is_valid() && pending_peer->get_ready_state() == WebSocketPeer::STATE_OPEN;
|
||||
}
|
||||
|
||||
Ref<RemoteDebuggerPeer> EditorDebuggerServerWebSocket::take_connection() {
|
||||
ERR_FAIL_COND_V(!is_connection_available(), Ref<RemoteDebuggerPeer>());
|
||||
RemoteDebuggerPeer *peer = memnew(RemoteDebuggerPeerWebSocket(pending_peer));
|
||||
pending_peer.unref();
|
||||
return peer;
|
||||
}
|
||||
|
||||
EditorDebuggerServerWebSocket::EditorDebuggerServerWebSocket() {
|
||||
tcp_server.instantiate();
|
||||
}
|
||||
|
||||
EditorDebuggerServerWebSocket::~EditorDebuggerServerWebSocket() {
|
||||
stop();
|
||||
}
|
||||
|
||||
EditorDebuggerServer *EditorDebuggerServerWebSocket::create(const String &p_protocol) {
|
||||
ERR_FAIL_COND_V(p_protocol != "ws://", nullptr);
|
||||
return memnew(EditorDebuggerServerWebSocket);
|
||||
}
|
63
modules/websocket/editor/editor_debugger_server_websocket.h
Normal file
63
modules/websocket/editor/editor_debugger_server_websocket.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/**************************************************************************/
|
||||
/* editor_debugger_server_websocket.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../websocket_peer.h"
|
||||
|
||||
#include "core/io/tcp_server.h"
|
||||
#include "editor/debugger/editor_debugger_server.h"
|
||||
|
||||
class EditorDebuggerServerWebSocket : public EditorDebuggerServer {
|
||||
GDCLASS(EditorDebuggerServerWebSocket, EditorDebuggerServer);
|
||||
|
||||
private:
|
||||
Ref<TCPServer> tcp_server;
|
||||
Ref<WebSocketPeer> pending_peer;
|
||||
uint64_t pending_timer = 0;
|
||||
String endpoint;
|
||||
|
||||
public:
|
||||
static EditorDebuggerServer *create(const String &p_protocol);
|
||||
|
||||
void _peer_connected(int p_peer, String p_protocol);
|
||||
void _peer_disconnected(int p_peer, bool p_was_clean);
|
||||
|
||||
virtual void poll() override;
|
||||
virtual String get_uri() const override;
|
||||
virtual Error start(const String &p_uri = "") override;
|
||||
virtual void stop() override;
|
||||
virtual bool is_active() const override;
|
||||
virtual bool is_connection_available() const override;
|
||||
virtual Ref<RemoteDebuggerPeer> take_connection() override;
|
||||
|
||||
EditorDebuggerServerWebSocket();
|
||||
~EditorDebuggerServerWebSocket();
|
||||
};
|
245
modules/websocket/emws_peer.cpp
Normal file
245
modules/websocket/emws_peer.cpp
Normal file
@@ -0,0 +1,245 @@
|
||||
/**************************************************************************/
|
||||
/* emws_peer.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "emws_peer.h"
|
||||
|
||||
#ifdef WEB_ENABLED
|
||||
|
||||
#include "core/io/ip.h"
|
||||
|
||||
void EMWSPeer::_esws_on_connect(void *p_obj, char *p_proto) {
|
||||
EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj);
|
||||
peer->ready_state = STATE_OPEN;
|
||||
peer->selected_protocol.clear();
|
||||
peer->selected_protocol.append_utf8(p_proto);
|
||||
}
|
||||
|
||||
void EMWSPeer::_esws_on_message(void *p_obj, const uint8_t *p_data, int p_data_size, int p_is_string) {
|
||||
EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj);
|
||||
uint8_t is_string = p_is_string ? 1 : 0;
|
||||
peer->in_buffer.write_packet(p_data, p_data_size, &is_string);
|
||||
}
|
||||
|
||||
void EMWSPeer::_esws_on_error(void *p_obj) {
|
||||
EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj);
|
||||
peer->ready_state = STATE_CLOSED;
|
||||
}
|
||||
|
||||
void EMWSPeer::_esws_on_close(void *p_obj, int p_code, const char *p_reason, int p_was_clean) {
|
||||
EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj);
|
||||
peer->close_code = p_code;
|
||||
peer->close_reason.clear();
|
||||
peer->close_reason.append_utf8(p_reason);
|
||||
peer->ready_state = STATE_CLOSED;
|
||||
}
|
||||
|
||||
Error EMWSPeer::connect_to_url(const String &p_url, Ref<TLSOptions> p_tls_options) {
|
||||
ERR_FAIL_COND_V(p_url.is_empty(), ERR_INVALID_PARAMETER);
|
||||
ERR_FAIL_COND_V(p_tls_options.is_valid() && p_tls_options->is_server(), ERR_INVALID_PARAMETER);
|
||||
ERR_FAIL_COND_V(ready_state != STATE_CLOSED && ready_state != STATE_CLOSING, ERR_ALREADY_IN_USE);
|
||||
|
||||
_clear();
|
||||
|
||||
String host;
|
||||
String path;
|
||||
String scheme;
|
||||
String fragment;
|
||||
int port = 0;
|
||||
Error err = p_url.parse_url(scheme, host, port, path, fragment);
|
||||
ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url);
|
||||
|
||||
if (scheme.is_empty()) {
|
||||
scheme = "ws://";
|
||||
}
|
||||
ERR_FAIL_COND_V_MSG(scheme != "ws://" && scheme != "wss://", ERR_INVALID_PARAMETER, vformat("Invalid protocol: \"%s\" (must be either \"ws://\" or \"wss://\").", scheme));
|
||||
|
||||
String proto_string;
|
||||
for (int i = 0; i < supported_protocols.size(); i++) {
|
||||
if (i != 0) {
|
||||
proto_string += ",";
|
||||
}
|
||||
proto_string += supported_protocols[i];
|
||||
}
|
||||
|
||||
if (handshake_headers.size()) {
|
||||
WARN_PRINT_ONCE("Custom headers are not supported in Web platform.");
|
||||
}
|
||||
|
||||
requested_url = scheme + host;
|
||||
|
||||
if (port && ((scheme == "ws://" && port != 80) || (scheme == "wss://" && port != 443))) {
|
||||
requested_url += ":" + String::num_int64(port);
|
||||
}
|
||||
|
||||
if (!path.is_empty()) {
|
||||
requested_url += path;
|
||||
}
|
||||
|
||||
peer_sock = godot_js_websocket_create(this, requested_url.utf8().get_data(), proto_string.utf8().get_data(), &_esws_on_connect, &_esws_on_message, &_esws_on_error, &_esws_on_close);
|
||||
if (peer_sock == -1) {
|
||||
return FAILED;
|
||||
}
|
||||
in_buffer.resize(nearest_shift((uint32_t)inbound_buffer_size), max_queued_packets);
|
||||
packet_buffer.resize(inbound_buffer_size);
|
||||
ready_state = STATE_CONNECTING;
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error EMWSPeer::accept_stream(Ref<StreamPeer> p_stream) {
|
||||
WARN_PRINT_ONCE("Acting as WebSocket server is not supported in Web platforms.");
|
||||
return ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
Error EMWSPeer::_send(const uint8_t *p_buffer, int p_buffer_size, bool p_binary) {
|
||||
ERR_FAIL_COND_V(outbound_buffer_size > 0 && (get_current_outbound_buffered_amount() + p_buffer_size >= outbound_buffer_size), ERR_OUT_OF_MEMORY);
|
||||
|
||||
if (godot_js_websocket_send(peer_sock, p_buffer, p_buffer_size, p_binary ? 1 : 0) != 0) {
|
||||
return FAILED;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error EMWSPeer::send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) {
|
||||
return _send(p_buffer, p_buffer_size, p_mode == WRITE_MODE_BINARY);
|
||||
}
|
||||
|
||||
Error EMWSPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) {
|
||||
return _send(p_buffer, p_buffer_size, true);
|
||||
}
|
||||
|
||||
Error EMWSPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) {
|
||||
if (in_buffer.packets_left() == 0) {
|
||||
return ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
int read = 0;
|
||||
Error err = in_buffer.read_packet(packet_buffer.ptrw(), packet_buffer.size(), &was_string, read);
|
||||
ERR_FAIL_COND_V(err != OK, err);
|
||||
|
||||
*r_buffer = packet_buffer.ptr();
|
||||
r_buffer_size = read;
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
int EMWSPeer::get_available_packet_count() const {
|
||||
return in_buffer.packets_left();
|
||||
}
|
||||
|
||||
int EMWSPeer::get_current_outbound_buffered_amount() const {
|
||||
if (peer_sock != -1) {
|
||||
return godot_js_websocket_buffered_amount(peer_sock);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool EMWSPeer::was_string_packet() const {
|
||||
return was_string;
|
||||
}
|
||||
|
||||
void EMWSPeer::_clear() {
|
||||
if (peer_sock != -1) {
|
||||
godot_js_websocket_destroy(peer_sock);
|
||||
peer_sock = -1;
|
||||
}
|
||||
ready_state = STATE_CLOSED;
|
||||
was_string = 0;
|
||||
close_code = -1;
|
||||
close_reason.clear();
|
||||
selected_protocol.clear();
|
||||
requested_url.clear();
|
||||
in_buffer.clear();
|
||||
packet_buffer.clear();
|
||||
}
|
||||
|
||||
void EMWSPeer::close(int p_code, String p_reason) {
|
||||
if (p_code < 0) {
|
||||
if (peer_sock != -1) {
|
||||
godot_js_websocket_destroy(peer_sock);
|
||||
peer_sock = -1;
|
||||
}
|
||||
ready_state = STATE_CLOSED;
|
||||
}
|
||||
if (ready_state == STATE_CONNECTING || ready_state == STATE_OPEN) {
|
||||
ready_state = STATE_CLOSING;
|
||||
if (peer_sock != -1) {
|
||||
godot_js_websocket_close(peer_sock, p_code, p_reason.utf8().get_data());
|
||||
} else {
|
||||
ready_state = STATE_CLOSED;
|
||||
}
|
||||
}
|
||||
in_buffer.clear();
|
||||
packet_buffer.clear();
|
||||
}
|
||||
|
||||
void EMWSPeer::poll() {
|
||||
// Automatically polled by the navigator.
|
||||
}
|
||||
|
||||
WebSocketPeer::State EMWSPeer::get_ready_state() const {
|
||||
return ready_state;
|
||||
}
|
||||
|
||||
int EMWSPeer::get_close_code() const {
|
||||
return close_code;
|
||||
}
|
||||
|
||||
String EMWSPeer::get_close_reason() const {
|
||||
return close_reason;
|
||||
}
|
||||
|
||||
String EMWSPeer::get_selected_protocol() const {
|
||||
return selected_protocol;
|
||||
}
|
||||
|
||||
String EMWSPeer::get_requested_url() const {
|
||||
return requested_url;
|
||||
}
|
||||
|
||||
IPAddress EMWSPeer::get_connected_host() const {
|
||||
ERR_FAIL_V_MSG(IPAddress(), "Not supported in Web export.");
|
||||
}
|
||||
|
||||
uint16_t EMWSPeer::get_connected_port() const {
|
||||
ERR_FAIL_V_MSG(0, "Not supported in Web export.");
|
||||
}
|
||||
|
||||
void EMWSPeer::set_no_delay(bool p_enabled) {
|
||||
ERR_FAIL_MSG("'set_no_delay' is not supported in Web export.");
|
||||
}
|
||||
|
||||
EMWSPeer::EMWSPeer() {
|
||||
}
|
||||
|
||||
EMWSPeer::~EMWSPeer() {
|
||||
_clear();
|
||||
}
|
||||
|
||||
#endif // WEB_ENABLED
|
111
modules/websocket/emws_peer.h
Normal file
111
modules/websocket/emws_peer.h
Normal file
@@ -0,0 +1,111 @@
|
||||
/**************************************************************************/
|
||||
/* emws_peer.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef WEB_ENABLED
|
||||
|
||||
#include "packet_buffer.h"
|
||||
#include "websocket_peer.h"
|
||||
|
||||
#include "core/io/packet_peer.h"
|
||||
#include "core/templates/ring_buffer.h"
|
||||
|
||||
#include <emscripten.h>
|
||||
|
||||
extern "C" {
|
||||
typedef void (*WSOnOpen)(void *p_ref, char *p_protocol);
|
||||
typedef void (*WSOnMessage)(void *p_ref, const uint8_t *p_buf, int p_buf_len, int p_is_string);
|
||||
typedef void (*WSOnClose)(void *p_ref, int p_code, const char *p_reason, int p_is_clean);
|
||||
typedef void (*WSOnError)(void *p_ref);
|
||||
|
||||
extern int godot_js_websocket_create(void *p_ref, const char *p_url, const char *p_proto, WSOnOpen p_on_open, WSOnMessage p_on_message, WSOnError p_on_error, WSOnClose p_on_close);
|
||||
extern int godot_js_websocket_send(int p_id, const uint8_t *p_buf, int p_buf_len, int p_raw);
|
||||
extern int godot_js_websocket_buffered_amount(int p_id);
|
||||
extern void godot_js_websocket_close(int p_id, int p_code, const char *p_reason);
|
||||
extern void godot_js_websocket_destroy(int p_id);
|
||||
}
|
||||
|
||||
class EMWSPeer : public WebSocketPeer {
|
||||
private:
|
||||
int peer_sock = -1;
|
||||
|
||||
State ready_state = STATE_CLOSED;
|
||||
Vector<uint8_t> packet_buffer;
|
||||
PacketBuffer<uint8_t> in_buffer;
|
||||
uint8_t was_string = 0;
|
||||
int close_code = -1;
|
||||
String close_reason;
|
||||
String selected_protocol;
|
||||
String requested_url;
|
||||
|
||||
static WebSocketPeer *_create(bool p_notify_postinitialize) { return static_cast<WebSocketPeer *>(ClassDB::creator<EMWSPeer>(p_notify_postinitialize)); }
|
||||
static void _esws_on_connect(void *obj, char *proto);
|
||||
static void _esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string);
|
||||
static void _esws_on_error(void *obj);
|
||||
static void _esws_on_close(void *obj, int code, const char *reason, int was_clean);
|
||||
|
||||
void _clear();
|
||||
Error _send(const uint8_t *p_buffer, int p_buffer_size, bool p_binary);
|
||||
|
||||
public:
|
||||
static void initialize() { WebSocketPeer::_create = EMWSPeer::_create; }
|
||||
|
||||
// PacketPeer
|
||||
virtual int get_available_packet_count() const override;
|
||||
virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size) override;
|
||||
virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override;
|
||||
virtual int get_max_packet_size() const override { return packet_buffer.size(); }
|
||||
|
||||
// WebSocketPeer
|
||||
virtual Error send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) override;
|
||||
virtual Error connect_to_url(const String &p_url, Ref<TLSOptions> p_tls_client_options) override;
|
||||
virtual Error accept_stream(Ref<StreamPeer> p_stream) override;
|
||||
virtual void close(int p_code = 1000, String p_reason = "") override;
|
||||
virtual void poll() override;
|
||||
|
||||
virtual State get_ready_state() const override;
|
||||
virtual int get_close_code() const override;
|
||||
virtual String get_close_reason() const override;
|
||||
virtual int get_current_outbound_buffered_amount() const override;
|
||||
|
||||
virtual IPAddress get_connected_host() const override;
|
||||
virtual uint16_t get_connected_port() const override;
|
||||
virtual String get_selected_protocol() const override;
|
||||
virtual String get_requested_url() const override;
|
||||
|
||||
virtual bool was_string_packet() const override;
|
||||
virtual void set_no_delay(bool p_enabled) override;
|
||||
|
||||
EMWSPeer();
|
||||
~EMWSPeer();
|
||||
};
|
||||
|
||||
#endif // WEB_ENABLED
|
206
modules/websocket/library_godot_websocket.js
Normal file
206
modules/websocket/library_godot_websocket.js
Normal file
@@ -0,0 +1,206 @@
|
||||
/**************************************************************************/
|
||||
/* library_godot_websocket.js */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
const GodotWebSocket = {
|
||||
// Our socket implementation that forwards events to C++.
|
||||
$GodotWebSocket__deps: ['$IDHandler', '$GodotRuntime'],
|
||||
$GodotWebSocket: {
|
||||
// Connection opened, report selected protocol
|
||||
_onopen: function (p_id, callback, event) {
|
||||
const ref = IDHandler.get(p_id);
|
||||
if (!ref) {
|
||||
return; // Godot object is gone.
|
||||
}
|
||||
const c_str = GodotRuntime.allocString(ref.protocol);
|
||||
callback(c_str);
|
||||
GodotRuntime.free(c_str);
|
||||
},
|
||||
|
||||
// Message received, report content and type (UTF8 vs binary)
|
||||
_onmessage: function (p_id, callback, event) {
|
||||
const ref = IDHandler.get(p_id);
|
||||
if (!ref) {
|
||||
return; // Godot object is gone.
|
||||
}
|
||||
let buffer;
|
||||
let is_string = 0;
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
buffer = new Uint8Array(event.data);
|
||||
} else if (event.data instanceof Blob) {
|
||||
GodotRuntime.error('Blob type not supported');
|
||||
return;
|
||||
} else if (typeof event.data === 'string') {
|
||||
is_string = 1;
|
||||
buffer = new TextEncoder('utf-8').encode(event.data);
|
||||
} else {
|
||||
GodotRuntime.error('Unknown message type');
|
||||
return;
|
||||
}
|
||||
const len = buffer.length * buffer.BYTES_PER_ELEMENT;
|
||||
const out = GodotRuntime.malloc(len);
|
||||
HEAPU8.set(buffer, out);
|
||||
callback(out, len, is_string);
|
||||
GodotRuntime.free(out);
|
||||
},
|
||||
|
||||
// An error happened, 'onclose' will be called after this.
|
||||
_onerror: function (p_id, callback, event) {
|
||||
const ref = IDHandler.get(p_id);
|
||||
if (!ref) {
|
||||
return; // Godot object is gone.
|
||||
}
|
||||
callback();
|
||||
},
|
||||
|
||||
// Connection is closed, this is always fired. Report close code, reason, and clean status.
|
||||
_onclose: function (p_id, callback, event) {
|
||||
const ref = IDHandler.get(p_id);
|
||||
if (!ref) {
|
||||
return; // Godot object is gone.
|
||||
}
|
||||
const c_str = GodotRuntime.allocString(event.reason);
|
||||
callback(event.code, c_str, event.wasClean ? 1 : 0);
|
||||
GodotRuntime.free(c_str);
|
||||
},
|
||||
|
||||
// Send a message
|
||||
send: function (p_id, p_data) {
|
||||
const ref = IDHandler.get(p_id);
|
||||
if (!ref || ref.readyState !== ref.OPEN) {
|
||||
return 1; // Godot object is gone or socket is not in a ready state.
|
||||
}
|
||||
ref.send(p_data);
|
||||
return 0;
|
||||
},
|
||||
|
||||
// Get current bufferedAmount
|
||||
bufferedAmount: function (p_id) {
|
||||
const ref = IDHandler.get(p_id);
|
||||
if (!ref) {
|
||||
return 0; // Godot object is gone.
|
||||
}
|
||||
return ref.bufferedAmount;
|
||||
},
|
||||
|
||||
create: function (socket, p_on_open, p_on_message, p_on_error, p_on_close) {
|
||||
const id = IDHandler.add(socket);
|
||||
socket.onopen = GodotWebSocket._onopen.bind(null, id, p_on_open);
|
||||
socket.onmessage = GodotWebSocket._onmessage.bind(null, id, p_on_message);
|
||||
socket.onerror = GodotWebSocket._onerror.bind(null, id, p_on_error);
|
||||
socket.onclose = GodotWebSocket._onclose.bind(null, id, p_on_close);
|
||||
return id;
|
||||
},
|
||||
|
||||
// Closes the JavaScript WebSocket (if not already closing) associated to a given C++ object.
|
||||
close: function (p_id, p_code, p_reason) {
|
||||
const ref = IDHandler.get(p_id);
|
||||
if (ref && ref.readyState < ref.CLOSING) {
|
||||
const code = p_code;
|
||||
const reason = p_reason;
|
||||
ref.close(code, reason);
|
||||
}
|
||||
},
|
||||
|
||||
// Deletes the reference to a C++ object (closing any connected socket if necessary).
|
||||
destroy: function (p_id) {
|
||||
const ref = IDHandler.get(p_id);
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
GodotWebSocket.close(p_id, 3001, 'destroyed');
|
||||
IDHandler.remove(p_id);
|
||||
ref.onopen = null;
|
||||
ref.onmessage = null;
|
||||
ref.onerror = null;
|
||||
ref.onclose = null;
|
||||
},
|
||||
},
|
||||
|
||||
godot_js_websocket_create__proxy: 'sync',
|
||||
godot_js_websocket_create__sig: 'iiiiiiii',
|
||||
godot_js_websocket_create: function (p_ref, p_url, p_proto, p_on_open, p_on_message, p_on_error, p_on_close) {
|
||||
const on_open = GodotRuntime.get_func(p_on_open).bind(null, p_ref);
|
||||
const on_message = GodotRuntime.get_func(p_on_message).bind(null, p_ref);
|
||||
const on_error = GodotRuntime.get_func(p_on_error).bind(null, p_ref);
|
||||
const on_close = GodotRuntime.get_func(p_on_close).bind(null, p_ref);
|
||||
const url = GodotRuntime.parseString(p_url);
|
||||
const protos = GodotRuntime.parseString(p_proto);
|
||||
let socket = null;
|
||||
try {
|
||||
if (protos) {
|
||||
socket = new WebSocket(url, protos.split(','));
|
||||
} else {
|
||||
socket = new WebSocket(url);
|
||||
}
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
socket.binaryType = 'arraybuffer';
|
||||
return GodotWebSocket.create(socket, on_open, on_message, on_error, on_close);
|
||||
},
|
||||
|
||||
godot_js_websocket_send__proxy: 'sync',
|
||||
godot_js_websocket_send__sig: 'iiiii',
|
||||
godot_js_websocket_send: function (p_id, p_buf, p_buf_len, p_raw) {
|
||||
const bytes_array = new Uint8Array(p_buf_len);
|
||||
let i = 0;
|
||||
for (i = 0; i < p_buf_len; i++) {
|
||||
bytes_array[i] = GodotRuntime.getHeapValue(p_buf + i, 'i8');
|
||||
}
|
||||
let out = bytes_array.buffer;
|
||||
if (!p_raw) {
|
||||
out = new TextDecoder('utf-8').decode(bytes_array);
|
||||
}
|
||||
return GodotWebSocket.send(p_id, out);
|
||||
},
|
||||
|
||||
godot_js_websocket_buffered_amount__proxy: 'sync',
|
||||
godot_js_websocket_buffered_amount__sig: 'ii',
|
||||
godot_js_websocket_buffered_amount: function (p_id) {
|
||||
return GodotWebSocket.bufferedAmount(p_id);
|
||||
},
|
||||
|
||||
godot_js_websocket_close__proxy: 'sync',
|
||||
godot_js_websocket_close__sig: 'viii',
|
||||
godot_js_websocket_close: function (p_id, p_code, p_reason) {
|
||||
const code = p_code;
|
||||
const reason = GodotRuntime.parseString(p_reason);
|
||||
GodotWebSocket.close(p_id, code, reason);
|
||||
},
|
||||
|
||||
godot_js_websocket_destroy__proxy: 'sync',
|
||||
godot_js_websocket_destroy__sig: 'vi',
|
||||
godot_js_websocket_destroy: function (p_id) {
|
||||
GodotWebSocket.destroy(p_id);
|
||||
},
|
||||
};
|
||||
|
||||
autoAddDeps(GodotWebSocket, '$GodotWebSocket');
|
||||
mergeInto(LibraryManager.library, GodotWebSocket);
|
129
modules/websocket/packet_buffer.h
Normal file
129
modules/websocket/packet_buffer.h
Normal file
@@ -0,0 +1,129 @@
|
||||
/**************************************************************************/
|
||||
/* packet_buffer.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/templates/ring_buffer.h"
|
||||
|
||||
template <typename T>
|
||||
class PacketBuffer {
|
||||
private:
|
||||
typedef struct {
|
||||
uint32_t size;
|
||||
T info;
|
||||
} _Packet;
|
||||
|
||||
Vector<_Packet> _packets;
|
||||
int _queued = 0;
|
||||
int _write_pos = 0;
|
||||
int _read_pos = 0;
|
||||
RingBuffer<uint8_t> _payload;
|
||||
|
||||
public:
|
||||
Error write_packet(const uint8_t *p_payload, uint32_t p_size, const T *p_info) {
|
||||
ERR_FAIL_COND_V_MSG(p_payload && (uint32_t)_payload.space_left() < p_size, ERR_OUT_OF_MEMORY, "Buffer payload full! Dropping data.");
|
||||
ERR_FAIL_COND_V_MSG(p_info && _queued >= _packets.size(), ERR_OUT_OF_MEMORY, "Too many packets in queue! Dropping data.");
|
||||
|
||||
// If p_info is nullptr, only the payload is written
|
||||
if (p_info) {
|
||||
ERR_FAIL_COND_V(_write_pos > _packets.size(), ERR_OUT_OF_MEMORY);
|
||||
_Packet p;
|
||||
p.size = p_size;
|
||||
p.info = *p_info;
|
||||
_packets.write[_write_pos] = p;
|
||||
_queued += 1;
|
||||
_write_pos++;
|
||||
if (_write_pos >= _packets.size()) {
|
||||
_write_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If p_payload is nullptr, only the packet information is written.
|
||||
if (p_payload) {
|
||||
_payload.write((const uint8_t *)p_payload, p_size);
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error read_packet(uint8_t *r_payload, int p_bytes, T *r_info, int &r_read) {
|
||||
ERR_FAIL_COND_V(_queued < 1, ERR_UNAVAILABLE);
|
||||
_Packet p = _packets[_read_pos];
|
||||
_read_pos += 1;
|
||||
if (_read_pos >= _packets.size()) {
|
||||
_read_pos = 0;
|
||||
}
|
||||
_queued -= 1;
|
||||
|
||||
ERR_FAIL_COND_V(_payload.data_left() < (int)p.size, ERR_BUG);
|
||||
ERR_FAIL_COND_V(p_bytes < (int)p.size, ERR_OUT_OF_MEMORY);
|
||||
|
||||
r_read = p.size;
|
||||
memcpy(r_info, &p.info, sizeof(T));
|
||||
_payload.read(r_payload, p.size);
|
||||
return OK;
|
||||
}
|
||||
|
||||
void resize(int p_buf_shift, int p_max_packets) {
|
||||
_payload.resize(p_buf_shift);
|
||||
_packets.resize(p_max_packets);
|
||||
_read_pos = 0;
|
||||
_write_pos = 0;
|
||||
_queued = 0;
|
||||
}
|
||||
|
||||
int packets_left() const {
|
||||
return _queued;
|
||||
}
|
||||
|
||||
int payload_space_left() const {
|
||||
return _payload.space_left();
|
||||
}
|
||||
|
||||
int packets_space_left() const {
|
||||
return _packets.size() - _queued;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_payload.resize(0);
|
||||
_packets.resize(0);
|
||||
_read_pos = 0;
|
||||
_write_pos = 0;
|
||||
_queued = 0;
|
||||
}
|
||||
|
||||
PacketBuffer() {
|
||||
clear();
|
||||
}
|
||||
|
||||
~PacketBuffer() {
|
||||
clear();
|
||||
}
|
||||
};
|
90
modules/websocket/register_types.cpp
Normal file
90
modules/websocket/register_types.cpp
Normal file
@@ -0,0 +1,90 @@
|
||||
/**************************************************************************/
|
||||
/* register_types.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "register_types.h"
|
||||
|
||||
#include "remote_debugger_peer_websocket.h"
|
||||
#include "websocket_multiplayer_peer.h"
|
||||
#include "websocket_peer.h"
|
||||
|
||||
#ifdef WEB_ENABLED
|
||||
#include "emws_peer.h"
|
||||
#else
|
||||
#include "wsl_peer.h"
|
||||
#endif
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
#include "editor/editor_debugger_server_websocket.h"
|
||||
#endif
|
||||
|
||||
#include "core/debugger/engine_debugger.h"
|
||||
#include "core/error/error_macros.h"
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
#include "editor/debugger/editor_debugger_server.h"
|
||||
#include "editor/editor_node.h"
|
||||
#endif
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
static void _editor_init_callback() {
|
||||
EditorDebuggerServer::register_protocol_handler("ws://", EditorDebuggerServerWebSocket::create);
|
||||
}
|
||||
#endif
|
||||
|
||||
void initialize_websocket_module(ModuleInitializationLevel p_level) {
|
||||
if (p_level == MODULE_INITIALIZATION_LEVEL_CORE) {
|
||||
#ifdef WEB_ENABLED
|
||||
EMWSPeer::initialize();
|
||||
#else
|
||||
WSLPeer::initialize();
|
||||
#endif
|
||||
|
||||
GDREGISTER_CLASS(WebSocketMultiplayerPeer);
|
||||
ClassDB::register_custom_instance_class<WebSocketPeer>();
|
||||
|
||||
EngineDebugger::register_uri_handler("ws://", RemoteDebuggerPeerWebSocket::create);
|
||||
EngineDebugger::register_uri_handler("wss://", RemoteDebuggerPeerWebSocket::create);
|
||||
}
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
|
||||
EditorNode::add_init_callback(&_editor_init_callback);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void uninitialize_websocket_module(ModuleInitializationLevel p_level) {
|
||||
if (p_level != MODULE_INITIALIZATION_LEVEL_CORE) {
|
||||
return;
|
||||
}
|
||||
#ifndef WEB_ENABLED
|
||||
WSLPeer::deinitialize();
|
||||
#endif
|
||||
}
|
36
modules/websocket/register_types.h
Normal file
36
modules/websocket/register_types.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/**************************************************************************/
|
||||
/* register_types.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "modules/register_module_types.h"
|
||||
|
||||
void initialize_websocket_module(ModuleInitializationLevel p_level);
|
||||
void uninitialize_websocket_module(ModuleInitializationLevel p_level);
|
141
modules/websocket/remote_debugger_peer_websocket.cpp
Normal file
141
modules/websocket/remote_debugger_peer_websocket.cpp
Normal file
@@ -0,0 +1,141 @@
|
||||
/**************************************************************************/
|
||||
/* remote_debugger_peer_websocket.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "remote_debugger_peer_websocket.h"
|
||||
|
||||
#include "core/config/project_settings.h"
|
||||
|
||||
Error RemoteDebuggerPeerWebSocket::connect_to_host(const String &p_uri) {
|
||||
ws_peer = Ref<WebSocketPeer>(WebSocketPeer::create());
|
||||
ERR_FAIL_COND_V(ws_peer.is_null(), ERR_BUG);
|
||||
|
||||
Vector<String> protocols;
|
||||
protocols.push_back("binary"); // Compatibility for emscripten TCP-to-WebSocket.
|
||||
|
||||
ws_peer->set_supported_protocols(protocols);
|
||||
ws_peer->set_max_queued_packets(max_queued_messages);
|
||||
ws_peer->set_inbound_buffer_size((1 << 23) - 1);
|
||||
ws_peer->set_outbound_buffer_size((1 << 23) - 1);
|
||||
|
||||
Error err = ws_peer->connect_to_url(p_uri);
|
||||
ERR_FAIL_COND_V(err != OK, err);
|
||||
|
||||
ws_peer->poll();
|
||||
WebSocketPeer::State ready_state = ws_peer->get_ready_state();
|
||||
if (ready_state != WebSocketPeer::STATE_CONNECTING && ready_state != WebSocketPeer::STATE_OPEN) {
|
||||
ERR_PRINT(vformat("Remote Debugger: Unable to connect. State: %s.", ws_peer->get_ready_state()));
|
||||
return FAILED;
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
bool RemoteDebuggerPeerWebSocket::is_peer_connected() {
|
||||
return ws_peer.is_valid() && (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN || ws_peer->get_ready_state() == WebSocketPeer::STATE_CONNECTING);
|
||||
}
|
||||
|
||||
void RemoteDebuggerPeerWebSocket::poll() {
|
||||
ERR_FAIL_COND(ws_peer.is_null());
|
||||
ws_peer->poll();
|
||||
|
||||
while (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN && ws_peer->get_available_packet_count() > 0 && in_queue.size() < max_queued_messages) {
|
||||
Variant var;
|
||||
Error err = ws_peer->get_var(var);
|
||||
ERR_CONTINUE(err != OK);
|
||||
ERR_CONTINUE(var.get_type() != Variant::ARRAY);
|
||||
in_queue.push_back(var);
|
||||
}
|
||||
|
||||
while (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN && out_queue.size() > 0) {
|
||||
Array var = out_queue.front()->get();
|
||||
Error err = ws_peer->put_var(var);
|
||||
ERR_BREAK(err != OK); // Peer buffer full?
|
||||
out_queue.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
int RemoteDebuggerPeerWebSocket::get_max_message_size() const {
|
||||
ERR_FAIL_COND_V(ws_peer.is_null(), 0);
|
||||
return ws_peer->get_max_packet_size();
|
||||
}
|
||||
|
||||
bool RemoteDebuggerPeerWebSocket::has_message() {
|
||||
return in_queue.size() > 0;
|
||||
}
|
||||
|
||||
Array RemoteDebuggerPeerWebSocket::get_message() {
|
||||
ERR_FAIL_COND_V(in_queue.is_empty(), Array());
|
||||
Array msg = in_queue.front()->get();
|
||||
in_queue.pop_front();
|
||||
return msg;
|
||||
}
|
||||
|
||||
Error RemoteDebuggerPeerWebSocket::put_message(const Array &p_arr) {
|
||||
if (out_queue.size() >= max_queued_messages) {
|
||||
return ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
out_queue.push_back(p_arr);
|
||||
return OK;
|
||||
}
|
||||
|
||||
void RemoteDebuggerPeerWebSocket::close() {
|
||||
if (ws_peer.is_valid()) {
|
||||
ws_peer.unref();
|
||||
}
|
||||
}
|
||||
|
||||
bool RemoteDebuggerPeerWebSocket::can_block() const {
|
||||
#ifdef WEB_ENABLED
|
||||
return false;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
RemoteDebuggerPeerWebSocket::RemoteDebuggerPeerWebSocket(Ref<WebSocketPeer> p_peer) {
|
||||
max_queued_messages = (int)GLOBAL_GET("network/limits/debugger/max_queued_messages");
|
||||
ws_peer = p_peer;
|
||||
if (ws_peer.is_valid()) {
|
||||
ws_peer->set_max_queued_packets(max_queued_messages);
|
||||
ws_peer->set_inbound_buffer_size((1 << 23) - 1);
|
||||
ws_peer->set_outbound_buffer_size((1 << 23) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
RemoteDebuggerPeer *RemoteDebuggerPeerWebSocket::create(const String &p_uri) {
|
||||
ERR_FAIL_COND_V(!p_uri.begins_with("ws://") && !p_uri.begins_with("wss://"), nullptr);
|
||||
RemoteDebuggerPeerWebSocket *peer = memnew(RemoteDebuggerPeerWebSocket);
|
||||
Error err = peer->connect_to_host(p_uri);
|
||||
if (err != OK) {
|
||||
memdelete(peer);
|
||||
return nullptr;
|
||||
}
|
||||
return peer;
|
||||
}
|
59
modules/websocket/remote_debugger_peer_websocket.h
Normal file
59
modules/websocket/remote_debugger_peer_websocket.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/**************************************************************************/
|
||||
/* remote_debugger_peer_websocket.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "websocket_peer.h"
|
||||
|
||||
#include "core/debugger/remote_debugger_peer.h"
|
||||
|
||||
class RemoteDebuggerPeerWebSocket : public RemoteDebuggerPeer {
|
||||
Ref<WebSocketPeer> ws_peer;
|
||||
List<Array> in_queue;
|
||||
List<Array> out_queue;
|
||||
|
||||
int max_queued_messages;
|
||||
|
||||
public:
|
||||
static RemoteDebuggerPeer *create(const String &p_uri);
|
||||
|
||||
Error connect_to_host(const String &p_uri);
|
||||
|
||||
bool is_peer_connected() override;
|
||||
int get_max_message_size() const override;
|
||||
bool has_message() override;
|
||||
Error put_message(const Array &p_arr) override;
|
||||
Array get_message() override;
|
||||
void close() override;
|
||||
void poll() override;
|
||||
bool can_block() const override;
|
||||
|
||||
RemoteDebuggerPeerWebSocket(Ref<WebSocketPeer> p_peer = Ref<WebSocketPeer>());
|
||||
};
|
495
modules/websocket/websocket_multiplayer_peer.cpp
Normal file
495
modules/websocket/websocket_multiplayer_peer.cpp
Normal file
@@ -0,0 +1,495 @@
|
||||
/**************************************************************************/
|
||||
/* websocket_multiplayer_peer.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "websocket_multiplayer_peer.h"
|
||||
|
||||
#include "core/io/stream_peer_tls.h"
|
||||
#include "core/os/os.h"
|
||||
|
||||
WebSocketMultiplayerPeer::WebSocketMultiplayerPeer() {
|
||||
peer_config = Ref<WebSocketPeer>(WebSocketPeer::create());
|
||||
}
|
||||
|
||||
WebSocketMultiplayerPeer::~WebSocketMultiplayerPeer() {
|
||||
_clear();
|
||||
}
|
||||
|
||||
Ref<WebSocketPeer> WebSocketMultiplayerPeer::_create_peer() {
|
||||
Ref<WebSocketPeer> peer = Ref<WebSocketPeer>(WebSocketPeer::create());
|
||||
peer->set_supported_protocols(get_supported_protocols());
|
||||
peer->set_handshake_headers(get_handshake_headers());
|
||||
peer->set_inbound_buffer_size(get_inbound_buffer_size());
|
||||
peer->set_outbound_buffer_size(get_outbound_buffer_size());
|
||||
peer->set_max_queued_packets(get_max_queued_packets());
|
||||
return peer;
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::_clear() {
|
||||
connection_status = CONNECTION_DISCONNECTED;
|
||||
unique_id = 0;
|
||||
peers_map.clear();
|
||||
tcp_server.unref();
|
||||
pending_peers.clear();
|
||||
tls_server_options.unref();
|
||||
if (current_packet.data != nullptr) {
|
||||
memfree(current_packet.data);
|
||||
current_packet.data = nullptr;
|
||||
}
|
||||
|
||||
for (Packet &E : incoming_packets) {
|
||||
memfree(E.data);
|
||||
E.data = nullptr;
|
||||
}
|
||||
|
||||
incoming_packets.clear();
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("create_client", "url", "tls_client_options"), &WebSocketMultiplayerPeer::create_client, DEFVAL(Ref<TLSOptions>()));
|
||||
ClassDB::bind_method(D_METHOD("create_server", "port", "bind_address", "tls_server_options"), &WebSocketMultiplayerPeer::create_server, DEFVAL("*"), DEFVAL(Ref<TLSOptions>()));
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_peer", "peer_id"), &WebSocketMultiplayerPeer::get_peer);
|
||||
ClassDB::bind_method(D_METHOD("get_peer_address", "id"), &WebSocketMultiplayerPeer::get_peer_address);
|
||||
ClassDB::bind_method(D_METHOD("get_peer_port", "id"), &WebSocketMultiplayerPeer::get_peer_port);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_supported_protocols"), &WebSocketMultiplayerPeer::get_supported_protocols);
|
||||
ClassDB::bind_method(D_METHOD("set_supported_protocols", "protocols"), &WebSocketMultiplayerPeer::set_supported_protocols);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_handshake_headers"), &WebSocketMultiplayerPeer::get_handshake_headers);
|
||||
ClassDB::bind_method(D_METHOD("set_handshake_headers", "protocols"), &WebSocketMultiplayerPeer::set_handshake_headers);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_inbound_buffer_size"), &WebSocketMultiplayerPeer::get_inbound_buffer_size);
|
||||
ClassDB::bind_method(D_METHOD("set_inbound_buffer_size", "buffer_size"), &WebSocketMultiplayerPeer::set_inbound_buffer_size);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_outbound_buffer_size"), &WebSocketMultiplayerPeer::get_outbound_buffer_size);
|
||||
ClassDB::bind_method(D_METHOD("set_outbound_buffer_size", "buffer_size"), &WebSocketMultiplayerPeer::set_outbound_buffer_size);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_handshake_timeout"), &WebSocketMultiplayerPeer::get_handshake_timeout);
|
||||
ClassDB::bind_method(D_METHOD("set_handshake_timeout", "timeout"), &WebSocketMultiplayerPeer::set_handshake_timeout);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("set_max_queued_packets", "max_queued_packets"), &WebSocketMultiplayerPeer::set_max_queued_packets);
|
||||
ClassDB::bind_method(D_METHOD("get_max_queued_packets"), &WebSocketMultiplayerPeer::get_max_queued_packets);
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "supported_protocols"), "set_supported_protocols", "get_supported_protocols");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "handshake_headers"), "set_handshake_headers", "get_handshake_headers");
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "inbound_buffer_size"), "set_inbound_buffer_size", "get_inbound_buffer_size");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "outbound_buffer_size"), "set_outbound_buffer_size", "get_outbound_buffer_size");
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "handshake_timeout"), "set_handshake_timeout", "get_handshake_timeout");
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "max_queued_packets"), "set_max_queued_packets", "get_max_queued_packets");
|
||||
}
|
||||
|
||||
//
|
||||
// PacketPeer
|
||||
//
|
||||
int WebSocketMultiplayerPeer::get_available_packet_count() const {
|
||||
return incoming_packets.size();
|
||||
}
|
||||
|
||||
Error WebSocketMultiplayerPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) {
|
||||
ERR_FAIL_COND_V(get_connection_status() != CONNECTION_CONNECTED, ERR_UNCONFIGURED);
|
||||
|
||||
r_buffer_size = 0;
|
||||
|
||||
if (current_packet.data != nullptr) {
|
||||
memfree(current_packet.data);
|
||||
current_packet.data = nullptr;
|
||||
}
|
||||
|
||||
ERR_FAIL_COND_V(incoming_packets.is_empty(), ERR_UNAVAILABLE);
|
||||
|
||||
current_packet = incoming_packets.front()->get();
|
||||
incoming_packets.pop_front();
|
||||
|
||||
*r_buffer = current_packet.data;
|
||||
r_buffer_size = current_packet.size;
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error WebSocketMultiplayerPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) {
|
||||
ERR_FAIL_COND_V(get_connection_status() != CONNECTION_CONNECTED, ERR_UNCONFIGURED);
|
||||
|
||||
if (is_server()) {
|
||||
if (target_peer > 0) {
|
||||
ERR_FAIL_COND_V_MSG(!peers_map.has(target_peer), ERR_INVALID_PARAMETER, "Peer not found: " + itos(target_peer));
|
||||
get_peer(target_peer)->put_packet(p_buffer, p_buffer_size);
|
||||
} else {
|
||||
for (KeyValue<int, Ref<WebSocketPeer>> &E : peers_map) {
|
||||
if (target_peer && -target_peer == E.key) {
|
||||
continue; // Excluded.
|
||||
}
|
||||
E.value->put_packet(p_buffer, p_buffer_size);
|
||||
}
|
||||
}
|
||||
return OK;
|
||||
} else {
|
||||
return get_peer(1)->put_packet(p_buffer, p_buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// MultiplayerPeer
|
||||
//
|
||||
void WebSocketMultiplayerPeer::set_target_peer(int p_target_peer) {
|
||||
target_peer = p_target_peer;
|
||||
}
|
||||
|
||||
int WebSocketMultiplayerPeer::get_packet_peer() const {
|
||||
ERR_FAIL_COND_V(incoming_packets.is_empty(), 1);
|
||||
|
||||
return incoming_packets.front()->get().source;
|
||||
}
|
||||
|
||||
int WebSocketMultiplayerPeer::get_unique_id() const {
|
||||
return unique_id;
|
||||
}
|
||||
|
||||
int WebSocketMultiplayerPeer::get_max_packet_size() const {
|
||||
return get_outbound_buffer_size() - PROTO_SIZE;
|
||||
}
|
||||
|
||||
Error WebSocketMultiplayerPeer::create_server(int p_port, IPAddress p_bind_ip, Ref<TLSOptions> p_options) {
|
||||
ERR_FAIL_COND_V(get_connection_status() != CONNECTION_DISCONNECTED, ERR_ALREADY_IN_USE);
|
||||
ERR_FAIL_COND_V(p_options.is_valid() && !p_options->is_server(), ERR_INVALID_PARAMETER);
|
||||
_clear();
|
||||
tcp_server.instantiate();
|
||||
Error err = tcp_server->listen(p_port, p_bind_ip);
|
||||
if (err != OK) {
|
||||
tcp_server.unref();
|
||||
return err;
|
||||
}
|
||||
unique_id = 1;
|
||||
connection_status = CONNECTION_CONNECTED;
|
||||
tls_server_options = p_options;
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error WebSocketMultiplayerPeer::create_client(const String &p_url, Ref<TLSOptions> p_options) {
|
||||
ERR_FAIL_COND_V(get_connection_status() != CONNECTION_DISCONNECTED, ERR_ALREADY_IN_USE);
|
||||
ERR_FAIL_COND_V(p_options.is_valid() && p_options->is_server(), ERR_INVALID_PARAMETER);
|
||||
_clear();
|
||||
Ref<WebSocketPeer> peer = _create_peer();
|
||||
Error err = peer->connect_to_url(p_url, p_options);
|
||||
if (err != OK) {
|
||||
return err;
|
||||
}
|
||||
PendingPeer pending;
|
||||
pending.time = OS::get_singleton()->get_ticks_msec();
|
||||
pending_peers[1] = pending;
|
||||
peers_map[1] = peer;
|
||||
connection_status = CONNECTION_CONNECTING;
|
||||
return OK;
|
||||
}
|
||||
|
||||
bool WebSocketMultiplayerPeer::is_server() const {
|
||||
return tcp_server.is_valid();
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::_poll_client() {
|
||||
ERR_FAIL_COND(connection_status == CONNECTION_DISCONNECTED); // Bug.
|
||||
ERR_FAIL_COND(!peers_map.has(1) || peers_map[1].is_null()); // Bug.
|
||||
Ref<WebSocketPeer> peer = peers_map[1];
|
||||
peer->poll(); // Update state and fetch packets.
|
||||
WebSocketPeer::State ready_state = peer->get_ready_state();
|
||||
if (ready_state == WebSocketPeer::STATE_OPEN) {
|
||||
if (connection_status == CONNECTION_CONNECTING) {
|
||||
if (peer->get_available_packet_count() > 0) {
|
||||
const uint8_t *in_buffer;
|
||||
int size = 0;
|
||||
Error err = peer->get_packet(&in_buffer, size);
|
||||
if (err != OK || size != 4) {
|
||||
peer->close(); // Will cause connection error on next poll.
|
||||
ERR_FAIL_MSG("Invalid ID received from server");
|
||||
}
|
||||
unique_id = *((int32_t *)in_buffer);
|
||||
if (unique_id < 2) {
|
||||
peer->close(); // Will cause connection error on next poll.
|
||||
ERR_FAIL_MSG("Invalid ID received from server");
|
||||
}
|
||||
connection_status = CONNECTION_CONNECTED;
|
||||
emit_signal("peer_connected", 1);
|
||||
} else {
|
||||
return; // Still waiting for an ID.
|
||||
}
|
||||
}
|
||||
int pkts = peer->get_available_packet_count();
|
||||
while (pkts > 0 && peer->get_ready_state() == WebSocketPeer::STATE_OPEN) {
|
||||
const uint8_t *in_buffer;
|
||||
int size = 0;
|
||||
Error err = peer->get_packet(&in_buffer, size);
|
||||
ERR_FAIL_COND(err != OK);
|
||||
ERR_FAIL_COND(size <= 0);
|
||||
Packet packet;
|
||||
packet.data = (uint8_t *)memalloc(size);
|
||||
memcpy(packet.data, in_buffer, size);
|
||||
packet.size = size;
|
||||
packet.source = 1;
|
||||
incoming_packets.push_back(packet);
|
||||
pkts--;
|
||||
}
|
||||
} else if (peer->get_ready_state() == WebSocketPeer::STATE_CLOSED) {
|
||||
if (connection_status == CONNECTION_CONNECTED) {
|
||||
emit_signal(SNAME("peer_disconnected"), 1);
|
||||
}
|
||||
_clear();
|
||||
return;
|
||||
}
|
||||
if (connection_status == CONNECTION_CONNECTING) {
|
||||
// Still connecting
|
||||
ERR_FAIL_COND(!pending_peers.has(1)); // Bug.
|
||||
if (OS::get_singleton()->get_ticks_msec() - pending_peers[1].time > handshake_timeout) {
|
||||
print_verbose(vformat("WebSocket handshake timed out after %.3f seconds.", handshake_timeout * 0.001));
|
||||
_clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::_poll_server() {
|
||||
ERR_FAIL_COND(connection_status != CONNECTION_CONNECTED); // Bug.
|
||||
ERR_FAIL_COND(tcp_server.is_null() || !tcp_server->is_listening()); // Bug.
|
||||
|
||||
// Accept new connections.
|
||||
if (!is_refusing_new_connections() && tcp_server->is_connection_available()) {
|
||||
PendingPeer peer;
|
||||
peer.time = OS::get_singleton()->get_ticks_msec();
|
||||
peer.tcp = tcp_server->take_connection();
|
||||
peer.connection = peer.tcp;
|
||||
pending_peers[generate_unique_id()] = peer;
|
||||
}
|
||||
|
||||
// Process pending peers.
|
||||
HashSet<int> to_remove;
|
||||
for (KeyValue<int, PendingPeer> &E : pending_peers) {
|
||||
PendingPeer &peer = E.value;
|
||||
int id = E.key;
|
||||
|
||||
if (OS::get_singleton()->get_ticks_msec() - peer.time > handshake_timeout) {
|
||||
print_verbose(vformat("WebSocket handshake timed out after %.3f seconds.", handshake_timeout * 0.001));
|
||||
to_remove.insert(id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (peer.ws.is_valid()) {
|
||||
peer.ws->poll();
|
||||
WebSocketPeer::State state = peer.ws->get_ready_state();
|
||||
if (state == WebSocketPeer::STATE_OPEN) {
|
||||
// Connected.
|
||||
to_remove.insert(id);
|
||||
if (is_refusing_new_connections()) {
|
||||
// The user does not want new connections, dropping it.
|
||||
continue;
|
||||
}
|
||||
int32_t peer_id = id;
|
||||
Error err = peer.ws->put_packet((const uint8_t *)&peer_id, sizeof(peer_id));
|
||||
if (err == OK) {
|
||||
peers_map[id] = peer.ws;
|
||||
emit_signal("peer_connected", id);
|
||||
} else {
|
||||
ERR_PRINT("Failed to send ID to newly connected peer.");
|
||||
}
|
||||
continue;
|
||||
} else if (state == WebSocketPeer::STATE_CONNECTING) {
|
||||
continue; // Still connecting.
|
||||
}
|
||||
to_remove.insert(id); // Error.
|
||||
continue;
|
||||
}
|
||||
if (peer.tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) {
|
||||
to_remove.insert(id); // Error.
|
||||
continue;
|
||||
}
|
||||
if (tls_server_options.is_null()) {
|
||||
peer.ws = _create_peer();
|
||||
peer.ws->accept_stream(peer.tcp);
|
||||
continue;
|
||||
} else {
|
||||
if (peer.connection == peer.tcp) {
|
||||
Ref<StreamPeerTLS> tls = Ref<StreamPeerTLS>(StreamPeerTLS::create());
|
||||
Error err = tls->accept_stream(peer.tcp, tls_server_options);
|
||||
if (err != OK) {
|
||||
to_remove.insert(id);
|
||||
continue;
|
||||
}
|
||||
peer.connection = tls;
|
||||
}
|
||||
Ref<StreamPeerTLS> tls = static_cast<Ref<StreamPeerTLS>>(peer.connection);
|
||||
tls->poll();
|
||||
if (tls->get_status() == StreamPeerTLS::STATUS_CONNECTED) {
|
||||
peer.ws = _create_peer();
|
||||
peer.ws->accept_stream(peer.connection);
|
||||
continue;
|
||||
} else if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) {
|
||||
// Still connecting.
|
||||
continue;
|
||||
} else {
|
||||
// Error
|
||||
to_remove.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove disconnected pending peers.
|
||||
for (const int &pid : to_remove) {
|
||||
pending_peers.erase(pid);
|
||||
}
|
||||
to_remove.clear();
|
||||
|
||||
// Process connected peers.
|
||||
for (KeyValue<int, Ref<WebSocketPeer>> &E : peers_map) {
|
||||
Ref<WebSocketPeer> ws = E.value;
|
||||
int id = E.key;
|
||||
ws->poll();
|
||||
if (ws->get_ready_state() != WebSocketPeer::STATE_OPEN) {
|
||||
to_remove.insert(id); // Disconnected.
|
||||
continue;
|
||||
}
|
||||
// Fetch packets
|
||||
int pkts = ws->get_available_packet_count();
|
||||
while (pkts > 0 && ws->get_ready_state() == WebSocketPeer::STATE_OPEN) {
|
||||
const uint8_t *in_buffer;
|
||||
int size = 0;
|
||||
Error err = ws->get_packet(&in_buffer, size);
|
||||
if (err != OK || size <= 0) {
|
||||
break;
|
||||
}
|
||||
Packet packet;
|
||||
packet.data = (uint8_t *)memalloc(size);
|
||||
memcpy(packet.data, in_buffer, size);
|
||||
packet.size = size;
|
||||
packet.source = E.key;
|
||||
incoming_packets.push_back(packet);
|
||||
pkts--;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove disconnected peers.
|
||||
for (const int &pid : to_remove) {
|
||||
emit_signal(SNAME("peer_disconnected"), pid);
|
||||
peers_map.erase(pid);
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::poll() {
|
||||
if (connection_status == CONNECTION_DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
if (is_server()) {
|
||||
_poll_server();
|
||||
} else {
|
||||
_poll_client();
|
||||
}
|
||||
}
|
||||
|
||||
MultiplayerPeer::ConnectionStatus WebSocketMultiplayerPeer::get_connection_status() const {
|
||||
return connection_status;
|
||||
}
|
||||
|
||||
Ref<WebSocketPeer> WebSocketMultiplayerPeer::get_peer(int p_id) const {
|
||||
ERR_FAIL_COND_V(!peers_map.has(p_id), Ref<WebSocketPeer>());
|
||||
return peers_map[p_id];
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::set_supported_protocols(const Vector<String> &p_protocols) {
|
||||
peer_config->set_supported_protocols(p_protocols);
|
||||
}
|
||||
|
||||
Vector<String> WebSocketMultiplayerPeer::get_supported_protocols() const {
|
||||
return peer_config->get_supported_protocols();
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::set_handshake_headers(const Vector<String> &p_headers) {
|
||||
peer_config->set_handshake_headers(p_headers);
|
||||
}
|
||||
|
||||
Vector<String> WebSocketMultiplayerPeer::get_handshake_headers() const {
|
||||
return peer_config->get_handshake_headers();
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::set_outbound_buffer_size(int p_buffer_size) {
|
||||
peer_config->set_outbound_buffer_size(p_buffer_size);
|
||||
}
|
||||
|
||||
int WebSocketMultiplayerPeer::get_outbound_buffer_size() const {
|
||||
return peer_config->get_outbound_buffer_size();
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::set_inbound_buffer_size(int p_buffer_size) {
|
||||
peer_config->set_inbound_buffer_size(p_buffer_size);
|
||||
}
|
||||
|
||||
int WebSocketMultiplayerPeer::get_inbound_buffer_size() const {
|
||||
return peer_config->get_inbound_buffer_size();
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::set_max_queued_packets(int p_max_queued_packets) {
|
||||
peer_config->set_max_queued_packets(p_max_queued_packets);
|
||||
}
|
||||
|
||||
int WebSocketMultiplayerPeer::get_max_queued_packets() const {
|
||||
return peer_config->get_max_queued_packets();
|
||||
}
|
||||
|
||||
float WebSocketMultiplayerPeer::get_handshake_timeout() const {
|
||||
return handshake_timeout / 1000.0;
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::set_handshake_timeout(float p_timeout) {
|
||||
ERR_FAIL_COND(p_timeout <= 0.0);
|
||||
handshake_timeout = p_timeout * 1000;
|
||||
}
|
||||
|
||||
IPAddress WebSocketMultiplayerPeer::get_peer_address(int p_peer_id) const {
|
||||
ERR_FAIL_COND_V(!peers_map.has(p_peer_id), IPAddress());
|
||||
return peers_map[p_peer_id]->get_connected_host();
|
||||
}
|
||||
|
||||
int WebSocketMultiplayerPeer::get_peer_port(int p_peer_id) const {
|
||||
ERR_FAIL_COND_V(!peers_map.has(p_peer_id), 0);
|
||||
return peers_map[p_peer_id]->get_connected_port();
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::disconnect_peer(int p_peer_id, bool p_force) {
|
||||
ERR_FAIL_COND(!peers_map.has(p_peer_id));
|
||||
peers_map[p_peer_id]->close();
|
||||
if (p_force) {
|
||||
peers_map.erase(p_peer_id);
|
||||
if (!is_server()) {
|
||||
_clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketMultiplayerPeer::close() {
|
||||
_clear();
|
||||
}
|
140
modules/websocket/websocket_multiplayer_peer.h
Normal file
140
modules/websocket/websocket_multiplayer_peer.h
Normal file
@@ -0,0 +1,140 @@
|
||||
/**************************************************************************/
|
||||
/* websocket_multiplayer_peer.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "websocket_peer.h"
|
||||
|
||||
#include "core/io/tcp_server.h"
|
||||
#include "core/templates/list.h"
|
||||
#include "scene/main/multiplayer_peer.h"
|
||||
|
||||
class WebSocketMultiplayerPeer : public MultiplayerPeer {
|
||||
GDCLASS(WebSocketMultiplayerPeer, MultiplayerPeer);
|
||||
|
||||
private:
|
||||
Ref<WebSocketPeer> _create_peer();
|
||||
|
||||
protected:
|
||||
enum {
|
||||
SYS_NONE = 0,
|
||||
SYS_ADD = 1,
|
||||
SYS_DEL = 2,
|
||||
SYS_ID = 3,
|
||||
|
||||
PROTO_SIZE = 9
|
||||
};
|
||||
|
||||
struct Packet {
|
||||
int source = 0;
|
||||
uint8_t *data = nullptr;
|
||||
uint32_t size = 0;
|
||||
};
|
||||
|
||||
struct PendingPeer {
|
||||
uint64_t time = 0;
|
||||
Ref<StreamPeerTCP> tcp;
|
||||
Ref<StreamPeer> connection;
|
||||
Ref<WebSocketPeer> ws;
|
||||
};
|
||||
|
||||
uint64_t handshake_timeout = 3000;
|
||||
Ref<WebSocketPeer> peer_config;
|
||||
HashMap<int, PendingPeer> pending_peers;
|
||||
Ref<TCPServer> tcp_server;
|
||||
Ref<TLSOptions> tls_server_options;
|
||||
|
||||
ConnectionStatus connection_status = CONNECTION_DISCONNECTED;
|
||||
|
||||
List<Packet> incoming_packets;
|
||||
HashMap<int, Ref<WebSocketPeer>> peers_map;
|
||||
Packet current_packet;
|
||||
|
||||
int target_peer = 0;
|
||||
int unique_id = 0;
|
||||
|
||||
static void _bind_methods();
|
||||
|
||||
void _poll_client();
|
||||
void _poll_server();
|
||||
void _clear();
|
||||
|
||||
public:
|
||||
/* MultiplayerPeer */
|
||||
virtual void set_target_peer(int p_target_peer) override;
|
||||
virtual int get_packet_peer() const override;
|
||||
virtual int get_packet_channel() const override { return 0; }
|
||||
virtual TransferMode get_packet_mode() const override { return TRANSFER_MODE_RELIABLE; }
|
||||
virtual int get_unique_id() const override;
|
||||
virtual bool is_server_relay_supported() const override { return true; }
|
||||
|
||||
virtual int get_max_packet_size() const override;
|
||||
virtual bool is_server() const override;
|
||||
virtual void poll() override;
|
||||
virtual void close() override;
|
||||
virtual void disconnect_peer(int p_peer_id, bool p_force = false) override;
|
||||
|
||||
virtual ConnectionStatus get_connection_status() const override;
|
||||
|
||||
/* PacketPeer */
|
||||
virtual int get_available_packet_count() const override;
|
||||
virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size) override;
|
||||
virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override;
|
||||
|
||||
/* WebSocketPeer */
|
||||
virtual Ref<WebSocketPeer> get_peer(int p_peer_id) const;
|
||||
|
||||
Error create_client(const String &p_url, Ref<TLSOptions> p_options);
|
||||
Error create_server(int p_port, IPAddress p_bind_ip, Ref<TLSOptions> p_options);
|
||||
|
||||
void set_supported_protocols(const Vector<String> &p_protocols);
|
||||
Vector<String> get_supported_protocols() const;
|
||||
|
||||
void set_handshake_headers(const Vector<String> &p_headers);
|
||||
Vector<String> get_handshake_headers() const;
|
||||
|
||||
void set_outbound_buffer_size(int p_buffer_size);
|
||||
int get_outbound_buffer_size() const;
|
||||
|
||||
void set_inbound_buffer_size(int p_buffer_size);
|
||||
int get_inbound_buffer_size() const;
|
||||
|
||||
float get_handshake_timeout() const;
|
||||
void set_handshake_timeout(float p_timeout);
|
||||
|
||||
IPAddress get_peer_address(int p_peer_id) const;
|
||||
int get_peer_port(int p_peer_id) const;
|
||||
|
||||
void set_max_queued_packets(int p_max_queued_packets);
|
||||
int get_max_queued_packets() const;
|
||||
|
||||
WebSocketMultiplayerPeer();
|
||||
~WebSocketMultiplayerPeer();
|
||||
};
|
167
modules/websocket/websocket_peer.cpp
Normal file
167
modules/websocket/websocket_peer.cpp
Normal file
@@ -0,0 +1,167 @@
|
||||
/**************************************************************************/
|
||||
/* websocket_peer.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "websocket_peer.h"
|
||||
|
||||
WebSocketPeer *(*WebSocketPeer::_create)(bool p_notify_postinitialize) = nullptr;
|
||||
|
||||
WebSocketPeer::WebSocketPeer() {
|
||||
}
|
||||
|
||||
WebSocketPeer::~WebSocketPeer() {
|
||||
}
|
||||
|
||||
void WebSocketPeer::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("connect_to_url", "url", "tls_client_options"), &WebSocketPeer::connect_to_url, DEFVAL(Ref<TLSOptions>()));
|
||||
ClassDB::bind_method(D_METHOD("accept_stream", "stream"), &WebSocketPeer::accept_stream);
|
||||
ClassDB::bind_method(D_METHOD("send", "message", "write_mode"), &WebSocketPeer::_send_bind, DEFVAL(WRITE_MODE_BINARY));
|
||||
ClassDB::bind_method(D_METHOD("send_text", "message"), &WebSocketPeer::send_text);
|
||||
ClassDB::bind_method(D_METHOD("was_string_packet"), &WebSocketPeer::was_string_packet);
|
||||
ClassDB::bind_method(D_METHOD("poll"), &WebSocketPeer::poll);
|
||||
ClassDB::bind_method(D_METHOD("close", "code", "reason"), &WebSocketPeer::close, DEFVAL(1000), DEFVAL(""));
|
||||
ClassDB::bind_method(D_METHOD("get_connected_host"), &WebSocketPeer::get_connected_host);
|
||||
ClassDB::bind_method(D_METHOD("get_connected_port"), &WebSocketPeer::get_connected_port);
|
||||
ClassDB::bind_method(D_METHOD("get_selected_protocol"), &WebSocketPeer::get_selected_protocol);
|
||||
ClassDB::bind_method(D_METHOD("get_requested_url"), &WebSocketPeer::get_requested_url);
|
||||
ClassDB::bind_method(D_METHOD("set_no_delay", "enabled"), &WebSocketPeer::set_no_delay);
|
||||
ClassDB::bind_method(D_METHOD("get_current_outbound_buffered_amount"), &WebSocketPeer::get_current_outbound_buffered_amount);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_ready_state"), &WebSocketPeer::get_ready_state);
|
||||
ClassDB::bind_method(D_METHOD("get_close_code"), &WebSocketPeer::get_close_code);
|
||||
ClassDB::bind_method(D_METHOD("get_close_reason"), &WebSocketPeer::get_close_reason);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_supported_protocols"), &WebSocketPeer::_get_supported_protocols);
|
||||
ClassDB::bind_method(D_METHOD("set_supported_protocols", "protocols"), &WebSocketPeer::set_supported_protocols);
|
||||
ClassDB::bind_method(D_METHOD("get_handshake_headers"), &WebSocketPeer::_get_handshake_headers);
|
||||
ClassDB::bind_method(D_METHOD("set_handshake_headers", "protocols"), &WebSocketPeer::set_handshake_headers);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_inbound_buffer_size"), &WebSocketPeer::get_inbound_buffer_size);
|
||||
ClassDB::bind_method(D_METHOD("set_inbound_buffer_size", "buffer_size"), &WebSocketPeer::set_inbound_buffer_size);
|
||||
ClassDB::bind_method(D_METHOD("get_outbound_buffer_size"), &WebSocketPeer::get_outbound_buffer_size);
|
||||
ClassDB::bind_method(D_METHOD("set_outbound_buffer_size", "buffer_size"), &WebSocketPeer::set_outbound_buffer_size);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("set_max_queued_packets", "buffer_size"), &WebSocketPeer::set_max_queued_packets);
|
||||
ClassDB::bind_method(D_METHOD("get_max_queued_packets"), &WebSocketPeer::get_max_queued_packets);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("set_heartbeat_interval", "interval"), &WebSocketPeer::set_heartbeat_interval);
|
||||
ClassDB::bind_method(D_METHOD("get_heartbeat_interval"), &WebSocketPeer::get_heartbeat_interval);
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "supported_protocols"), "set_supported_protocols", "get_supported_protocols");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "handshake_headers"), "set_handshake_headers", "get_handshake_headers");
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "inbound_buffer_size"), "set_inbound_buffer_size", "get_inbound_buffer_size");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "outbound_buffer_size"), "set_outbound_buffer_size", "get_outbound_buffer_size");
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "max_queued_packets"), "set_max_queued_packets", "get_max_queued_packets");
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "heartbeat_interval"), "set_heartbeat_interval", "get_heartbeat_interval");
|
||||
|
||||
BIND_ENUM_CONSTANT(WRITE_MODE_TEXT);
|
||||
BIND_ENUM_CONSTANT(WRITE_MODE_BINARY);
|
||||
|
||||
BIND_ENUM_CONSTANT(STATE_CONNECTING);
|
||||
BIND_ENUM_CONSTANT(STATE_OPEN);
|
||||
BIND_ENUM_CONSTANT(STATE_CLOSING);
|
||||
BIND_ENUM_CONSTANT(STATE_CLOSED);
|
||||
}
|
||||
|
||||
Error WebSocketPeer::_send_bind(const PackedByteArray &p_message, WriteMode p_mode) {
|
||||
return send(p_message.ptr(), p_message.size(), p_mode);
|
||||
}
|
||||
|
||||
Error WebSocketPeer::send_text(const String &p_text) {
|
||||
const CharString cs = p_text.utf8();
|
||||
return send((const uint8_t *)cs.ptr(), cs.length(), WRITE_MODE_TEXT);
|
||||
}
|
||||
|
||||
void WebSocketPeer::set_supported_protocols(const Vector<String> &p_protocols) {
|
||||
// Strip edges from protocols.
|
||||
supported_protocols.resize(p_protocols.size());
|
||||
for (int i = 0; i < p_protocols.size(); i++) {
|
||||
supported_protocols.write[i] = p_protocols[i].strip_edges();
|
||||
}
|
||||
}
|
||||
|
||||
const Vector<String> WebSocketPeer::get_supported_protocols() const {
|
||||
return supported_protocols;
|
||||
}
|
||||
|
||||
Vector<String> WebSocketPeer::_get_supported_protocols() const {
|
||||
Vector<String> out;
|
||||
out.append_array(supported_protocols);
|
||||
return out;
|
||||
}
|
||||
|
||||
void WebSocketPeer::set_handshake_headers(const Vector<String> &p_headers) {
|
||||
handshake_headers = p_headers;
|
||||
}
|
||||
|
||||
const Vector<String> WebSocketPeer::get_handshake_headers() const {
|
||||
return handshake_headers;
|
||||
}
|
||||
|
||||
Vector<String> WebSocketPeer::_get_handshake_headers() const {
|
||||
Vector<String> out;
|
||||
out.append_array(handshake_headers);
|
||||
return out;
|
||||
}
|
||||
|
||||
void WebSocketPeer::set_outbound_buffer_size(int p_buffer_size) {
|
||||
outbound_buffer_size = p_buffer_size;
|
||||
}
|
||||
|
||||
int WebSocketPeer::get_outbound_buffer_size() const {
|
||||
return outbound_buffer_size;
|
||||
}
|
||||
|
||||
void WebSocketPeer::set_inbound_buffer_size(int p_buffer_size) {
|
||||
inbound_buffer_size = p_buffer_size;
|
||||
}
|
||||
|
||||
int WebSocketPeer::get_inbound_buffer_size() const {
|
||||
return inbound_buffer_size;
|
||||
}
|
||||
|
||||
void WebSocketPeer::set_max_queued_packets(int p_max_queued_packets) {
|
||||
max_queued_packets = p_max_queued_packets;
|
||||
}
|
||||
|
||||
int WebSocketPeer::get_max_queued_packets() const {
|
||||
return max_queued_packets;
|
||||
}
|
||||
|
||||
double WebSocketPeer::get_heartbeat_interval() const {
|
||||
return heartbeat_interval_msec / 1000.0;
|
||||
}
|
||||
|
||||
void WebSocketPeer::set_heartbeat_interval(double p_interval) {
|
||||
ERR_FAIL_COND(p_interval < 0);
|
||||
heartbeat_interval_msec = p_interval * 1000.0;
|
||||
}
|
127
modules/websocket/websocket_peer.h
Normal file
127
modules/websocket/websocket_peer.h
Normal file
@@ -0,0 +1,127 @@
|
||||
/**************************************************************************/
|
||||
/* websocket_peer.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/crypto/crypto.h"
|
||||
#include "core/io/packet_peer.h"
|
||||
|
||||
class WebSocketPeer : public PacketPeer {
|
||||
GDCLASS(WebSocketPeer, PacketPeer);
|
||||
|
||||
public:
|
||||
enum State {
|
||||
STATE_CONNECTING,
|
||||
STATE_OPEN,
|
||||
STATE_CLOSING,
|
||||
STATE_CLOSED
|
||||
};
|
||||
|
||||
enum WriteMode {
|
||||
WRITE_MODE_TEXT,
|
||||
WRITE_MODE_BINARY,
|
||||
};
|
||||
|
||||
enum {
|
||||
DEFAULT_BUFFER_SIZE = 65535,
|
||||
};
|
||||
|
||||
private:
|
||||
virtual Error _send_bind(const PackedByteArray &p_data, WriteMode p_mode = WRITE_MODE_BINARY);
|
||||
|
||||
protected:
|
||||
static WebSocketPeer *(*_create)(bool p_notify_postinitialize);
|
||||
|
||||
static void _bind_methods();
|
||||
|
||||
Vector<String> supported_protocols;
|
||||
Vector<String> handshake_headers;
|
||||
|
||||
Vector<String> _get_supported_protocols() const;
|
||||
Vector<String> _get_handshake_headers() const;
|
||||
|
||||
int outbound_buffer_size = DEFAULT_BUFFER_SIZE;
|
||||
int inbound_buffer_size = DEFAULT_BUFFER_SIZE;
|
||||
int max_queued_packets = 4096;
|
||||
uint64_t heartbeat_interval_msec = 0;
|
||||
|
||||
public:
|
||||
static WebSocketPeer *create(bool p_notify_postinitialize = true) {
|
||||
if (!_create) {
|
||||
return nullptr;
|
||||
}
|
||||
return _create(p_notify_postinitialize);
|
||||
}
|
||||
|
||||
virtual Error connect_to_url(const String &p_url, Ref<TLSOptions> p_options = Ref<TLSOptions>()) = 0;
|
||||
virtual Error accept_stream(Ref<StreamPeer> p_stream) = 0;
|
||||
|
||||
virtual Error send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) = 0;
|
||||
virtual void close(int p_code = 1000, String p_reason = "") = 0;
|
||||
|
||||
virtual IPAddress get_connected_host() const = 0;
|
||||
virtual uint16_t get_connected_port() const = 0;
|
||||
virtual bool was_string_packet() const = 0;
|
||||
virtual void set_no_delay(bool p_enabled) = 0;
|
||||
virtual int get_current_outbound_buffered_amount() const = 0;
|
||||
virtual String get_selected_protocol() const = 0;
|
||||
virtual String get_requested_url() const = 0;
|
||||
|
||||
virtual void poll() = 0;
|
||||
virtual State get_ready_state() const = 0;
|
||||
virtual int get_close_code() const = 0;
|
||||
virtual String get_close_reason() const = 0;
|
||||
|
||||
Error send_text(const String &p_text);
|
||||
|
||||
void set_supported_protocols(const Vector<String> &p_protocols);
|
||||
const Vector<String> get_supported_protocols() const;
|
||||
|
||||
void set_handshake_headers(const Vector<String> &p_headers);
|
||||
const Vector<String> get_handshake_headers() const;
|
||||
|
||||
void set_outbound_buffer_size(int p_buffer_size);
|
||||
int get_outbound_buffer_size() const;
|
||||
|
||||
void set_inbound_buffer_size(int p_buffer_size);
|
||||
int get_inbound_buffer_size() const;
|
||||
|
||||
void set_max_queued_packets(int p_max_queued_packets);
|
||||
int get_max_queued_packets() const;
|
||||
|
||||
double get_heartbeat_interval() const;
|
||||
void set_heartbeat_interval(double p_interval);
|
||||
|
||||
WebSocketPeer();
|
||||
~WebSocketPeer();
|
||||
};
|
||||
|
||||
VARIANT_ENUM_CAST(WebSocketPeer::WriteMode);
|
||||
VARIANT_ENUM_CAST(WebSocketPeer::State);
|
934
modules/websocket/wsl_peer.cpp
Normal file
934
modules/websocket/wsl_peer.cpp
Normal file
@@ -0,0 +1,934 @@
|
||||
/**************************************************************************/
|
||||
/* wsl_peer.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "wsl_peer.h"
|
||||
|
||||
#ifndef WEB_ENABLED
|
||||
|
||||
#include "core/io/stream_peer_tls.h"
|
||||
|
||||
CryptoCore::RandomGenerator *WSLPeer::_static_rng = nullptr;
|
||||
|
||||
void WSLPeer::initialize() {
|
||||
WebSocketPeer::_create = WSLPeer::_create;
|
||||
_static_rng = memnew(CryptoCore::RandomGenerator);
|
||||
_static_rng->init();
|
||||
}
|
||||
|
||||
void WSLPeer::deinitialize() {
|
||||
if (_static_rng) {
|
||||
memdelete(_static_rng);
|
||||
_static_rng = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Resolver
|
||||
///
|
||||
void WSLPeer::Resolver::start(const String &p_host, int p_port) {
|
||||
stop();
|
||||
|
||||
port = p_port;
|
||||
if (p_host.is_valid_ip_address()) {
|
||||
ip_candidates.push_back(IPAddress(p_host));
|
||||
} else {
|
||||
// Queue hostname for resolution.
|
||||
resolver_id = IP::get_singleton()->resolve_hostname_queue_item(p_host);
|
||||
ERR_FAIL_COND(resolver_id == IP::RESOLVER_INVALID_ID);
|
||||
// Check if it was found in cache.
|
||||
IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(resolver_id);
|
||||
if (ip_status == IP::RESOLVER_STATUS_DONE) {
|
||||
ip_candidates = IP::get_singleton()->get_resolve_item_addresses(resolver_id);
|
||||
IP::get_singleton()->erase_resolve_item(resolver_id);
|
||||
resolver_id = IP::RESOLVER_INVALID_ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WSLPeer::Resolver::stop() {
|
||||
if (resolver_id != IP::RESOLVER_INVALID_ID) {
|
||||
IP::get_singleton()->erase_resolve_item(resolver_id);
|
||||
resolver_id = IP::RESOLVER_INVALID_ID;
|
||||
}
|
||||
port = 0;
|
||||
}
|
||||
|
||||
void WSLPeer::Resolver::try_next_candidate(Ref<StreamPeerTCP> &p_tcp) {
|
||||
// Check if we still need resolving.
|
||||
if (resolver_id != IP::RESOLVER_INVALID_ID) {
|
||||
IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(resolver_id);
|
||||
if (ip_status == IP::RESOLVER_STATUS_WAITING) {
|
||||
return;
|
||||
}
|
||||
if (ip_status == IP::RESOLVER_STATUS_DONE) {
|
||||
ip_candidates = IP::get_singleton()->get_resolve_item_addresses(resolver_id);
|
||||
}
|
||||
IP::get_singleton()->erase_resolve_item(resolver_id);
|
||||
resolver_id = IP::RESOLVER_INVALID_ID;
|
||||
}
|
||||
|
||||
// Try the current candidate if we have one.
|
||||
if (p_tcp->get_status() != StreamPeerTCP::STATUS_NONE) {
|
||||
p_tcp->poll();
|
||||
StreamPeerTCP::Status status = p_tcp->get_status();
|
||||
if (status == StreamPeerTCP::STATUS_CONNECTED) {
|
||||
// On Windows, setting TCP_NODELAY may fail if the socket is still connecting.
|
||||
p_tcp->set_no_delay(true);
|
||||
ip_candidates.clear();
|
||||
return;
|
||||
} else if (status == StreamPeerTCP::STATUS_CONNECTING) {
|
||||
return; // Keep connecting.
|
||||
} else {
|
||||
p_tcp->disconnect_from_host();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep trying next candidate.
|
||||
while (ip_candidates.size()) {
|
||||
Error err = p_tcp->connect_to_host(ip_candidates.pop_front(), port);
|
||||
if (err == OK) {
|
||||
return;
|
||||
} else {
|
||||
p_tcp->disconnect_from_host();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Server functions
|
||||
///
|
||||
Error WSLPeer::accept_stream(Ref<StreamPeer> p_stream) {
|
||||
ERR_FAIL_COND_V(p_stream.is_null(), ERR_INVALID_PARAMETER);
|
||||
ERR_FAIL_COND_V(ready_state != STATE_CLOSED && ready_state != STATE_CLOSING, ERR_ALREADY_IN_USE);
|
||||
|
||||
_clear();
|
||||
|
||||
if (p_stream->is_class_ptr(StreamPeerTCP::get_class_ptr_static())) {
|
||||
tcp = p_stream;
|
||||
connection = p_stream;
|
||||
use_tls = false;
|
||||
} else if (p_stream->is_class_ptr(StreamPeerTLS::get_class_ptr_static())) {
|
||||
Ref<StreamPeer> base_stream = static_cast<Ref<StreamPeerTLS>>(p_stream)->get_stream();
|
||||
ERR_FAIL_COND_V(base_stream.is_null() || !base_stream->is_class_ptr(StreamPeerTCP::get_class_ptr_static()), ERR_INVALID_PARAMETER);
|
||||
tcp = static_cast<Ref<StreamPeerTCP>>(base_stream);
|
||||
connection = p_stream;
|
||||
use_tls = true;
|
||||
}
|
||||
ERR_FAIL_COND_V(connection.is_null() || tcp.is_null(), ERR_INVALID_PARAMETER);
|
||||
is_server = true;
|
||||
tcp->set_no_delay(true);
|
||||
ready_state = STATE_CONNECTING;
|
||||
handshake_buffer->resize(WSL_MAX_HEADER_SIZE);
|
||||
handshake_buffer->seek(0);
|
||||
return OK;
|
||||
}
|
||||
|
||||
bool WSLPeer::_parse_client_request() {
|
||||
Vector<String> psa = String::ascii(Span((const char *)handshake_buffer->get_data_array().ptr(), handshake_buffer->get_position() - 4)).split("\r\n");
|
||||
int len = psa.size();
|
||||
ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers, got: " + itos(len) + ", expected >= 4.");
|
||||
|
||||
Vector<String> req = psa[0].split(" ", false);
|
||||
ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code.");
|
||||
|
||||
// Wrong protocol
|
||||
ERR_FAIL_COND_V_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", false, "Invalid method or HTTP version.");
|
||||
|
||||
HashMap<String, String> headers;
|
||||
for (int i = 1; i < len; i++) {
|
||||
Vector<String> header = psa[i].split(":", false, 1);
|
||||
ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i]);
|
||||
String name = header[0].to_lower();
|
||||
String value = header[1].strip_edges();
|
||||
if (headers.has(name)) {
|
||||
headers[name] += "," + value;
|
||||
} else {
|
||||
headers[name] = value;
|
||||
}
|
||||
}
|
||||
requested_host = headers.has("host") ? headers.get("host") : "";
|
||||
requested_url = (use_tls ? "wss://" : "ws://") + requested_host + req[1];
|
||||
#define WSL_CHECK(NAME, VALUE) \
|
||||
ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \
|
||||
"Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'.");
|
||||
#define WSL_CHECK_EX(NAME) \
|
||||
ERR_FAIL_COND_V_MSG(!headers.has(NAME), false, "Missing header '" + String(NAME) + "'.");
|
||||
WSL_CHECK("upgrade", "websocket");
|
||||
WSL_CHECK("sec-websocket-version", "13");
|
||||
WSL_CHECK_EX("sec-websocket-key");
|
||||
WSL_CHECK_EX("connection");
|
||||
#undef WSL_CHECK_EX
|
||||
#undef WSL_CHECK
|
||||
session_key = headers["sec-websocket-key"];
|
||||
if (headers.has("sec-websocket-protocol")) {
|
||||
Vector<String> protos = headers["sec-websocket-protocol"].split(",");
|
||||
for (int i = 0; i < protos.size(); i++) {
|
||||
String proto = protos[i].strip_edges();
|
||||
// Check if we have the given protocol
|
||||
for (int j = 0; j < supported_protocols.size(); j++) {
|
||||
if (proto != supported_protocols[j]) {
|
||||
continue;
|
||||
}
|
||||
selected_protocol = proto;
|
||||
break;
|
||||
}
|
||||
// Found a protocol
|
||||
if (!selected_protocol.is_empty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selected_protocol.is_empty()) { // Invalid protocol(s) requested
|
||||
return false;
|
||||
}
|
||||
} else if (supported_protocols.size() > 0) { // No protocol requested, but we need one
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Error WSLPeer::_do_server_handshake() {
|
||||
if (use_tls) {
|
||||
Ref<StreamPeerTLS> tls = static_cast<Ref<StreamPeerTLS>>(connection);
|
||||
if (tls.is_null()) {
|
||||
ERR_FAIL_V_MSG(ERR_BUG, "Couldn't get StreamPeerTLS for WebSocket handshake.");
|
||||
close(-1);
|
||||
return FAILED;
|
||||
}
|
||||
tls->poll();
|
||||
if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) {
|
||||
return OK; // Pending handshake
|
||||
} else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) {
|
||||
print_verbose(vformat("WebSocket SSL connection error during handshake (StreamPeerTLS status code %d).", tls->get_status()));
|
||||
close(-1);
|
||||
return FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
if (pending_request) {
|
||||
int read = 0;
|
||||
while (true) {
|
||||
ERR_FAIL_COND_V_MSG(handshake_buffer->get_available_bytes() < 1, ERR_OUT_OF_MEMORY, "WebSocket response headers are too big.");
|
||||
int pos = handshake_buffer->get_position();
|
||||
uint8_t byte;
|
||||
Error err = connection->get_partial_data(&byte, 1, read);
|
||||
if (err != OK) { // Got an error
|
||||
print_verbose(vformat("WebSocket error while getting partial data (StreamPeer error code %d).", err));
|
||||
close(-1);
|
||||
return FAILED;
|
||||
} else if (read != 1) { // Busy, wait next poll
|
||||
return OK;
|
||||
}
|
||||
handshake_buffer->put_u8(byte);
|
||||
const char *r = (const char *)handshake_buffer->get_data_array().ptr();
|
||||
int l = pos;
|
||||
if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') {
|
||||
if (!_parse_client_request()) {
|
||||
close(-1);
|
||||
return FAILED;
|
||||
}
|
||||
String s = "HTTP/1.1 101 Switching Protocols\r\n";
|
||||
s += "Upgrade: websocket\r\n";
|
||||
s += "Connection: Upgrade\r\n";
|
||||
s += "Sec-WebSocket-Accept: " + _compute_key_response(session_key) + "\r\n";
|
||||
if (!selected_protocol.is_empty()) {
|
||||
s += "Sec-WebSocket-Protocol: " + selected_protocol + "\r\n";
|
||||
}
|
||||
for (int i = 0; i < handshake_headers.size(); i++) {
|
||||
s += handshake_headers[i] + "\r\n";
|
||||
}
|
||||
s += "\r\n";
|
||||
CharString cs = s.utf8();
|
||||
handshake_buffer->clear();
|
||||
handshake_buffer->put_data((const uint8_t *)cs.get_data(), cs.length());
|
||||
handshake_buffer->seek(0);
|
||||
pending_request = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pending_request) { // Still pending.
|
||||
return OK;
|
||||
}
|
||||
|
||||
int left = handshake_buffer->get_available_bytes();
|
||||
if (left) {
|
||||
Vector<uint8_t> data = handshake_buffer->get_data_array();
|
||||
int pos = handshake_buffer->get_position();
|
||||
int sent = 0;
|
||||
Error err = connection->put_partial_data(data.ptr() + pos, left, sent);
|
||||
if (err != OK) {
|
||||
print_verbose(vformat("WebSocket error while putting partial data (StreamPeer error code %d).", err));
|
||||
close(-1);
|
||||
return err;
|
||||
}
|
||||
handshake_buffer->seek(pos + sent);
|
||||
left -= sent;
|
||||
if (left == 0) {
|
||||
resolver.stop();
|
||||
// Response sent, initialize wslay context.
|
||||
wslay_event_context_server_init(&wsl_ctx, &_wsl_callbacks, this);
|
||||
wslay_event_config_set_no_buffering(wsl_ctx, 1);
|
||||
wslay_event_config_set_max_recv_msg_length(wsl_ctx, inbound_buffer_size);
|
||||
in_buffer.resize(nearest_shift((uint32_t)inbound_buffer_size), max_queued_packets);
|
||||
packet_buffer.resize(inbound_buffer_size);
|
||||
ready_state = STATE_OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
///
|
||||
/// Client functions
|
||||
///
|
||||
void WSLPeer::_do_client_handshake() {
|
||||
ERR_FAIL_COND(tcp.is_null());
|
||||
|
||||
// Try to connect to candidates.
|
||||
if (resolver.has_more_candidates() || tcp->get_status() == StreamPeerTCP::STATUS_CONNECTING) {
|
||||
resolver.try_next_candidate(tcp);
|
||||
if (resolver.has_more_candidates()) {
|
||||
return; // Still pending.
|
||||
}
|
||||
}
|
||||
|
||||
tcp->poll();
|
||||
if (tcp->get_status() == StreamPeerTCP::STATUS_CONNECTING) {
|
||||
return; // Keep connecting.
|
||||
} else if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) {
|
||||
close(-1); // Failed to connect.
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_tls) {
|
||||
Ref<StreamPeerTLS> tls;
|
||||
if (connection == tcp) {
|
||||
// Start SSL handshake
|
||||
tls = Ref<StreamPeerTLS>(StreamPeerTLS::create());
|
||||
ERR_FAIL_COND(tls.is_null());
|
||||
if (tls->connect_to_stream(tcp, requested_host, tls_options) != OK) {
|
||||
close(-1);
|
||||
return; // Error.
|
||||
}
|
||||
connection = tls;
|
||||
} else {
|
||||
tls = static_cast<Ref<StreamPeerTLS>>(connection);
|
||||
ERR_FAIL_COND(tls.is_null());
|
||||
tls->poll();
|
||||
}
|
||||
if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) {
|
||||
return; // Need more polling.
|
||||
} else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) {
|
||||
close(-1);
|
||||
return; // Error.
|
||||
}
|
||||
}
|
||||
|
||||
// Do websocket handshake.
|
||||
if (pending_request) {
|
||||
int left = handshake_buffer->get_available_bytes();
|
||||
int pos = handshake_buffer->get_position();
|
||||
const Vector<uint8_t> data = handshake_buffer->get_data_array();
|
||||
int sent = 0;
|
||||
Error err = connection->put_partial_data(data.ptr() + pos, left, sent);
|
||||
// Sending handshake failed
|
||||
if (err != OK) {
|
||||
close(-1);
|
||||
return; // Error.
|
||||
}
|
||||
handshake_buffer->seek(pos + sent);
|
||||
if (handshake_buffer->get_available_bytes() == 0) {
|
||||
pending_request = false;
|
||||
handshake_buffer->clear();
|
||||
handshake_buffer->resize(WSL_MAX_HEADER_SIZE);
|
||||
handshake_buffer->seek(0);
|
||||
}
|
||||
} else {
|
||||
int read = 0;
|
||||
while (true) {
|
||||
int left = handshake_buffer->get_available_bytes();
|
||||
int pos = handshake_buffer->get_position();
|
||||
if (left == 0) {
|
||||
// Header is too big
|
||||
close(-1);
|
||||
ERR_FAIL_MSG("Response headers too big.");
|
||||
}
|
||||
|
||||
uint8_t byte;
|
||||
Error err = connection->get_partial_data(&byte, 1, read);
|
||||
if (err != OK) {
|
||||
// Got some error.
|
||||
close(-1);
|
||||
return;
|
||||
} else if (read != 1) {
|
||||
// Busy, wait next poll.
|
||||
break;
|
||||
}
|
||||
handshake_buffer->put_u8(byte);
|
||||
|
||||
// Check "\r\n\r\n" header terminator
|
||||
const char *r = (const char *)handshake_buffer->get_data_array().ptr();
|
||||
int l = pos;
|
||||
if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') {
|
||||
// Response is over, verify headers and initialize wslay context/
|
||||
if (!_verify_server_response()) {
|
||||
close(-1);
|
||||
ERR_FAIL_MSG("Invalid response headers.");
|
||||
}
|
||||
wslay_event_context_client_init(&wsl_ctx, &_wsl_callbacks, this);
|
||||
wslay_event_config_set_no_buffering(wsl_ctx, 1);
|
||||
wslay_event_config_set_max_recv_msg_length(wsl_ctx, inbound_buffer_size);
|
||||
in_buffer.resize(nearest_shift((uint32_t)inbound_buffer_size), max_queued_packets);
|
||||
packet_buffer.resize(inbound_buffer_size);
|
||||
ready_state = STATE_OPEN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool WSLPeer::_verify_server_response() {
|
||||
Vector<String> psa = String::ascii(Span((const char *)handshake_buffer->get_data_array().ptr(), handshake_buffer->get_position() - 4)).split("\r\n");
|
||||
int len = psa.size();
|
||||
ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers. Got: " + itos(len) + ", expected >= 4.");
|
||||
|
||||
Vector<String> req = psa[0].split(" ", false);
|
||||
ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code. Got '" + psa[0] + "', expected 'HTTP/1.1 101'.");
|
||||
|
||||
// Wrong protocol
|
||||
ERR_FAIL_COND_V_MSG(req[0] != "HTTP/1.1", false, "Invalid protocol. Got: '" + req[0] + "', expected 'HTTP/1.1'.");
|
||||
ERR_FAIL_COND_V_MSG(req[1] != "101", false, "Invalid status code. Got: '" + req[1] + "', expected '101'.");
|
||||
|
||||
HashMap<String, String> headers;
|
||||
for (int i = 1; i < len; i++) {
|
||||
Vector<String> header = psa[i].split(":", false, 1);
|
||||
ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i] + ".");
|
||||
String name = header[0].to_lower();
|
||||
String value = header[1].strip_edges();
|
||||
if (headers.has(name)) {
|
||||
headers[name] += "," + value;
|
||||
} else {
|
||||
headers[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
#define WSL_CHECK(NAME, VALUE) \
|
||||
ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \
|
||||
"Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'.");
|
||||
#define WSL_CHECK_NC(NAME, VALUE) \
|
||||
ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME] != VALUE, false, \
|
||||
"Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'.");
|
||||
WSL_CHECK("connection", "upgrade");
|
||||
WSL_CHECK("upgrade", "websocket");
|
||||
WSL_CHECK_NC("sec-websocket-accept", _compute_key_response(session_key));
|
||||
#undef WSL_CHECK_NC
|
||||
#undef WSL_CHECK
|
||||
if (supported_protocols.is_empty()) {
|
||||
// We didn't request a custom protocol
|
||||
ERR_FAIL_COND_V_MSG(headers.has("sec-websocket-protocol"), false, "Received unrequested sub-protocol -> " + headers["sec-websocket-protocol"]);
|
||||
} else {
|
||||
// We requested at least one custom protocol but didn't receive one
|
||||
ERR_FAIL_COND_V_MSG(!headers.has("sec-websocket-protocol"), false, "Requested sub-protocol(s) but received none.");
|
||||
// Check received sub-protocol was one of those requested.
|
||||
selected_protocol = headers["sec-websocket-protocol"];
|
||||
bool valid = false;
|
||||
for (int i = 0; i < supported_protocols.size(); i++) {
|
||||
if (supported_protocols[i] != selected_protocol) {
|
||||
continue;
|
||||
}
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
if (!valid) {
|
||||
ERR_FAIL_V_MSG(false, "Received unrequested sub-protocol -> " + selected_protocol);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Error WSLPeer::connect_to_url(const String &p_url, Ref<TLSOptions> p_options) {
|
||||
ERR_FAIL_COND_V(p_url.is_empty(), ERR_INVALID_PARAMETER);
|
||||
ERR_FAIL_COND_V(p_options.is_valid() && p_options->is_server(), ERR_INVALID_PARAMETER);
|
||||
ERR_FAIL_COND_V(ready_state != STATE_CLOSED && ready_state != STATE_CLOSING, ERR_ALREADY_IN_USE);
|
||||
|
||||
_clear();
|
||||
|
||||
String host;
|
||||
String path;
|
||||
String scheme;
|
||||
String fragment;
|
||||
int port = 0;
|
||||
Error err = p_url.parse_url(scheme, host, port, path, fragment);
|
||||
ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url);
|
||||
if (scheme.is_empty()) {
|
||||
scheme = "ws://";
|
||||
}
|
||||
ERR_FAIL_COND_V_MSG(scheme != "ws://" && scheme != "wss://", ERR_INVALID_PARAMETER, vformat("Invalid protocol: \"%s\" (must be either \"ws://\" or \"wss://\").", scheme));
|
||||
|
||||
use_tls = false;
|
||||
if (scheme == "wss://") {
|
||||
use_tls = true;
|
||||
}
|
||||
if (port == 0) {
|
||||
port = use_tls ? 443 : 80;
|
||||
}
|
||||
if (path.is_empty()) {
|
||||
path = "/";
|
||||
}
|
||||
|
||||
ERR_FAIL_COND_V_MSG(use_tls && !StreamPeerTLS::is_available(), ERR_UNAVAILABLE, "WSS is not available in this build.");
|
||||
|
||||
requested_url = p_url;
|
||||
requested_host = host;
|
||||
|
||||
if (p_options.is_valid()) {
|
||||
tls_options = p_options;
|
||||
} else {
|
||||
tls_options = TLSOptions::client();
|
||||
}
|
||||
|
||||
tcp.instantiate();
|
||||
|
||||
resolver.start(host, port);
|
||||
resolver.try_next_candidate(tcp);
|
||||
|
||||
if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTING && tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED && !resolver.has_more_candidates()) {
|
||||
_clear();
|
||||
return FAILED;
|
||||
}
|
||||
connection = tcp;
|
||||
|
||||
// Prepare handshake request.
|
||||
session_key = _generate_key();
|
||||
String request = "GET " + path + " HTTP/1.1\r\n";
|
||||
String port_string;
|
||||
if ((port != 80 && !use_tls) || (port != 443 && use_tls)) {
|
||||
port_string = ":" + itos(port);
|
||||
}
|
||||
request += "Host: " + host + port_string + "\r\n";
|
||||
request += "Upgrade: websocket\r\n";
|
||||
request += "Connection: Upgrade\r\n";
|
||||
request += "Sec-WebSocket-Key: " + session_key + "\r\n";
|
||||
request += "Sec-WebSocket-Version: 13\r\n";
|
||||
if (supported_protocols.size() > 0) {
|
||||
request += "Sec-WebSocket-Protocol: ";
|
||||
for (int i = 0; i < supported_protocols.size(); i++) {
|
||||
if (i != 0) {
|
||||
request += ",";
|
||||
}
|
||||
request += supported_protocols[i];
|
||||
}
|
||||
request += "\r\n";
|
||||
}
|
||||
for (int i = 0; i < handshake_headers.size(); i++) {
|
||||
request += handshake_headers[i] + "\r\n";
|
||||
}
|
||||
request += "\r\n";
|
||||
CharString cs = request.utf8();
|
||||
handshake_buffer->put_data((const uint8_t *)cs.get_data(), cs.length());
|
||||
handshake_buffer->seek(0);
|
||||
ready_state = STATE_CONNECTING;
|
||||
is_server = false;
|
||||
return OK;
|
||||
}
|
||||
|
||||
///
|
||||
/// Callback functions.
|
||||
///
|
||||
ssize_t WSLPeer::_wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data) {
|
||||
WSLPeer *peer = (WSLPeer *)user_data;
|
||||
Ref<StreamPeer> conn = peer->connection;
|
||||
if (conn.is_null()) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
|
||||
return -1;
|
||||
}
|
||||
// Make sure we don't read more than what our buffer can hold.
|
||||
size_t buffer_limit = MIN(peer->in_buffer.payload_space_left(), peer->in_buffer.packets_space_left() * 2); // The minimum size of a websocket message is 2 bytes.
|
||||
size_t to_read = MIN(len, buffer_limit);
|
||||
if (to_read == 0) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
|
||||
return -1;
|
||||
}
|
||||
int read = 0;
|
||||
Error err = conn->get_partial_data(data, to_read, read);
|
||||
if (err != OK) {
|
||||
print_verbose("Websocket get data error: " + itos(err) + ", read (should be 0!): " + itos(read));
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
|
||||
return -1;
|
||||
}
|
||||
if (read == 0) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
|
||||
return -1;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
void WSLPeer::_wsl_recv_start_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_frame_recv_start_arg *arg, void *user_data) {
|
||||
WSLPeer *peer = (WSLPeer *)user_data;
|
||||
uint8_t op = arg->opcode;
|
||||
if (op == WSLAY_TEXT_FRAME || op == WSLAY_BINARY_FRAME) {
|
||||
// Get ready to process a data package.
|
||||
PendingMessage &pm = peer->pending_message;
|
||||
pm.opcode = op;
|
||||
}
|
||||
}
|
||||
|
||||
void WSLPeer::_wsl_frame_recv_chunk_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_frame_recv_chunk_arg *arg, void *user_data) {
|
||||
WSLPeer *peer = (WSLPeer *)user_data;
|
||||
PendingMessage &pm = peer->pending_message;
|
||||
if (pm.opcode != 0) {
|
||||
// Only write the payload.
|
||||
peer->in_buffer.write_packet(arg->data, arg->data_length, nullptr);
|
||||
pm.payload_size += arg->data_length;
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t WSLPeer::_wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data) {
|
||||
WSLPeer *peer = (WSLPeer *)user_data;
|
||||
Ref<StreamPeer> conn = peer->connection;
|
||||
if (conn.is_null()) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
|
||||
return -1;
|
||||
}
|
||||
int sent = 0;
|
||||
Error err = conn->put_partial_data(data, len, sent);
|
||||
if (err != OK) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
|
||||
return -1;
|
||||
}
|
||||
if (sent == 0) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
|
||||
return -1;
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
int WSLPeer::_wsl_genmask_callback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data) {
|
||||
ERR_FAIL_NULL_V(_static_rng, WSLAY_ERR_CALLBACK_FAILURE);
|
||||
Error err = _static_rng->get_random_bytes(buf, len);
|
||||
ERR_FAIL_COND_V(err != OK, WSLAY_ERR_CALLBACK_FAILURE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void WSLPeer::_wsl_msg_recv_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data) {
|
||||
WSLPeer *peer = (WSLPeer *)user_data;
|
||||
uint8_t op = arg->opcode;
|
||||
|
||||
if (op == WSLAY_CONNECTION_CLOSE) {
|
||||
// Close request or confirmation.
|
||||
peer->close_code = arg->status_code;
|
||||
size_t len = arg->msg_length;
|
||||
peer->close_reason.clear();
|
||||
if (len > 2 /* first 2 bytes = close code */) {
|
||||
peer->close_reason.append_utf8((char *)arg->msg + 2, len - 2);
|
||||
}
|
||||
if (peer->ready_state == STATE_OPEN) {
|
||||
peer->ready_state = STATE_CLOSING;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (op == WSLAY_PONG) {
|
||||
peer->heartbeat_waiting = false;
|
||||
} else if (op == WSLAY_TEXT_FRAME || op == WSLAY_BINARY_FRAME) {
|
||||
PendingMessage &pm = peer->pending_message;
|
||||
ERR_FAIL_COND(pm.opcode != op);
|
||||
// Only write the packet (since it's now completed).
|
||||
uint8_t is_string = pm.opcode == WSLAY_TEXT_FRAME ? 1 : 0;
|
||||
peer->in_buffer.write_packet(nullptr, pm.payload_size, &is_string);
|
||||
pm.clear();
|
||||
}
|
||||
// Ping.
|
||||
}
|
||||
|
||||
wslay_event_callbacks WSLPeer::_wsl_callbacks = {
|
||||
_wsl_recv_callback,
|
||||
_wsl_send_callback,
|
||||
_wsl_genmask_callback,
|
||||
_wsl_recv_start_callback,
|
||||
_wsl_frame_recv_chunk_callback,
|
||||
nullptr,
|
||||
_wsl_msg_recv_callback
|
||||
};
|
||||
|
||||
String WSLPeer::_generate_key() {
|
||||
// Random key
|
||||
Vector<uint8_t> bkey;
|
||||
int len = 16; // 16 bytes, as per RFC
|
||||
bkey.resize(len);
|
||||
_wsl_genmask_callback(nullptr, bkey.ptrw(), len, nullptr);
|
||||
return CryptoCore::b64_encode_str(bkey.ptrw(), len);
|
||||
}
|
||||
|
||||
String WSLPeer::_compute_key_response(String p_key) {
|
||||
String key = p_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // Magic UUID as per RFC
|
||||
Vector<uint8_t> sha = key.sha1_buffer();
|
||||
return CryptoCore::b64_encode_str(sha.ptr(), sha.size());
|
||||
}
|
||||
|
||||
void WSLPeer::poll() {
|
||||
// Nothing to do.
|
||||
if (ready_state == STATE_CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ready_state == STATE_CONNECTING) {
|
||||
if (is_server) {
|
||||
_do_server_handshake();
|
||||
} else {
|
||||
_do_client_handshake();
|
||||
}
|
||||
}
|
||||
|
||||
if (ready_state == STATE_OPEN || ready_state == STATE_CLOSING) {
|
||||
ERR_FAIL_NULL(wsl_ctx);
|
||||
uint64_t ticks = OS::get_singleton()->get_ticks_msec();
|
||||
int err = 0;
|
||||
if (heartbeat_interval_msec != 0 && ticks - last_heartbeat > heartbeat_interval_msec && ready_state == STATE_OPEN) {
|
||||
if (heartbeat_waiting) {
|
||||
wslay_event_context_free(wsl_ctx);
|
||||
wsl_ctx = nullptr;
|
||||
close(-1);
|
||||
return;
|
||||
}
|
||||
heartbeat_waiting = true;
|
||||
struct wslay_event_msg msg;
|
||||
msg.opcode = WSLAY_PING;
|
||||
msg.msg = nullptr;
|
||||
msg.msg_length = 0;
|
||||
err = wslay_event_queue_msg(wsl_ctx, &msg);
|
||||
if (err == 0) {
|
||||
last_heartbeat = ticks;
|
||||
} else {
|
||||
print_verbose("Websocket (wslay) failed to send ping: " + itos(err));
|
||||
wslay_event_context_free(wsl_ctx);
|
||||
wsl_ctx = nullptr;
|
||||
close(-1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ((err = wslay_event_recv(wsl_ctx)) != 0 || (err = wslay_event_send(wsl_ctx)) != 0) {
|
||||
// Error close.
|
||||
print_verbose("Websocket (wslay) poll error: " + itos(err));
|
||||
wslay_event_context_free(wsl_ctx);
|
||||
wsl_ctx = nullptr;
|
||||
close(-1);
|
||||
return;
|
||||
}
|
||||
if (wslay_event_get_close_sent(wsl_ctx)) {
|
||||
if (wslay_event_get_close_received(wsl_ctx)) {
|
||||
// Clean close.
|
||||
wslay_event_context_free(wsl_ctx);
|
||||
wsl_ctx = nullptr;
|
||||
close(-1);
|
||||
return;
|
||||
} else if (!wslay_event_get_read_enabled(wsl_ctx)) {
|
||||
// Some protocol error caused wslay to stop processing incoming events, we'll never receive a close from the other peer.
|
||||
close_code = wslay_event_get_status_code_sent(wsl_ctx);
|
||||
switch (close_code) {
|
||||
case WSLAY_CODE_MESSAGE_TOO_BIG:
|
||||
close_reason = "Message too big";
|
||||
break;
|
||||
case WSLAY_CODE_PROTOCOL_ERROR:
|
||||
close_reason = "Protocol error";
|
||||
break;
|
||||
case WSLAY_CODE_ABNORMAL_CLOSURE:
|
||||
close_reason = "Abnormal closure";
|
||||
break;
|
||||
case WSLAY_CODE_INVALID_FRAME_PAYLOAD_DATA:
|
||||
close_reason = "Invalid frame payload data";
|
||||
break;
|
||||
default:
|
||||
close_reason = "Unknown";
|
||||
}
|
||||
wslay_event_context_free(wsl_ctx);
|
||||
wsl_ctx = nullptr;
|
||||
close(-1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Error WSLPeer::_send(const uint8_t *p_buffer, int p_buffer_size, wslay_opcode p_opcode) {
|
||||
ERR_FAIL_COND_V(ready_state != STATE_OPEN, FAILED);
|
||||
ERR_FAIL_COND_V(wslay_event_get_queued_msg_count(wsl_ctx) >= (uint32_t)max_queued_packets, ERR_OUT_OF_MEMORY);
|
||||
ERR_FAIL_COND_V(outbound_buffer_size > 0 && (wslay_event_get_queued_msg_length(wsl_ctx) + p_buffer_size > (uint32_t)outbound_buffer_size), ERR_OUT_OF_MEMORY);
|
||||
|
||||
struct wslay_event_msg msg;
|
||||
msg.opcode = p_opcode;
|
||||
msg.msg = p_buffer;
|
||||
msg.msg_length = p_buffer_size;
|
||||
|
||||
// Queue & send message.
|
||||
if (wslay_event_queue_msg(wsl_ctx, &msg) != 0 || wslay_event_send(wsl_ctx) != 0) {
|
||||
close(-1);
|
||||
return FAILED;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error WSLPeer::send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) {
|
||||
wslay_opcode opcode = p_mode == WRITE_MODE_TEXT ? WSLAY_TEXT_FRAME : WSLAY_BINARY_FRAME;
|
||||
return _send(p_buffer, p_buffer_size, opcode);
|
||||
}
|
||||
|
||||
Error WSLPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) {
|
||||
return _send(p_buffer, p_buffer_size, WSLAY_BINARY_FRAME);
|
||||
}
|
||||
|
||||
Error WSLPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) {
|
||||
r_buffer_size = 0;
|
||||
|
||||
ERR_FAIL_COND_V(ready_state != STATE_OPEN, FAILED);
|
||||
|
||||
if (in_buffer.packets_left() == 0) {
|
||||
return ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
int read = 0;
|
||||
uint8_t *rw = packet_buffer.ptrw();
|
||||
in_buffer.read_packet(rw, packet_buffer.size(), &was_string, read);
|
||||
|
||||
*r_buffer = rw;
|
||||
r_buffer_size = read;
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
int WSLPeer::get_available_packet_count() const {
|
||||
if (ready_state != STATE_OPEN) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return in_buffer.packets_left();
|
||||
}
|
||||
|
||||
int WSLPeer::get_current_outbound_buffered_amount() const {
|
||||
if (ready_state != STATE_OPEN) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return wslay_event_get_queued_msg_length(wsl_ctx);
|
||||
}
|
||||
|
||||
void WSLPeer::close(int p_code, String p_reason) {
|
||||
if (p_code < 0) {
|
||||
// Force immediate close.
|
||||
ready_state = STATE_CLOSED;
|
||||
}
|
||||
|
||||
if (ready_state == STATE_OPEN && !wslay_event_get_close_sent(wsl_ctx)) {
|
||||
CharString cs = p_reason.utf8();
|
||||
wslay_event_queue_close(wsl_ctx, p_code, (uint8_t *)cs.ptr(), cs.length());
|
||||
wslay_event_send(wsl_ctx);
|
||||
ready_state = STATE_CLOSING;
|
||||
} else if (ready_state == STATE_CONNECTING || ready_state == STATE_CLOSED) {
|
||||
ready_state = STATE_CLOSED;
|
||||
connection.unref();
|
||||
if (tcp.is_valid()) {
|
||||
tcp->disconnect_from_host();
|
||||
tcp.unref();
|
||||
}
|
||||
}
|
||||
|
||||
if (ready_state == STATE_CLOSED) {
|
||||
heartbeat_waiting = false;
|
||||
in_buffer.clear();
|
||||
packet_buffer.resize(0);
|
||||
pending_message.clear();
|
||||
}
|
||||
}
|
||||
|
||||
IPAddress WSLPeer::get_connected_host() const {
|
||||
ERR_FAIL_COND_V(tcp.is_null(), IPAddress());
|
||||
return tcp->get_connected_host();
|
||||
}
|
||||
|
||||
uint16_t WSLPeer::get_connected_port() const {
|
||||
ERR_FAIL_COND_V(tcp.is_null(), 0);
|
||||
return tcp->get_connected_port();
|
||||
}
|
||||
|
||||
String WSLPeer::get_selected_protocol() const {
|
||||
return selected_protocol;
|
||||
}
|
||||
|
||||
String WSLPeer::get_requested_url() const {
|
||||
return requested_url;
|
||||
}
|
||||
|
||||
void WSLPeer::set_no_delay(bool p_enabled) {
|
||||
ERR_FAIL_COND(tcp.is_null());
|
||||
tcp->set_no_delay(p_enabled);
|
||||
}
|
||||
|
||||
void WSLPeer::_clear() {
|
||||
// Connection info.
|
||||
ready_state = STATE_CLOSED;
|
||||
is_server = false;
|
||||
connection.unref();
|
||||
if (tcp.is_valid()) {
|
||||
tcp->disconnect_from_host();
|
||||
tcp.unref();
|
||||
}
|
||||
if (wsl_ctx) {
|
||||
wslay_event_context_free(wsl_ctx);
|
||||
wsl_ctx = nullptr;
|
||||
}
|
||||
|
||||
resolver.stop();
|
||||
requested_url.clear();
|
||||
requested_host.clear();
|
||||
pending_request = true;
|
||||
handshake_buffer->clear();
|
||||
selected_protocol.clear();
|
||||
session_key.clear();
|
||||
|
||||
// Pending packets info.
|
||||
was_string = 0;
|
||||
in_buffer.clear();
|
||||
packet_buffer.clear();
|
||||
|
||||
// Close code info.
|
||||
close_code = -1;
|
||||
close_reason.clear();
|
||||
}
|
||||
|
||||
WSLPeer::WSLPeer() {
|
||||
handshake_buffer.instantiate();
|
||||
}
|
||||
|
||||
WSLPeer::~WSLPeer() {
|
||||
close(-1);
|
||||
}
|
||||
|
||||
#endif // WEB_ENABLED
|
168
modules/websocket/wsl_peer.h
Normal file
168
modules/websocket/wsl_peer.h
Normal file
@@ -0,0 +1,168 @@
|
||||
/**************************************************************************/
|
||||
/* wsl_peer.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef WEB_ENABLED
|
||||
|
||||
#include "packet_buffer.h"
|
||||
#include "websocket_peer.h"
|
||||
|
||||
#include "core/crypto/crypto_core.h"
|
||||
#include "core/io/stream_peer_tcp.h"
|
||||
|
||||
#include <wslay/wslay.h>
|
||||
|
||||
#define WSL_MAX_HEADER_SIZE 4096
|
||||
|
||||
class WSLPeer : public WebSocketPeer {
|
||||
private:
|
||||
static CryptoCore::RandomGenerator *_static_rng;
|
||||
static WebSocketPeer *_create(bool p_notify_postinitialize) { return static_cast<WebSocketPeer *>(ClassDB::creator<WSLPeer>(p_notify_postinitialize)); }
|
||||
|
||||
// Callbacks.
|
||||
static ssize_t _wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data);
|
||||
static void _wsl_recv_start_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_frame_recv_start_arg *arg, void *user_data);
|
||||
static void _wsl_frame_recv_chunk_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_frame_recv_chunk_arg *arg, void *user_data);
|
||||
|
||||
static ssize_t _wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data);
|
||||
static int _wsl_genmask_callback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data);
|
||||
static void _wsl_msg_recv_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data);
|
||||
|
||||
static wslay_event_callbacks _wsl_callbacks;
|
||||
|
||||
// Helpers
|
||||
static String _compute_key_response(String p_key);
|
||||
static String _generate_key();
|
||||
|
||||
// Client IP resolver.
|
||||
class Resolver {
|
||||
Array ip_candidates;
|
||||
IP::ResolverID resolver_id = IP::RESOLVER_INVALID_ID;
|
||||
int port = 0;
|
||||
|
||||
public:
|
||||
bool has_more_candidates() {
|
||||
return ip_candidates.size() > 0 || resolver_id != IP::RESOLVER_INVALID_ID;
|
||||
}
|
||||
|
||||
void try_next_candidate(Ref<StreamPeerTCP> &p_tcp);
|
||||
void start(const String &p_host, int p_port);
|
||||
void stop();
|
||||
Resolver() {}
|
||||
};
|
||||
|
||||
struct PendingMessage {
|
||||
size_t payload_size = 0;
|
||||
uint8_t opcode = 0;
|
||||
|
||||
void clear() {
|
||||
payload_size = 0;
|
||||
opcode = 0;
|
||||
}
|
||||
};
|
||||
|
||||
Resolver resolver;
|
||||
|
||||
// WebSocket connection state.
|
||||
WebSocketPeer::State ready_state = WebSocketPeer::STATE_CLOSED;
|
||||
bool is_server = false;
|
||||
Ref<StreamPeerTCP> tcp;
|
||||
Ref<StreamPeer> connection;
|
||||
wslay_event_context_ptr wsl_ctx = nullptr;
|
||||
|
||||
String requested_url;
|
||||
String requested_host;
|
||||
bool pending_request = true;
|
||||
Ref<StreamPeerBuffer> handshake_buffer;
|
||||
String selected_protocol;
|
||||
String session_key;
|
||||
|
||||
int close_code = -1;
|
||||
String close_reason;
|
||||
uint8_t was_string = 0;
|
||||
uint64_t last_heartbeat = 0;
|
||||
bool heartbeat_waiting = false;
|
||||
PendingMessage pending_message;
|
||||
|
||||
// WebSocket configuration.
|
||||
bool use_tls = true;
|
||||
Ref<TLSOptions> tls_options;
|
||||
|
||||
// Packet buffers.
|
||||
Vector<uint8_t> packet_buffer;
|
||||
// Our packet info is just a boolean (is_string), using uint8_t for it.
|
||||
PacketBuffer<uint8_t> in_buffer;
|
||||
|
||||
Error _send(const uint8_t *p_buffer, int p_buffer_size, wslay_opcode p_opcode);
|
||||
|
||||
Error _do_server_handshake();
|
||||
bool _parse_client_request();
|
||||
|
||||
void _do_client_handshake();
|
||||
bool _verify_server_response();
|
||||
|
||||
void _clear();
|
||||
|
||||
public:
|
||||
static void initialize();
|
||||
static void deinitialize();
|
||||
|
||||
// PacketPeer
|
||||
virtual int get_available_packet_count() const override;
|
||||
virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size) override;
|
||||
virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override;
|
||||
virtual int get_max_packet_size() const override { return packet_buffer.size(); }
|
||||
|
||||
// WebSocketPeer
|
||||
virtual Error send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) override;
|
||||
virtual Error connect_to_url(const String &p_url, Ref<TLSOptions> p_options = Ref<TLSOptions>()) override;
|
||||
virtual Error accept_stream(Ref<StreamPeer> p_stream) override;
|
||||
virtual void close(int p_code = 1000, String p_reason = "") override;
|
||||
virtual void poll() override;
|
||||
|
||||
virtual State get_ready_state() const override { return ready_state; }
|
||||
virtual int get_close_code() const override { return close_code; }
|
||||
virtual String get_close_reason() const override { return close_reason; }
|
||||
virtual int get_current_outbound_buffered_amount() const override;
|
||||
|
||||
virtual IPAddress get_connected_host() const override;
|
||||
virtual uint16_t get_connected_port() const override;
|
||||
virtual String get_selected_protocol() const override;
|
||||
virtual String get_requested_url() const override;
|
||||
|
||||
virtual bool was_string_packet() const override { return was_string; }
|
||||
virtual void set_no_delay(bool p_enabled) override;
|
||||
|
||||
WSLPeer();
|
||||
~WSLPeer();
|
||||
};
|
||||
|
||||
#endif // WEB_ENABLED
|
Reference in New Issue
Block a user