From f3d4dab4541c095e212c1a15cc18730ca3f313e6 Mon Sep 17 00:00:00 2001 From: Sandu Liviu Catalin Date: Sun, 5 Sep 2021 12:05:38 +0300 Subject: [PATCH] Basic web-socket client implementation. --- module/PocoLib/Net.cpp | 81 +++++++++++++++++++-- module/PocoLib/Net.hpp | 158 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 4 deletions(-) diff --git a/module/PocoLib/Net.cpp b/module/PocoLib/Net.cpp index 8aabcf50..5b04e1fd 100644 --- a/module/PocoLib/Net.cpp +++ b/module/PocoLib/Net.cpp @@ -1,18 +1,91 @@ // ------------------------------------------------------------------------------------------------ #include "PocoLib/Net.hpp" +// ------------------------------------------------------------------------------------------------ +#include + // ------------------------------------------------------------------------------------------------ namespace SqMod { // ------------------------------------------------------------------------------------------------ - +SQMOD_DECL_TYPENAME(SqWsClient, _SC("SqWsClient")) // ================================================================================================ void Register_POCO_Net(HSQUIRRELVM vm, Table &) { - //Table ns(vm); - - //RootTable(vm).Bind(_SC("SqNet"), ns); + Table ns(vm); + // -------------------------------------------------------------------------------------------- + ns.Bind(_SC("WsClient"), + Class< WsClient, NoCopy< WsClient > >(ns.GetVM(), SqWsClient::Str) + // Constructors + .Ctor< StackStrF &, uint16_t, StackStrF & >() + // Meta-methods + .SquirrelFunc(_SC("_typename"), &SqWsClient::Fn) + // Member Variables + .Var(_SC("Flags"), &WsClient::mFlags) + .Var(_SC("State"), &WsClient::mState) + // Properties + .Prop(_SC("MaxPayloadSize"), &WsClient::GetMaxPayloadSize, &WsClient::SetMaxPayloadSize) + // Member Methods + .Func(_SC("Shutdown"), &WsClient::Shutdown) + .FmtFunc(_SC("ShutdownWith"), &WsClient::ShutdownWith) + .Func(_SC("SendFrame"), &WsClient::SendFrame) + .FmtFunc(_SC("SendStringFrame"), &WsClient::SendStringFrame) + .Func(_SC("RecvFrame"), &WsClient::RecvFrame) + .Func(_SC("RecvStringFrame"), &WsClient::RecvStringFrame) + ); + // -------------------------------------------------------------------------------------------- + RootTable(vm).Bind(_SC("SqNet"), ns); + // -------------------------------------------------------------------------------------------- + ConstTable(vm).Enum(_SC("SqWsFrameFlags"), Enumeration(vm) + .Const(_SC("FIN"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_FLAG_FIN)) + .Const(_SC("RSV1"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_FLAG_RSV1)) + .Const(_SC("RSV2"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_FLAG_RSV2)) + .Const(_SC("RSV3"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_FLAG_RSV3)) + ); + // -------------------------------------------------------------------------------------------- + ConstTable(vm).Enum(_SC("SqWsFrameOpcodes"), Enumeration(vm) + .Const(_SC("CONT"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_OP_CONT)) + .Const(_SC("TEXT"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_OP_TEXT)) + .Const(_SC("BINARY"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_OP_BINARY)) + .Const(_SC("CLOSE"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_OP_CLOSE)) + .Const(_SC("PING"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_OP_PING)) + .Const(_SC("PONG"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_OP_PONG)) + .Const(_SC("BITMASK"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_OP_BITMASK)) + .Const(_SC("SETRAW"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_OP_SETRAW)) + ); + // -------------------------------------------------------------------------------------------- + ConstTable(vm).Enum(_SC("SqWsSendFlags"), Enumeration(vm) + .Const(_SC("TEXT"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_TEXT)) + .Const(_SC("BINARY"), static_cast< SQInteger >(Poco::Net::WebSocket::FRAME_BINARY)) + ); + // -------------------------------------------------------------------------------------------- + ConstTable(vm).Enum(_SC("SqWsStatusCodes"), Enumeration(vm) + .Const(_SC("NormalClose"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_NORMAL_CLOSE)) + .Const(_SC("EndpointGoingAway"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_ENDPOINT_GOING_AWAY)) + .Const(_SC("ProtocolError"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_PROTOCOL_ERROR)) + .Const(_SC("PayloadNotAcceptable"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_PAYLOAD_NOT_ACCEPTABLE)) + .Const(_SC("Reserved"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_RESERVED)) + .Const(_SC("ReservedNoStatusCode"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_RESERVED_NO_STATUS_CODE)) + .Const(_SC("ReservedAbnormalClose"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_RESERVED_ABNORMAL_CLOSE)) + .Const(_SC("MalformedPayload"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_MALFORMED_PAYLOAD)) + .Const(_SC("PolicyViolation"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_POLICY_VIOLATION)) + .Const(_SC("PayloadTooBig"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_PAYLOAD_TOO_BIG)) + .Const(_SC("ExtensionRequired"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_EXTENSION_REQUIRED)) + .Const(_SC("UnexpectedCondition"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_UNEXPECTED_CONDITION)) + .Const(_SC("ReservedTlsFailure"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_RESERVED_TLS_FAILURE)) + ); + // -------------------------------------------------------------------------------------------- + ConstTable(vm).Enum(_SC("SqWsSendFlags"), Enumeration(vm) + .Const(_SC("NoHandshake"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_ERR_NO_HANDSHAKE)) + .Const(_SC("HandshakeNoVersion"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_ERR_HANDSHAKE_NO_VERSION)) + .Const(_SC("HandshakeUnsupportedVersion"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_ERR_HANDSHAKE_UNSUPPORTED_VERSION)) + .Const(_SC("HandshakeNoKey"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_ERR_HANDSHAKE_NO_KEY)) + .Const(_SC("HandshakeAccept"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_ERR_HANDSHAKE_ACCEPT)) + .Const(_SC("Unauthorized"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_ERR_UNAUTHORIZED)) + .Const(_SC("PayloadTooBig"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_ERR_PAYLOAD_TOO_BIG)) + .Const(_SC("IncompleteFrame"), static_cast< SQInteger >(Poco::Net::WebSocket::WS_ERR_INCOMPLETE_FRAME)) + ); } } // Namespace:: SqMod diff --git a/module/PocoLib/Net.hpp b/module/PocoLib/Net.hpp index 4eca0ee6..c867c8fb 100644 --- a/module/PocoLib/Net.hpp +++ b/module/PocoLib/Net.hpp @@ -2,10 +2,168 @@ // ------------------------------------------------------------------------------------------------ #include "Core/Common.hpp" +#include "Library/IO/Buffer.hpp" + +// ------------------------------------------------------------------------------------------------ +#include + +// ------------------------------------------------------------------------------------------------ +#include +#include +#include +#include +#include // ------------------------------------------------------------------------------------------------ namespace SqMod { +/* ------------------------------------------------------------------------------------------------ + * Implements a WebSocket according to the WebSocket protocol specification in RFC 6455. +*/ +struct WsClient +{ + /* -------------------------------------------------------------------------------------------- + * HTTP client session instance. + */ + Poco::Net::HTTPClientSession mClient; + /* -------------------------------------------------------------------------------------------- + * HTTP request instance. + */ + Poco::Net::HTTPRequest mRequest; + + /* -------------------------------------------------------------------------------------------- + * HTTP response instance. + */ + Poco::Net::HTTPResponse mResponse; + + /* -------------------------------------------------------------------------------------------- + * WebSocket instance. + */ + Poco::Net::WebSocket mWebSocket; + + /* -------------------------------------------------------------------------------------------- + * Receiving buffer instance. + */ + Poco::Buffer< char > mBuffer; + + /* -------------------------------------------------------------------------------------------- + * Flags received in the last call to Recv[String]Frame() (will be overwritten on next call). + */ + int mFlags{0}; + + /* -------------------------------------------------------------------------------------------- + * Return value from the last call to Recv[String]Frame() (will be overwritten on next call). + * A return value of 0, with flags also 0, means that the peer has shut down or closed the connection. + * A return value of 0, with non-zero flags, indicates an reception of an empty frame (e.g., in case of a PING). + */ + int mState{0}; + + /* -------------------------------------------------------------------------------------------- + * Base constructor. + */ + WsClient(StackStrF &host, uint16_t port, StackStrF &uri) + : mClient(host.ToStr(), port), + mRequest(Poco::Net::HTTPRequest::HTTP_GET, uri.ToStr(), Poco::Net::HTTPRequest::HTTP_1_1), mResponse(), + mWebSocket(mClient, mRequest, mResponse), mBuffer(0), mFlags(0), mState(0) + { + } + + /* -------------------------------------------------------------------------------------------- + * Sends a Close control frame to the server end of the connection to initiate an orderly shutdown of the connection. + */ + void Shutdown() { + mWebSocket.shutdown(); + } + + /* -------------------------------------------------------------------------------------------- + * Sends a Close control frame to the server end of the connection to initiate an orderly shutdown of the connection. + */ + void ShutdownWith(SQInteger code, StackStrF &msg) { + mWebSocket.shutdown(static_cast< uint16_t >(code), msg.ToStr()); + } + + /* -------------------------------------------------------------------------------------------- + * Sends the contents of the given buffer through the socket as a single frame. + * Returns the number of bytes sent, which may be less than the number of bytes specified. + */ + SQInteger SendFrame(SqBuffer & buf, SQInteger flags) { + return mWebSocket.sendFrame(buf.Valid().Data(), static_cast< int >(buf.Valid().Position()), static_cast< int >(flags)); + } + + /* -------------------------------------------------------------------------------------------- + * Sends the contents of the given string through the socket as a single frame. + * Returns the number of bytes sent, which may be less than the number of bytes specified. + */ + SQInteger SendStringFrame(SQInteger flags, StackStrF &str) { + return mWebSocket.sendFrame(str.mPtr, static_cast< int >(str.mLen), static_cast< int >(flags)); + } + + /* -------------------------------------------------------------------------------------------- + * Receives a frame from the socket and return it as a buffer. + * The frame's payload size must not exceed the maximum payload size set with SetMaxPayloadSize(). + */ + LightObj RecvFrame() + { + // Attempt to receive data + mState = mWebSocket.receiveFrame(mBuffer, mFlags); + // If something was returned + if (mState != 0) + { + // Fetch buffer information + const auto cap = static_cast< Buffer::SzType >(mBuffer.capacityBytes()); + const auto len = static_cast< Buffer::SzType >(mBuffer.sizeBytes()); + // Steal buffer memory + Buffer::Pointer ptr = mBuffer.steal(); + // Construct our buffer + Buffer b(ptr, cap, len, Buffer::OwnIt{}); + // Transform it into a script object and return it + return LightObj(SqTypeIdentity< SqBuffer >{}, SqVM(), std::move(b)); + } + // Default to null + return LightObj{}; + } + + /* -------------------------------------------------------------------------------------------- + * Receives a frame from the socket and return it as a string. + * The frame's payload size must not exceed the maximum payload size set with SetMaxPayloadSize(). + */ + LightObj RecvStringFrame() + { + // Attempt to receive data + mState = mWebSocket.receiveFrame(mBuffer, mFlags); + // If something was returned + if (mState != 0) + { + // Create a string with buffer contents + LightObj obj(SqTypeIdentity< SqBuffer >{}, SqVM(), + const_cast< const SQChar * >(mBuffer.begin()), + static_cast< SQInteger >(mBuffer.sizeBytes())); + // Discard buffer contents for next request + mBuffer.resize(0); + // Return the string object + return obj; + } + // Default to null + return LightObj{}; + } + + /* -------------------------------------------------------------------------------------------- + * Sets the maximum payload size for RecvFrame(). The default is std::numeric_limits::max(). + */ + WsClient & SetMaxPayloadSize(SQInteger size) + { + mWebSocket.setMaxPayloadSize(static_cast< int >(size)); + return *this; + } + + /* -------------------------------------------------------------------------------------------- + * Returns the maximum payload size for RecvFrame(). The default is std::numeric_limits::max(). + */ + SQMOD_NODISCARD SQInteger GetMaxPayloadSize() const + { + return mWebSocket.getMaxPayloadSize(); + } +}; } // Namespace:: SqMod