diff --git a/Import/VlppOS.Linux.cpp b/Import/VlppOS.Linux.cpp index be881e99..6afeaebb 100644 --- a/Import/VlppOS.Linux.cpp +++ b/Import/VlppOS.Linux.cpp @@ -1482,6 +1482,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } } +/*********************************************************************** +OwnedFileDescriptor +***********************************************************************/ + class OwnedFileDescriptor { private: @@ -1514,6 +1518,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +OperationDrain +***********************************************************************/ + class OperationDrain : public Object { private: @@ -1560,6 +1568,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +RingOperationOwner +***********************************************************************/ + class RingOperationOwner : public Object { friend class RingRuntime; @@ -1575,6 +1587,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket virtual void HandleOperationFailure(Ptr retainedOwner) = 0; }; +/*********************************************************************** +RingOperation +***********************************************************************/ + class RingOperation { friend class RingRuntime; @@ -1597,6 +1613,32 @@ namespace vl::inter_process::async_tcp_socket::linux_socket virtual void Handle(vint result) = 0; }; +/*********************************************************************** +RuntimeWakeOperation +***********************************************************************/ + + class RuntimeWakeOperation : public RingOperation + { + public: + RuntimeWakeOperation(vuint64_t id) + : RingOperation(id) + { + } + + void Prepare(io_uring_sqe* sqe) noexcept override + { + io_uring_prep_nop(sqe); + } + + void Handle(vint) override + { + } + }; + +/*********************************************************************** +RingRuntime +***********************************************************************/ + class RingRuntime : public Object { private: @@ -1918,24 +1960,6 @@ namespace vl::inter_process::async_tcp_socket::linux_socket void Stop(); }; - class RuntimeWakeOperation : public RingOperation - { - public: - RuntimeWakeOperation(vuint64_t id) - : RingOperation(id) - { - } - - void Prepare(io_uring_sqe* sqe) noexcept override - { - io_uring_prep_nop(sqe); - } - - void Handle(vint) override - { - } - }; - void RingRuntime::Stop() { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::RingRuntime::Stop()#" @@ -1998,6 +2022,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket #undef ERROR_MESSAGE_PREFIX } +/*********************************************************************** +ConnectionState +***********************************************************************/ + class ConnectionState : public RingOperationOwner { friend class AsyncSocketConnection; @@ -2009,7 +2037,7 @@ namespace vl::inter_process::async_tcp_socket::linux_socket class CancelOperation; Ptr runtime; - AsyncSocketConnection* owner = nullptr; + IAsyncSocketConnection* owner = nullptr; // covers all fields below, callback counts, and target operation counts CriticalSection lockState; @@ -2193,44 +2221,9 @@ namespace vl::inter_process::async_tcp_socket::linux_socket ClientStatus GetStatus(); }; - class AsyncSocketConnection : public Object, public virtual IAsyncSocketConnection - { - private: - Ptr state; - - public: - AsyncSocketConnection(Ptr _state) - : state(_state) - { - state->owner = this; - } - - ~AsyncSocketConnection() - { - state->Stop(state); - state->owner = nullptr; - } - - void InstallCallback(IAsyncSocketCallback* callback) override - { - state->InstallCallback(state, callback); - } - - void BeginReadingLoopUnsafe() override - { - state->BeginReading(state); - } - - void WriteAsync(Ptr buffer) override - { - state->Write(state, buffer); - } - - void Stop() override - { - state->Stop(state); - } - }; +/*********************************************************************** +ConnectionState::CancelOperation +***********************************************************************/ class ConnectionState::CancelOperation : public RingOperation { @@ -2258,6 +2251,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +ConnectionState::ReceiveOperation +***********************************************************************/ + class ConnectionState::ReceiveOperation : public RingOperation { public: @@ -2318,6 +2315,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +ConnectionState::WriteOperation +***********************************************************************/ + class ConnectionState::WriteOperation : public RingOperation { public: @@ -2471,6 +2472,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +ConnectionState::ConnectOperation +***********************************************************************/ + class ConnectionState::ConnectOperation : public RingOperation { public: @@ -2545,6 +2550,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +ConnectionState::RetryOperation +***********************************************************************/ + class ConnectionState::RetryOperation : public RingOperation { public: @@ -2592,6 +2601,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +ConnectionState +***********************************************************************/ + void ConnectionState::InstallCallback(Ptr, IAsyncSocketCallback* value) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::InstallCallback(Ptr, IAsyncSocketCallback*)#" @@ -3009,6 +3022,53 @@ namespace vl::inter_process::async_tcp_socket::linux_socket return result; } +/*********************************************************************** +AsyncSocketConnection +***********************************************************************/ + + class AsyncSocketConnection : public Object, public virtual IAsyncSocketConnection + { + private: + Ptr state; + + public: + AsyncSocketConnection(Ptr _state) + : state(_state) + { + state->owner = this; + } + + ~AsyncSocketConnection() + { + state->Stop(state); + state->owner = nullptr; + } + + void InstallCallback(IAsyncSocketCallback* callback) override + { + state->InstallCallback(state, callback); + } + + void BeginReadingLoopUnsafe() override + { + state->BeginReading(state); + } + + void WriteAsync(Ptr buffer) override + { + state->Write(state, buffer); + } + + void Stop() override + { + state->Stop(state); + } + }; + +/*********************************************************************** +ServerState +***********************************************************************/ + class ServerState : public RingOperationOwner { private: @@ -3016,18 +3076,19 @@ namespace vl::inter_process::async_tcp_socket::linux_socket class CancelOperation; Ptr runtime; - AsyncSocketServer* owner = nullptr; vint port = 0; // covers all fields below, callback counts, and target operation counts CriticalSection lockState; ConditionVariable cvCallbacks; + IAsyncSocketServerCallback* callback = nullptr; vint listener = -1; bool startCalled = false; bool starting = false; bool started = false; bool stopping = false; bool stopped = false; + bool unexpectedStopNotified = false; EventObject eventStartFinished; vuint64_t acceptOperationId = 0; bool acceptCancelRequested = false; @@ -3110,6 +3171,38 @@ namespace vl::inter_process::async_tcp_socket::linux_socket { std::abort(); } + HandleUnexpectedStop(retainedState); + } + + void HandleUnexpectedStop(Ptr retainedState) + { + IAsyncSocketServerCallback* installedCallback = nullptr; + CS_LOCK(lockState) + { + if (started && !stopping && !unexpectedStopNotified) + { + unexpectedStopNotified = true; + installedCallback = callback; + if (installedCallback) + { + activeCallbacks++; + } + } + } + if (installedCallback) + { + ServerCallbackFrame frame{ this, currentServerCallbackFrame }; + currentServerCallbackFrame = &frame; + try + { + installedCallback->OnServerStopped(); + } + catch (...) + { + } + currentServerCallbackFrame = frame.previous; + EndCallback(); + } Stop(retainedState); } @@ -3138,21 +3231,24 @@ namespace vl::inter_process::async_tcp_socket::linux_socket bool PostAccept(Ptr retainedState); public: - ServerState(Ptr _runtime, AsyncSocketServer* _owner, vint _port) + ServerState(Ptr _runtime, vint _port) : runtime(_runtime) - , owner(_owner) , port(_port) { -#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ServerState::ServerState(Ptr, AsyncSocketServer*, vint)#" +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ServerState::ServerState(Ptr, vint)#" CHECK_ERROR(eventStartFinished.CreateManualUnsignal(true), ERROR_MESSAGE_PREFIX L"Failed to create the server startup drain event."); #undef ERROR_MESSAGE_PREFIX } - void Start(Ptr retainedState); + void Start(Ptr retainedState, IAsyncSocketServerCallback* value); void Stop(Ptr retainedState); bool IsStopped(); }; +/*********************************************************************** +ServerState::CancelOperation +***********************************************************************/ + class ServerState::CancelOperation : public RingOperation { public: @@ -3179,6 +3275,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +ServerState::AcceptOperation +***********************************************************************/ + class ServerState::AcceptOperation : public RingOperation { public: @@ -3234,12 +3334,12 @@ namespace vl::inter_process::async_tcp_socket::linux_socket { if (!server->PostAccept(server)) { - server->Stop(server); + server->HandleUnexpectedStop(server); } } else if (running && result != -ECANCELED) { - server->Stop(server); + server->HandleUnexpectedStop(server); } return; } @@ -3253,23 +3353,23 @@ namespace vl::inter_process::async_tcp_socket::linux_socket // Keep exactly one accept pending, and rearm before invoking user code. if (!server->PostAccept(server)) { - server->Stop(server); + server->HandleUnexpectedStop(server); return; } auto connectionState = Ptr(new ConnectionState(server->runtime, acceptedSocket.Get())); auto connection = Ptr(new AsyncSocketConnection(connectionState)); acceptedSocket.Detach(); - bool invoke = false; + IAsyncSocketServerCallback* installedCallback = nullptr; CS_LOCK(server->lockState) { - if (server->started && !server->stopping && server->owner) + if (server->started && !server->stopping && server->callback) { server->connections.Add(connection); + installedCallback = server->callback; server->activeCallbacks++; - invoke = true; } } - if (!invoke) + if (!installedCallback) { connection->Stop(); return; @@ -3280,26 +3380,33 @@ namespace vl::inter_process::async_tcp_socket::linux_socket currentServerCallbackFrame = &frame; try { - acceptResult = server->owner->OnClientConnected(connection.Obj()); + acceptResult = installedCallback->OnClientConnected(connection.Obj()); } catch (...) { } currentServerCallbackFrame = frame.previous; - server->EndCallback(); - - bool stillRunning = false; + bool accepted = false; CS_LOCK(server->lockState) { - stillRunning = server->started && !server->stopping; + accepted = acceptResult == WaitForClientResult::Accept && server->started && !server->stopping; + if (!accepted) + { + server->connections.Remove(connection.Obj()); + } } - if (acceptResult == WaitForClientResult::Reject || !stillRunning) + server->EndCallback(); + if (!accepted) { connection->Stop(); } } }; +/*********************************************************************** +ServerState +***********************************************************************/ + bool ServerState::PostAccept(Ptr retainedState) { auto operationId = runtime->ReserveOperationId(); @@ -3330,14 +3437,16 @@ namespace vl::inter_process::async_tcp_socket::linux_socket return !submissionFailed; } - void ServerState::Start(Ptr retainedState) + void ServerState::Start(Ptr retainedState, IAsyncSocketServerCallback* value) { -#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ServerState::Start(Ptr)#" +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ServerState::Start(Ptr, IAsyncSocketServerCallback*)#" + CHECK_ERROR(value != nullptr, ERROR_MESSAGE_PREFIX L"Requires a callback."); CS_LOCK(lockState) { CHECK_ERROR(!startCalled && !stopping, ERROR_MESSAGE_PREFIX L"Can only be called once before stopping."); startCalled = true; starting = true; + callback = value; eventStartFinished.Unsignal(); } StartScope startScope(this); @@ -3371,9 +3480,11 @@ namespace vl::inter_process::async_tcp_socket::linux_socket { stopping = true; stopped = true; + callback = nullptr; } runtime->Stop(); - CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to create the loopback listener."); + auto failure = setupError == EADDRINUSE ? AsyncSocketServerStartFailure::AddressInUse : AsyncSocketServerStartFailure::Other; + throw AsyncSocketServerStartException(failure, LinuxSocketErrorMessage(L"AsyncSocketServer listener setup", setupError)); } bool committed = false; @@ -3397,25 +3508,35 @@ namespace vl::inter_process::async_tcp_socket::linux_socket started = false; stopping = true; stopped = true; + callback = nullptr; } runtime->Stop(); - throw; + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"Failed to start the listener runtime."); } - if (committed) + if (!committed) { - if (!PostAccept(retainedState)) + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"The listener was stopped during startup."); + } + if (!PostAccept(retainedState)) + { + CS_LOCK(lockState) { - CS_LOCK(lockState) + started = false; + stopping = true; + stopped = true; + callback = nullptr; + if (listener >= 0) { - started = false; - stopping = true; - stopped = true; - CloseFileDescriptor(listener); - listener = -1; + shutdown((int)listener, SHUT_RDWR); + if (targetOperations == 0) + { + CloseFileDescriptor(listener); + listener = -1; + } } - runtime->Stop(); - CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to submit the first accept operation."); } + runtime->Stop(); + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"Failed to submit the first accept operation."); } #undef ERROR_MESSAGE_PREFIX } @@ -3498,6 +3619,13 @@ namespace vl::inter_process::async_tcp_socket::linux_socket connection->Stop(); } runtime->Stop(); + if (callbackDepth == 0) + { + CS_LOCK(lockState) + { + callback = nullptr; + } + } } bool ServerState::IsStopped() @@ -3510,16 +3638,22 @@ namespace vl::inter_process::async_tcp_socket::linux_socket return result; } +/*********************************************************************** +AsyncSocketServer::Impl +***********************************************************************/ + class AsyncSocketServer::Impl : public Object { private: + vint port = 0; Ptr runtime; Ptr state; public: - Impl(AsyncSocketServer* owner, vint port) - : runtime(RingRuntime::Create(false)) - , state(Ptr(new ServerState(runtime, owner, port))) + Impl(vint _port) + : port(_port) + , runtime(RingRuntime::Create(false)) + , state(Ptr(new ServerState(runtime, _port))) { } @@ -3528,9 +3662,14 @@ namespace vl::inter_process::async_tcp_socket::linux_socket Stop(); } - void Start() + vint GetPort() { - state->Start(state); + return port; + } + + void Start(IAsyncSocketServerCallback* callback) + { + state->Start(state, callback); } void Stop() @@ -3544,12 +3683,16 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +AsyncSocketServer +***********************************************************************/ + AsyncSocketServer::AsyncSocketServer(vint port) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::AsyncSocketServer::AsyncSocketServer(vint)#" CHECK_ERROR(1 <= port && port <= 65535, ERROR_MESSAGE_PREFIX L"The port must be in 1..65535."); #undef ERROR_MESSAGE_PREFIX - impl = new Impl(this, port); + impl = new Impl(port); } AsyncSocketServer::~AsyncSocketServer() @@ -3557,14 +3700,14 @@ namespace vl::inter_process::async_tcp_socket::linux_socket delete impl; } - WaitForClientResult AsyncSocketServer::OnClientConnected(IAsyncSocketConnection*) + vint AsyncSocketServer::GetPort() { - return WaitForClientResult::Accept; + return impl->GetPort(); } - void AsyncSocketServer::Start() + void AsyncSocketServer::Start(IAsyncSocketServerCallback* callback) { - impl->Start(); + impl->Start(callback); } void AsyncSocketServer::Stop() @@ -3577,17 +3720,23 @@ namespace vl::inter_process::async_tcp_socket::linux_socket return impl->IsStopped(); } +/*********************************************************************** +AsyncSocketClient::Impl +***********************************************************************/ + class AsyncSocketClient::Impl : public Object { private: + vint port = 0; Ptr runtime; Ptr state; Ptr connection; public: - Impl(vint port) - : runtime(RingRuntime::Create(true)) - , state(Ptr(new ConnectionState(runtime, true, port))) + Impl(vint _port) + : port(_port) + , runtime(RingRuntime::Create(true)) + , state(Ptr(new ConnectionState(runtime, true, _port))) , connection(Ptr(new AsyncSocketConnection(state))) { } @@ -3597,6 +3746,11 @@ namespace vl::inter_process::async_tcp_socket::linux_socket Stop(); } + vint GetPort() + { + return port; + } + void Stop() { connection->Stop(); @@ -3619,6 +3773,10 @@ namespace vl::inter_process::async_tcp_socket::linux_socket } }; +/*********************************************************************** +AsyncSocketClient +***********************************************************************/ + AsyncSocketClient::AsyncSocketClient(vint port) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::AsyncSocketClient::AsyncSocketClient(vint)#" @@ -3632,6 +3790,16 @@ namespace vl::inter_process::async_tcp_socket::linux_socket delete impl; } + vint AsyncSocketClient::GetPort() + { + return impl->GetPort(); + } + + Ptr AsyncSocketClient::CreateSameEndpointClient() + { + return Ptr(new AsyncSocketClient(GetPort())); + } + IAsyncSocketConnection* AsyncSocketClient::GetConnection() { return impl->GetConnection(); @@ -3646,10 +3814,25 @@ namespace vl::inter_process::async_tcp_socket::linux_socket { return impl->GetStatus(); } + +} + +namespace vl::inter_process::async_tcp_socket +{ + Ptr CreateDefaultAsyncSocketServer(vint port) + { + return Ptr(new linux_socket::AsyncSocketServer(port)); + } + + Ptr CreateDefaultAsyncSocketClient(vint port) + { + return Ptr(new linux_socket::AsyncSocketClient(port)); + } } #endif + /*********************************************************************** .\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET.MACOS.CPP ***********************************************************************/ @@ -3679,6 +3862,10 @@ namespace vl::inter_process::async_tcp_socket::macos_socket static char connectionQueueKey; static char serverQueueKey; +/*********************************************************************** +NativeConnectionContext +***********************************************************************/ + class NativeConnectionContext : public Object { friend class ConnectionState; @@ -3696,6 +3883,24 @@ namespace vl::inter_process::async_tcp_socket::macos_socket ~NativeConnectionContext(); }; + NativeConnectionContext::NativeConnectionContext(Ptr _state, nw_connection_t _connection) + : state(_state) + , connection(_connection) + { + } + + NativeConnectionContext::~NativeConnectionContext() + { + if (connection) + { + nw_release(connection); + } + } + +/*********************************************************************** +ConnectionState +***********************************************************************/ + class ConnectionState : public Object { friend class NativeConnectionContext; @@ -4819,19 +5024,9 @@ namespace vl::inter_process::async_tcp_socket::macos_socket } }; - NativeConnectionContext::NativeConnectionContext(Ptr _state, nw_connection_t _connection) - : state(_state) - , connection(_connection) - { - } - - NativeConnectionContext::~NativeConnectionContext() - { - if (connection) - { - nw_release(connection); - } - } +/*********************************************************************** +AsyncSocketConnection +***********************************************************************/ class AsyncSocketConnection : public Object, public virtual IAsyncSocketConnection { @@ -4887,6 +5082,10 @@ namespace vl::inter_process::async_tcp_socket::macos_socket } }; +/*********************************************************************** +ServerState +***********************************************************************/ + class ServerState : public Object { private: @@ -4894,12 +5093,18 @@ namespace vl::inter_process::async_tcp_socket::macos_socket CriticalSection lockState; ConditionVariable cvState; dispatch_queue_t queue = nullptr; - AsyncSocketServer* owner = nullptr; + IAsyncSocketServerCallback* callback = nullptr; vint port = 0; bool startCalled = false; + bool starting = false; + bool startupResolved = false; + bool startupReady = false; + AsyncSocketServerStartFailure startupFailure = AsyncSocketServerStartFailure::Other; + vint startupError = 0; bool started = false; bool stopping = false; bool stopped = false; + bool unexpectedStopNotified = false; bool stopFinalizing = false; bool stopCompleted = false; nw_listener_t listener = nullptr; @@ -4958,14 +5163,94 @@ namespace vl::inter_process::async_tcp_socket::macos_socket } } - void OnListenerState(Ptr retainedState, nw_listener_state_t state) + void OnListenerState(Ptr retainedState, nw_listener_state_t state, nw_error_t error) { switch (state) { + case nw_listener_state_waiting: + { + bool startupFailed = false; + CS_LOCK(lockState) + { + if (starting && !startupResolved) + { + starting = false; + startupResolved = true; + startupReady = false; + startupError = error ? (vint)nw_error_get_error_code(error) : 0; + startupFailure = error && nw_error_get_error_domain(error) == nw_error_domain_posix && startupError == EADDRINUSE + ? AsyncSocketServerStartFailure::AddressInUse + : AsyncSocketServerStartFailure::Other; + startupFailed = true; + cvState.WakeAllPendings(); + } + } + if (startupFailed) + { + Stop(); + } + } + break; + case nw_listener_state_ready: + CS_LOCK(lockState) + { + if (starting && !startupResolved && !stopping) + { + starting = false; + startupResolved = true; + startupReady = true; + started = true; + cvState.WakeAllPendings(); + } + } + break; case nw_listener_state_failed: + { + IAsyncSocketServerCallback* installedCallback = nullptr; + lockState.Enter(); + if (starting && !startupResolved) + { + starting = false; + startupResolved = true; + startupReady = false; + startupError = error ? (vint)nw_error_get_error_code(error) : 0; + startupFailure = error && nw_error_get_error_domain(error) == nw_error_domain_posix && startupError == EADDRINUSE + ? AsyncSocketServerStartFailure::AddressInUse + : AsyncSocketServerStartFailure::Other; + cvState.WakeAllPendings(); + } + else if (started && !stopping && !unexpectedStopNotified) + { + unexpectedStopNotified = true; + installedCallback = callback; + } + lockState.Leave(); + + if (installedCallback) + { + try + { + installedCallback->OnServerStopped(); + } + catch (...) + { + } + } + } Stop(); break; case nw_listener_state_cancelled: + CS_LOCK(lockState) + { + if (starting && !startupResolved) + { + starting = false; + startupResolved = true; + startupReady = false; + startupFailure = AsyncSocketServerStartFailure::Other; + cvState.WakeAllPendings(); + } + } OnListenerCancelled(retainedState); break; default: @@ -4985,20 +5270,21 @@ namespace vl::inter_process::async_tcp_socket::macos_socket void OnNewConnection(nw_connection_t connection) { auto wrapper = CreateConnection(connection); - bool offer = false; - AsyncSocketServer* installedOwner = nullptr; + IAsyncSocketServerCallback* installedCallback = nullptr; CS_LOCK(lockState) { - offer = started && !stopping && owner; - installedOwner = owner; + if (started && !stopping) + { + installedCallback = callback; + } } WaitForClientResult result = WaitForClientResult::Reject; - if (offer) + if (installedCallback) { try { - result = installedOwner->OnClientConnected(wrapper.Obj()); + result = installedCallback->OnClientConnected(wrapper.Obj()); } catch (...) { @@ -5025,9 +5311,8 @@ namespace vl::inter_process::async_tcp_socket::macos_socket } public: - ServerState(AsyncSocketServer* _owner, vint _port) - : owner(_owner) - , port(_port) + ServerState(vint _port) + : port(_port) { queue = dispatch_queue_create("vlppos.async-socket.listener", DISPATCH_QUEUE_SERIAL); CHECK_ERROR(queue != nullptr, L"AsyncSocketServer failed to create its dispatch queue."); @@ -5040,26 +5325,21 @@ namespace vl::inter_process::async_tcp_socket::macos_socket dispatch_release(queue); } - void DetachOwner() - { - CS_LOCK(lockState) - { - owner = nullptr; - } - } - - void Start(Ptr retainedState) + void Start(Ptr retainedState, IAsyncSocketServerCallback* value) { +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::macos_socket::ServerState::Start(Ptr, IAsyncSocketServerCallback*)#" + CHECK_ERROR(value != nullptr, ERROR_MESSAGE_PREFIX L"Requires a callback."); bool begin = false; CS_LOCK(lockState) { if (!startCalled && !stopping) { startCalled = true; + callback = value; begin = true; } } - CHECK_ERROR(begin, L"AsyncSocketServer::Start can only be called once."); + CHECK_ERROR(begin, ERROR_MESSAGE_PREFIX L"Can only be called once before stopping."); auto portText = itoa(port); auto endpoint = nw_endpoint_create_host("127.0.0.1", portText.Buffer()); @@ -5088,8 +5368,9 @@ namespace vl::inter_process::async_tcp_socket::macos_socket { stopping = true; stopped = true; + callback = nullptr; } - CHECK_ERROR(false, L"AsyncSocketServer failed to create its Network.framework listener."); + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"Failed to create the Network.framework listener."); } bool installed = false; @@ -5098,13 +5379,13 @@ namespace vl::inter_process::async_tcp_socket::macos_socket { listener = createdListener; pendingListener = 1; - started = true; + starting = true; nw_listener_set_queue(listener, queue); auto retainedForState = retainedState; - nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t) + nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t error) { - retainedForState->OnListenerState(retainedForState, state); + retainedForState->OnListenerState(retainedForState, state, error); }); auto retainedForConnection = retainedState; @@ -5119,7 +5400,27 @@ namespace vl::inter_process::async_tcp_socket::macos_socket if (!installed) { nw_release(createdListener); + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"The listener was stopped during startup."); } + + AsyncSocketServerStartFailure failure = AsyncSocketServerStartFailure::Other; + vint error = 0; + bool ready = false; + lockState.Enter(); + while (!startupResolved) + { + cvState.SleepWith(lockState); + } + ready = startupReady; + failure = startupFailure; + error = startupError; + lockState.Leave(); + if (!ready) + { + Stop(); + throw AsyncSocketServerStartException(failure, ERROR_MESSAGE_PREFIX L"Network.framework listener startup failed with error " + itow(error) + L"."); + } +#undef ERROR_MESSAGE_PREFIX } void Stop() @@ -5132,6 +5433,14 @@ namespace vl::inter_process::async_tcp_socket::macos_socket stopping = true; started = false; stopped = true; + if (starting && !startupResolved) + { + starting = false; + startupResolved = true; + startupReady = false; + startupFailure = AsyncSocketServerStartFailure::Other; + cvState.WakeAllPendings(); + } } RequestListenerCancelLocked(); for (auto connection : connections) @@ -5193,6 +5502,7 @@ namespace vl::inter_process::async_tcp_socket::macos_socket CS_LOCK(lockState) { connections.Clear(); + callback = nullptr; stopFinalizing = false; stopCompleted = true; cvState.WakeAllPendings(); @@ -5210,26 +5520,36 @@ namespace vl::inter_process::async_tcp_socket::macos_socket } }; +/*********************************************************************** +AsyncSocketServer::Impl +***********************************************************************/ + class AsyncSocketServer::Impl : public Object { private: + vint port = 0; Ptr state; public: - Impl(AsyncSocketServer* owner, vint port) - : state(Ptr(new ServerState(owner, port))) + Impl(vint _port) + : port(_port) + , state(Ptr(new ServerState(_port))) { } ~Impl() { state->Stop(); - state->DetachOwner(); } - void Start() + vint GetPort() { - state->Start(state); + return port; + } + + void Start(IAsyncSocketServerCallback* callback) + { + state->Start(state, callback); } void Stop() @@ -5243,10 +5563,14 @@ namespace vl::inter_process::async_tcp_socket::macos_socket } }; +/*********************************************************************** +AsyncSocketServer +***********************************************************************/ + AsyncSocketServer::AsyncSocketServer(vint port) { CHECK_ERROR(1 <= port && port <= 65535, L"AsyncSocketServer requires a port in 1..65535."); - impl = new Impl(this, port); + impl = new Impl(port); } AsyncSocketServer::~AsyncSocketServer() @@ -5254,14 +5578,14 @@ namespace vl::inter_process::async_tcp_socket::macos_socket delete impl; } - WaitForClientResult AsyncSocketServer::OnClientConnected(IAsyncSocketConnection*) + vint AsyncSocketServer::GetPort() { - return WaitForClientResult::Accept; + return impl->GetPort(); } - void AsyncSocketServer::Start() + void AsyncSocketServer::Start(IAsyncSocketServerCallback* callback) { - impl->Start(); + impl->Start(callback); } void AsyncSocketServer::Stop() @@ -5274,15 +5598,21 @@ namespace vl::inter_process::async_tcp_socket::macos_socket return impl->IsStopped(); } +/*********************************************************************** +AsyncSocketClient::Impl +***********************************************************************/ + class AsyncSocketClient::Impl : public Object { private: + vint port = 0; Ptr state; Ptr connection; public: - Impl(vint port) - : state(Ptr(new ConnectionState(true, port))) + Impl(vint _port) + : port(_port) + , state(Ptr(new ConnectionState(true, _port))) , connection(Ptr(new AsyncSocketConnection(state))) { } @@ -5292,6 +5622,11 @@ namespace vl::inter_process::async_tcp_socket::macos_socket connection->Stop(); } + vint GetPort() + { + return port; + } + IAsyncSocketConnection* GetConnection() { return connection.Obj(); @@ -5308,6 +5643,10 @@ namespace vl::inter_process::async_tcp_socket::macos_socket } }; +/*********************************************************************** +AsyncSocketClient +***********************************************************************/ + AsyncSocketClient::AsyncSocketClient(vint port) { CHECK_ERROR(1 <= port && port <= 65535, L"AsyncSocketClient requires a port in 1..65535."); @@ -5319,6 +5658,16 @@ namespace vl::inter_process::async_tcp_socket::macos_socket delete impl; } + vint AsyncSocketClient::GetPort() + { + return impl->GetPort(); + } + + Ptr AsyncSocketClient::CreateSameEndpointClient() + { + return Ptr(new AsyncSocketClient(GetPort())); + } + IAsyncSocketConnection* AsyncSocketClient::GetConnection() { return impl->GetConnection(); @@ -5333,5 +5682,20 @@ namespace vl::inter_process::async_tcp_socket::macos_socket { return impl->GetStatus(); } + +} + +namespace vl::inter_process::async_tcp_socket +{ + Ptr CreateDefaultAsyncSocketServer(vint port) + { + return Ptr(new macos_socket::AsyncSocketServer(port)); + } + + Ptr CreateDefaultAsyncSocketClient(vint port) + { + return Ptr(new macos_socket::AsyncSocketClient(port)); + } } #endif + diff --git a/Import/VlppOS.Linux.h b/Import/VlppOS.Linux.h index d90c13b0..db36e761 100644 --- a/Import/VlppOS.Linux.h +++ b/Import/VlppOS.Linux.h @@ -34,8 +34,8 @@ namespace vl::inter_process::async_tcp_socket::linux_socket AsyncSocketServer(vint port); ~AsyncSocketServer(); - WaitForClientResult OnClientConnected(IAsyncSocketConnection* connection) override; - void Start() override; + vint GetPort() override; + void Start(IAsyncSocketServerCallback* callback) override; void Stop() override; bool IsStopped() override; }; @@ -50,6 +50,8 @@ namespace vl::inter_process::async_tcp_socket::linux_socket AsyncSocketClient(vint port); ~AsyncSocketClient(); + vint GetPort() override; + Ptr CreateSameEndpointClient() override; IAsyncSocketConnection* GetConnection() override; void WaitForServer() override; ClientStatus GetStatus() override; @@ -90,8 +92,8 @@ namespace vl::inter_process::async_tcp_socket::macos_socket AsyncSocketServer(vint port); ~AsyncSocketServer(); - WaitForClientResult OnClientConnected(IAsyncSocketConnection* connection) override; - void Start() override; + vint GetPort() override; + void Start(IAsyncSocketServerCallback* callback) override; void Stop() override; bool IsStopped() override; }; @@ -106,6 +108,8 @@ namespace vl::inter_process::async_tcp_socket::macos_socket AsyncSocketClient(vint port); ~AsyncSocketClient(); + vint GetPort() override; + Ptr CreateSameEndpointClient() override; IAsyncSocketConnection* GetConnection() override; void WaitForServer() override; ClientStatus GetStatus() override; diff --git a/Import/VlppOS.Windows.cpp b/Import/VlppOS.Windows.cpp index 62f32631..dc459f62 100644 --- a/Import/VlppOS.Windows.cpp +++ b/Import/VlppOS.Windows.cpp @@ -1668,8 +1668,8 @@ namespace vl::inter_process::async_tcp_socket::windows_socket class IocpOperation; class IocpRuntime; - static thread_local IocpRuntime* currentCompletionRuntime = nullptr; - static thread_local IocpRuntime* currentCallbackRuntime = nullptr; + class ConnectionState; + class AsyncSocketConnection; struct NativeOverlapped { @@ -1677,6 +1677,25 @@ namespace vl::inter_process::async_tcp_socket::windows_socket IocpOperation* operation = nullptr; }; + struct CallbackFrame + { + ConnectionState* connection = nullptr; + CallbackFrame* previous = nullptr; + }; + + static thread_local IocpRuntime* currentCompletionRuntime = nullptr; + static thread_local IocpRuntime* currentCallbackRuntime = nullptr; + static thread_local CallbackFrame* currentCallbackFrame = nullptr; + + WString SocketErrorMessage(const wchar_t* operation, DWORD error) + { + return WString::Unmanaged(operation) + L" failed with Windows error " + itow((vint)error) + L"."; + } + +/*********************************************************************** +IocpOperation +***********************************************************************/ + class IocpOperation { public: @@ -1693,6 +1712,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket virtual void EndPending() = 0; }; +/*********************************************************************** +IocpRuntime +***********************************************************************/ + class IocpRuntime : public Object { private: @@ -1918,15 +1941,9 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } }; - class ConnectionState; - - struct CallbackFrame - { - ConnectionState* connection = nullptr; - CallbackFrame* previous = nullptr; - }; - - static thread_local CallbackFrame* currentCallbackFrame = nullptr; +/*********************************************************************** +ReadBlock +***********************************************************************/ class ReadBlock : public Object { @@ -1939,7 +1956,9 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } }; - class AsyncSocketConnection; +/*********************************************************************** +ConnectionState +***********************************************************************/ class ConnectionState : public Object { @@ -2145,6 +2164,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket ClientStatus GetStatus(); }; +/*********************************************************************** +AsyncSocketConnection +***********************************************************************/ + class AsyncSocketConnection : public Object, public virtual IAsyncSocketConnection { private: @@ -2189,16 +2212,9 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } }; - Ptr ConnectionState::Retain() - { - CHECK_ERROR(owner != nullptr, L"IAsyncSocketConnection lost its canonical state owner."); - return owner->GetState(); - } - - WString SocketErrorMessage(const wchar_t* operation, DWORD error) - { - return WString::Unmanaged(operation) + L" failed with Windows error " + itow((vint)error) + L"."; - } +/*********************************************************************** +ConnectionState::ReadOperation +***********************************************************************/ class ConnectionState::ReadOperation : public IocpOperation { @@ -2250,6 +2266,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } }; +/*********************************************************************** +ConnectionState::WriteOperation +***********************************************************************/ + class ConnectionState::WriteOperation : public IocpOperation { public: @@ -2330,6 +2350,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } }; +/*********************************************************************** +ConnectionState::ConnectOperation +***********************************************************************/ + class ConnectionState::ConnectOperation : public IocpOperation { public: @@ -2356,6 +2380,16 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } }; +/*********************************************************************** +ConnectionState +***********************************************************************/ + + Ptr ConnectionState::Retain() + { + CHECK_ERROR(owner != nullptr, L"IAsyncSocketConnection lost its canonical state owner."); + return owner->GetState(); + } + void ConnectionState::InstallCallback(IAsyncSocketCallback* value) { if (!value) @@ -2934,6 +2968,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } } +/*********************************************************************** +AsyncSocketServer::Impl +***********************************************************************/ + class AsyncSocketServer::Impl : public Object { private: @@ -2963,21 +3001,54 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } }; - AsyncSocketServer* owner = nullptr; vint port = 0; Ptr runtime; CriticalSection lockState; + IAsyncSocketServerCallback* callback = nullptr; + bool startCalled = false; + bool starting = false; + vint startingThreadId = -1; bool started = false; bool stopping = false; bool stopped = false; + bool unexpectedStopQueued = false; + bool unexpectedStopNotified = false; SOCKET listener = INVALID_SOCKET; LPFN_ACCEPTEX acceptEx = nullptr; bool acceptPending = false; vint pendingAccepts = 0; EventObject eventAcceptDrained; + EventObject eventStartFinished; EventObject eventStopped; List> connections; + void FinishStart() + { + CS_LOCK(lockState) + { + starting = false; + startingThreadId = -1; + eventStartFinished.Signal(); + } + } + + class StartScope + { + private: + Impl* server = nullptr; + + public: + StartScope(Impl* _server) + : server(_server) + { + } + + ~StartScope() + { + server->FinishStart(); + } + }; + void BeginAcceptPendingLocked() { if (pendingAccepts++ == 0) @@ -3063,6 +3134,48 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } } + void QueueUnexpectedStop() + { + bool queue = false; + CS_LOCK(lockState) + { + if (started && !stopping && !unexpectedStopQueued) + { + unexpectedStopQueued = true; + queue = true; + } + } + if (!queue) + { + return; + } + + auto self = this; + runtime->QueueCallback(Func([self]() + { + IAsyncSocketServerCallback* installed = nullptr; + CS_LOCK(self->lockState) + { + if (self->started && !self->stopping && !self->unexpectedStopNotified) + { + self->unexpectedStopNotified = true; + installed = self->callback; + } + } + if (installed) + { + try + { + installed->OnServerStopped(); + } + catch (...) + { + } + } + self->Stop(); + })); + } + void CompleteAccept(AcceptOperation* operation, DWORD error) { bool running = false; @@ -3078,7 +3191,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket closesocket(acceptedSocket); if (running) { - PostAccept(); + if (!PostAccept()) + { + QueueUnexpectedStop(); + } } return; } @@ -3086,7 +3202,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket if (setsockopt(acceptedSocket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (CHAR*)&listener, sizeof(listener)) == SOCKET_ERROR || !runtime->Associate(acceptedSocket)) { closesocket(acceptedSocket); - PostAccept(); + if (!PostAccept()) + { + QueueUnexpectedStop(); + } return; } @@ -3102,7 +3221,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } } - PostAccept(); + if (!PostAccept()) + { + QueueUnexpectedStop(); + } if (!retain) { connection->Stop(); @@ -3113,9 +3235,14 @@ namespace vl::inter_process::async_tcp_socket::windows_socket runtime->QueueCallback(Func([self, connection]() { bool invoke = false; + IAsyncSocketServerCallback* installed = nullptr; CS_LOCK(self->lockState) { invoke = self->started && !self->stopping; + if (invoke) + { + installed = self->callback; + } } if (!invoke) { @@ -3126,25 +3253,29 @@ namespace vl::inter_process::async_tcp_socket::windows_socket WaitForClientResult result = WaitForClientResult::Reject; try { - result = self->owner->OnClientConnected(connection.Obj()); + result = installed->OnClientConnected(connection.Obj()); } catch (...) { } if (result == WaitForClientResult::Reject) { + CS_LOCK(self->lockState) + { + self->connections.Remove(connection.Obj()); + } connection->Stop(); } })); } public: - Impl(AsyncSocketServer* _owner, vint _port) - : owner(_owner) - , port(_port) + Impl(vint _port) + : port(_port) , runtime(Ptr(new IocpRuntime)) { CHECK_ERROR(eventAcceptDrained.CreateManualUnsignal(true), L"AsyncSocketServer failed to create its accept drain event."); + CHECK_ERROR(eventStartFinished.CreateManualUnsignal(true), L"AsyncSocketServer failed to create its startup drain event."); CHECK_ERROR(eventStopped.CreateManualUnsignal(false), L"AsyncSocketServer failed to create its stop event."); } @@ -3153,31 +3284,63 @@ namespace vl::inter_process::async_tcp_socket::windows_socket Stop(); } - void Start() + vint GetPort() { + return port; + } + + void Start(IAsyncSocketServerCallback* _callback) + { + CHECK_ERROR(_callback != nullptr, L"AsyncSocketServer::Start requires a callback."); + bool begin = false; + CS_LOCK(lockState) + { + if (!startCalled && !stopping) + { + startCalled = true; + starting = true; + startingThreadId = Thread::GetCurrentThreadId(); + eventStartFinished.Unsignal(); + begin = true; + } + } + CHECK_ERROR(begin, L"AsyncSocketServer::Start can only be called once."); + StartScope startScope(this); SOCKET createdListener = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_OVERLAPPED); - CHECK_ERROR(createdListener != INVALID_SOCKET, L"AsyncSocketServer failed to create its listener socket."); + if (createdListener == INVALID_SOCKET) + { + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to create its listener socket."); + } BOOL exclusive = TRUE; if (setsockopt(createdListener, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (CHAR*)&exclusive, sizeof(exclusive)) == SOCKET_ERROR) { closesocket(createdListener); - CHECK_ERROR(false, L"AsyncSocketServer failed to apply SO_EXCLUSIVEADDRUSE."); + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to apply SO_EXCLUSIVEADDRUSE."); } SOCKADDR_IN address = {}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = htons((u_short)port); - if (bind(createdListener, (SOCKADDR*)&address, sizeof(address)) == SOCKET_ERROR || listen(createdListener, SOMAXCONN) == SOCKET_ERROR) + if (bind(createdListener, (SOCKADDR*)&address, sizeof(address)) == SOCKET_ERROR) { + auto error = WSAGetLastError(); closesocket(createdListener); - CHECK_ERROR(false, L"AsyncSocketServer failed to bind or listen on 127.0.0.1."); + auto failure = error == WSAEADDRINUSE || error == WSAEACCES ? AsyncSocketServerStartFailure::AddressInUse : AsyncSocketServerStartFailure::Other; + throw AsyncSocketServerStartException(failure, SocketErrorMessage(L"AsyncSocketServer bind", error)); + } + if (listen(createdListener, SOMAXCONN) == SOCKET_ERROR) + { + auto error = WSAGetLastError(); + closesocket(createdListener); + auto failure = error == WSAEADDRINUSE || error == WSAEACCES ? AsyncSocketServerStartFailure::AddressInUse : AsyncSocketServerStartFailure::Other; + throw AsyncSocketServerStartException(failure, SocketErrorMessage(L"AsyncSocketServer listen", error)); } if (!runtime->Associate(createdListener)) { closesocket(createdListener); - CHECK_ERROR(false, L"AsyncSocketServer failed to associate its listener with the IO completion port."); + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to associate its listener with the IO completion port."); } LPFN_ACCEPTEX loadedAcceptEx = nullptr; @@ -3196,16 +3359,17 @@ namespace vl::inter_process::async_tcp_socket::windows_socket ) == SOCKET_ERROR) { closesocket(createdListener); - CHECK_ERROR(false, L"AsyncSocketServer failed to load AcceptEx."); + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to load AcceptEx."); } bool canStart = false; CS_LOCK(lockState) { - if (!started && !stopping) + if (!stopping) { listener = createdListener; acceptEx = loadedAcceptEx; + callback = _callback; started = true; canStart = true; } @@ -3213,12 +3377,12 @@ namespace vl::inter_process::async_tcp_socket::windows_socket if (!canStart) { closesocket(createdListener); + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer was stopped during startup."); } - CHECK_ERROR(canStart, L"AsyncSocketServer::Start can only be called once."); if (!PostAccept()) { Stop(); - CHECK_ERROR(false, L"AsyncSocketServer failed to post AcceptEx."); + throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to post AcceptEx."); } } @@ -3226,7 +3390,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket { SOCKET closingListener = INVALID_SOCKET; bool first = false; + bool waitForStart = false; + bool selfStarting = false; auto selfWorker = currentCallbackRuntime == runtime.Obj() || currentCompletionRuntime == runtime.Obj(); + auto currentThreadId = Thread::GetCurrentThreadId(); CS_LOCK(lockState) { if (!stopping) @@ -3237,9 +3404,19 @@ namespace vl::inter_process::async_tcp_socket::windows_socket listener = INVALID_SOCKET; first = true; } + selfStarting = starting && startingThreadId == currentThreadId; + waitForStart = starting && !selfStarting; + } + if (waitForStart) + { + eventStartFinished.Wait(); } if (!first) { + if (selfStarting) + { + return; + } // A runtime callback must not wait for the caller that is draining it. if (!selfWorker) { @@ -3247,6 +3424,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket // A callback-worker caller requests runtime exit but cannot join itself. // An external repeated Stop completes that deferred finalization here. runtime->Stop(); + CS_LOCK(lockState) + { + callback = nullptr; + } } return; } @@ -3273,6 +3454,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket runtime->Stop(); CS_LOCK(lockState) { + if (!selfWorker) + { + callback = nullptr; + } stopped = true; } eventStopped.Signal(); @@ -3289,10 +3474,14 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } }; +/*********************************************************************** +AsyncSocketServer +***********************************************************************/ + AsyncSocketServer::AsyncSocketServer(vint port) { CHECK_ERROR(1 <= port && port <= 65535, L"AsyncSocketServer requires a port in 1..65535."); - impl = new Impl(this, port); + impl = new Impl(port); } AsyncSocketServer::~AsyncSocketServer() @@ -3300,14 +3489,14 @@ namespace vl::inter_process::async_tcp_socket::windows_socket delete impl; } - WaitForClientResult AsyncSocketServer::OnClientConnected(IAsyncSocketConnection*) + vint AsyncSocketServer::GetPort() { - return WaitForClientResult::Accept; + return impl->GetPort(); } - void AsyncSocketServer::Start() + void AsyncSocketServer::Start(IAsyncSocketServerCallback* callback) { - impl->Start(); + impl->Start(callback); } void AsyncSocketServer::Stop() @@ -3320,9 +3509,14 @@ namespace vl::inter_process::async_tcp_socket::windows_socket return impl->IsStopped(); } +/*********************************************************************** +AsyncSocketClient::Impl +***********************************************************************/ + class AsyncSocketClient::Impl : public Object { private: + vint port = 0; Ptr runtime; Ptr state; Ptr connection; @@ -3330,9 +3524,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket bool stopped = false; public: - Impl(vint port) - : runtime(Ptr(new IocpRuntime)) - , state(Ptr(new ConnectionState(runtime.Obj(), true, port))) + Impl(vint _port) + : port(_port) + , runtime(Ptr(new IocpRuntime)) + , state(Ptr(new ConnectionState(runtime.Obj(), true, _port))) , connection(Ptr(new AsyncSocketConnection(state))) { } @@ -3342,6 +3537,11 @@ namespace vl::inter_process::async_tcp_socket::windows_socket Stop(); } + vint GetPort() + { + return port; + } + void Stop() { bool first = false; @@ -3377,6 +3577,10 @@ namespace vl::inter_process::async_tcp_socket::windows_socket } }; +/*********************************************************************** +AsyncSocketClient +***********************************************************************/ + AsyncSocketClient::AsyncSocketClient(vint port) { CHECK_ERROR(1 <= port && port <= 65535, L"AsyncSocketClient requires a port in 1..65535."); @@ -3388,6 +3592,16 @@ namespace vl::inter_process::async_tcp_socket::windows_socket delete impl; } + vint AsyncSocketClient::GetPort() + { + return impl->GetPort(); + } + + Ptr AsyncSocketClient::CreateSameEndpointClient() + { + return Ptr(new AsyncSocketClient(GetPort())); + } + IAsyncSocketConnection* AsyncSocketClient::GetConnection() { return impl->GetConnection(); @@ -3402,6 +3616,20 @@ namespace vl::inter_process::async_tcp_socket::windows_socket { return impl->GetStatus(); } + +} + +namespace vl::inter_process::async_tcp_socket +{ + Ptr CreateDefaultAsyncSocketServer(vint port) + { + return Ptr(new windows_socket::AsyncSocketServer(port)); + } + + Ptr CreateDefaultAsyncSocketClient(vint port) + { + return Ptr(new windows_socket::AsyncSocketClient(port)); + } } @@ -3440,7 +3668,7 @@ bool HttpClient::IsStopping() void HttpClient::BeginReadingLoopUnsafe() { - SendHttpRequest(HttpRequestType::Request, L"POST", urlRequest, WString::Empty); + SendHttpRequest(HttpRequestType::Request, urlRequest, WString::Empty); } /*********************************************************************** @@ -3484,7 +3712,7 @@ void HttpClient::WaitForServer() } eventWaitForServer.Unsignal(); - if (!SendHttpRequest(HttpRequestType::Connect, L"GET", urlConnect, WString::Empty)) + if (!SendHttpRequest(HttpRequestType::Connect, urlConnect, WString::Empty)) { return; } @@ -3553,7 +3781,7 @@ ClientStatus HttpClient::GetStatus() HttpClient (Writing) ***********************************************************************/ -bool HttpClient::SendHttpRequest(HttpRequestType requestType, const wchar_t* method, const WString& url, const WString& body, vint attempt) +bool HttpClient::SendHttpRequest(HttpRequestType requestType, const WString& url, const WString& body, vint attempt) { Ptr api; { @@ -3578,23 +3806,26 @@ bool HttpClient::SendHttpRequest(HttpRequestType requestType, const wchar_t* met if (!api) return false; - HttpRequest request; - request.method = method; - request.query = url; - request.acceptTypes.Add(JsonContentType); + HttpRequest encodedBody; if (requestType == HttpRequestType::Response) { - request.contentType = JsonContentType; - request.keepAliveOnStop = true; + encodedBody.SetBodyUtf8(body); } - else if (requestType == HttpRequestType::Request) + + HttpRequest request; + switch (requestType) { + case HttpRequestType::Connect: + request = CreateHttpNetworkProtocolConnectRequest(url); + break; + case HttpRequestType::Request: + request = CreateHttpNetworkProtocolReceiveRequest(url); request.receiveTimeout = 0; - } - if (body.Length() > 0) - { - request.contentType = JsonContentType; - request.SetBodyUtf8(body); + break; + case HttpRequestType::Response: + request = CreateHttpNetworkProtocolSendRequest(url, encodedBody.body); + request.keepAliveOnStop = true; + break; } api->HttpQuery(request, [this, requestType, body, attempt](Variant result) @@ -3616,12 +3847,12 @@ void HttpClient::OnHttpRequestFailed(HttpRequestType requestType, const WString& RaiseLocalError(errorMessage, fatal); if (!fatal && !IsStopping()) { - SendHttpRequest(HttpRequestType::Connect, L"GET", urlConnect, WString::Empty, attempt + 1); + SendHttpRequest(HttpRequestType::Connect, urlConnect, WString::Empty, attempt + 1); } } break; case HttpRequestType::Request: - SendHttpRequest(HttpRequestType::Request, L"POST", urlRequest, WString::Empty, attempt + 1); + SendHttpRequest(HttpRequestType::Request, urlRequest, WString::Empty, attempt + 1); break; case HttpRequestType::Response: { @@ -3629,7 +3860,7 @@ void HttpClient::OnHttpRequestFailed(HttpRequestType requestType, const WString& RaiseLocalError(errorMessage, fatal); if (!fatal && !IsStopping()) { - SendHttpRequest(HttpRequestType::Response, L"POST", urlResponse, body, attempt + 1); + SendHttpRequest(HttpRequestType::Response, urlResponse, body, attempt + 1); } } break; @@ -3673,7 +3904,7 @@ void HttpClient::OnHttpRequestCompleted(HttpRequestType requestType, WString bod return; } - if (response.contentType != JsonContentType) + if (response.contentType != HttpNetworkProtocolContentType) { switch (requestType) { @@ -3717,7 +3948,7 @@ void HttpClient::OnHttpRequestCompleted(HttpRequestType requestType, WString bod void HttpClient::SendString(const WString& str) { - SendHttpRequest(HttpRequestType::Response, L"POST", urlResponse, str); + SendHttpRequest(HttpRequestType::Response, urlResponse, str); } /*********************************************************************** @@ -3797,41 +4028,6 @@ namespace vl::inter_process::windows_http using namespace vl::collections; -/*********************************************************************** -HttpRequest -***********************************************************************/ - -void HttpRequest::SetBodyUtf8(const WString& bodyString) -{ - vint utf8Size = WideCharToMultiByte(CP_UTF8, 0, bodyString.Buffer(), (int)bodyString.Length(), NULL, 0, NULL, NULL); - body.Resize(utf8Size); - if (utf8Size > 0) - { - WideCharToMultiByte(CP_UTF8, 0, bodyString.Buffer(), (int)bodyString.Length(), &body[0], (int)utf8Size, NULL, NULL); - } -} - -/*********************************************************************** -HttpResponse -***********************************************************************/ - -WString HttpResponse::GetBodyUtf8() const -{ - if (body.Count() == 0) - { - return WString::Empty; - } - - vint utf16Size = MultiByteToWideChar(CP_UTF8, 0, &body[0], (int)body.Count(), NULL, 0); - Array utf16(utf16Size + 1); - ZeroMemory(&utf16[0], utf16.Count() * sizeof(wchar_t)); - if (utf16Size > 0) - { - MultiByteToWideChar(CP_UTF8, 0, &body[0], (int)body.Count(), &utf16[0], (int)utf16Size); - } - return &utf16[0]; -} - /*********************************************************************** HttpClientApi ***********************************************************************/ @@ -3845,14 +4041,6 @@ HttpError HttpClientApi::MakeError(const WString& operation, DWORD errorCode) return error; } -vint HttpClientApi::HexValue(wchar_t c) -{ - if (L'0' <= c && c <= L'9') return c - L'0'; - if (L'a' <= c && c <= L'f') return c - L'a' + 10; - if (L'A' <= c && c <= L'F') return c - L'A' + 10; - return -1; -} - bool HttpClientApi::IsStopping() { bool result = false; @@ -3873,9 +4061,12 @@ void HttpClientApi::BeginPendingCallback() void HttpClientApi::EndPendingCallback() { - if (--pendingCallbacks == 0) + SPIN_LOCK(lockActiveRequests) { - eventPendingCallbacks.Signal(); + if (--pendingCallbacks == 0) + { + eventPendingCallbacks.Signal(); + } } } @@ -4187,6 +4378,12 @@ HttpClientApi::HttpClientApi(const WString& _server, vint _port) WINHTTP_FLAG_ASYNC); CHECK_ERROR(httpSession != NULL, L"WinHttpOpen failed."); +#ifdef WINHTTP_OPTION_IPV6_FAST_FALLBACK + // This option is unavailable before Windows 10 1903. Keep the default address fallback when unsupported. + BOOL ipv6FastFallback = TRUE; + WinHttpSetOption(httpSession, WINHTTP_OPTION_IPV6_FAST_FALLBACK, &ipv6FastFallback, sizeof(ipv6FastFallback)); +#endif + httpConnection = WinHttpConnect( httpSession, server.Buffer(), @@ -4476,91 +4673,12 @@ void HttpClientApi::Stop() WString HttpClientApi::UrlEncodeQuery(const WString& query) { - vint utf8Size = WideCharToMultiByte(CP_UTF8, 0, query.Buffer(), (int)query.Length(), NULL, 0, NULL, NULL); - Array utf8(utf8Size); - if (utf8Size > 0) - { - WideCharToMultiByte(CP_UTF8, 0, query.Buffer(), (int)query.Length(), &utf8[0], (int)utf8Size, NULL, NULL); - } - - Array encoded(utf8Size * 3 + 1); - ZeroMemory(&encoded[0], encoded.Count() * sizeof(wchar_t)); - wchar_t* writing = &encoded[0]; - for (vint i = 0; i < utf8Size; i++) - { - unsigned char x = (unsigned char)utf8[i]; - if ((L'a' <= x && x <= L'z') || (L'A' <= x && x <= L'Z') || (L'0' <= x && x <= L'9')) - { - writing[0] = x; - writing += 1; - } - else - { - writing[0] = L'%'; - writing[1] = L"0123456789ABCDEF"[x / 16]; - writing[2] = L"0123456789ABCDEF"[x % 16]; - writing += 3; - } - } - - return &encoded[0]; + return HttpUrlEncodeQuery(query); } WString HttpClientApi::UrlDecodeQuery(const WString& query) { - List utf8; - for (vint i = 0; i < query.Length(); i++) - { - wchar_t c = query[i]; - if (c == L'%' && i + 2 < query.Length()) - { - vint high = HexValue(query[i + 1]); - vint low = HexValue(query[i + 2]); - if (high != -1 && low != -1) - { - utf8.Add((char)(high * 16 + low)); - i += 2; - continue; - } - } - - if (c == L'+') - { - utf8.Add(' '); - } - else if (c <= 0x7F) - { - utf8.Add((char)c); - } - else - { - wchar_t single[] = { c, 0 }; - vint utf8Size = WideCharToMultiByte(CP_UTF8, 0, single, 1, NULL, 0, NULL, NULL); - if (utf8Size > 0) - { - Array singleUtf8(utf8Size); - WideCharToMultiByte(CP_UTF8, 0, single, 1, &singleUtf8[0], (int)utf8Size, NULL, NULL); - for (vint j = 0; j < utf8Size; j++) - { - utf8.Add(singleUtf8[j]); - } - } - } - } - - if (utf8.Count() == 0) - { - return WString::Empty; - } - - vint utf16Size = MultiByteToWideChar(CP_UTF8, 0, &utf8[0], (int)utf8.Count(), NULL, 0); - Array utf16(utf16Size + 1); - ZeroMemory(&utf16[0], utf16.Count() * sizeof(wchar_t)); - if (utf16Size > 0) - { - MultiByteToWideChar(CP_UTF8, 0, &utf8[0], (int)utf8.Count(), &utf16[0], (int)utf16Size); - } - return &utf16[0]; + return HttpUrlDecodeQuery(query); } } @@ -4704,7 +4822,7 @@ void HttpServerConnection::SendString(const WString& str) } else if (httpPendingRequestId != HTTP_NULL_ID) { - ULONG result = HttpServerApi::SendResponse(server->GetHttpRequestQueue(), httpPendingRequestId, { 200, L"OK", str, L"application/json; charset=utf8" }); + ULONG result = HttpServerApi::SendResponse(server->GetHttpRequestQueue(), httpPendingRequestId, { 200, L"OK", str, HttpNetworkProtocolContentType }); if (result == NO_ERROR) { httpPendingRequestId = HTTP_NULL_ID; @@ -4813,7 +4931,7 @@ void HttpServer::OnHttpRequestReceived(PHTTP_REQUEST pRequest) { auto completeUrlRequest = WString::Unmanaged(HttpServerUrl_Request) + L"/" + newGuid; auto completeUrlResponse = WString::Unmanaged(HttpServerUrl_Response) + L"/" + newGuid; - HttpServerApi::SendResponseUtf8(GetHttpRequestQueue(), pRequest->RequestId, completeUrlRequest + L";" + completeUrlResponse); + HttpServerApi::SendResponseUtf8(GetHttpRequestQueue(), pRequest->RequestId, CreateHttpNetworkProtocolConnectBody(completeUrlRequest, completeUrlResponse)); } } else if (pRequest->Verb == HttpVerbPOST && isValidRequest) @@ -4833,7 +4951,7 @@ void HttpServer::OnHttpRequestReceived(PHTTP_REQUEST pRequest) if (auto connection = FindExistingConnection(guid)) { auto responseToClient = connection->SubmitResponse(pRequest); - auto result = HttpServerApi::SendResponse(GetHttpRequestQueue(), pRequest->RequestId, { 200, L"OK", responseToClient, L"application/json; charset=utf8" }); + auto result = HttpServerApi::SendResponse(GetHttpRequestQueue(), pRequest->RequestId, { 200, L"OK", responseToClient, HttpNetworkProtocolContentType }); CHECK_ERROR( result == NO_ERROR || result == ERROR_CONNECTION_INVALID || result == ERROR_OPERATION_ABORTED, L"HttpSendHttpResponse failed for responding /Response." @@ -5326,7 +5444,7 @@ ULONG HttpServerApi::SendResponse(HANDLE httpRequestQueue, HTTP_REQUEST_ID reque void HttpServerApi::SendResponseUtf8(HANDLE httpRequestQueue, HTTP_REQUEST_ID requestId, WString body) { - auto result = SendResponse(httpRequestQueue, requestId, { 200, WString::Unmanaged(L"OK"), body, L"application/json; charset=utf8" }); + auto result = SendResponse(httpRequestQueue, requestId, { 200, WString::Unmanaged(L"OK"), body, HttpNetworkProtocolContentType }); CHECK_ERROR(result == NO_ERROR, L"HttpSendHttpResponse failed for responding UTF-8 body."); } diff --git a/Import/VlppOS.Windows.h b/Import/VlppOS.Windows.h index 056e71ea..459527a6 100644 --- a/Import/VlppOS.Windows.h +++ b/Import/VlppOS.Windows.h @@ -41,8 +41,8 @@ namespace vl::inter_process::async_tcp_socket::windows_socket AsyncSocketServer(vint port); ~AsyncSocketServer(); - WaitForClientResult OnClientConnected(IAsyncSocketConnection* connection) override; - void Start() override; + vint GetPort() override; + void Start(IAsyncSocketServerCallback* callback) override; void Stop() override; bool IsStopped() override; }; @@ -57,6 +57,8 @@ namespace vl::inter_process::async_tcp_socket::windows_socket AsyncSocketClient(vint port); ~AsyncSocketClient(); + vint GetPort() override; + Ptr CreateSameEndpointClient() override; IAsyncSocketConnection* GetConnection() override; void WaitForServer() override; ClientStatus GetStatus() override; @@ -90,35 +92,6 @@ Interfaces: #include -namespace vl::inter_process -{ - /* - * GET: /Request - * To connect and initialize the server. - * Returns available URLs. - * - * It can only be called once, all subsequence calls will be rejected. - */ - constexpr const wchar_t* HttpServerUrl_Connect = L"/VlppInterProcess/Connect"; - - /* - * POST: /Request/GUID - * Client should always maintain a living request on the server. - * - * Returns only when a request is issued. - * It will be pending or timeout if no request is issued. - * If a request is issued but no living request available, it waits. - */ - constexpr const wchar_t* HttpServerUrl_Request = L"/VlppInterProcess/Request"; - - /* - * POST: /Response/GUID - * To send responses or events to the server. - * Returns nothing. - */ - constexpr const wchar_t* HttpServerUrl_Response = L"/VlppInterProcess/Response"; -} - #endif @@ -141,75 +114,6 @@ Interfaces: namespace vl::inter_process::windows_http { -/// An http request. -class HttpRequest -{ - typedef collections::Array BodyBuffer; - typedef collections::List StringList; - typedef collections::Dictionary HeaderMap; -public: - /// Query of the request, like "/index.html". - WString query; - /// Set to true if the request uses SSL, or https. - bool secure = false; - /// User name to authorize. Set to empty if authorization is not needed. - WString username; - /// Password to authorize. Set to empty if authorization is not needed. - WString password; - /// HTTP method, like "GET", "POST", "PUT", "DELETE", etc. - WString method; - /// Cookie. Set to empty if cookie is not needed. - WString cookie; - /// Request body. This is a byte array. - BodyBuffer body; - /// Content type, like "text/xml". - WString contentType; - /// Accept type list, elements like "text/xml". - StringList acceptTypes; - /// A dictionary to contain extra headers. - HeaderMap extraHeaders; - /// Set to true to let this request finish when is called. - bool keepAliveOnStop = false; - /// Timeout for resolving the host name. 0 or -1 means infinite. - vint resolveTimeout = 0; - /// Timeout for connecting to the server. 0 or -1 means infinite. - vint connectTimeout = 60000; - /// Timeout for sending the request. 0 or -1 means infinite. - vint sendTimeout = 30000; - /// Timeout for receiving the response. 0 or -1 means infinite. - vint receiveTimeout = 30000; - - HttpRequest() = default; - void SetBodyUtf8(const WString& bodyString); -}; - -/// A type representing an http response. -class HttpResponse -{ - typedef collections::Array BodyBuffer; -public: - /// Status code, like 200. - vint statusCode = 0; - /// Response body. This is a byte array. - BodyBuffer body; - /// Returned cookie from the server. - WString cookie; - /// Returned content type from the server. - WString contentType; - - HttpResponse() = default; - WString GetBodyUtf8() const; -}; - -/// A transport error reported by the underlying Windows HTTP API. -class HttpError -{ -public: - DWORD errorCode = 0; - WString operation; - WString message; -}; - /// A Windows-only async HTTP client for a single host and port. class HttpClientApi : public Object { @@ -244,7 +148,6 @@ class HttpClientApi : public Object static void CALLBACK HttpStatusCallback(HINTERNET httpRequest, DWORD_PTR context, DWORD status, LPVOID statusInformation, DWORD statusInformationLength); static HttpError MakeError(const WString& operation, DWORD errorCode); - static vint HexValue(wchar_t c); bool IsStopping(); void BeginPendingCallback(); @@ -324,8 +227,6 @@ HttpClient (Reading) ***********************************************************************/ protected: - static constexpr const wchar_t* JsonContentType = L"application/json; charset=utf8"; - void RaiseLocalError(WString errorMessage, bool fatal); bool IsStopping(); public: @@ -363,7 +264,7 @@ protected: Response, }; - bool SendHttpRequest(HttpRequestType requestType, const wchar_t* method, const WString& url, const WString& body, vint attempt = 1); + bool SendHttpRequest(HttpRequestType requestType, const WString& url, const WString& body, vint attempt = 1); void OnHttpRequestCompleted(HttpRequestType requestType, WString body, vint attempt, Variant result); void OnHttpRequestFailed(HttpRequestType requestType, const WString& body, vint attempt, const WString& errorMessage); diff --git a/Import/VlppOS.cpp b/Import/VlppOS.cpp index d2395347..0bd75655 100644 --- a/Import/VlppOS.cpp +++ b/Import/VlppOS.cpp @@ -1204,9 +1204,13 @@ SpinLock void SpinLock::Enter() { - vint expected = 0; - while (!token.compare_exchange_strong(expected, 1)) + while (true) { + vint expected = 0; + if (token.compare_exchange_strong(expected, 1)) + { + return; + } while (token != 0) { #ifdef VCZH_ARM @@ -4911,3 +4915,10398 @@ RecorderStream } } } + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET.CPP +***********************************************************************/ +#include +#include +#include + +namespace vl::inter_process::async_tcp_socket +{ + thread_local NetworkProtocolCallbackDomain::CallbackFrame* NetworkProtocolCallbackDomain::currentCallbackFrame = nullptr; + thread_local NetworkProtocolConnection::CallbackFrame* NetworkProtocolConnection::currentCallbackFrame = nullptr; + thread_local NetworkProtocolConnection::SocketCallbackFrame* + NetworkProtocolConnection::currentSocketCallbackFrame = nullptr; + +/*********************************************************************** +IAsyncSocketCallback +***********************************************************************/ + + void IAsyncSocketCallback::OnWriteCompleted(Ptr) + { + } + + void IAsyncSocketCallback::OnError(const WString&, bool) + { + } + + void IAsyncSocketCallback::OnConnected() + { + } + + void IAsyncSocketCallback::OnDisconnected() + { + } + + AsyncSocketServerStartException::AsyncSocketServerStartException(AsyncSocketServerStartFailure _failure, const WString& message) + : Exception(message) + , failure(_failure) + { + } + + AsyncSocketServerStartFailure AsyncSocketServerStartException::GetFailure()const + { + return failure; + } + + void IAsyncSocketServerCallback::OnServerStopped() + { + } + +/*********************************************************************** +NetworkProtocolCallbackDomain::CallbackFrame +***********************************************************************/ + + NetworkProtocolCallbackDomain::CallbackFrame::CallbackFrame(Ptr _domain) + : domain(_domain) + { + if (domain) + { + previous = currentCallbackFrame; + currentCallbackFrame = this; + CS_LOCK(domain->lockState) + { + domain->activeCallbacks++; + } + } + } + + NetworkProtocolCallbackDomain::CallbackFrame::~CallbackFrame() + { + if (domain) + { + currentCallbackFrame = previous; + CS_LOCK(domain->lockState) + { + domain->activeCallbacks--; + domain->cvState.WakeAllPendings(); + } + } + } + +/*********************************************************************** +NetworkProtocolCallbackDomain +***********************************************************************/ + + vint NetworkProtocolCallbackDomain::CurrentCallbackDepth() + { + vint depth = 0; + for (auto frame = currentCallbackFrame; frame; frame = frame->previous) + { + if (frame->domain.Obj() == this) + { + depth++; + } + } + return depth; + } + + void NetworkProtocolCallbackDomain::WaitForCallbacks(vint callbackDepth) + { + CS_LOCK(lockState) + { + while (activeCallbacks > callbackDepth) + { + cvState.SleepWith(lockState); + } + } + } + +/*********************************************************************** +NetworkProtocolConnectionLifecycle +***********************************************************************/ + + void NetworkProtocolConnectionLifecycle::TakeRetainedAdapterIfDrained(Ptr& releasing) + { + if (stopFinished && disconnectFinished && activeCallbacks == 0 && activeSocketCallbacks == 0 && activeSocketCalls == 0) + { + releasing = std::move(retainedAdapter); + } + } + +/*********************************************************************** +NetworkProtocolConnection::CallbackFrame +***********************************************************************/ + + NetworkProtocolConnection::CallbackFrame::CallbackFrame(Ptr _state) + : state(_state) + , previous(currentCallbackFrame) + , domainFrame(state->callbackDomain) + { + currentCallbackFrame = this; + } + + NetworkProtocolConnection::CallbackFrame::~CallbackFrame() + { + currentCallbackFrame = previous; + Ptr releasing; + CS_LOCK(state->lockState) + { + state->activeCallbacks--; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + } + +/*********************************************************************** +NetworkProtocolConnection::SocketCallbackFrame +***********************************************************************/ + + NetworkProtocolConnection::SocketCallbackFrame::SocketCallbackFrame(Ptr _state) + : state(_state) + , previous(currentSocketCallbackFrame) + { + currentSocketCallbackFrame = this; + CS_LOCK(state->lockState) + { + state->activeSocketCallbacks++; + } + } + + NetworkProtocolConnection::SocketCallbackFrame::~SocketCallbackFrame() + { + currentSocketCallbackFrame = previous; + Ptr releasing; + CS_LOCK(state->lockState) + { + state->activeSocketCallbacks--; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + } + +/*********************************************************************** +NetworkProtocolConnection +***********************************************************************/ + + vint NetworkProtocolConnection::CurrentCallbackDepth(Ptr state) + { + vint depth = 0; + for (auto frame = currentCallbackFrame; frame; frame = frame->previous) + { + if (frame->state.Obj() == state.Obj()) + { + depth++; + } + } + return depth; + } + + vint NetworkProtocolConnection::CurrentSocketCallbackDepth(Ptr state) + { + vint depth = 0; + for (auto frame = currentSocketCallbackFrame; frame; frame = frame->previous) + { + if (frame->state.Obj() == state.Obj()) + { + depth++; + } + } + return depth; + } + + void NetworkProtocolConnection::FinishSocketCall(Ptr state) + { + CS_LOCK(state->lockState) + { + state->activeSocketCalls--; + state->cvState.WakeAllPendings(); + } + } + + template + void NetworkProtocolConnection::InvokeProtocolCallback(Ptr state, bool allowTerminal, TCallback&& invoke) + { + INetworkProtocolCallback* installed = nullptr; + auto callbackDepth = CurrentCallbackDepth(state); + state->lockState.Enter(); + while (state->callbackInstalling && callbackDepth == 0 && state->callback) + { + state->cvState.SleepWith(state->lockState); + } + if (state->callback && (allowTerminal || (!state->stopStarted && !state->terminal))) + { + installed = state->callback; + state->activeCallbacks++; + } + state->lockState.Leave(); + + if (installed) + { + CallbackFrame frame(state); + invoke(installed); + } + } + + void NetworkProtocolConnection::SubmitWrite(Ptr state, IAsyncSocketConnection* connection, Ptr buffer) + { + try + { + connection->WriteAsync(buffer); + } + catch (...) + { + CS_LOCK(state->lockState) + { + state->queuedWrites.Clear(); + state->writePending = false; + state->cvState.WakeAllPendings(); + } + FinishSocketCall(state); + throw; + } + FinishSocketCall(state); + } + + void NetworkProtocolConnection::NotifyProtocolDisconnected(Ptr state) + { + auto callbackDepth = CurrentCallbackDepth(state); + state->lockState.Enter(); + if (!state->disconnectedNotified) + { + state->disconnectedNotified = true; + state->terminal = true; + state->queuedWrites.Clear(); + state->writePending = false; + state->cvState.WakeAllPendings(); + } + if (state->disconnectFinished) + { + state->lockState.Leave(); + return; + } + + if (state->disconnectDelivering) + { + if (callbackDepth == 0) + { + while (!state->disconnectFinished) + { + state->cvState.SleepWith(state->lockState); + } + } + state->lockState.Leave(); + return; + } + + if (callbackDepth == 0) + { + while (state->activeCallbacks > 0 && !state->disconnectDelivering && !state->disconnectFinished) + { + state->cvState.SleepWith(state->lockState); + } + if (state->disconnectFinished) + { + state->lockState.Leave(); + return; + } + if (state->disconnectDelivering) + { + while (!state->disconnectFinished) + { + state->cvState.SleepWith(state->lockState); + } + state->lockState.Leave(); + return; + } + } + + state->disconnectDelivering = true; + while (state->activeCallbacks > callbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + state->lockState.Leave(); + + try + { + InvokeProtocolCallback(state, true, [](INetworkProtocolCallback* installed) + { + installed->OnDisconnected(); + }); + } + catch (...) + { + Ptr releasing; + CS_LOCK(state->lockState) + { + state->callback = nullptr; + while (state->activeCallbacks > callbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + state->disconnectFinished = true; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + throw; + } + + Ptr releasing; + CS_LOCK(state->lockState) + { + state->callback = nullptr; + while (state->activeCallbacks > callbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + state->disconnectFinished = true; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + } + + void NetworkProtocolConnection::DetachSocketCallback(Ptr state, IAsyncSocketConnection* connection) + { + if (connection) + { + connection->InstallCallback(nullptr); + } + CS_LOCK(state->lockState) + { + if (state->socketConnection == connection) + { + state->socketConnection = nullptr; + } + state->cvState.WakeAllPendings(); + } + } + + void NetworkProtocolConnection::StopConnection(Ptr state, Ptr retainedAdapter) + { + auto callbackDepth = CurrentCallbackDepth(state); + auto socketCallbackDepth = CurrentSocketCallbackDepth(state); + auto nestedCallback = callbackDepth > 0 || socketCallbackDepth > 0; + IAsyncSocketConnection* connection = nullptr; + bool executeStop = false; + bool nestedFollower = false; + Ptr releasing; + + state->lockState.Enter(); + if (retainedAdapter) + { + state->retainedAdapter = retainedAdapter; + } + if (!state->stopStarted) + { + state->stopStarted = true; + if (!nestedCallback && !state->terminal && state->queuedWrites.Count() > 0) + { + state->drainWrites = true; + auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(WriteDrainTimeout); + while (state->queuedWrites.Count() > 0 && !state->terminal) + { + auto now = std::chrono::steady_clock::now(); + if (now >= deadline) + { + break; + } + auto remaining = std::chrono::ceil(deadline - now).count(); + state->cvState.SleepWithForTime(state->lockState, (vint)remaining); + } + state->drainWrites = false; + } + state->queuedWrites.Clear(); + state->writePending = false; + while (state->activeSocketCalls > 0) + { + state->cvState.SleepWith(state->lockState); + } + connection = state->socketConnection; + executeStop = true; + } + else if (nestedCallback) + { + connection = state->socketConnection; + nestedFollower = true; + } + else + { + while (!state->stopFinished) + { + state->cvState.SleepWith(state->lockState); + } + while (state->activeCallbacks > 0 || state->activeSocketCallbacks > 0 || state->activeSocketCalls > 0) + { + state->cvState.SleepWith(state->lockState); + } + state->TakeRetainedAdapterIfDrained(releasing); + state->lockState.Leave(); + return; + } + state->lockState.Leave(); + + if (nestedFollower) + { + if (connection && socketCallbackDepth > 0) + { + connection->Stop(); + } + NotifyProtocolDisconnected(state); + return; + } + + if (executeStop && connection) + { + connection->Stop(); + } + NotifyProtocolDisconnected(state); + + CS_LOCK(state->lockState) + { + while (state->activeCallbacks > callbackDepth || state->activeSocketCallbacks > socketCallbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + state->stopFinished = true; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + } + + void NetworkProtocolConnection::ReportFatalError(Ptr state, const WString& error) + { + bool report = false; + CS_LOCK(state->lockState) + { + if (!state->terminal && !state->stopStarted) + { + state->terminal = true; + state->queuedWrites.Clear(); + state->writePending = false; + state->cvState.WakeAllPendings(); + report = true; + } + } + if (report) + { + try + { + InvokeProtocolCallback(state, true, [&](INetworkProtocolCallback* installed) + { + installed->OnLocalError(error, true); + }); + } + catch (...) + { + StopConnection(state); + throw; + } + StopConnection(state); + } + } + + void NetworkProtocolConnection::StopWithRetainedAdapter(Ptr retainedAdapter) + { + StopConnection(lifecycle, retainedAdapter); + } + + NetworkProtocolConnection::NetworkProtocolConnection(IAsyncSocketConnection* connection, Ptr callbackDomain) + : lifecycle(Ptr(new Lifecycle)) + { + CHECK_ERROR(connection, L"NetworkProtocolConnection requires a valid async socket connection."); + lifecycle->socketConnection = connection; + lifecycle->callbackDomain = callbackDomain; + connection->InstallCallback(this); + } + + NetworkProtocolConnection::~NetworkProtocolConnection() + { + StopConnection(lifecycle); + } + + void NetworkProtocolConnection::InstallCallback(INetworkProtocolCallback* value) + { + auto state = lifecycle; + if (!value) + { + auto callbackDepth = CurrentCallbackDepth(state); + bool uninstallOwner = false; + CS_LOCK(state->lockState) + { + uninstallOwner = state->callback != nullptr; + state->callback = nullptr; + while ((callbackDepth == 0 || uninstallOwner) && state->activeCallbacks > callbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + } + return; + } + + bool canInstall = false; + CS_LOCK(state->lockState) + { + if (!state->callback && !state->callbackInstalling && !state->stopStarted && !state->terminal) + { + state->callback = value; + state->callbackInstalling = true; + state->activeCallbacks++; + canInstall = true; + } + } + CHECK_ERROR(canInstall, L"NetworkProtocolConnection::InstallCallback cannot replace a callback or install one on a stopped connection."); + + CallbackFrame frame(state); + try + { + value->OnInstalled(this); + } + catch (...) + { + CS_LOCK(state->lockState) + { + if (state->callback == value) + { + state->callback = nullptr; + } + state->callbackInstalling = false; + state->cvState.WakeAllPendings(); + } + throw; + } + + CS_LOCK(state->lockState) + { + state->callbackInstalling = false; + state->cvState.WakeAllPendings(); + } + } + + void NetworkProtocolConnection::BeginReadingLoopUnsafe() + { + auto state = lifecycle; + IAsyncSocketConnection* connection = nullptr; + CS_LOCK(state->lockState) + { + if (!state->stopStarted && !state->terminal && state->socketConnection) + { + connection = state->socketConnection; + state->activeSocketCalls++; + } + } + if (!connection) + { + return; + } + + try + { + connection->BeginReadingLoopUnsafe(); + } + catch (...) + { + FinishSocketCall(state); + throw; + } + FinishSocketCall(state); + } + + void NetworkProtocolConnection::SendString(const WString& str) + { + auto state = lifecycle; + auto length = str.Length(); + CHECK_ERROR(length >= 0 && length <= (std::numeric_limits::max)(), L"NetworkProtocolConnection::SendString cannot encode a string longer than vint32_t."); + CHECK_ERROR((size_t)length <= ((size_t)(std::numeric_limits::max)() - sizeof(vint32_t)) / sizeof(wchar_t), L"NetworkProtocolConnection::SendString frame size overflow."); + + auto buffer = Ptr(new AsyncSocketBuffer); + auto characterBytes = (size_t)length * sizeof(wchar_t); + auto frameBytes = sizeof(vint32_t) + characterBytes; + buffer->data.Resize((vint)frameBytes); + auto encodedLength = (vint32_t)length; + std::memcpy(&buffer->data[0], &encodedLength, sizeof(encodedLength)); + if (characterBytes > 0) + { + std::memcpy(&buffer->data[sizeof(encodedLength)], str.Buffer(), characterBytes); + } + + IAsyncSocketConnection* connection = nullptr; + Ptr submitting; + CS_LOCK(state->lockState) + { + if (!state->stopStarted && !state->terminal && state->socketConnection) + { + state->queuedWrites.Add(buffer); + if (!state->writePending) + { + state->writePending = true; + connection = state->socketConnection; + submitting = state->queuedWrites[0]; + state->activeSocketCalls++; + } + } + } + if (submitting) + { + SubmitWrite(state, connection, submitting); + } + } + + void NetworkProtocolConnection::Stop() + { + StopConnection(lifecycle); + } + + void NetworkProtocolConnection::OnRead(const vuint8_t* buffer, vint size) + { + auto state = lifecycle; + SocketCallbackFrame socketCallbackFrame(state); + if (!buffer || size <= 0) + { + return; + } + + collections::List completedStrings; + bool malformed = false; + CS_LOCK(state->lockParser) + { + if (state->parserFailed) + { + return; + } + + auto reading = buffer; + auto available = size; + while (available > 0) + { + if (state->expectedCharacters == -1) + { + auto required = (vint)sizeof(vint32_t) - state->lengthBytesReceived; + auto copied = required < available ? required : available; + std::memcpy(state->lengthBytes + state->lengthBytesReceived, reading, (size_t)copied); + state->lengthBytesReceived += copied; + reading += copied; + available -= copied; + if (state->lengthBytesReceived < (vint)sizeof(vint32_t)) + { + continue; + } + + std::memcpy(&state->expectedCharacters, state->lengthBytes, sizeof(state->expectedCharacters)); + state->lengthBytesReceived = 0; + if (state->expectedCharacters < 0 || (size_t)state->expectedCharacters > ((size_t)(std::numeric_limits::max)() - sizeof(vint32_t)) / sizeof(wchar_t)) + { + state->parserFailed = true; + malformed = true; + break; + } + + state->characterBuffer.Resize((vint)state->expectedCharacters); + state->characterBytesReceived = 0; + if (state->expectedCharacters == 0) + { + completedStrings.Add(WString()); + state->expectedCharacters = -1; + } + } + + if (state->expectedCharacters >= 0) + { + auto characterBytes = (vint)((size_t)state->expectedCharacters * sizeof(wchar_t)); + auto required = characterBytes - state->characterBytesReceived; + auto copied = required < available ? required : available; + std::memcpy((vuint8_t*)&state->characterBuffer[0] + state->characterBytesReceived, reading, (size_t)copied); + state->characterBytesReceived += copied; + reading += copied; + available -= copied; + if (state->characterBytesReceived == characterBytes) + { + completedStrings.Add(WString::CopyFrom(&state->characterBuffer[0], state->expectedCharacters)); + state->characterBuffer.Resize(0); + state->characterBytesReceived = 0; + state->expectedCharacters = -1; + } + } + } + } + + for (auto&& str : completedStrings) + { + InvokeProtocolCallback(state, false, [&](INetworkProtocolCallback* installed) + { + installed->OnReadString(str); + }); + } + if (malformed) + { + ReportFatalError(state, L"The async socket protocol received an invalid string length."); + } + } + + void NetworkProtocolConnection::OnWriteCompleted(Ptr buffer) + { + auto state = lifecycle; + SocketCallbackFrame socketCallbackFrame(state); + IAsyncSocketConnection* connection = nullptr; + Ptr submitting; + bool mismatched = false; + CS_LOCK(state->lockState) + { + if (state->queuedWrites.Count() == 0) + { + return; + } + if (state->queuedWrites[0].Obj() != buffer.Obj()) + { + state->queuedWrites.Clear(); + state->writePending = false; + mismatched = true; + state->cvState.WakeAllPendings(); + } + else + { + state->queuedWrites.RemoveAt(0); + if ((!state->stopStarted || state->drainWrites) && !state->terminal && state->socketConnection && state->queuedWrites.Count() > 0) + { + connection = state->socketConnection; + submitting = state->queuedWrites[0]; + state->activeSocketCalls++; + } + else + { + state->writePending = false; + } + state->cvState.WakeAllPendings(); + } + } + CHECK_ERROR(!mismatched, L"NetworkProtocolConnection received a completion for an unexpected async socket buffer."); + if (submitting) + { + SubmitWrite(state, connection, submitting); + } + } + + void NetworkProtocolConnection::OnError(const WString& error, bool fatal) + { + auto state = lifecycle; + SocketCallbackFrame socketCallbackFrame(state); + if (fatal) + { + ReportFatalError(state, error); + } + else + { + InvokeProtocolCallback(state, false, [&](INetworkProtocolCallback* installed) + { + installed->OnLocalError(error, false); + }); + } + } + + void NetworkProtocolConnection::OnConnected() + { + auto state = lifecycle; + SocketCallbackFrame socketCallbackFrame(state); + InvokeProtocolCallback(state, false, [](INetworkProtocolCallback* installed) + { + installed->OnConnected(); + }); + } + + void NetworkProtocolConnection::OnDisconnected() + { + auto state = lifecycle; + SocketCallbackFrame socketCallbackFrame(state); + IAsyncSocketConnection* connection = nullptr; + CS_LOCK(state->lockState) + { + while (state->activeSocketCalls > 0) + { + state->cvState.SleepWith(state->lockState); + } + connection = state->socketConnection; + } + + try + { + DetachSocketCallback(state, connection); + } + catch (...) + { + CS_LOCK(state->lockState) + { + if (state->socketConnection == connection) + { + state->socketConnection = nullptr; + } + state->cvState.WakeAllPendings(); + } + NotifyProtocolDisconnected(state); + throw; + } + NotifyProtocolDisconnected(state); + } + + void NetworkProtocolConnection::OnInstalled(IAsyncSocketConnection* connection) + { + auto state = lifecycle; + SocketCallbackFrame socketCallbackFrame(state); + CHECK_ERROR(connection == state->socketConnection, L"NetworkProtocolConnection was installed on an unexpected async socket connection."); + } +} + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPCLIENT.CPP +***********************************************************************/ + + +namespace vl::inter_process::async_tcp_socket +{ + using namespace vl::collections; + + namespace + { + constexpr vint HttpRequestMaxAttempts = 3; + constexpr vint SendDrainTimeout = 1000; + + class SameEndpointClientContractException : public Exception + { + public: + SameEndpointClientContractException(const WString& message) + : Exception(message) + { + } + }; + + wchar_t FoldServerCharacter(wchar_t c) + { + return L'A' <= c && c <= L'Z' ? c - L'A' + L'a' : c; + } + + bool ValidateLoopbackServer(const WString& server) + { + if (server == L"127.0.0.1") return true; + const wchar_t localhost[] = L"localhost"; + if (server.Length() != 9) return false; + for (vint i = 0; i < 9; i++) + { + if (FoldServerCharacter(server[i]) != localhost[i]) return false; + } + return true; + } + + WString NormalizeUrlPrefix(const WString& urlPrefix) + { + auto normalizedUrlPrefix = urlPrefix; + while (normalizedUrlPrefix.Length() > 0 && normalizedUrlPrefix[normalizedUrlPrefix.Length() - 1] == L'/') + { + normalizedUrlPrefix = normalizedUrlPrefix.Left(normalizedUrlPrefix.Length() - 1); + } + return normalizedUrlPrefix; + } + + WString DescribeHttpError(const wchar_t* operation, const windows_http::HttpError& error) + { + return WString::Unmanaged(operation) + L" failed: " + error.message; + } + + bool IsResponseNotFoundError(const windows_http::HttpError& error) + { + return error.errorCode == (vuint32_t)SocketHttpClientErrorCode::ResponseNotFound; + } + + bool DecodeSuccessfulResponse( + const windows_http::HttpResponse& response, + const wchar_t* operation, + WString& body, + WString& error + ) + { + if (response.statusCode != 200) + { + error = WString::Unmanaged(operation) + L" returned status code " + itow(response.statusCode) + L"."; + return false; + } + if (response.contentType != HttpNetworkProtocolContentType) + { + error = WString::Unmanaged(operation) + L" did not return the required content type."; + return false; + } + if (!response.TryGetBodyUtf8(body) || (body.Length() > 0 && !IsValidHttpNetworkProtocolMessage(body))) + { + error = WString::Unmanaged(operation) + L" returned malformed UTF-8 or an embedded NUL."; + return false; + } + return true; + } + + BEGIN_GLOBAL_STORAGE_CLASS(SocketHttpClientTestHooks) + SpinLock lock; + Func receiveSubmitted; + Func fatalReserved; + Func stopStarted; + INITIALIZE_GLOBAL_STORAGE_CLASS + FINALIZE_GLOBAL_STORAGE_CLASS + SPIN_LOCK(lock) + { + receiveSubmitted = {}; + fatalReserved = {}; + stopStarted = {}; + } + END_GLOBAL_STORAGE_CLASS(SocketHttpClientTestHooks) + + void InvokeReceiveSubmittedForTesting() + { + Func callback; + auto& hooks = GetSocketHttpClientTestHooks(); + SPIN_LOCK(hooks.lock) + { + callback = hooks.receiveSubmitted; + } + if (callback) + { + try { callback(); } + catch (...) {} + } + } + + void InvokeFatalReservedForTesting() + { + Func callback; + auto& hooks = GetSocketHttpClientTestHooks(); + SPIN_LOCK(hooks.lock) + { + callback = hooks.fatalReserved; + } + if (callback) + { + try { callback(); } + catch (...) {} + } + } + + void InvokeStopStartedForTesting() + { + Func callback; + auto& hooks = GetSocketHttpClientTestHooks(); + SPIN_LOCK(hooks.lock) + { + callback = hooks.stopStarted; + } + if (callback) + { + try { callback(); } + catch (...) {} + } + } + + } + + void SetSocketHttpClientReceiveSubmittedCallbackForTesting(const Func& callback) + { + auto& hooks = GetSocketHttpClientTestHooks(); + SPIN_LOCK(hooks.lock) + { + hooks.receiveSubmitted = callback; + } + } + + void ResetSocketHttpClientReceiveSubmittedCallbackForTesting() + { + SetSocketHttpClientReceiveSubmittedCallbackForTesting({}); + } + + void SetSocketHttpClientFatalStopCallbacksForTesting( + const Func& fatalReserved, + const Func& stopStarted + ) + { + auto& hooks = GetSocketHttpClientTestHooks(); + SPIN_LOCK(hooks.lock) + { + hooks.fatalReserved = fatalReserved; + hooks.stopStarted = stopStarted; + } + } + + void ResetSocketHttpClientFatalStopCallbacksForTesting() + { + SetSocketHttpClientFatalStopCallbacksForTesting({}, {}); + } + +/*********************************************************************** +SocketHttpClient::Impl +***********************************************************************/ + + class SocketHttpClient::Impl : public Object + { + using QueryResult = Variant; + + enum class State + { + Ready, + WaitingForServer, + Connected, + Stopping, + }; + + class SendItem : public Object + { + public: + Array body; + vint attempt = 1; + }; + + class QueryWaiter : public Object + { + private: + CriticalSection lock; + ConditionVariable cv; + Ptr result; + + public: + void Complete(QueryResult value) + { + CS_LOCK(lock) + { + if (!result) + { + result = Ptr(new QueryResult(std::move(value))); + cv.WakeAllPendings(); + } + } + } + + Ptr Wait() + { + CS_LOCK(lock) + { + while (!result) cv.SleepWith(lock); + return result; + } + return nullptr; + } + }; + + struct CallbackFrame + { + Ptr state; + CallbackFrame* previous = nullptr; + NetworkProtocolCallbackDomain::CallbackFrame + domainFrame; + + CallbackFrame(Ptr _state) + : state(_state) + , previous(currentCallbackFrame) + , domainFrame(state->callbackDomain) + { + currentCallbackFrame = this; + } + + ~CallbackFrame() + { + currentCallbackFrame = previous; + CS_LOCK(state->lockState) + { + state->activeCallbacks--; + state->cvState.WakeAllPendings(); + } + } + }; + + struct WorkerFrame + { + Ptr state; + WorkerFrame* previous = nullptr; + + WorkerFrame(Ptr _state) + : state(_state) + , previous(currentWorkerFrame) + { + currentWorkerFrame = this; + } + + ~WorkerFrame() + { + currentWorkerFrame = previous; + CS_LOCK(state->lockState) + { + state->activeWorkers--; + if (state->hardStopping) + { + state->sendReconnecting = false; + state->receiveReconnecting = false; + } + state->cvState.WakeAllPendings(); + } + } + }; + + struct WaitFrame + { + Ptr state; + WaitFrame* previous = nullptr; + + WaitFrame(Ptr _state) + : state(_state) + , previous(currentWaitFrame) + { + currentWaitFrame = this; + CS_LOCK(state->lockState) + { + state->activeWaits++; + } + } + + ~WaitFrame() + { + currentWaitFrame = previous; + CS_LOCK(state->lockState) + { + state->activeWaits--; + state->cvState.WakeAllPendings(); + } + } + }; + + static thread_local CallbackFrame* currentCallbackFrame; + static thread_local WorkerFrame* currentWorkerFrame; + static thread_local WaitFrame* currentWaitFrame; + + SocketHttpClient* owner = nullptr; + WString server; + vint port = 0; + Ptr clientSource; + Ptr initialClient; + WString urlPrefix; + WString urlConnect; + WString urlRequest; + WString urlResponse; + Ptr selfReference; + Ptr callbackDomain = Ptr(new NetworkProtocolCallbackDomain); + + CriticalSection lockState; + CriticalSection lockClientCreation; + ConditionVariable cvState; + State state = State::Ready; + INetworkProtocolCallback* callback = nullptr; + bool callbackInstalling = false; + vint activeCallbacks = 0; + vint activeWorkers = 0; + vint activeWaits = 0; + bool readingStarted = false; + bool receivePollActive = false; + bool receiveReconnecting = false; + bool sendActive = false; + bool sendReconnecting = false; + bool stopStarted = false; + bool drainSends = false; + bool hardStopping = false; + bool stopFinished = false; + bool fatalStarted = false; + bool disconnectedNotified = false; + bool disconnectDelivering = false; + bool disconnectFinished = false; + Ptr sendApi; + Ptr receiveApi; + List> sendQueue; + + Ptr RetainSelf() + { + CS_LOCK(lockState) + { + return selfReference; + } + return nullptr; + } + + vint CurrentCallbackDepth() + { + vint depth = 0; + for (auto frame = currentCallbackFrame; frame; frame = frame->previous) + { + if (frame->state.Obj() == this) depth++; + } + return depth; + } + + vint CurrentWorkerDepth() + { + vint depth = 0; + for (auto frame = currentWorkerFrame; frame; frame = frame->previous) + { + if (frame->state.Obj() == this) depth++; + } + return depth; + } + + vint CurrentWaitDepth() + { + vint depth = 0; + for (auto frame = currentWaitFrame; frame; frame = frame->previous) + { + if (frame->state.Obj() == this) depth++; + } + return depth; + } + + template + void InvokeProtocolCallback(bool allowStopping, TCallback&& invoke) + { + INetworkProtocolCallback* installed = nullptr; + auto callbackDepth = CurrentCallbackDepth(); + lockState.Enter(); + while (callbackInstalling && callbackDepth == 0 && callback) + { + cvState.SleepWith(lockState); + } + if (callback && (allowStopping || !stopStarted)) + { + installed = callback; + activeCallbacks++; + } + lockState.Leave(); + + if (installed) + { + CallbackFrame frame(RetainSelf()); + invoke(installed); + } + } + + bool IsStopped() + { + CS_LOCK(lockState) + { + return stopStarted; + } + return true; + } + + bool CanReceiveUnsafe() + { + return state == State::Connected && readingStarted && !stopStarted && !hardStopping; + } + + bool CanSendUnsafe() + { + return !hardStopping && (state == State::Connected || drainSends); + } + + void StopApiNoThrow(Ptr api) + { + if (!api) return; + try + { + api->Stop(); + } + catch (...) + { + } + } + + Ptr CreateApi() + { + Ptr nativeClient; + bool sameEndpointClient = false; + try + { + CS_LOCK(lockClientCreation) + { + nativeClient = initialClient; + initialClient = nullptr; + if (!nativeClient) + { + sameEndpointClient = true; + nativeClient = clientSource->CreateSameEndpointClient(); + } + } + if (!nativeClient) + { + throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient returned null."); + } + if (sameEndpointClient && nativeClient.Obj() == clientSource.Obj()) + { + throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient returned the source client instead of a fresh client."); + } + if (nativeClient->GetPort() != port) + { + throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient returned a client for a different port."); + } + if (nativeClient->GetStatus() != ClientStatus::Ready) + { + throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient returned a client that is not ready."); + } + return Ptr(new SocketHttpClientApi(nativeClient, server)); + } + catch (const SameEndpointClientContractException&) + { + throw; + } + catch (const Exception& exception) + { + throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient failed: " + exception.Message()); + } + catch (...) + { + throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient failed."); + } + } + + bool WaitApiForServer(Ptr api) + { + api->WaitForServer(); + return api->GetStatus() == ClientStatus::Connected; + } + + void ReportLocalError(const WString& error, bool fatal) + { + InvokeProtocolCallback(fatal, [&](INetworkProtocolCallback* installed) + { + installed->OnLocalError(error, fatal); + }); + } + + void NotifyDisconnected() + { + auto callbackDepth = CurrentCallbackDepth(); + lockState.Enter(); + if (!disconnectedNotified) + { + disconnectedNotified = true; + cvState.WakeAllPendings(); + } + if (disconnectFinished) + { + lockState.Leave(); + return; + } + if (disconnectDelivering) + { + if (callbackDepth == 0) + { + while (!disconnectFinished) cvState.SleepWith(lockState); + } + lockState.Leave(); + return; + } + if (callbackDepth == 0) + { + while (activeCallbacks > 0 && !disconnectDelivering && !disconnectFinished) + { + cvState.SleepWith(lockState); + } + if (disconnectFinished) + { + lockState.Leave(); + return; + } + } + disconnectDelivering = true; + while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState); + lockState.Leave(); + + try + { + InvokeProtocolCallback(true, [](INetworkProtocolCallback* installed) + { + installed->OnDisconnected(); + }); + } + catch (...) + { + CS_LOCK(lockState) + { + callback = nullptr; + disconnectFinished = true; + cvState.WakeAllPendings(); + } + throw; + } + + CS_LOCK(lockState) + { + callback = nullptr; + disconnectFinished = true; + cvState.WakeAllPendings(); + } + } + + void ReportFatalError(const WString& error) + { + INetworkProtocolCallback* installed = nullptr; + auto callbackDepth = CurrentCallbackDepth(); + bool report = false; + lockState.Enter(); + while (callbackInstalling && callbackDepth == 0 && callback && !stopStarted) + { + cvState.SleepWith(lockState); + } + if (!fatalStarted && !stopStarted) + { + fatalStarted = true; + report = true; + if (callback) + { + installed = callback; + activeCallbacks++; + } + } + lockState.Leave(); + if (!report) return; + InvokeFatalReservedForTesting(); + + try + { + if (installed) + { + CallbackFrame frame(RetainSelf()); + installed->OnLocalError(error, true); + } + } + catch (...) + { + Stop(true); + throw; + } + Stop(true); + } + + bool HandleConnectFailure(WString error, vint& attempt) + { + if (IsStopped()) return false; + if (attempt >= HttpRequestMaxAttempts) + { + ReportFatalError(error); + return false; + } + ReportLocalError(error, false); + attempt++; + return !IsStopped(); + } + + Ptr QueryConnect(Ptr api) + { + auto request = CreateHttpNetworkProtocolConnectRequest(urlConnect); + + auto waiter = Ptr(new QueryWaiter); + api->HttpQuery(request, [waiter](QueryResult result) + { + waiter->Complete(std::move(result)); + }); + return waiter->Wait(); + } + + bool ValidateConnectResponse( + const windows_http::HttpResponse& response, + WString& requestUrl, + WString& responseUrl, + WString& error + ) + { + WString body; + if (!DecodeSuccessfulResponse(response, L"/Connect", body, error)) return false; + if (body.Length() == 0) + { + error = L"/Connect returned an empty body."; + return false; + } + + WString requestPath; + WString responsePath; + if (!ParseHttpNetworkProtocolConnectBody(body, requestPath, responsePath)) + { + error = L"/Connect did not return exactly two paths."; + return false; + } + if (!ValidateHttpNetworkProtocolEndpointPath(requestPath) || !ValidateHttpNetworkProtocolEndpointPath(responsePath)) + { + error = L"/Connect returned an illegal path."; + return false; + } + + auto requestTarget = urlPrefix + requestPath; + auto responseTarget = urlPrefix + responsePath; + if ( + ValidateHttpRequestLine(L"POST", requestTarget) != HttpRequestLineValidationResult::Succeeded || + ValidateHttpRequestLine(L"POST", responseTarget) != HttpRequestLineValidationResult::Succeeded + ) + { + error = L"/Connect returned a path exceeding the HTTP request-target contract."; + return false; + } + requestUrl = std::move(requestTarget); + responseUrl = std::move(responseTarget); + return true; + } + + bool PublishApi(Ptr api, bool receive) + { + CS_LOCK(lockState) + { + if (stopStarted || hardStopping) return false; + if (receive) + { + receiveApi = api; + } + else + { + sendApi = api; + } + return true; + } + return false; + } + + void ClearApi(Ptr api, bool receive) + { + CS_LOCK(lockState) + { + if (receive) + { + if (receiveApi == api) receiveApi = nullptr; + } + else + { + if (sendApi == api) sendApi = nullptr; + } + cvState.WakeAllPendings(); + } + } + + void SubmitReceivePoll(Ptr api) + { + auto self = RetainSelf(); + if (!self) return; + auto request = CreateHttpNetworkProtocolReceiveRequest(urlRequest); + request.receiveTimeout = 0; + + try + { + api->HttpQuery(request, [self, api](QueryResult result) + { + self->OnReceiveCompleted(api, std::move(result)); + }); + InvokeReceiveSubmittedForTesting(); + } + catch (...) + { + HandleReceiveTransportFailure(api); + } + } + + bool ReserveReceivePoll(Ptr api) + { + CS_LOCK(lockState) + { + if (!CanReceiveUnsafe() || receiveReconnecting || receivePollActive || receiveApi != api) return false; + receivePollActive = true; + return true; + } + return false; + } + + void HandleReceiveTransportFailure(Ptr api) + { + bool schedule = false; + CS_LOCK(lockState) + { + if (receiveApi == api && CanReceiveUnsafe() && !receiveReconnecting) + { + receivePollActive = false; + receiveReconnecting = true; + activeWorkers++; + schedule = true; + } + } + if (!schedule) return; + + auto self = RetainSelf(); + bool queued = false; + try + { + auto worker = Func([self, api]() + { + WorkerFrame frame(self); + try + { + self->RunReceiveReplacement(api); + } + catch (...) + { + try + { + self->ReportFatalError(L"/Request replacement worker failed unexpectedly."); + } + catch (...) + { + self->Stop(); + } + } + }); + if (self) + { + try + { + queued = ThreadPoolLite::Queue(worker); + } + catch (...) + { + } + if (!queued) + { + try + { + queued = Thread::CreateAndStart(worker) != nullptr; + } + catch (...) + { + } + } + } + } + catch (...) + { + } + if (!queued) + { + CS_LOCK(lockState) + { + activeWorkers--; + receiveReconnecting = false; + cvState.WakeAllPendings(); + } + ReportFatalError(L"/Request could not queue its replacement worker."); + } + } + + void RunReceiveReplacement(Ptr deadApi) + { + StopApiNoThrow(deadApi); + ClearApi(deadApi, true); + + while (true) + { + CS_LOCK(lockState) + { + if (!CanReceiveUnsafe() || !receiveReconnecting) return; + } + + Ptr api; + try + { + api = CreateApi(); + } + catch (const SameEndpointClientContractException& exception) + { + ReportFatalError(exception.Message()); + return; + } + catch (...) + { + ReportFatalError(L"IAsyncSocketClient::CreateSameEndpointClient failed."); + return; + } + if (!PublishApi(api, true)) + { + StopApiNoThrow(api); + return; + } + + bool connected = false; + try + { + connected = WaitApiForServer(api); + } + catch (...) + { + } + if (!connected) + { + StopApiNoThrow(api); + ClearApi(api, true); + continue; + } + + bool submit = false; + CS_LOCK(lockState) + { + if (receiveApi == api && CanReceiveUnsafe() && receiveReconnecting) + { + receiveReconnecting = false; + receivePollActive = true; + submit = true; + } + } + if (!submit) + { + StopApiNoThrow(api); + return; + } + SubmitReceivePoll(api); + return; + } + } + + void OnReceiveCompleted(Ptr api, QueryResult result) + { + bool current = false; + CS_LOCK(lockState) + { + if (receiveApi == api && receivePollActive) + { + receivePollActive = false; + current = true; + } + } + if (!current) return; + + if (auto httpError = result.TryGet()) + { + if (IsResponseNotFoundError(*httpError)) + { + ReportFatalError(DescribeHttpError(L"/Request", *httpError)); + return; + } + HandleReceiveTransportFailure(api); + return; + } + + WString body; + WString error; + auto valid = DecodeSuccessfulResponse(result.Get(), L"/Request", body, error); + if (ReserveReceivePoll(api)) + { + // SocketHttpClientApi starts this replacement from inside its response + // callback before any user callback below can create a receive gap. + SubmitReceivePoll(api); + } + if (valid && body.Length() > 0) + { + InvokeProtocolCallback(false, [&](INetworkProtocolCallback* installed) + { + installed->OnReadString(body); + }); + } + } + + bool IsCurrentSendUnsafe(Ptr api, Ptr item) + { + return sendQueue.Count() > 0 && sendQueue[0] == item && sendApi == api; + } + + void SubmitSend(Ptr api, Ptr item) + { + bool submit = false; + CS_LOCK(lockState) + { + submit = CanSendUnsafe() && sendActive && IsCurrentSendUnsafe(api, item); + } + if (!submit) return; + + auto request = CreateHttpNetworkProtocolSendRequest(urlResponse, item->body); + + auto self = RetainSelf(); + try + { + api->HttpQuery(request, [self, api, item](QueryResult result) + { + self->OnSendCompleted(api, item, std::move(result)); + }); + } + catch (...) + { + HandleSendFailure(api, item, L"/Response could not submit the HTTP exchange.", true); + } + } + + void QueueSendReplacement(Ptr deadApi, Ptr item) + { + bool schedule = false; + CS_LOCK(lockState) + { + if (CanSendUnsafe() && sendReconnecting && IsCurrentSendUnsafe(deadApi, item)) + { + activeWorkers++; + schedule = true; + } + else + { + sendReconnecting = false; + } + } + if (!schedule) return; + + auto self = RetainSelf(); + bool queued = false; + try + { + auto worker = Func([self, deadApi, item]() + { + WorkerFrame frame(self); + try + { + self->RunSendReplacement(deadApi, item); + } + catch (...) + { + try + { + self->ReportFatalError(L"/Response replacement worker failed unexpectedly."); + } + catch (...) + { + self->Stop(); + } + } + }); + if (self) + { + try + { + queued = ThreadPoolLite::Queue(worker); + } + catch (...) + { + } + if (!queued) + { + try + { + queued = Thread::CreateAndStart(worker) != nullptr; + } + catch (...) + { + } + } + } + } + catch (...) + { + } + if (!queued) + { + CS_LOCK(lockState) + { + activeWorkers--; + sendReconnecting = false; + cvState.WakeAllPendings(); + } + ReportFatalError(L"/Response could not queue its replacement worker."); + } + } + + bool HandleSendPreparationFailure(Ptr item, const WString& error) + { + bool fatal = false; + CS_LOCK(lockState) + { + if (!CanSendUnsafe() || !sendReconnecting || sendQueue.Count() == 0 || sendQueue[0] != item) return false; + fatal = item->attempt >= HttpRequestMaxAttempts; + if (!fatal) item->attempt++; + } + if (fatal) + { + ReportFatalError(error); + return false; + } + ReportLocalError(error, false); + CS_LOCK(lockState) + { + return CanSendUnsafe() && sendReconnecting && sendQueue.Count() > 0 && sendQueue[0] == item; + } + return false; + } + + void RunSendReplacement(Ptr deadApi, Ptr item) + { + StopApiNoThrow(deadApi); + ClearApi(deadApi, false); + + while (true) + { + CS_LOCK(lockState) + { + if (!CanSendUnsafe() || !sendReconnecting || sendQueue.Count() == 0 || sendQueue[0] != item) return; + } + + Ptr api; + try + { + api = CreateApi(); + } + catch (const SameEndpointClientContractException& exception) + { + ReportFatalError(exception.Message()); + return; + } + catch (...) + { + ReportFatalError(L"IAsyncSocketClient::CreateSameEndpointClient failed."); + return; + } + if (!PublishApi(api, false)) + { + StopApiNoThrow(api); + return; + } + + bool connected = false; + try + { + connected = WaitApiForServer(api); + } + catch (...) + { + } + if (!connected) + { + StopApiNoThrow(api); + ClearApi(api, false); + if (!HandleSendPreparationFailure(item, L"/Response replacement failed to connect.")) return; + continue; + } + + bool submit = false; + CS_LOCK(lockState) + { + if (sendApi == api && CanSendUnsafe() && sendReconnecting && sendQueue.Count() > 0 && sendQueue[0] == item) + { + sendReconnecting = false; + sendActive = true; + submit = true; + } + } + if (!submit) + { + StopApiNoThrow(api); + return; + } + SubmitSend(api, item); + return; + } + } + + void HandleSendFailure(Ptr api, Ptr item, WString error, bool transportFailure) + { + bool fatal = false; + bool retryHealthy = false; + bool replace = false; + CS_LOCK(lockState) + { + if (!sendActive || !CanSendUnsafe() || !IsCurrentSendUnsafe(api, item)) return; + sendActive = false; + fatal = item->attempt >= HttpRequestMaxAttempts; + if (!fatal) + { + item->attempt++; + if (transportFailure) + { + sendReconnecting = true; + replace = true; + } + else + { + sendActive = true; + retryHealthy = true; + } + } + cvState.WakeAllPendings(); + } + + if (fatal) + { + ReportFatalError(error); + return; + } + + try + { + ReportLocalError(error, false); + } + catch (...) + { + if (replace) QueueSendReplacement(api, item); + if (retryHealthy) SubmitSend(api, item); + throw; + } + if (replace) QueueSendReplacement(api, item); + if (retryHealthy) SubmitSend(api, item); + } + + void OnSendCompleted(Ptr api, Ptr item, QueryResult result) + { + if (auto httpError = result.TryGet()) + { + if (IsResponseNotFoundError(*httpError)) + { + ReportFatalError(DescribeHttpError(L"/Response", *httpError)); + return; + } + HandleSendFailure(api, item, DescribeHttpError(L"/Response", *httpError), true); + return; + } + + WString body; + WString error; + if (!DecodeSuccessfulResponse(result.Get(), L"/Response", body, error)) + { + HandleSendFailure(api, item, error, false); + return; + } + + Ptr next; + CS_LOCK(lockState) + { + if (!sendActive || !IsCurrentSendUnsafe(api, item)) return; + sendActive = false; + sendQueue.RemoveAt(0); + if (CanSendUnsafe() && sendQueue.Count() > 0) + { + next = sendQueue[0]; + sendActive = true; + } + cvState.WakeAllPendings(); + } + + // Preserve FIFO ownership by submitting the next accepted send before + // delivering a piggybacked message to callback-reentrant application code. + if (next) SubmitSend(api, next); + if (body.Length() > 0) + { + InvokeProtocolCallback(false, [&](INetworkProtocolCallback* installed) + { + installed->OnReadString(body); + }); + } + } + + public: + Impl( + SocketHttpClient* _owner, + Ptr _initialClient, + const WString& _server, + const WString& _urlPrefix + ) + : owner(_owner) + , server(_server) + , clientSource(_initialClient) + , initialClient(_initialClient) + , urlPrefix(NormalizeUrlPrefix(_urlPrefix)) + , urlConnect(urlPrefix + HttpServerUrl_Connect) + { + CHECK_ERROR(owner, L"SocketHttpClient requires an owning adapter."); + CHECK_ERROR(initialClient, L"SocketHttpClient requires an initial native client."); + CHECK_ERROR(ValidateLoopbackServer(server), L"SocketHttpClient requires an explicit loopback server."); + port = initialClient->GetPort(); + CHECK_ERROR(1 <= port && port <= 65535, L"SocketHttpClient requires the initial client port to be in 1..65535."); + CHECK_ERROR(initialClient->GetStatus() == ClientStatus::Ready, L"SocketHttpClient requires an initial client in the ready state."); + CHECK_ERROR(ValidateHttpNetworkProtocolBaseUrl(urlPrefix), L"SocketHttpClient requires an empty or legal origin-form URL prefix."); + CHECK_ERROR(ValidateHttpRequestLine(L"GET", urlConnect) == HttpRequestLineValidationResult::Succeeded, L"SocketHttpClient URL prefix makes /Connect exceed the HTTP request-line limit."); + } + + void Initialize(Ptr self) + { + CS_LOCK(lockState) + { + selfReference = self; + } + } + + void ReleaseSelf() + { + CS_LOCK(lockState) + { + owner = nullptr; + selfReference = nullptr; + } + } + + INetworkProtocolConnection* GetConnection() + { + return owner; + } + + void WaitForServer() + { + auto self = RetainSelf(); + CS_LOCK(lockState) + { + CHECK_ERROR(state == State::Ready && !stopStarted, L"SocketHttpClient::WaitForServer can only be called once."); + state = State::WaitingForServer; + } + WaitFrame waitFrame(self); + + vint attempt = 1; + Ptr api; + while (!IsStopped()) + { + if (!api) + { + try + { + api = CreateApi(); + } + catch (const SameEndpointClientContractException& exception) + { + ReportFatalError(exception.Message()); + return; + } + catch (...) + { + ReportFatalError(L"IAsyncSocketClient::CreateSameEndpointClient failed."); + return; + } + if (!PublishApi(api, false)) + { + StopApiNoThrow(api); + return; + } + bool connected = false; + try + { + connected = WaitApiForServer(api); + } + catch (...) + { + } + if (!connected) + { + StopApiNoThrow(api); + ClearApi(api, false); + api = nullptr; + if (!HandleConnectFailure(L"/Connect native connection failed.", attempt)) return; + continue; + } + } + + Ptr result; + try + { + result = QueryConnect(api); + } + catch (...) + { + StopApiNoThrow(api); + ClearApi(api, false); + api = nullptr; + if (!HandleConnectFailure(L"/Connect could not submit its HTTP exchange.", attempt)) return; + continue; + } + if (IsStopped()) return; + + if (auto httpError = result->TryGet()) + { + auto error = DescribeHttpError(L"/Connect", *httpError); + if (IsResponseNotFoundError(*httpError)) + { + ReportFatalError(error); + return; + } + StopApiNoThrow(api); + ClearApi(api, false); + api = nullptr; + if (!HandleConnectFailure(error, attempt)) return; + continue; + } + + WString requestUrl; + WString responseUrl; + WString error; + if (!ValidateConnectResponse(result->Get(), requestUrl, responseUrl, error)) + { + if (!HandleConnectFailure(error, attempt)) return; + continue; + } + + CS_LOCK(lockState) + { + if (stopStarted) return; + urlRequest = requestUrl; + urlResponse = responseUrl; + } + break; + } + if (IsStopped()) return; + + // The logical token is already fixed. Failures in this second physical + // bootstrap are silent and never repeat /Connect. + while (!IsStopped()) + { + Ptr apiReceive; + try + { + apiReceive = CreateApi(); + } + catch (const SameEndpointClientContractException& exception) + { + ReportFatalError(exception.Message()); + return; + } + catch (...) + { + ReportFatalError(L"IAsyncSocketClient::CreateSameEndpointClient failed."); + return; + } + if (!PublishApi(apiReceive, true)) + { + StopApiNoThrow(apiReceive); + return; + } + bool connected = false; + try + { + connected = WaitApiForServer(apiReceive); + } + catch (...) + { + } + if (!connected) + { + StopApiNoThrow(apiReceive); + ClearApi(apiReceive, true); + continue; + } + + bool publishConnected = false; + CS_LOCK(lockState) + { + if (!stopStarted && receiveApi == apiReceive) + { + state = State::Connected; + publishConnected = true; + } + } + if (!publishConnected) + { + StopApiNoThrow(apiReceive); + return; + } + InvokeProtocolCallback(false, [](INetworkProtocolCallback* installed) + { + installed->OnConnected(); + }); + return; + } + } + + ClientStatus GetStatus() + { + CS_LOCK(lockState) + { + switch (state) + { + case State::Ready: + return ClientStatus::Ready; + case State::WaitingForServer: + return ClientStatus::WaitingForServer; + case State::Connected: + return ClientStatus::Connected; + default: + return ClientStatus::Disconnected; + } + } + return ClientStatus::Disconnected; + } + + void InstallCallback(INetworkProtocolCallback* value) + { + if (!value) + { + auto callbackDepth = CurrentCallbackDepth(); + bool uninstallOwner = false; + CS_LOCK(lockState) + { + uninstallOwner = callback != nullptr; + callback = nullptr; + while ((callbackDepth == 0 || uninstallOwner) && activeCallbacks > callbackDepth) + { + cvState.SleepWith(lockState); + } + } + return; + } + + bool canInstall = false; + CS_LOCK(lockState) + { + if (!callback && !callbackInstalling && !stopStarted) + { + callback = value; + callbackInstalling = true; + activeCallbacks++; + canInstall = true; + } + } + CHECK_ERROR(canInstall, L"SocketHttpClient::InstallCallback cannot replace a callback or install one on a stopped client."); + + CallbackFrame frame(RetainSelf()); + try + { + value->OnInstalled(owner); + } + catch (...) + { + CS_LOCK(lockState) + { + if (callback == value) callback = nullptr; + callbackInstalling = false; + cvState.WakeAllPendings(); + } + throw; + } + CS_LOCK(lockState) + { + callbackInstalling = false; + cvState.WakeAllPendings(); + } + } + + void BeginReadingLoopUnsafe() + { + Ptr api; + CS_LOCK(lockState) + { + CHECK_ERROR(state == State::Connected && !stopStarted, L"SocketHttpClient::BeginReadingLoopUnsafe requires a connected client."); + CHECK_ERROR(!readingStarted, L"SocketHttpClient::BeginReadingLoopUnsafe can only be called once."); + readingStarted = true; + api = receiveApi; + CHECK_ERROR(api, L"SocketHttpClient has no receive API after connecting."); + receivePollActive = true; + } + SubmitReceivePoll(api); + } + + void SendString(const WString& str) + { + CHECK_ERROR(str.Length() > 0, L"SocketHttpClient::SendString does not accept an empty string."); + CHECK_ERROR(IsValidHttpNetworkProtocolMessage(str), L"SocketHttpClient::SendString requires valid Unicode without embedded NUL characters."); + Array validated; + CHECK_ERROR(EncodeStrictUtf8(str, validated), L"SocketHttpClient::SendString requires valid Unicode without embedded NUL characters."); + CHECK_ERROR(validated.Count() <= HttpBodySizeLimit, L"SocketHttpClient::SendString exceeds the HTTP body size limit."); + + auto item = Ptr(new SendItem); + item->body.Resize(validated.Count()); + for (vint i = 0; i < validated.Count(); i++) + { + item->body[i] = (char)validated[i]; + } + Ptr api; + bool submit = false; + CS_LOCK(lockState) + { + CHECK_ERROR(state == State::Connected && !stopStarted, L"SocketHttpClient::SendString requires a connected client."); + sendQueue.Add(item); + if (!sendActive && !sendReconnecting) + { + api = sendApi; + CHECK_ERROR(api, L"SocketHttpClient has no send API after connecting."); + sendActive = true; + submit = true; + } + } + if (submit) SubmitSend(api, item); + } + + void Stop(bool internalFollower = false) + { + auto self = RetainSelf(); + auto callbackDepth = CurrentCallbackDepth(); + auto workerDepth = CurrentWorkerDepth(); + auto waitDepth = CurrentWaitDepth(); + auto nested = internalFollower || callbackDepth > 0 || workerDepth > 0 || waitDepth > 0; + bool executeStop = false; + Ptr cancellingReceive; + + lockState.Enter(); + if (stopFinished) + { + while (activeCallbacks > callbackDepth || activeWorkers > workerDepth || activeWaits > waitDepth) + { + cvState.SleepWith(lockState); + } + lockState.Leave(); + return; + } + if (!stopStarted) + { + stopStarted = true; + state = State::Stopping; + drainSends = !fatalStarted && sendQueue.Count() > 0; + cancellingReceive = receiveApi; + executeStop = true; + } + else if (nested) + { + lockState.Leave(); + return; + } + else + { + while (!stopFinished) cvState.SleepWith(lockState); + while (activeCallbacks > 0 || activeWorkers > 0 || activeWaits > 0) cvState.SleepWith(lockState); + lockState.Leave(); + return; + } + lockState.Leave(); + + if (!executeStop) return; + InvokeStopStartedForTesting(); + // The infinite receive exchange is always cancelled before the bounded + // opportunity given to already accepted send-lane messages. + StopApiNoThrow(cancellingReceive); + + lockState.Enter(); + if (drainSends) + { + auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(SendDrainTimeout); + while (sendQueue.Count() > 0) + { + auto now = std::chrono::steady_clock::now(); + if (now >= deadline) break; + auto remaining = std::chrono::ceil(deadline - now).count(); + cvState.SleepWithForTime(lockState, (vint)remaining); + } + } + drainSends = false; + hardStopping = true; + sendQueue.Clear(); + sendActive = false; + receivePollActive = false; + auto stoppingSend = sendApi; + auto stoppingReceive = receiveApi; + lockState.Leave(); + + StopApiNoThrow(stoppingReceive); + StopApiNoThrow(stoppingSend); + + lockState.Enter(); + while (activeWorkers > workerDepth || activeWaits > waitDepth) + { + cvState.SleepWith(lockState); + } + lockState.Leave(); + + try + { + NotifyDisconnected(); + } + catch (...) + { + CS_LOCK(lockState) + { + while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState); + stopFinished = true; + cvState.WakeAllPendings(); + } + throw; + } + CS_LOCK(lockState) + { + while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState); + stopFinished = true; + cvState.WakeAllPendings(); + } + } + }; + + thread_local SocketHttpClient::Impl::CallbackFrame* SocketHttpClient::Impl::currentCallbackFrame = nullptr; + thread_local SocketHttpClient::Impl::WorkerFrame* SocketHttpClient::Impl::currentWorkerFrame = nullptr; + thread_local SocketHttpClient::Impl::WaitFrame* SocketHttpClient::Impl::currentWaitFrame = nullptr; + +/*********************************************************************** +SocketHttpClient +***********************************************************************/ + + SocketHttpClient::SocketHttpClient( + Ptr client, + const WString& server, + const WString& urlPrefix + ) + { +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpClient::SocketHttpClient(Ptr, const WString&, const WString&)#" + CHECK_ERROR(client, ERROR_MESSAGE_PREFIX L"An initial native client is required."); + auto created = Ptr(new Impl(this, client, server, urlPrefix)); + created->Initialize(created); + impl = created; +#undef ERROR_MESSAGE_PREFIX + } + + SocketHttpClient::~SocketHttpClient() + { + try + { + impl->Stop(); + } + catch (...) + { + } + impl->ReleaseSelf(); + } + + INetworkProtocolConnection* SocketHttpClient::GetConnection() + { + return impl->GetConnection(); + } + + void SocketHttpClient::WaitForServer() + { + impl->WaitForServer(); + } + + ClientStatus SocketHttpClient::GetStatus() + { + return impl->GetStatus(); + } + + void SocketHttpClient::InstallCallback(INetworkProtocolCallback* callback) + { + impl->InstallCallback(callback); + } + + void SocketHttpClient::BeginReadingLoopUnsafe() + { + impl->BeginReadingLoopUnsafe(); + } + + void SocketHttpClient::SendString(const WString& str) + { + impl->SendString(str); + } + + void SocketHttpClient::Stop() + { + impl->Stop(); + } +} + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPCLIENTAPI.CPP +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Interfaces: + SocketHttpClientApi + +***********************************************************************/ + + +namespace vl::inter_process::async_tcp_socket +{ + using namespace vl::collections; + + namespace + { + wchar_t FoldAscii(wchar_t c) + { + return L'A' <= c && c <= L'Z' ? c - L'A' + L'a' : c; + } + + bool AsciiEqualsIgnoreCase(const WString& a, const WString& b) + { + if (a.Length() != b.Length()) return false; + for (vint i = 0; i < a.Length(); i++) + { + if (FoldAscii(a[i]) != FoldAscii(b[i])) return false; + } + return true; + } + + WString FoldAsciiFieldName(const WString& name) + { + Array characters(name.Length()); + for (vint i = 0; i < name.Length(); i++) + { + characters[i] = FoldAscii(name[i]); + } + return characters.Count() == 0 ? WString() : WString::CopyFrom(&characters[0], characters.Count()); + } + + WString TrimHttpWhitespace(const WString& value) + { + vint begin = 0; + vint end = value.Length(); + while (begin < end && (value[begin] == L' ' || value[begin] == L'\t')) begin++; + while (begin < end && (value[end - 1] == L' ' || value[end - 1] == L'\t')) end--; + return value.Sub(begin, end - begin); + } + + bool ContainsOnlyIdentityCoding(const WString& value) + { + vint begin = 0; + bool found = false; + while (begin <= value.Length()) + { + vint end = begin; + while (end < value.Length() && value[end] != L',') end++; + auto coding = TrimHttpWhitespace(value.Sub(begin, end - begin)); + if (!AsciiEqualsIgnoreCase(coding, L"identity")) return false; + found = true; + if (end == value.Length()) break; + begin = end + 1; + } + return found; + } + + bool ValidateServer(const WString& server) + { + return + AsciiEqualsIgnoreCase(server, L"localhost") || + server == L"127.0.0.1"; + } + + HttpField CreateField(const WString& name, const WString& value) + { + HttpField field; + field.name = FoldAsciiFieldName(name); + auto utf8 = wtou8(value); + field.value.Resize(utf8.Length()); + if (utf8.Length() > 0) + { + memcpy(&field.value[0], utf8.Buffer(), utf8.Length()); + } + return field; + } + + WString DecodeFieldValue(const Array& value) + { + if (value.Count() == 0) return WString::Empty; + Array utf8(value.Count()); + for (vint i = 0; i < value.Count(); i++) + { + utf8[i] = (char8_t)value[i]; + } + return u8tow(U8String::CopyFrom(&utf8[0], utf8.Count())); + } + + windows_http::HttpError MakeError(const WString& operation, const WString& message, SocketHttpClientErrorCode code) + { + windows_http::HttpError error; + error.operation = operation; + error.errorCode = (vuint32_t)code; + error.message = message; + return error; + } + } + +/*********************************************************************** +SocketHttpClientApi::Impl +***********************************************************************/ + + class SocketHttpClientApi::Impl : public Object, public virtual IHttpRequestCallback + { + using QueryResult = Variant; + using QueryCallback = Func; + + class Query : public Object + { + public: + Ptr request; + vint responseTimeout = 0; + QueryCallback callback; + bool completed = false; + }; + + struct CallbackFrame + { + Impl* owner = nullptr; + Ptr self; + CallbackFrame* previous = nullptr; + + CallbackFrame(Impl* _owner, Ptr _self) + : owner(_owner) + , self(_self) + , previous(currentCallbackFrame) + { + currentCallbackFrame = this; + } + + ~CallbackFrame() + { + currentCallbackFrame = previous; + CS_LOCK(owner->lockState) + { + owner->activeCallbacks--; + owner->cvState.WakeAllPendings(); + } + } + }; + + struct ResponseFrame + { + Impl* owner = nullptr; + Ptr self; + ResponseFrame* previous = nullptr; + + ResponseFrame(Impl* _owner, Ptr _self) + : owner(_owner) + , self(_self) + , previous(currentResponseFrame) + { + currentResponseFrame = this; + } + + ~ResponseFrame() + { + currentResponseFrame = previous; + } + }; + + static thread_local CallbackFrame* currentCallbackFrame; + static thread_local ResponseFrame* currentResponseFrame; + + Ptr client; + IHttpRequestConnection* connection = nullptr; + WString authority; + Ptr selfReference; + + CriticalSection lockState; + ConditionVariable cvState; + Ptr activeQuery; + List> queuedQueries; + List> pendingQueries; + vint activeCallbacks = 0; + bool waitStarted = false; + bool readingStarted = false; + bool responseDispatching = false; + bool terminal = false; + bool stopStarted = false; + bool stopFinished = false; + + Ptr RetainSelf() + { + Ptr self; + CS_LOCK(lockState) + { + self = selfReference; + } + return self; + } + + vint CurrentCallbackDepth() + { + vint depth = 0; + for (auto frame = currentCallbackFrame; frame; frame = frame->previous) + { + if (frame->owner == this) depth++; + } + return depth; + } + + bool IsInsideResponseCallback() + { + for (auto frame = currentResponseFrame; frame; frame = frame->previous) + { + if (frame->owner == this) return true; + } + return false; + } + + Ptr TakeFirstQueuedUnsafe() + { + auto query = queuedQueries[0]; + queuedQueries.RemoveAt(0); + return query; + } + + void TakeAllQueriesUnsafe(List>& queries) + { + if (activeQuery) + { + queries.Add(activeQuery); + activeQuery = nullptr; + } + for (auto query : queuedQueries) + { + queries.Add(query); + } + queuedQueries.Clear(); + for (auto query : pendingQueries) + { + queries.Add(query); + } + pendingQueries.Clear(); + } + + void MoveAllQueriesToPendingUnsafe() + { + if (activeQuery) + { + pendingQueries.Add(activeQuery); + activeQuery = nullptr; + } + for (auto query : queuedQueries) + { + pendingQueries.Add(query); + } + queuedQueries.Clear(); + } + + bool ReserveCallbackUnsafe(Ptr query, QueryCallback& callback) + { + if (!query || query->completed) return false; + query->completed = true; + callback = query->callback; + if (callback) activeCallbacks++; + return true; + } + + void InvokeReserved(QueryCallback callback, QueryResult result) + { + if (!callback) return; + CallbackFrame frame(this, RetainSelf()); + try + { + callback(std::move(result)); + } + catch (...) + { + } + } + + void CompleteWithError(Ptr query, const windows_http::HttpError& error) + { + QueryCallback callback; + bool reserved = false; + CS_LOCK(lockState) + { + reserved = ReserveCallbackUnsafe(query, callback); + } + if (reserved) + { + InvokeReserved(callback, QueryResult(error)); + } + } + + void CompleteAllWithError(List>& queries, const windows_http::HttpError& error) + { + for (auto query : queries) + { + CompleteWithError(query, error); + } + } + + void CompletePendingWithError(const windows_http::HttpError& error) + { + while (true) + { + QueryCallback callback; + bool reserved = false; + CS_LOCK(lockState) + { + if (!stopStarted && pendingQueries.Count() > 0) + { + auto query = pendingQueries[0]; + pendingQueries.RemoveAt(0); + reserved = ReserveCallbackUnsafe(query, callback); + } + } + if (!reserved) return; + InvokeReserved(callback, QueryResult(error)); + } + } + + void InvokeDirect(QueryCallback callback, const windows_http::HttpError& error) + { + if (!callback) return; + CS_LOCK(lockState) + { + activeCallbacks++; + } + InvokeReserved(callback, QueryResult(error)); + } + + Ptr CreateQuery(const windows_http::HttpRequest& request, windows_http::HttpError& error) + { + if (request.secure) + { + error = MakeError(L"SocketHttpClientApi::HttpQuery", L"TLS is not supported by SocketHttpClientApi.", SocketHttpClientErrorCode::InvalidRequest); + return nullptr; + } + if (request.username != WString::Empty || request.password != WString::Empty) + { + error = MakeError(L"SocketHttpClientApi::HttpQuery", L"Credentials are not supported by SocketHttpClientApi.", SocketHttpClientErrorCode::InvalidRequest); + return nullptr; + } + if (request.keepAliveOnStop) + { + error = MakeError(L"SocketHttpClientApi::HttpQuery", L"keepAliveOnStop is not supported by SocketHttpClientApi.", SocketHttpClientErrorCode::InvalidRequest); + return nullptr; + } + + auto query = Ptr(new Query); + query->request = Ptr(new HttpRequest); + query->responseTimeout = request.receiveTimeout; + query->callback = {}; + query->request->method = request.method == WString::Empty ? WString::Unmanaged(L"GET") : request.method; + query->request->requestTarget = request.query == WString::Empty ? WString::Unmanaged(L"/") : request.query; + + query->request->headers.Add(CreateAsciiHttpField(L"Host", authority)); + query->request->headers.Add(CreateAsciiHttpField(L"Accept-Encoding", L"identity")); + for (vint i = 0; i < request.acceptTypes.Count(); i++) + { + query->request->headers.Add(CreateField(L"Accept", request.acceptTypes.Get(i))); + } + if (request.contentType != WString::Empty) + { + query->request->headers.Add(CreateField(L"Content-Type", request.contentType)); + } + if (request.cookie != WString::Empty) + { + query->request->headers.Add(CreateField(L"Cookie", request.cookie)); + } + + for (vint i = 0; i < request.extraHeaders.Count(); i++) + { + auto name = request.extraHeaders.Keys()[i]; + auto value = request.extraHeaders.Values()[i]; + if (AsciiEqualsIgnoreCase(name, L"Host")) + { + if (!AsciiEqualsIgnoreCase(TrimHttpWhitespace(value), authority)) + { + error = MakeError(L"SocketHttpClientApi::HttpQuery", L"A caller-supplied Host field conflicts with the configured server and injected client port.", SocketHttpClientErrorCode::InvalidRequest); + return nullptr; + } + continue; + } + if (AsciiEqualsIgnoreCase(name, L"Accept-Encoding")) + { + if (!ContainsOnlyIdentityCoding(value)) + { + error = MakeError(L"SocketHttpClientApi::HttpQuery", L"SocketHttpClientApi only supports Accept-Encoding: identity.", SocketHttpClientErrorCode::InvalidRequest); + return nullptr; + } + continue; + } + query->request->headers.Add(CreateField(name, value)); + } + + if (request.body.Count() > 0) + { + HttpBodyChunk chunk; + chunk.data.Resize(request.body.Count()); + memcpy(&chunk.data[0], &request.body.Get(0), request.body.Count()); + query->request->body.chunks.Add(std::move(chunk)); + } + return query; + } + + bool ConvertResponse(Ptr response, windows_http::HttpResponse& output, windows_http::HttpError& error) + { + if (!response) + { + error = MakeError(L"SocketHttpClientApi::OnReadResponse", L"The HTTP request layer returned an empty response.", SocketHttpClientErrorCode::Transport); + return false; + } + + output.statusCode = response->statusCode; + bool contentTypeAssigned = false; + bool cookieAssigned = false; + auto processField = [&](const HttpField& field) + { + if (AsciiEqualsIgnoreCase(field.name, L"Content-Encoding")) + { + if (!ContainsOnlyIdentityCoding(DecodeFieldValue(field.value))) + { + error = MakeError(L"SocketHttpClientApi::OnReadResponse", L"The server returned an unsupported Content-Encoding.", SocketHttpClientErrorCode::UnsupportedCoding); + return false; + } + } + else if (!contentTypeAssigned && AsciiEqualsIgnoreCase(field.name, L"Content-Type")) + { + output.contentType = DecodeFieldValue(field.value); + contentTypeAssigned = true; + } + else if (!cookieAssigned && AsciiEqualsIgnoreCase(field.name, L"Set-Cookie")) + { + output.cookie = DecodeFieldValue(field.value); + cookieAssigned = true; + } + return true; + }; + for (auto&& field : response->headers) + { + if (!processField(field)) return false; + } + for (auto&& field : response->body.trailers) + { + if (!processField(field)) return false; + } + + Array body; + if (!FlattenHttpBody(response->body, body)) + { + error = MakeError(L"SocketHttpClientApi::OnReadResponse", L"The response body is too large to flatten.", SocketHttpClientErrorCode::Transport); + return false; + } + output.body.Resize(body.Count()); + for (vint i = 0; i < body.Count(); i++) + { + output.body[i] = (char)body[i]; + } + return true; + } + + bool TrySend(Ptr query, windows_http::HttpError& error) + { + try + { + connection->SendRequest(query->request, query->responseTimeout); + return true; + } + catch (...) + { + error = MakeError(L"SocketHttpClientApi::HttpQuery", L"The HTTP request layer rejected the exchange.", SocketHttpClientErrorCode::Transport); + return false; + } + } + + void StopConnectionNoThrow() + { + try + { + connection->Stop(); + } + catch (...) + { + } + } + + void HandleSendFailure(Ptr query, const windows_http::HttpError& error) + { + bool stopConnection = false; + CS_LOCK(lockState) + { + if (!query->completed && !stopStarted && !terminal) + { + terminal = true; + responseDispatching = false; + MoveAllQueriesToPendingUnsafe(); + stopConnection = true; + } + } + if (stopConnection) + { + StopConnectionNoThrow(); + CompletePendingWithError(error); + } + } + + void HandleTerminalError(const windows_http::HttpError& error, bool stopConnectionImmediately) + { + bool shouldStop = false; + CS_LOCK(lockState) + { + if (!stopStarted && !terminal) + { + terminal = true; + responseDispatching = false; + MoveAllQueriesToPendingUnsafe(); + shouldStop = stopConnectionImmediately; + } + } + if (shouldStop) + { + StopConnectionNoThrow(); + } + CompletePendingWithError(error); + } + + public: + Impl(Ptr socketClient, const WString& _authority) + : client(new HttpRequestClient(socketClient)) + , authority(_authority) + { + connection = client->GetConnection(); + } + + void Initialize(Ptr self) + { + CS_LOCK(lockState) + { + selfReference = self; + } + try + { + connection->InstallCallback(this); + } + catch (...) + { + CS_LOCK(lockState) + { + selfReference = nullptr; + } + throw; + } + } + + void ReleaseSelf() + { + CS_LOCK(lockState) + { + selfReference = nullptr; + } + } + + void WaitForServer() + { + CS_LOCK(lockState) + { + CHECK_ERROR(!waitStarted && !stopStarted && !terminal, L"SocketHttpClientApi::WaitForServer can only be called once on an active client."); + waitStarted = true; + } + + try + { + client->WaitForServer(); + } + catch (...) + { + CS_LOCK(lockState) + { + terminal = true; + } + throw; + } + + bool beginReading = false; + CS_LOCK(lockState) + { + beginReading = !stopStarted && !terminal; + } + if (!beginReading) return; + + try + { + connection->BeginReadingLoopUnsafe(); + } + catch (...) + { + CS_LOCK(lockState) + { + terminal = true; + } + throw; + } + CS_LOCK(lockState) + { + readingStarted = !stopStarted && !terminal; + } + } + + ClientStatus GetStatus() + { + return client->GetStatus(); + } + + void HttpQuery(const windows_http::HttpRequest& request, QueryCallback callback) + { + auto self = RetainSelf(); + windows_http::HttpError error; + Ptr query; + try + { + query = CreateQuery(request, error); + } + catch (...) + { + error = MakeError(L"SocketHttpClientApi::HttpQuery", L"The request could not be translated to the socket HTTP representation.", SocketHttpClientErrorCode::InvalidRequest); + } + if (!query) + { + InvokeDirect(callback, error); + return; + } + query->callback = callback; + + bool start = false; + bool reject = false; + bool notReady = false; + CS_LOCK(lockState) + { + if (stopStarted || terminal) + { + reject = true; + } + else if (!readingStarted) + { + notReady = true; + } + else if (!activeQuery && queuedQueries.Count() == 0 && (!responseDispatching || IsInsideResponseCallback())) + { + activeQuery = query; + start = true; + } + else + { + queuedQueries.Add(query); + } + } + + if (reject) + { + CompleteWithError(query, MakeError(L"SocketHttpClientApi::HttpQuery", L"SocketHttpClientApi has stopped accepting work.", SocketHttpClientErrorCode::Stopped)); + } + else if (notReady) + { + CompleteWithError(query, MakeError(L"SocketHttpClientApi::HttpQuery", L"WaitForServer must complete before sending an HTTP query.", SocketHttpClientErrorCode::InvalidRequest)); + } + else if (start) + { + if (!TrySend(query, error)) + { + HandleSendFailure(query, error); + } + } + } + + void Stop() + { + auto self = RetainSelf(); + List> cancelledQueries; + auto callbackDepth = CurrentCallbackDepth(); + bool executeStop = false; + lockState.Enter(); + if (stopFinished) + { + while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState); + lockState.Leave(); + return; + } + if (!stopStarted) + { + stopStarted = true; + terminal = true; + responseDispatching = false; + TakeAllQueriesUnsafe(cancelledQueries); + executeStop = true; + } + else if (callbackDepth > 0) + { + lockState.Leave(); + return; + } + else + { + while (!stopFinished) cvState.SleepWith(lockState); + while (activeCallbacks > 0) cvState.SleepWith(lockState); + lockState.Leave(); + return; + } + lockState.Leave(); + + if (executeStop) + { + StopConnectionNoThrow(); + CompleteAllWithError( + cancelledQueries, + MakeError(L"SocketHttpClientApi::Stop", L"The HTTP query was cancelled because the client stopped.", SocketHttpClientErrorCode::Stopped) + ); + } + + CS_LOCK(lockState) + { + while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState); + stopFinished = true; + cvState.WakeAllPendings(); + } + } + + void OnReadRequest(Ptr) override + { + auto self = RetainSelf(); + if (!self) return; + HandleTerminalError( + MakeError(L"SocketHttpClientApi::OnReadRequest", L"The client connection received a request instead of a response.", SocketHttpClientErrorCode::Transport), + true + ); + } + + void OnReadRequestFailure(HttpRequestFailure) override + { + auto self = RetainSelf(); + if (!self) return; + HandleTerminalError( + MakeError(L"SocketHttpClientApi::OnReadRequestFailure", L"The client connection reported a request parsing failure.", SocketHttpClientErrorCode::Transport), + true + ); + } + + void OnReadResponse(Ptr response) override + { + auto self = RetainSelf(); + if (!self) return; + ResponseFrame responseFrame(this, self); + + windows_http::HttpResponse convertedResponse; + windows_http::HttpError responseError; + auto converted = ConvertResponse(response, convertedResponse, responseError); + Ptr completedQuery; + Ptr nextQuery; + QueryCallback completedCallback; + bool reserved = false; + bool stopForResponse = false; + windows_http::HttpError terminalError; + CS_LOCK(lockState) + { + if (activeQuery && !activeQuery->completed) + { + responseDispatching = true; + completedQuery = activeQuery; + activeQuery = nullptr; + reserved = ReserveCallbackUnsafe(completedQuery, completedCallback); + if (!converted) + { + terminal = true; + MoveAllQueriesToPendingUnsafe(); + stopForResponse = true; + terminalError = responseError; + } + else if (!stopStarted && !terminal && queuedQueries.Count() > 0) + { + nextQuery = TakeFirstQueuedUnsafe(); + activeQuery = nextQuery; + } + } + } + if (!reserved) return; + + windows_http::HttpError sendError; + bool sendFailed = nextQuery && !TrySend(nextQuery, sendError); + if (sendFailed) + { + CS_LOCK(lockState) + { + if (!nextQuery->completed && !stopStarted && !terminal) + { + terminal = true; + MoveAllQueriesToPendingUnsafe(); + stopForResponse = true; + terminalError = sendError; + } + } + } + + if (converted) + { + InvokeReserved(completedCallback, QueryResult(std::move(convertedResponse))); + } + else + { + InvokeReserved(completedCallback, QueryResult(responseError)); + } + + Ptr lateQuery; + if (!stopForResponse) + { + CS_LOCK(lockState) + { + if (!stopStarted && !terminal && !activeQuery && queuedQueries.Count() > 0) + { + lateQuery = TakeFirstQueuedUnsafe(); + activeQuery = lateQuery; + } + responseDispatching = false; + } + if (lateQuery && !TrySend(lateQuery, sendError)) + { + CS_LOCK(lockState) + { + if (!lateQuery->completed && !stopStarted && !terminal) + { + terminal = true; + MoveAllQueriesToPendingUnsafe(); + stopForResponse = true; + terminalError = sendError; + } + } + } + } + else + { + CS_LOCK(lockState) + { + responseDispatching = false; + } + } + + if (stopForResponse) + { + StopConnectionNoThrow(); + CompletePendingWithError(terminalError); + } + } + + void OnReadResponseFailure(HttpResponseFailure failure) override + { + auto self = RetainSelf(); + if (!self) return; +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpClientApi::Impl::OnReadResponseFailure(HttpResponseFailure)#" + CHECK_ERROR(failure == HttpResponseFailure::NotFound, ERROR_MESSAGE_PREFIX L"Received an unsupported response failure."); + HandleTerminalError( + MakeError(L"SocketHttpClientApi::OnReadResponseFailure", L"The server returned 404 Not Found.", SocketHttpClientErrorCode::ResponseNotFound), + false + ); +#undef ERROR_MESSAGE_PREFIX + } + + void OnWriteCompleted() override + { + } + + void OnError(const WString& error, bool fatal) override + { + auto self = RetainSelf(); + if (!self) return; + // The raw layer stops after delivering a fatal error. A nonfatal error + // needs an explicit stop after this wrapper terminalizes its queue. + auto stopConnectionImmediately = !fatal; + HandleTerminalError( + MakeError(L"SocketHttpClientApi::OnError", error, SocketHttpClientErrorCode::Transport), + stopConnectionImmediately + ); + } + + void OnConnected() override + { + } + + void OnDisconnected() override + { + auto self = RetainSelf(); + if (!self) return; + HandleTerminalError( + MakeError(L"SocketHttpClientApi::OnDisconnected", L"The HTTP connection was disconnected.", SocketHttpClientErrorCode::Transport), + false + ); + } + + void OnInstalled(IHttpRequestConnection* installedConnection) override + { + CHECK_ERROR(installedConnection == connection, L"SocketHttpClientApi was installed on an unexpected HTTP connection."); + } + }; + + thread_local SocketHttpClientApi::Impl::CallbackFrame* SocketHttpClientApi::Impl::currentCallbackFrame = nullptr; + thread_local SocketHttpClientApi::Impl::ResponseFrame* SocketHttpClientApi::Impl::currentResponseFrame = nullptr; + +/*********************************************************************** +SocketHttpClientApi +***********************************************************************/ + + SocketHttpClientApi::SocketHttpClientApi(Ptr client, const WString& server) + { + CHECK_ERROR(client, L"SocketHttpClientApi requires an asynchronous socket client."); + CHECK_ERROR(ValidateServer(server), L"SocketHttpClientApi requires an explicit loopback server."); + auto port = client->GetPort(); + CHECK_ERROR(1 <= port && port <= 65535, L"SocketHttpClientApi requires the injected client port to be in 1..65535."); + auto authority = server + WString::Unmanaged(L":") + itow(port); + auto created = Ptr(new Impl(client, authority)); + created->Initialize(created); + impl = created; + } + + SocketHttpClientApi::~SocketHttpClientApi() + { + impl->Stop(); + impl->ReleaseSelf(); + } + + void SocketHttpClientApi::WaitForServer() + { + impl->WaitForServer(); + } + + ClientStatus SocketHttpClientApi::GetStatus() + { + return impl->GetStatus(); + } + + void SocketHttpClientApi::HttpQuery( + const windows_http::HttpRequest& request, + Func)> callback + ) + { + impl->HttpQuery(request, callback); + } + + void SocketHttpClientApi::Stop() + { + impl->Stop(); + } + + WString SocketHttpClientApi::UrlEncodeQuery(const WString& query) + { + return HttpUrlEncodeQuery(query); + } + + WString SocketHttpClientApi::UrlDecodeQuery(const WString& query) + { + return HttpUrlDecodeQuery(query); + } +} + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPREQUEST.CPP +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Async Socket HTTP/1.1 Connection + +***********************************************************************/ + + + +namespace vl::inter_process::async_tcp_socket +{ + using namespace collections; + + namespace + { + constexpr vint HttpWireMessageSizeLimit = 128 * 1024 * 1024; + constexpr vint HttpChunkCountLimit = 64 * 1024; + + bool IsOws(vuint8_t c) + { + return c == ' ' || c == '\t'; + } + + bool IsDigit(vuint8_t c) + { + return c >= '0' && c <= '9'; + } + + bool IsHexDigit(vuint8_t c) + { + return IsDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + } + + vuint8_t HexDigitValue(vuint8_t c) + { + if (IsDigit(c)) return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return c - 'a' + 10; + } + + bool IsTokenCharacter(vuint8_t c) + { + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) + { + return true; + } + switch (c) + { + case '!': case '#': case '$': case '%': case '&': case '\'': case '*': case '+': + case '-': case '.': case '^': case '_': case '`': case '|': case '~': + return true; + default: + return false; + } + } + + bool IsFieldValueCharacter(vuint8_t c) + { + return c == '\t' || (c >= 0x20 && c != 0x7F); + } + + bool IsQuotedTextCharacter(vuint8_t c) + { + return c == '\t' || c == ' ' || c == '!' || (c >= '#' && c <= '[') || (c >= ']' && c != 0x7F); + } + + vint FindCrlf(const vuint8_t* buffer, vint begin, vint end) + { + for (vint i = begin; i + 1 < end; i++) + { + if (buffer[i] == '\r' && buffer[i + 1] == '\n') + { + return i; + } + } + return -1; + } + + WString CopyAscii(const vuint8_t* buffer, vint begin, vint end, bool lowercase) + { + if (begin == end) + { + return L""; + } + Array text(end - begin); + for (vint i = begin; i < end; i++) + { + auto c = buffer[i]; + if (lowercase && c >= 'A' && c <= 'Z') + { + c += 'a' - 'A'; + } + text[i - begin] = (wchar_t)c; + } + return WString::CopyFrom(&text[0], text.Count()); + } + + bool IsAsciiEqual(const WString& text, const wchar_t* expected) + { + return text == expected; + } + + bool ParseQuotedString(const vuint8_t* buffer, vint& reading, vint end) + { + if (reading >= end || buffer[reading] != '"') + { + return false; + } + reading++; + while (reading < end) + { + auto c = buffer[reading++]; + if (c == '"') + { + return true; + } + if (c == '\\') + { + if (reading >= end || !IsFieldValueCharacter(buffer[reading])) + { + return false; + } + reading++; + } + else if (!IsQuotedTextCharacter(c)) + { + return false; + } + } + return false; + } + + bool ParseToken(const vuint8_t* buffer, vint& reading, vint end, WString* output = nullptr) + { + vint begin = reading; + while (reading < end && IsTokenCharacter(buffer[reading])) + { + reading++; + } + if (reading == begin) + { + return false; + } + if (output) + { + *output = CopyAscii(buffer, begin, reading, true); + } + return true; + } + + bool ParseSemicolonParameters(const vuint8_t* buffer, vint& reading, vint end) + { + while (true) + { + while (reading < end && IsOws(buffer[reading])) reading++; + if (reading == end) + { + return true; + } + if (buffer[reading++] != ';') + { + return false; + } + while (reading < end && IsOws(buffer[reading])) reading++; + if (!ParseToken(buffer, reading, end)) + { + return false; + } + while (reading < end && IsOws(buffer[reading])) reading++; + if (reading < end && buffer[reading] == '=') + { + reading++; + while (reading < end && IsOws(buffer[reading])) reading++; + if (reading < end && buffer[reading] == '"') + { + if (!ParseQuotedString(buffer, reading, end)) return false; + } + else if (!ParseToken(buffer, reading, end)) + { + return false; + } + } + } + } + + bool ValidateChunkSizeLinePrefix(const vuint8_t* buffer, vint begin, vint end) + { + vint reading = begin; + if (reading == end) return true; + if (!IsHexDigit(buffer[reading])) return false; + vuint64_t chunkSize = 0; + while (reading < end && IsHexDigit(buffer[reading])) + { + auto digit = (vuint64_t)HexDigitValue(buffer[reading++]); + if (chunkSize > ((std::numeric_limits::max)() - digit) / 16) return false; + chunkSize = chunkSize * 16 + digit; + if (chunkSize > (vuint64_t)HttpBodySizeLimit) return false; + } + while (true) + { + while (reading < end && IsOws(buffer[reading])) reading++; + if (reading == end) return true; + if (buffer[reading++] != ';') return false; + while (reading < end && IsOws(buffer[reading])) reading++; + if (reading == end) return true; + vint nameBegin = reading; + while (reading < end && IsTokenCharacter(buffer[reading])) reading++; + if (reading == nameBegin) return false; + while (reading < end && IsOws(buffer[reading])) reading++; + if (reading == end) return true; + if (buffer[reading] != '=') continue; + reading++; + while (reading < end && IsOws(buffer[reading])) reading++; + if (reading == end) return true; + if (buffer[reading] == '"') + { + reading++; + bool closed = false; + while (reading < end) + { + auto c = buffer[reading++]; + if (c == '"') + { + closed = true; + break; + } + if (c == '\\') + { + if (reading == end) return true; + if (!IsFieldValueCharacter(buffer[reading++])) return false; + } + else if (!IsQuotedTextCharacter(c)) + { + return false; + } + } + if (!closed) return true; + } + else + { + vint valueBegin = reading; + while (reading < end && IsTokenCharacter(buffer[reading])) reading++; + if (reading == valueBegin) return false; + } + } + } + + bool ValidateCompleteChunkSizeLine(const vuint8_t* buffer, vint begin, vint end) + { + vint reading = begin; + vuint64_t chunkSize = 0; + while (reading < end && IsHexDigit(buffer[reading])) + { + auto digit = (vuint64_t)HexDigitValue(buffer[reading++]); + if (chunkSize > ((std::numeric_limits::max)() - digit) / 16) return false; + chunkSize = chunkSize * 16 + digit; + } + if (reading == begin || chunkSize > (vuint64_t)HttpBodySizeLimit) return false; + if (reading < end) + { + vint firstExtension = reading; + while (firstExtension < end && IsOws(buffer[firstExtension])) firstExtension++; + if (firstExtension == end || buffer[firstExtension] != ';') return false; + } + return ParseSemicolonParameters(buffer, reading, end); + } + + bool ParseFieldLine(const vuint8_t* buffer, vint begin, vint end, HttpField& field) + { + vint colon = -1; + for (vint i = begin; i < end; i++) + { + if (buffer[i] == ':') + { + colon = i; + break; + } + if (!IsTokenCharacter(buffer[i])) + { + return false; + } + } + if (colon == begin || colon == -1) + { + return false; + } + for (vint i = begin; i < colon; i++) + { + if (!IsTokenCharacter(buffer[i])) return false; + } + + vint valueBegin = colon + 1; + vint valueEnd = end; + while (valueBegin < valueEnd && IsOws(buffer[valueBegin])) valueBegin++; + while (valueBegin < valueEnd && IsOws(buffer[valueEnd - 1])) valueEnd--; + for (vint i = valueBegin; i < valueEnd; i++) + { + if (!IsFieldValueCharacter(buffer[i])) return false; + } + + field.name = CopyAscii(buffer, begin, colon, true); + field.value.Resize(valueEnd - valueBegin); + if (valueEnd > valueBegin) + { + std::memcpy(&field.value[0], buffer + valueBegin, (size_t)(valueEnd - valueBegin)); + } + return true; + } + + bool ParseUnsignedDecimal(const Array& value, vint begin, vint end, vuint64_t& number) + { + if (begin == end) return false; + number = 0; + for (vint i = begin; i < end; i++) + { + if (!IsDigit(value[i])) return false; + auto digit = (vuint64_t)(value[i] - '0'); + if (number > ((std::numeric_limits::max)() - digit) / 10) + { + return false; + } + number = number * 10 + digit; + } + return true; + } + + bool ParseContentLength(const Array& value, bool& initialized, vuint64_t& contentLength, vint& valueCount) + { + vint reading = 0; + while (true) + { + while (reading < value.Count() && IsOws(value[reading])) reading++; + vint begin = reading; + while (reading < value.Count() && IsDigit(value[reading])) reading++; + vint end = reading; + while (reading < value.Count() && IsOws(value[reading])) reading++; + vuint64_t number = 0; + if (!ParseUnsignedDecimal(value, begin, end, number)) + { + return false; + } + valueCount++; + if (!initialized) + { + initialized = true; + contentLength = number; + } + else if (contentLength != number) + { + return false; + } + if (reading == value.Count()) return true; + if (value[reading++] != ',') return false; + if (reading == value.Count()) return false; + } + } + + bool ParseTransferCodings(const Array& value, List& codings, bool& hasParameters) + { + vint reading = 0; + while (true) + { + while (reading < value.Count() && IsOws(value[reading])) reading++; + WString coding; + if (!ParseToken(value.Count() == 0 ? nullptr : &value[0], reading, value.Count(), &coding)) + { + return false; + } + codings.Add(coding); + while (reading < value.Count() && IsOws(value[reading])) reading++; + while (reading < value.Count() && value[reading] == ';') + { + hasParameters = true; + reading++; + while (reading < value.Count() && IsOws(value[reading])) reading++; + if (!ParseToken(&value[0], reading, value.Count())) return false; + while (reading < value.Count() && IsOws(value[reading])) reading++; + if (reading >= value.Count() || value[reading] != '=') + { + return false; + } + else + { + reading++; + while (reading < value.Count() && IsOws(value[reading])) reading++; + if (reading < value.Count() && value[reading] == '"') + { + if (!ParseQuotedString(&value[0], reading, value.Count())) return false; + } + else if (!ParseToken(&value[0], reading, value.Count())) + { + return false; + } + } + while (reading < value.Count() && IsOws(value[reading])) reading++; + } + if (reading == value.Count()) return true; + if (value[reading++] != ',' || reading == value.Count()) return false; + } + } + + bool HasConnectionClose(const Array& value) + { + vint reading = 0; + while (reading < value.Count()) + { + while (reading < value.Count() && IsOws(value[reading])) reading++; + vint begin = reading; + while (reading < value.Count() && value[reading] != ',') reading++; + vint end = reading; + while (begin < end && IsOws(value[begin])) begin++; + while (begin < end && IsOws(value[end - 1])) end--; + if (end - begin == 5) + { + const wchar_t* close = L"close"; + bool matched = true; + for (vint i = 0; i < 5; i++) + { + auto c = value[begin + i]; + if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + if (c != close[i]) matched = false; + } + if (matched) return true; + } + if (reading < value.Count()) reading++; + } + return false; + } + + bool ParseHttpVersion(const vuint8_t* buffer, vint begin, vint end, HttpVersion& version) + { + const wchar_t* prefix = L"HTTP/"; + if (end - begin < 8) return false; + for (vint i = 0; i < 5; i++) + { + if (buffer[begin + i] != prefix[i]) return false; + } + vint reading = begin + 5; + vuint64_t major = 0; + vuint64_t minor = 0; + vint majorBegin = reading; + while (reading < end && IsDigit(buffer[reading])) + { + auto digit = (vuint64_t)(buffer[reading++] - '0'); + if (major > ((std::numeric_limits::max)() - digit) / 10) return false; + major = major * 10 + digit; + } + if (reading == majorBegin || reading >= end || buffer[reading++] != '.') return false; + vint minorBegin = reading; + while (reading < end && IsDigit(buffer[reading])) + { + auto digit = (vuint64_t)(buffer[reading++] - '0'); + if (minor > ((std::numeric_limits::max)() - digit) / 10) return false; + minor = minor * 10 + digit; + } + if (reading != end || reading == minorBegin || major > (vuint64_t)(std::numeric_limits::max)() || minor > (vuint64_t)(std::numeric_limits::max)()) + { + return false; + } + version.major = (vint)major; + version.minor = (vint)minor; + return true; + } + + bool ParseRequestLine(const vuint8_t* buffer, vint end, HttpVersion& version, WString& method, WString& target) + { + vint firstSpace = -1; + vint secondSpace = -1; + for (vint i = 0; i < end; i++) + { + if (buffer[i] == ' ') + { + if (firstSpace == -1) firstSpace = i; + else if (secondSpace == -1) secondSpace = i; + else return false; + } + } + if (firstSpace <= 0 || secondSpace <= firstSpace + 1 || secondSpace + 1 >= end) return false; + for (vint i = 0; i < firstSpace; i++) if (!IsTokenCharacter(buffer[i])) return false; + for (vint i = firstSpace + 1; i < secondSpace; i++) + { + if (buffer[i] < 0x21 || buffer[i] > 0x7E) return false; + } + method = CopyAscii(buffer, 0, firstSpace, false); + target = CopyAscii(buffer, firstSpace + 1, secondSpace, false); + return ParseHttpVersion(buffer, secondSpace + 1, end, version); + } + + bool ParseStatusLine(const vuint8_t* buffer, vint end, HttpVersion& version, vint& statusCode, WString& reason) + { + vint firstSpace = -1; + for (vint i = 0; i < end; i++) + { + if (buffer[i] == ' ') + { + firstSpace = i; + break; + } + } + if (firstSpace == -1 || !ParseHttpVersion(buffer, 0, firstSpace, version)) return false; + if (firstSpace + 4 >= end || !IsDigit(buffer[firstSpace + 1]) || !IsDigit(buffer[firstSpace + 2]) || !IsDigit(buffer[firstSpace + 3])) return false; + if (buffer[firstSpace + 4] != ' ') return false; + statusCode = (buffer[firstSpace + 1] - '0') * 100 + (buffer[firstSpace + 2] - '0') * 10 + buffer[firstSpace + 3] - '0'; + if (statusCode < 200 || statusCode > 599) return false; + vint reasonBegin = firstSpace + 5; + for (vint i = reasonBegin; i < end; i++) + { + if (buffer[i] < 0x20 || buffer[i] > 0x7E) return false; + } + reason = CopyAscii(buffer, reasonBegin, end, false); + return true; + } + + enum class HttpBodyDetailedParsingResult + { + Succeeded, + Incomplete, + BadRequest, + PayloadTooLarge, + TrailerFieldsTooLarge, + }; + + HttpBodyDetailedParsingResult ParseHttpRequestBodyToChunksDetailed( + const vuint8_t* buffer, + vint availableBytes, + HttpBody& output, + vint& consumedBytes + ); + + enum class HttpMessageParsingResult + { + Succeeded, + Incomplete, + BadRequest, + PayloadTooLarge, + UriTooLong, + ExpectationFailed, + RequestHeaderFieldsTooLarge, + NotImplemented, + HttpVersionNotSupported, + }; + + HttpRequestFailure GetHttpRequestFailure(HttpMessageParsingResult result) + { + switch (result) + { + case HttpMessageParsingResult::PayloadTooLarge: + return HttpRequestFailure::PayloadTooLarge; + case HttpMessageParsingResult::UriTooLong: + return HttpRequestFailure::UriTooLong; + case HttpMessageParsingResult::ExpectationFailed: + return HttpRequestFailure::ExpectationFailed; + case HttpMessageParsingResult::RequestHeaderFieldsTooLarge: + return HttpRequestFailure::RequestHeaderFieldsTooLarge; + case HttpMessageParsingResult::NotImplemented: + return HttpRequestFailure::NotImplemented; + case HttpMessageParsingResult::HttpVersionNotSupported: + return HttpRequestFailure::HttpVersionNotSupported; + default: + return HttpRequestFailure::BadRequest; + } + } + + bool HasHeader(const List& fields, const wchar_t* name) + { + for (auto&& field : fields) + { + if (IsAsciiEqual(field.name, name)) return true; + } + return false; + } + + bool TryGetHttpRequestLineSize(const WString& method, const WString& requestTarget, vint& size) + { + if ( + method.Length() > HttpRequestLineSizeLimit - 10 || + requestTarget.Length() > HttpRequestLineSizeLimit - 10 - method.Length() + ) + { + return false; + } + size = 10 + method.Length() + requestTarget.Length(); + return true; + } + + HttpMessageParsingResult ParseHttpMessage( + const vuint8_t* buffer, + vint availableBytes, + bool requestMessage, + const WString& responseToMethod, + WString& parsedRequestMethod, + Ptr& request, + Ptr& response, + vint& consumedBytes, + bool& connectionClose + ) + { + consumedBytes = 0; + connectionClose = false; + parsedRequestMethod = L""; + vint startLineEnd = FindCrlf(buffer, 0, availableBytes); + if (startLineEnd == -1) + { + auto possibleLineBytes = availableBytes > 0 && buffer[availableBytes - 1] == '\r' ? availableBytes - 1 : availableBytes; + return possibleLineBytes > HttpRequestLineSizeLimit + ? (requestMessage ? HttpMessageParsingResult::UriTooLong : HttpMessageParsingResult::BadRequest) + : HttpMessageParsingResult::Incomplete; + } + if (startLineEnd > HttpRequestLineSizeLimit) + { + return requestMessage ? HttpMessageParsingResult::UriTooLong : HttpMessageParsingResult::BadRequest; + } + + HttpVersion version; + WString method; + WString target; + vint statusCode = 0; + WString reason; + if (requestMessage) + { + if (!ParseRequestLine(buffer, startLineEnd, version, method, target)) return HttpMessageParsingResult::BadRequest; + parsedRequestMethod = method; + } + else + { + if (!ParseStatusLine(buffer, startLineEnd, version, statusCode, reason)) return HttpMessageParsingResult::BadRequest; + } + if (version.major != 1 || version.minor != 1) + { + return HttpMessageParsingResult::HttpVersionNotSupported; + } + + List headers; + vint headersBegin = startLineEnd + 2; + vint reading = headersBegin; + vint bodyBegin = -1; + while (true) + { + vint lineEnd = FindCrlf(buffer, reading, availableBytes); + if (lineEnd == -1) + { + return availableBytes - headersBegin >= HttpHeaderBlockSizeLimit + ? (requestMessage ? HttpMessageParsingResult::RequestHeaderFieldsTooLarge : HttpMessageParsingResult::BadRequest) + : HttpMessageParsingResult::Incomplete; + } + if (lineEnd + 2 - headersBegin > HttpHeaderBlockSizeLimit) + { + return requestMessage ? HttpMessageParsingResult::RequestHeaderFieldsTooLarge : HttpMessageParsingResult::BadRequest; + } + if (lineEnd == reading) + { + bodyBegin = lineEnd + 2; + break; + } + HttpField field; + if (!ParseFieldLine(buffer, reading, lineEnd, field)) return HttpMessageParsingResult::BadRequest; + headers.Add(std::move(field)); + reading = lineEnd + 2; + } + + HttpFraming framing; + auto framingResult = AnalyzeHttpFraming(headers, framing); + if (framingResult == HttpFramingAnalysisResult::Invalid) return HttpMessageParsingResult::BadRequest; + if (framingResult == HttpFramingAnalysisResult::UnsupportedTransferCoding) return HttpMessageParsingResult::NotImplemented; + if (requestMessage && HasHeader(headers, L"expect")) return HttpMessageParsingResult::ExpectationFailed; + auto hasContentLength = framing.kind == HttpFramingKind::ContentLength; + auto hasTransferEncoding = framing.kind == HttpFramingKind::Chunked; + + auto headResponse = !requestMessage && responseToMethod == L"HEAD"; + auto noContentResponse = !requestMessage && statusCode == 204; + auto notModifiedResponse = !requestMessage && statusCode == 304; + auto responseWithoutBody = headResponse || noContentResponse || notModifiedResponse; + if (noContentResponse && (hasContentLength || hasTransferEncoding)) return HttpMessageParsingResult::BadRequest; + if (notModifiedResponse && hasTransferEncoding) return HttpMessageParsingResult::BadRequest; + if (!requestMessage && !responseWithoutBody && !hasContentLength && !hasTransferEncoding) return HttpMessageParsingResult::BadRequest; + + HttpBody body; + vint bodyBytes = 0; + if (!responseWithoutBody && hasTransferEncoding) + { + auto result = ParseHttpRequestBodyToChunksDetailed(buffer + bodyBegin, availableBytes - bodyBegin, body, bodyBytes); + if (result == HttpBodyDetailedParsingResult::Incomplete) return HttpMessageParsingResult::Incomplete; + if (result == HttpBodyDetailedParsingResult::PayloadTooLarge) return HttpMessageParsingResult::PayloadTooLarge; + if (result == HttpBodyDetailedParsingResult::TrailerFieldsTooLarge) return HttpMessageParsingResult::RequestHeaderFieldsTooLarge; + if (result == HttpBodyDetailedParsingResult::BadRequest) return HttpMessageParsingResult::BadRequest; + } + else if (!responseWithoutBody && hasContentLength) + { + if (framing.contentLength > (vuint64_t)HttpBodySizeLimit) return HttpMessageParsingResult::PayloadTooLarge; + bodyBytes = (vint)framing.contentLength; + if (availableBytes - bodyBegin < bodyBytes) return HttpMessageParsingResult::Incomplete; + if (bodyBytes > 0) + { + HttpBodyChunk chunk; + chunk.data.Resize(bodyBytes); + std::memcpy(&chunk.data[0], buffer + bodyBegin, (size_t)bodyBytes); + body.chunks.Add(std::move(chunk)); + } + } + + consumedBytes = bodyBegin + bodyBytes; + connectionClose = framing.connectionClose; + if (requestMessage) + { + request = Ptr(new HttpRequest); + request->version = version; + request->method = method; + request->requestTarget = target; + request->headers = std::move(headers); + request->body = std::move(body); + } + else + { + response = Ptr(new HttpResponse); + response->version = version; + response->statusCode = statusCode; + response->reason = reason; + response->headers = std::move(headers); + response->body = std::move(body); + } + return HttpMessageParsingResult::Succeeded; + } + + bool ValidateField(const HttpField& field, bool trailer) + { + if (field.name.Length() == 0) return false; + for (vint i = 0; i < field.name.Length(); i++) + { + auto c = field.name[i]; + if ((vuint32_t)c > 0x7F || !IsTokenCharacter((vuint8_t)c) || (c >= L'A' && c <= L'Z')) return false; + } + for (auto c : field.value) + { + if (!IsFieldValueCharacter(c)) return false; + } + if (trailer && (field.name == L"content-length" || field.name == L"transfer-encoding")) return false; + return true; + } + + void AppendAscii(List& bytes, const wchar_t* text) + { + while (*text) + { + CHECK_ERROR(*text <= 0x7F, L"HTTP serialization requires ASCII protocol text."); + bytes.Add((vuint8_t)*text++); + } + } + + void AppendAscii(List& bytes, const WString& text) + { + for (vint i = 0; i < text.Length(); i++) + { + CHECK_ERROR(text[i] <= 0x7F, L"HTTP serialization requires ASCII protocol text."); + bytes.Add((vuint8_t)text[i]); + } + } + + void AppendDecimal(List& bytes, vint number) + { + CHECK_ERROR(number >= 0, L"HTTP serialization requires a non-negative decimal number."); + vuint8_t digits[32]; + auto count = 0; + auto value = (vuint64_t)number; + do + { + digits[count++] = (vuint8_t)('0' + value % 10); + value /= 10; + } while (value > 0); + for (vint i = count - 1; i >= 0; i--) bytes.Add(digits[i]); + } + + void AppendHexadecimal(List& bytes, vint number) + { + CHECK_ERROR(number > 0, L"HTTP chunk serialization requires a positive chunk size."); + vuint8_t digits[32]; + auto count = 0; + auto value = (vuint64_t)number; + const wchar_t* hex = L"0123456789abcdef"; + while (value > 0) + { + digits[count++] = (vuint8_t)hex[value % 16]; + value /= 16; + } + for (vint i = count - 1; i >= 0; i--) bytes.Add(digits[i]); + } + + void AppendCrlf(List& bytes) + { + bytes.Add('\r'); + bytes.Add('\n'); + } + + void AppendField(List& bytes, const HttpField& field) + { + AppendAscii(bytes, field.name); + bytes.Add(':'); + bytes.Add(' '); + for (auto c : field.value) bytes.Add(c); + AppendCrlf(bytes); + } + + vint DecimalDigitCount(vint number) + { + vint count = 1; + while (number >= 10) + { + number /= 10; + count++; + } + return count; + } + + vint HexadecimalDigitCount(vint number) + { + vint count = 1; + while (number >= 16) + { + number /= 16; + count++; + } + return count; + } + + vint AsciiLength(const wchar_t* text) + { + vint count = 0; + while (text[count]) count++; + return count; + } + + void AddBoundedSize(vint& total, vint adding, vint limit, const wchar_t* error) + { + CHECK_ERROR(adding >= 0 && total <= limit - adding, error); + total += adding; + } + + vint ValidateBody(const HttpBody& body) + { + CHECK_ERROR(body.chunks.Count() <= HttpChunkCountLimit, L"HTTP body contains too many chunks."); + vint total = 0; + for (auto&& chunk : body.chunks) + { + CHECK_ERROR(chunk.data.Count() > 0, L"HTTP body chunks must contain at least one octet."); + CHECK_ERROR(total <= HttpBodySizeLimit - chunk.data.Count(), L"HTTP body exceeds the configured size limit."); + total += chunk.data.Count(); + } + for (auto&& trailer : body.trailers) + { + CHECK_ERROR(ValidateField(trailer, true), L"HTTP body contains an invalid trailer field."); + } + return total; + } + + Ptr SerializeHttpMessage(HttpRequest* request, HttpResponse* response, const WString& responseToMethod, bool& connectionClose) + { + auto requestMessage = request != nullptr; + CHECK_ERROR(requestMessage != (response != nullptr), L"HTTP serialization requires exactly one message."); + auto&& version = requestMessage ? request->version : response->version; + auto&& headers = requestMessage ? request->headers : response->headers; + auto&& body = requestMessage ? request->body : response->body; + CHECK_ERROR(version.major == 1 && version.minor == 1, L"HTTP serialization only supports HTTP/1.1."); + + vint startLineSize = 0; + if (requestMessage) + { + switch (ValidateHttpRequestLine(request->method, request->requestTarget)) + { + case HttpRequestLineValidationResult::InvalidMethod: + CHECK_FAIL(L"HTTP request serialization received an invalid method."); + case HttpRequestLineValidationResult::InvalidRequestTarget: + CHECK_FAIL(L"HTTP request serialization received an invalid request target."); + case HttpRequestLineValidationResult::TooLong: + CHECK_FAIL(L"HTTP start line exceeds the configured size limit."); + default: + break; + } + CHECK_ERROR(TryGetHttpRequestLineSize(request->method, request->requestTarget, startLineSize), L"HTTP start line exceeds the configured size limit."); + } + else + { + CHECK_ERROR(response->statusCode >= 200 && response->statusCode <= 599, L"HTTP response serialization only supports final status codes from 200 through 599."); + for (vint i = 0; i < response->reason.Length(); i++) + { + CHECK_ERROR(response->reason[i] >= 0x20 && response->reason[i] <= 0x7E, L"HTTP response serialization received an invalid reason phrase."); + } + startLineSize = 13; + AddBoundedSize(startLineSize, response->reason.Length(), HttpRequestLineSizeLimit, L"HTTP start line exceeds the configured size limit."); + } + + vint headerBlockSize = 2; + for (auto&& field : headers) + { + CHECK_ERROR(ValidateField(field, false), L"HTTP serialization received an invalid header field."); + AddBoundedSize(headerBlockSize, field.name.Length(), HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit."); + AddBoundedSize(headerBlockSize, 2, HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit."); + AddBoundedSize(headerBlockSize, field.value.Count(), HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit."); + AddBoundedSize(headerBlockSize, 2, HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit."); + } + + HttpFraming framing; + CHECK_ERROR(AnalyzeHttpFraming(headers, framing) == HttpFramingAnalysisResult::Succeeded, L"HTTP serialization received invalid, ambiguous, or unsupported body framing."); + auto bodySize = ValidateBody(body); + auto headResponse = !requestMessage && responseToMethod == L"HEAD"; + auto noContentResponse = !requestMessage && response->statusCode == 204; + auto notModifiedResponse = !requestMessage && response->statusCode == 304; + auto suppressBody = headResponse || noContentResponse || notModifiedResponse; + auto hasContentLength = framing.kind == HttpFramingKind::ContentLength; + auto hasTransferEncoding = framing.kind == HttpFramingKind::Chunked; + auto chunked = hasTransferEncoding; + auto generateContentLength = false; + auto generateTransferEncoding = false; + if (noContentResponse) + { + CHECK_ERROR(body.chunks.Count() == 0 && body.trailers.Count() == 0, L"An HTTP 204 response cannot contain a body."); + CHECK_ERROR(!hasContentLength && !hasTransferEncoding, L"An HTTP 204 response cannot contain body framing."); + chunked = false; + } + else if (notModifiedResponse) + { + CHECK_ERROR(body.chunks.Count() == 0 && body.trailers.Count() == 0, L"An HTTP 304 response cannot contain a body."); + CHECK_ERROR(!hasTransferEncoding, L"An HTTP 304 response cannot contain Transfer-Encoding."); + chunked = false; + } + else if (!hasContentLength && !hasTransferEncoding) + { + chunked = body.chunks.Count() > 1 || body.trailers.Count() > 0; + generateTransferEncoding = chunked; + generateContentLength = !chunked && (!requestMessage || body.chunks.Count() > 0); + } + if (!noContentResponse && !notModifiedResponse && !chunked) + { + CHECK_ERROR(body.trailers.Count() == 0 && body.chunks.Count() <= 1, L"A fixed HTTP body cannot contain multiple chunks or trailers."); + if (hasContentLength) + { + if (!headResponse || body.chunks.Count() > 0) + { + CHECK_ERROR(framing.contentLength == (vuint64_t)bodySize, L"HTTP Content-Length does not match the supplied body."); + } + } + else if (!generateContentLength && !headResponse) + { + CHECK_ERROR(requestMessage && bodySize == 0, L"An HTTP response requires explicit body framing."); + } + } + if (generateContentLength) + { + AddBoundedSize(headerBlockSize, AsciiLength(L"content-length: ") + DecimalDigitCount(bodySize) + 2, HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit."); + } + if (generateTransferEncoding) + { + AddBoundedSize(headerBlockSize, AsciiLength(L"transfer-encoding: chunked\r\n"), HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit."); + } + + vint trailerBlockSize = 2; + for (auto&& trailer : body.trailers) + { + AddBoundedSize(trailerBlockSize, trailer.name.Length(), HttpTrailerBlockSizeLimit, L"HTTP trailer block exceeds the configured size limit."); + AddBoundedSize(trailerBlockSize, 2, HttpTrailerBlockSizeLimit, L"HTTP trailer block exceeds the configured size limit."); + AddBoundedSize(trailerBlockSize, trailer.value.Count(), HttpTrailerBlockSizeLimit, L"HTTP trailer block exceeds the configured size limit."); + AddBoundedSize(trailerBlockSize, 2, HttpTrailerBlockSizeLimit, L"HTTP trailer block exceeds the configured size limit."); + } + vint wireSize = startLineSize; + AddBoundedSize(wireSize, 2, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit."); + AddBoundedSize(wireSize, headerBlockSize, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit."); + if (!suppressBody && chunked) + { + for (auto&& chunk : body.chunks) + { + AddBoundedSize(wireSize, HexadecimalDigitCount(chunk.data.Count()) + 4, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit."); + AddBoundedSize(wireSize, chunk.data.Count(), HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit."); + } + AddBoundedSize(wireSize, 3, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit."); + AddBoundedSize(wireSize, trailerBlockSize, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit."); + } + else if (!suppressBody) + { + AddBoundedSize(wireSize, bodySize, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit."); + } + + List bytes; + if (requestMessage) + { + AppendAscii(bytes, request->method); + bytes.Add(' '); + AppendAscii(bytes, request->requestTarget); + bytes.Add(' '); + } + AppendAscii(bytes, L"HTTP/"); + AppendDecimal(bytes, version.major); + bytes.Add('.'); + AppendDecimal(bytes, version.minor); + if (!requestMessage) + { + bytes.Add(' '); + AppendDecimal(bytes, response->statusCode); + bytes.Add(' '); + AppendAscii(bytes, response->reason); + } + CHECK_ERROR(bytes.Count() <= HttpRequestLineSizeLimit, L"HTTP start line exceeds the configured size limit."); + AppendCrlf(bytes); + + for (auto&& field : headers) AppendField(bytes, field); + if (generateContentLength) + { + AppendAscii(bytes, L"content-length: "); + AppendDecimal(bytes, bodySize); + AppendCrlf(bytes); + } + if (generateTransferEncoding) + { + AppendAscii(bytes, L"transfer-encoding: chunked\r\n"); + } + AppendCrlf(bytes); + + if (!suppressBody && chunked) + { + for (auto&& chunk : body.chunks) + { + AppendHexadecimal(bytes, chunk.data.Count()); + AppendCrlf(bytes); + for (auto c : chunk.data) bytes.Add(c); + AppendCrlf(bytes); + } + AppendAscii(bytes, L"0\r\n"); + for (auto&& trailer : body.trailers) AppendField(bytes, trailer); + AppendCrlf(bytes); + } + else if (!suppressBody && body.chunks.Count() == 1) + { + for (auto c : body.chunks[0].data) bytes.Add(c); + } + CHECK_ERROR(bytes.Count() <= HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit."); + + auto buffer = Ptr(new AsyncSocketBuffer); + buffer->data.Resize(bytes.Count()); + if (bytes.Count() > 0) + { + std::memcpy(&buffer->data[0], &bytes[0], (size_t)bytes.Count()); + } + connectionClose = framing.connectionClose; + return buffer; + } + } + + HttpFramingAnalysisResult AnalyzeHttpFraming(const List& fields, HttpFraming& framing) + { + framing = HttpFraming(); + bool contentLengthInitialized = false; + bool hasTransferEncoding = false; + List transferCodings; + bool transferCodingParameters = false; + for (auto&& field : fields) + { + if (field.name == L"content-length") + { + framing.contentLengthFieldCount++; + if (field.value.Count() == 0) + { + framing.contentLengthValuesPlainDecimal = false; + } + else + { + for (auto c : field.value) + { + if (!IsDigit(c)) + { + framing.contentLengthValuesPlainDecimal = false; + break; + } + } + } + if (!ParseContentLength(field.value, contentLengthInitialized, framing.contentLength, framing.contentLengthValueCount)) + { + return HttpFramingAnalysisResult::Invalid; + } + } + else if (field.name == L"transfer-encoding") + { + hasTransferEncoding = true; + if (!ParseTransferCodings(field.value, transferCodings, transferCodingParameters)) + { + return HttpFramingAnalysisResult::Invalid; + } + } + else if (field.name == L"connection") + { + framing.connectionClose |= HasConnectionClose(field.value); + } + } + if (hasTransferEncoding && contentLengthInitialized) + { + return HttpFramingAnalysisResult::Invalid; + } + if (hasTransferEncoding) + { + if ( + transferCodings.Count() != 1 || + transferCodings[0] != L"chunked" || + transferCodingParameters + ) + { + return HttpFramingAnalysisResult::UnsupportedTransferCoding; + } + framing.kind = HttpFramingKind::Chunked; + } + else if (contentLengthInitialized) + { + framing.kind = HttpFramingKind::ContentLength; + } + return HttpFramingAnalysisResult::Succeeded; + } + + const HttpField* FindHttpField(const List& fields, const WString& normalizedName) + { + for (auto&& field : fields) + { + if (field.name == normalizedName) + { + return &field; + } + } + return nullptr; + } + + vint CountHttpFields(const List& fields, const WString& normalizedName) + { + vint count = 0; + for (auto&& field : fields) + { + if (field.name == normalizedName) + { + count++; + } + } + return count; + } + + HttpField CreateAsciiHttpField(const WString& name, const WString& value) + { + CHECK_ERROR(name.Length() > 0, L"An HTTP field name cannot be empty."); + HttpField field; + Array normalizedName(name.Length()); + for (vint i = 0; i < name.Length(); i++) + { + auto c = name[i]; + CHECK_ERROR((vuint32_t)c <= 0x7F && IsTokenCharacter((vuint8_t)c), L"An HTTP field name must contain only ASCII token characters."); + if (L'A' <= c && c <= L'Z') c += L'a' - L'A'; + normalizedName[i] = c; + } + field.name = WString::CopyFrom(&normalizedName[0], normalizedName.Count()); + field.value.Resize(value.Length()); + for (vint i = 0; i < value.Length(); i++) + { + auto c = value[i]; + CHECK_ERROR((vuint32_t)c <= 0x7F && IsFieldValueCharacter((vuint8_t)c), L"An HTTP field value must contain only valid ASCII field characters."); + field.value[i] = (vuint8_t)c; + } + return field; + } + + bool DecodeAsciiHttpFieldValue(const Array& value, WString& text) + { + for (auto c : value) + { + if (c > 0x7F) return false; + } + if (value.Count() == 0) + { + text = WString::Empty; + return true; + } + Array characters(value.Count()); + for (vint i = 0; i < value.Count(); i++) + { + characters[i] = (wchar_t)value[i]; + } + text = WString::CopyFrom(&characters[0], characters.Count()); + return true; + } + + bool HttpFieldValueEqualsAscii(const Array& value, const WString& expected) + { + if (value.Count() != expected.Length()) return false; + for (vint i = 0; i < value.Count(); i++) + { + if ((vuint32_t)expected[i] > 0x7F || value[i] != (vuint8_t)expected[i]) return false; + } + return true; + } + + bool TryGetHttpBodySize(const HttpBody& body, vint& size) + { + vint total = 0; + for (auto&& chunk : body.chunks) + { + if (chunk.data.Count() > HttpBodySizeLimit - total) return false; + total += chunk.data.Count(); + } + size = total; + return true; + } + + bool FlattenHttpBody(const HttpBody& body, Array& bytes) + { + vint size = 0; + if (!TryGetHttpBodySize(body, size)) return false; + Array flattened(size); + vint offset = 0; + for (auto&& chunk : body.chunks) + { + if (chunk.data.Count() > 0) + { + std::memcpy(&flattened[offset], &chunk.data[0], (size_t)chunk.data.Count()); + offset += chunk.data.Count(); + } + } + bytes = std::move(flattened); + return true; + } + + void SetHttpBodyBytes(HttpBody& body, Array&& bytes) + { + CHECK_ERROR(bytes.Count() <= HttpBodySizeLimit, L"HTTP body exceeds the configured size limit."); + Array replacement = std::move(bytes); + body.chunks.Clear(); + body.trailers.Clear(); + if (replacement.Count() > 0) + { + HttpBodyChunk chunk; + chunk.data = std::move(replacement); + body.chunks.Add(std::move(chunk)); + } + } + + bool EncodeStrictUtf8(const WString& text, Array& bytes) + { + List encoded; + for (vint i = 0; i < text.Length(); i++) + { + auto code = (vuint32_t)text[i]; + if constexpr (sizeof(wchar_t) == 2) + { + if (0xD800 <= code && code <= 0xDBFF) + { + if (++i == text.Length()) return false; + auto low = (vuint32_t)text[i]; + if (low < 0xDC00 || low > 0xDFFF) return false; + code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00); + } + else if (0xDC00 <= code && code <= 0xDFFF) + { + return false; + } + } + else if (code > 0x10FFFF || (0xD800 <= code && code <= 0xDFFF)) + { + return false; + } + + vint adding = code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4; + if (encoded.Count() > (std::numeric_limits::max)() - adding) return false; + if (adding == 1) + { + encoded.Add((vuint8_t)code); + } + else + { + static const vuint8_t prefixes[] = { 0, 0xC0, 0xE0, 0xF0 }; + vuint8_t output[4]; + for (vint j = adding - 1; j > 0; j--) + { + output[j] = 0x80 | (code & 0x3F); + code >>= 6; + } + output[0] = prefixes[adding - 1] | (vuint8_t)code; + for (vint j = 0; j < adding; j++) encoded.Add(output[j]); + } + } + Array result(encoded.Count()); + for (vint i = 0; i < encoded.Count(); i++) result[i] = encoded[i]; + bytes = std::move(result); + return true; + } + + bool DecodeStrictUtf8(const vuint8_t* bytes, vint count, WString& text) + { + if (count < 0 || (!bytes && count > 0)) return false; + List characters; + for (vint i = 0; i < count;) + { + auto first = bytes[i++]; + vuint32_t code = 0; + vint following = 0; + if (first < 0x80) + { + code = first; + } + else if (0xC2 <= first && first <= 0xDF) + { + code = first & 0x1F; + following = 1; + } + else if (0xE0 <= first && first <= 0xEF) + { + code = first & 0x0F; + following = 2; + } + else if (0xF0 <= first && first <= 0xF4) + { + code = first & 0x07; + following = 3; + } + else + { + return false; + } + if (following > count - i) return false; + for (vint j = 0; j < following; j++) + { + auto next = bytes[i++]; + if ((next & 0xC0) != 0x80) return false; + code = (code << 6) | (next & 0x3F); + } + if ( + (following == 1 && code < 0x80) || + (following == 2 && code < 0x800) || + (following == 3 && code < 0x10000) || + code > 0x10FFFF || + (0xD800 <= code && code <= 0xDFFF) + ) + { + return false; + } + if constexpr (sizeof(wchar_t) == 2) + { + if (code <= 0xFFFF) + { + characters.Add((wchar_t)code); + } + else + { + if (characters.Count() > (std::numeric_limits::max)() - 2) return false; + code -= 0x10000; + characters.Add((wchar_t)(0xD800 + (code >> 10))); + characters.Add((wchar_t)(0xDC00 + (code & 0x3FF))); + } + } + else + { + characters.Add((wchar_t)code); + } + } + text = characters.Count() == 0 ? WString::Empty : WString::CopyFrom(&characters[0], characters.Count()); + return true; + } + + HttpRequestLineValidationResult ValidateHttpRequestLine(const WString& method, const WString& requestTarget) + { + if (method.Length() == 0) return HttpRequestLineValidationResult::InvalidMethod; + for (vint i = 0; i < method.Length(); i++) + { + auto c = method[i]; + if ((vuint32_t)c > 0x7F || !IsTokenCharacter((vuint8_t)c)) return HttpRequestLineValidationResult::InvalidMethod; + } + if (requestTarget.Length() == 0) return HttpRequestLineValidationResult::InvalidRequestTarget; + for (vint i = 0; i < requestTarget.Length(); i++) + { + auto c = requestTarget[i]; + if (c < 0x21 || c > 0x7E) return HttpRequestLineValidationResult::InvalidRequestTarget; + } + vint requestLineSize = 0; + if (!TryGetHttpRequestLineSize(method, requestTarget, requestLineSize)) return HttpRequestLineValidationResult::TooLong; + return HttpRequestLineValidationResult::Succeeded; + } + +/*********************************************************************** +HttpRequestBody +***********************************************************************/ + + namespace + { + HttpBodyDetailedParsingResult ParseHttpRequestBodyToChunksDetailed( + const vuint8_t* buffer, + vint availableBytes, + HttpBody& output, + vint& consumedBytes + ) + { + consumedBytes = 0; + if (availableBytes < 0 || (!buffer && availableBytes > 0)) return HttpBodyDetailedParsingResult::BadRequest; + HttpBody parsed; + vint reading = 0; + vint decodedBytes = 0; + while (true) + { + vint lineEnd = FindCrlf(buffer, reading, availableBytes); + if (lineEnd == -1) + { + auto trailingCrlfPrefix = availableBytes > reading && buffer[availableBytes - 1] == '\r'; + vint prefixEnd = trailingCrlfPrefix ? availableBytes - 1 : availableBytes; + auto validPrefix = trailingCrlfPrefix + ? ValidateCompleteChunkSizeLine(buffer, reading, prefixEnd) + : ValidateChunkSizeLinePrefix(buffer, reading, prefixEnd); + if (prefixEnd - reading > HttpChunkSizeLineLimit || !validPrefix) + { + return HttpBodyDetailedParsingResult::BadRequest; + } + return HttpBodyDetailedParsingResult::Incomplete; + } + if (lineEnd - reading > HttpChunkSizeLineLimit) return HttpBodyDetailedParsingResult::BadRequest; + if (lineEnd + 2 > HttpWireMessageSizeLimit) return HttpBodyDetailedParsingResult::PayloadTooLarge; + vint numberEnd = reading; + vuint64_t chunkSize = 0; + while (numberEnd < lineEnd && IsHexDigit(buffer[numberEnd])) + { + auto digit = (vuint64_t)HexDigitValue(buffer[numberEnd++]); + if (chunkSize > ((std::numeric_limits::max)() - digit) / 16) return HttpBodyDetailedParsingResult::PayloadTooLarge; + chunkSize = chunkSize * 16 + digit; + } + if (numberEnd == reading) return HttpBodyDetailedParsingResult::BadRequest; + if (chunkSize > (vuint64_t)HttpBodySizeLimit) return HttpBodyDetailedParsingResult::PayloadTooLarge; + if (numberEnd < lineEnd) + { + vint firstExtension = numberEnd; + while (firstExtension < lineEnd && IsOws(buffer[firstExtension])) firstExtension++; + if (firstExtension == lineEnd || buffer[firstExtension] != ';') return HttpBodyDetailedParsingResult::BadRequest; + } + vint extensionReading = numberEnd; + if (!ParseSemicolonParameters(buffer, extensionReading, lineEnd)) return HttpBodyDetailedParsingResult::BadRequest; + reading = lineEnd + 2; + + if (chunkSize == 0) + { + vint trailerBegin = reading; + while (true) + { + vint trailerEnd = FindCrlf(buffer, reading, availableBytes); + if (trailerEnd == -1) + { + return availableBytes - trailerBegin >= HttpTrailerBlockSizeLimit + ? HttpBodyDetailedParsingResult::TrailerFieldsTooLarge + : HttpBodyDetailedParsingResult::Incomplete; + } + if (trailerEnd + 2 - trailerBegin > HttpTrailerBlockSizeLimit) return HttpBodyDetailedParsingResult::TrailerFieldsTooLarge; + if (trailerEnd + 2 > HttpWireMessageSizeLimit) return HttpBodyDetailedParsingResult::PayloadTooLarge; + if (trailerEnd == reading) + { + consumedBytes = trailerEnd + 2; + output = std::move(parsed); + return HttpBodyDetailedParsingResult::Succeeded; + } + HttpField trailer; + if (!ParseFieldLine(buffer, reading, trailerEnd, trailer) || trailer.name == L"content-length" || trailer.name == L"transfer-encoding") + { + return HttpBodyDetailedParsingResult::BadRequest; + } + parsed.trailers.Add(std::move(trailer)); + reading = trailerEnd + 2; + } + } + + if (chunkSize > (vuint64_t)(HttpBodySizeLimit - decodedBytes)) return HttpBodyDetailedParsingResult::PayloadTooLarge; + vint chunkBytes = (vint)chunkSize; + if (availableBytes - reading < chunkBytes) return HttpBodyDetailedParsingResult::Incomplete; + auto terminatorBytes = availableBytes - reading - chunkBytes; + if (terminatorBytes == 0) return HttpBodyDetailedParsingResult::Incomplete; + if (buffer[reading + chunkBytes] != '\r') return HttpBodyDetailedParsingResult::BadRequest; + if (terminatorBytes == 1) return HttpBodyDetailedParsingResult::Incomplete; + if (buffer[reading + chunkBytes + 1] != '\n') return HttpBodyDetailedParsingResult::BadRequest; + if (reading > HttpWireMessageSizeLimit - chunkBytes - 2) return HttpBodyDetailedParsingResult::PayloadTooLarge; + if (parsed.chunks.Count() >= HttpChunkCountLimit) return HttpBodyDetailedParsingResult::PayloadTooLarge; + HttpBodyChunk chunk; + chunk.data.Resize(chunkBytes); + std::memcpy(&chunk.data[0], buffer + reading, (size_t)chunkBytes); + parsed.chunks.Add(std::move(chunk)); + decodedBytes += chunkBytes; + reading += chunkBytes + 2; + } + } + } + + HttpRequestBodyParsingResult ParseHttpRequestBodyToChunks( + const vuint8_t* buffer, + vint availableBytes, + HttpBody& output, + vint& consumedBytes + ) + { + auto result = ParseHttpRequestBodyToChunksDetailed(buffer, availableBytes, output, consumedBytes); + switch (result) + { + case HttpBodyDetailedParsingResult::Succeeded: + return HttpRequestBodyParsingResult::Succeeded; + case HttpBodyDetailedParsingResult::Incomplete: + return HttpRequestBodyParsingResult::Incomplete; + default: + return HttpRequestBodyParsingResult::Invalid; + } + } + +/*********************************************************************** +IHttpRequestCallback +***********************************************************************/ + + void IHttpRequestCallback::OnReadRequest(Ptr) + { + } + + void IHttpRequestCallback::OnReadRequestFailure(HttpRequestFailure) + { + } + + void IHttpRequestCallback::OnReadResponse(Ptr) + { + } + + void IHttpRequestCallback::OnReadResponseFailure(HttpResponseFailure) + { + OnError(L"The HTTP client received a 404 Not Found response.", true); + } + + void IHttpRequestCallback::OnWriteCompleted() + { + } + + void IHttpRequestCallback::OnError(const WString&, bool) + { + } + + void IHttpRequestCallback::OnConnected() + { + } + + void IHttpRequestCallback::OnDisconnected() + { + } + +/*********************************************************************** +HttpRequestTimeoutController +***********************************************************************/ + + namespace + { + class HttpRequestTimeoutController : public Object, public virtual IHttpRequestTimeoutController + { + private: + class State : public Object + { + public: + CriticalSection lock; + ConditionVariable cv; + Func callback; + std::chrono::steady_clock::time_point + deadline; + vint duration = 0; + bool armed = false; + bool workerRunning = false; + vint activeCallbacks = 0; + }; + + static thread_local State* currentCallbackState; + Ptr state = Ptr(new State); + + static void Run(Ptr state) + { + while (true) + { + Func callback; + state->lock.Enter(); + while (state->armed) + { + auto now = std::chrono::steady_clock::now(); + if (now >= state->deadline) + { + callback = state->callback; + state->callback = {}; + state->armed = false; + state->activeCallbacks++; + break; + } + auto remaining = std::chrono::ceil(state->deadline - now).count(); + auto wait = remaining > (std::numeric_limits::max)() + ? (std::numeric_limits::max)() + : (vint)remaining; + state->cv.SleepWithForTime(state->lock, wait); + } + if (!callback) + { + state->workerRunning = false; + state->cv.WakeAllPendings(); + state->lock.Leave(); + return; + } + state->lock.Leave(); + + auto previous = currentCallbackState; + currentCallbackState = state.Obj(); + try + { + callback(); + } + catch (...) + { + } + currentCallbackState = previous; + + CS_LOCK(state->lock) + { + state->activeCallbacks--; + state->cv.WakeAllPendings(); + } + } + } + + public: + ~HttpRequestTimeoutController() + { + CancelAndWait(); + } + + void Arm(vint milliseconds, const Func& callback) override + { + CHECK_ERROR(milliseconds > 0, L"The HTTP timeout controller requires a positive duration."); + bool queueWorker = false; + CS_LOCK(state->lock) + { + CHECK_ERROR(!state->armed, L"The HTTP timeout controller is already armed."); + state->callback = callback; + state->duration = milliseconds; + state->deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds); + state->armed = true; + if (!state->workerRunning) + { + state->workerRunning = true; + queueWorker = true; + } + state->cv.WakeAllPendings(); + } + if (queueWorker) + { + auto captured = state; + auto queued = ThreadPoolLite::Queue(Func([captured]() + { + Run(captured); + })); + if (!queued) + { + CS_LOCK(state->lock) + { + state->callback = {}; + state->armed = false; + state->workerRunning = false; + state->cv.WakeAllPendings(); + } + CHECK_ERROR(false, L"The HTTP timeout controller could not queue its deadline worker."); + } + } + } + + void Refresh() override + { + CS_LOCK(state->lock) + { + if (state->armed) + { + state->deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(state->duration); + state->cv.WakeAllPendings(); + } + } + } + + void CancelAndWait() override + { + auto nestedCallback = currentCallbackState == state.Obj(); + state->lock.Enter(); + state->armed = false; + state->callback = {}; + state->cv.WakeAllPendings(); + if (!nestedCallback) + { + while (state->workerRunning || state->activeCallbacks > 0) + { + state->cv.SleepWith(state->lock); + } + } + state->lock.Leave(); + } + }; + + thread_local HttpRequestTimeoutController::State* HttpRequestTimeoutController::currentCallbackState = nullptr; + } + + Ptr CreateHttpRequestTimeoutController() + { + return Ptr(new HttpRequestTimeoutController); + } + +/*********************************************************************** +HttpRequestCallbackDomain +***********************************************************************/ + + thread_local HttpRequestCallbackDomain::CallbackFrame* HttpRequestCallbackDomain::currentCallbackFrame = nullptr; + + HttpRequestCallbackDomain::CallbackFrame::CallbackFrame(Ptr _domain) + : domain(_domain) + , previous(currentCallbackFrame) + { + currentCallbackFrame = this; + CS_LOCK(domain->lockState) + { + domain->activeCallbacks++; + } + } + + HttpRequestCallbackDomain::CallbackFrame::~CallbackFrame() + { + currentCallbackFrame = previous; + CS_LOCK(domain->lockState) + { + domain->activeCallbacks--; + domain->cvState.WakeAllPendings(); + } + } + + vint HttpRequestCallbackDomain::CurrentCallbackDepth() + { + vint depth = 0; + for (auto frame = currentCallbackFrame; frame; frame = frame->previous) + { + if (frame->domain.Obj() == this) depth++; + } + return depth; + } + + void HttpRequestCallbackDomain::WaitForCallbacks(vint callbackDepth) + { + CS_LOCK(lockState) + { + while (activeCallbacks > callbackDepth) + { + cvState.SleepWith(lockState); + } + } + } + +/*********************************************************************** +HttpRequestConnectionLifecycle +***********************************************************************/ + + class HttpRequestConnectionLifecycle : public Object + { + public: + class RetainedAdapterRelease + { + public: + Ptr adapter; + Func drainedCallback; + + ~RetainedAdapterRelease() + { + if (drainedCallback) + { + try + { + drainedCallback(); + } + catch (...) + { + } + } + } + }; + + IAsyncSocketConnection* socketConnection = nullptr; + HttpRequestConnectionDirection direction = HttpRequestConnectionDirection::Server; + bool responseNotFoundIsFatal = false; + Ptr callbackDomain; + Ptr timeoutController; + Ptr retainedAdapter; + Func drainedCallback; + + CriticalSection lockState; + ConditionVariable cvState; + IHttpRequestCallback* callback = nullptr; + bool callbackInstalling = false; + vint activeCallbacks = 0; + vint activeSocketCallbacks = 0; + vint activeSocketCalls = 0; + bool stopStarted = false; + bool stopFinished = false; + bool terminal = false; + bool disconnectedNotified = false; + bool disconnectDelivering = false; + bool disconnectFinished = false; + bool readingStarted = false; + bool parserFailed = false; + bool peerDisconnected = false; + + List receiveBuffer; + bool timeoutArmed = false; + bool awaitingResponse = false; + bool exchangeActive = false; + bool closeAfterExchange = false; + WString activeRequestMethod; + vint activeResponseTimeout = HttpIncompleteMessageTimeout; + Ptr heldResponse; + bool fatalAfterResponse = false; + bool responseDelivering = false; + bool responseFinalizing = false; + Ptr deferredRequestWrite; + bool deferredRequestClose = false; + WString deferredRequestMethod; + vint deferredResponseTimeout = HttpIncompleteMessageTimeout; + Ptr pendingWrite; + bool writePending = false; + + void TakeRetainedAdapterIfDrained(RetainedAdapterRelease& releasing) + { + if (stopFinished && disconnectFinished && activeCallbacks == 0 && activeSocketCallbacks == 0 && activeSocketCalls == 0) + { + releasing.adapter = std::move(retainedAdapter); + releasing.drainedCallback = drainedCallback; + drainedCallback = {}; + } + } + }; + +/*********************************************************************** +HttpRequestConnection callback frames +***********************************************************************/ + + struct HttpRequestConnection::CallbackFrame + { + Ptr state; + CallbackFrame* previous = nullptr; + HttpRequestCallbackDomain::CallbackFrame domainFrame; + + CallbackFrame(Ptr _state) + : state(_state) + , previous(currentCallbackFrame) + , domainFrame(state->callbackDomain) + { + currentCallbackFrame = this; + } + + ~CallbackFrame() + { + currentCallbackFrame = previous; + Lifecycle::RetainedAdapterRelease releasing; + CS_LOCK(state->lockState) + { + state->activeCallbacks--; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + } + }; + + struct HttpRequestConnection::SocketCallbackFrame + { + Ptr state; + SocketCallbackFrame* previous = nullptr; + + SocketCallbackFrame(Ptr _state) + : state(_state) + , previous(currentSocketCallbackFrame) + { + currentSocketCallbackFrame = this; + CS_LOCK(state->lockState) + { + state->activeSocketCallbacks++; + } + } + + ~SocketCallbackFrame() + { + currentSocketCallbackFrame = previous; + Lifecycle::RetainedAdapterRelease releasing; + CS_LOCK(state->lockState) + { + state->activeSocketCallbacks--; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + } + }; + + struct HttpRequestConnection::TimeoutCallbackFrame + { + Ptr state; + TimeoutCallbackFrame* previous = nullptr; + + TimeoutCallbackFrame(Ptr _state) + : state(_state) + , previous(currentTimeoutCallbackFrame) + { + currentTimeoutCallbackFrame = this; + } + + ~TimeoutCallbackFrame() + { + currentTimeoutCallbackFrame = previous; + } + }; + + thread_local HttpRequestConnection::CallbackFrame* HttpRequestConnection::currentCallbackFrame = nullptr; + thread_local HttpRequestConnection::SocketCallbackFrame* HttpRequestConnection::currentSocketCallbackFrame = nullptr; + thread_local HttpRequestConnection::TimeoutCallbackFrame* HttpRequestConnection::currentTimeoutCallbackFrame = nullptr; + +/*********************************************************************** +HttpRequestConnection helpers +***********************************************************************/ + + vint HttpRequestConnection::CurrentCallbackDepth(Ptr state) + { + vint depth = 0; + for (auto frame = currentCallbackFrame; frame; frame = frame->previous) + { + if (frame->state.Obj() == state.Obj()) depth++; + } + return depth; + } + + vint HttpRequestConnection::CurrentSocketCallbackDepth(Ptr state) + { + vint depth = 0; + for (auto frame = currentSocketCallbackFrame; frame; frame = frame->previous) + { + if (frame->state.Obj() == state.Obj()) depth++; + } + return depth; + } + + vint HttpRequestConnection::CurrentTimeoutCallbackDepth(Ptr state) + { + vint depth = 0; + for (auto frame = currentTimeoutCallbackFrame; frame; frame = frame->previous) + { + if (frame->state.Obj() == state.Obj()) depth++; + } + return depth; + } + + void HttpRequestConnection::FinishSocketCall(Ptr state) + { + bool completePeerStop = false; + CS_LOCK(state->lockState) + { + state->activeSocketCalls--; + completePeerStop = state->activeSocketCalls == 0 && state->peerDisconnected && !state->stopFinished; + state->cvState.WakeAllPendings(); + } + if (completePeerStop) + { + StopConnection(state); + } + + Lifecycle::RetainedAdapterRelease releasing; + CS_LOCK(state->lockState) + { + state->TakeRetainedAdapterIfDrained(releasing); + } + } + + template + void HttpRequestConnection::InvokeHttpCallback(Ptr state, bool allowTerminal, TCallback&& invoke) + { + IHttpRequestCallback* installed = nullptr; + auto callbackDepth = CurrentCallbackDepth(state); + state->lockState.Enter(); + while (state->callbackInstalling && callbackDepth == 0 && state->callback) + { + state->cvState.SleepWith(state->lockState); + } + if (state->callback && (allowTerminal || (!state->stopStarted && !state->terminal))) + { + installed = state->callback; + state->activeCallbacks++; + } + state->lockState.Leave(); + if (installed) + { + CallbackFrame frame(state); + invoke(installed); + } + } + + void HttpRequestConnection::SubmitWrite(Ptr state, IAsyncSocketConnection* connection, Ptr buffer) + { + try + { + connection->WriteAsync(buffer); + } + catch (...) + { + CS_LOCK(state->lockState) + { + if (state->pendingWrite.Obj() == buffer.Obj()) + { + state->pendingWrite = nullptr; + state->writePending = false; + if (state->direction == HttpRequestConnectionDirection::Client) + { + state->exchangeActive = false; + state->closeAfterExchange = false; + } + } + state->cvState.WakeAllPendings(); + } + FinishSocketCall(state); + throw; + } + FinishSocketCall(state); + } + + void HttpRequestConnection::ReportRequestFailure(Ptr state, HttpRequestFailure failure, bool timeoutOnly, bool reserved) + { + bool report = false; + bool cancelTimeout = false; + CS_LOCK(state->lockState) + { + if ( + reserved && + state->direction == HttpRequestConnectionDirection::Server && + !state->stopStarted && + !state->terminal && + state->parserFailed && + state->awaitingResponse + ) + { + cancelTimeout = state->timeoutArmed; + state->timeoutArmed = false; + report = true; + } + else if ( + state->direction == HttpRequestConnectionDirection::Server && + !state->stopStarted && + !state->terminal && + !state->parserFailed && + !state->awaitingResponse && + (!timeoutOnly || state->timeoutArmed) + ) + { + state->parserFailed = true; + state->awaitingResponse = true; + state->closeAfterExchange = true; + state->receiveBuffer.Clear(); + cancelTimeout = state->timeoutArmed; + state->timeoutArmed = false; + report = true; + state->cvState.WakeAllPendings(); + } + } + if (!report) + { + return; + } + if (cancelTimeout) + { + state->timeoutController->CancelAndWait(); + } + + try + { + InvokeHttpCallback(state, false, [&](IHttpRequestCallback* installed) + { + installed->OnReadRequestFailure(failure); + }); + } + catch (...) + { + StopConnection(state); + throw; + } + + bool closeWithoutResponse = false; + CS_LOCK(state->lockState) + { + closeWithoutResponse = !state->stopStarted && !state->terminal && !state->peerDisconnected && !state->writePending; + } + if (closeWithoutResponse) + { + StopConnection(state); + } + } + + void HttpRequestConnection::InstallTimeout(Ptr state, vint milliseconds, const WString& error) + { + CHECK_ERROR(milliseconds > 0, L"HttpRequestConnection requires a positive timeout when arming a deadline."); + auto captured = state; + auto capturedError = error; + try + { + state->timeoutController->Arm(milliseconds, Func([captured, capturedError]() + { + TimeoutCallbackFrame timeoutCallbackFrame(captured); + if (captured->direction == HttpRequestConnectionDirection::Server) + { + ReportRequestFailure(captured, HttpRequestFailure::RequestTimeout, true); + return; + } + + bool expired = false; + CS_LOCK(captured->lockState) + { + if (captured->timeoutArmed && !captured->stopStarted && !captured->terminal && !captured->parserFailed) + { + captured->timeoutArmed = false; + captured->parserFailed = true; + expired = true; + } + } + if (expired) + { + ReportFatalError(captured, capturedError); + } + })); + bool cancelStaleTimeout = false; + CS_LOCK(state->lockState) + { + cancelStaleTimeout = !state->timeoutArmed || state->stopStarted || state->terminal; + } + if (cancelStaleTimeout) + { + state->timeoutController->CancelAndWait(); + } + } + catch (...) + { + CS_LOCK(state->lockState) + { + state->timeoutArmed = false; + } + ReportFatalError(state, L"The HTTP timeout controller failed while arming a message timeout."); + } + } + + void HttpRequestConnection::RefreshTimeout(Ptr state) + { + try + { + state->timeoutController->Refresh(); + } + catch (...) + { + CS_LOCK(state->lockState) + { + state->timeoutArmed = false; + } + ReportFatalError(state, L"The HTTP timeout controller failed while refreshing a message timeout."); + } + } + + void HttpRequestConnection::ReportResponseFailure(Ptr state, HttpResponseFailure failure) + { + bool report = false; + CS_LOCK(state->lockState) + { + if (!state->terminal && !state->stopStarted) + { + state->terminal = true; + state->parserFailed = true; + state->timeoutArmed = false; + state->pendingWrite = nullptr; + state->writePending = false; + state->heldResponse = nullptr; + state->fatalAfterResponse = false; + state->responseDelivering = false; + state->deferredRequestWrite = nullptr; + state->deferredRequestClose = false; + state->deferredRequestMethod = L""; + report = true; + state->cvState.WakeAllPendings(); + } + } + if (report) + { + state->timeoutController->CancelAndWait(); + try + { + InvokeHttpCallback(state, true, [&](IHttpRequestCallback* installed) + { + installed->OnReadResponseFailure(failure); + }); + } + catch (...) + { + StopConnection(state); + throw; + } + StopConnection(state); + } + } + + void HttpRequestConnection::DeliverResponse(Ptr state, Ptr response, bool closeAfterDelivery) + { + if (state->responseNotFoundIsFatal && response->statusCode == (vint)HttpResponseFailure::NotFound) + { + ReportResponseFailure(state, HttpResponseFailure::NotFound); + return; + } + + try + { + InvokeHttpCallback(state, false, [&](IHttpRequestCallback* installed) + { + installed->OnReadResponse(response); + }); + } + catch (...) + { + bool notifyDisconnected = false; + CS_LOCK(state->lockState) + { + state->responseDelivering = false; + state->deferredRequestWrite = nullptr; + state->deferredRequestClose = false; + state->deferredRequestMethod = L""; + notifyDisconnected = state->peerDisconnected; + state->responseFinalizing = notifyDisconnected; + state->cvState.WakeAllPendings(); + } + if (notifyDisconnected) + { + StopConnection(state); + CS_LOCK(state->lockState) + { + state->responseFinalizing = false; + state->cvState.WakeAllPendings(); + } + } + throw; + } + + bool fatalAfterDelivery = false; + bool notifyDisconnected = false; + Ptr deferredRequestWrite; + IAsyncSocketConnection* deferredRequestConnection = nullptr; + CS_LOCK(state->lockState) + { + fatalAfterDelivery = state->fatalAfterResponse; + state->fatalAfterResponse = false; + notifyDisconnected = state->peerDisconnected; + state->responseDelivering = false; + state->responseFinalizing = fatalAfterDelivery || closeAfterDelivery || notifyDisconnected; + if (state->responseFinalizing) + { + state->deferredRequestWrite = nullptr; + state->deferredRequestClose = false; + state->deferredRequestMethod = L""; + } + else if (state->deferredRequestWrite && !state->stopStarted && !state->terminal && state->socketConnection) + { + deferredRequestWrite = std::move(state->deferredRequestWrite); + state->exchangeActive = true; + state->closeAfterExchange = state->deferredRequestClose; + state->deferredRequestClose = false; + state->activeRequestMethod = std::move(state->deferredRequestMethod); + state->activeResponseTimeout = state->deferredResponseTimeout; + state->pendingWrite = deferredRequestWrite; + state->writePending = true; + deferredRequestConnection = state->socketConnection; + state->activeSocketCalls++; + } + state->cvState.WakeAllPendings(); + } + + try + { + if (fatalAfterDelivery) + { + ReportFatalError(state, L"The HTTP client received an unsolicited response after its exchange completed."); + } + else if (notifyDisconnected) + { + StopConnection(state); + } + else if (closeAfterDelivery) + { + StopConnection(state); + } + } + catch (...) + { + CS_LOCK(state->lockState) + { + state->responseFinalizing = false; + state->cvState.WakeAllPendings(); + } + throw; + } + + CS_LOCK(state->lockState) + { + state->responseFinalizing = false; + state->cvState.WakeAllPendings(); + } + if (!fatalAfterDelivery && !closeAfterDelivery && !notifyDisconnected) + { + if (deferredRequestWrite) + { + try + { + SubmitWrite(state, deferredRequestConnection, deferredRequestWrite); + } + catch (...) + { + ReportFatalError(state, L"The HTTP client failed to submit a deferred request write."); + return; + } + } + ProcessBufferedInput(state); + } + } + + void HttpRequestConnection::ProcessBufferedInput(Ptr state) + { + Ptr request; + Ptr response; + bool cancelTimeout = false; + bool deliverRequest = false; + bool deliverResponse = false; + bool closeAfterDelivery = false; + bool requestFailure = false; + bool clientFailure = false; + HttpRequestFailure failure = HttpRequestFailure::BadRequest; + WString parsedRequestMethod; + + state->lockState.Enter(); + if (state->stopStarted || state->terminal || state->parserFailed || state->peerDisconnected) + { + state->lockState.Leave(); + return; + } + auto parseEnabled = state->direction == HttpRequestConnectionDirection::Server + ? !state->awaitingResponse + : state->exchangeActive && !state->heldResponse && !state->responseDelivering && !state->responseFinalizing; + if (!parseEnabled) + { + state->lockState.Leave(); + return; + } + if (state->receiveBuffer.Count() == 0) + { + state->lockState.Leave(); + return; + } + + vint consumedBytes = 0; + bool messageClose = false; + auto result = ParseHttpMessage( + &state->receiveBuffer[0], + state->receiveBuffer.Count(), + state->direction == HttpRequestConnectionDirection::Server, + state->activeRequestMethod, + parsedRequestMethod, + request, + response, + consumedBytes, + messageClose + ); + if (result == HttpMessageParsingResult::Incomplete) + { + bool armTimeout = false; + bool refreshTimeout = false; + auto serverSide = state->direction == HttpRequestConnectionDirection::Server; + if (serverSide && parsedRequestMethod.Length() > 0) + { + state->activeRequestMethod = parsedRequestMethod; + } + auto timeout = serverSide ? HttpIncompleteMessageTimeout : state->activeResponseTimeout; + auto canArm = timeout > 0 && (serverSide || !state->writePending); + if (canArm && !state->timeoutArmed) + { + state->timeoutArmed = true; + armTimeout = true; + } + else if (canArm && serverSide) + { + refreshTimeout = true; + } + state->lockState.Leave(); + if (armTimeout) + { + InstallTimeout(state, timeout, L"The HTTP peer timed out while sending an incomplete message."); + } + else if (refreshTimeout) + { + RefreshTimeout(state); + } + return; + } + if (result != HttpMessageParsingResult::Succeeded) + { + if (state->direction == HttpRequestConnectionDirection::Server) + { + state->activeRequestMethod = parsedRequestMethod; + state->parserFailed = true; + state->awaitingResponse = true; + state->closeAfterExchange = true; + state->receiveBuffer.Clear(); + failure = GetHttpRequestFailure(result); + requestFailure = true; + } + else + { + clientFailure = true; + } + state->lockState.Leave(); + } + else + { + state->receiveBuffer.RemoveRange(0, consumedBytes); + if (state->timeoutArmed) + { + state->timeoutArmed = false; + cancelTimeout = true; + } + state->closeAfterExchange |= messageClose; + if (state->direction == HttpRequestConnectionDirection::Server) + { + state->activeRequestMethod = request->method; + state->awaitingResponse = true; + deliverRequest = true; + } + else if (state->writePending) + { + state->heldResponse = response; + if (state->receiveBuffer.Count() > 0) + { + state->fatalAfterResponse = true; + state->receiveBuffer.Clear(); + } + } + else + { + state->exchangeActive = false; + state->responseDelivering = true; + deliverResponse = true; + closeAfterDelivery = state->closeAfterExchange; + if (state->receiveBuffer.Count() > 0) + { + state->fatalAfterResponse = true; + state->receiveBuffer.Clear(); + } + } + state->lockState.Leave(); + } + + if (requestFailure) + { + ReportRequestFailure(state, failure, false, true); + return; + } + if (clientFailure) + { + ReportFatalError(state, L"The HTTP peer sent malformed or unsafe HTTP/1.1 framing."); + return; + } + if (cancelTimeout) + { + state->timeoutController->CancelAndWait(); + } + if (deliverRequest) + { + InvokeHttpCallback(state, false, [&](IHttpRequestCallback* installed) + { + installed->OnReadRequest(request); + }); + } + else if (deliverResponse) + { + DeliverResponse(state, response, closeAfterDelivery); + } + } + + void HttpRequestConnection::NotifyDisconnected(Ptr state) + { + auto callbackDepth = CurrentCallbackDepth(state); + state->lockState.Enter(); + if (!state->disconnectedNotified) + { + state->disconnectedNotified = true; + state->terminal = true; + state->pendingWrite = nullptr; + state->writePending = false; + state->heldResponse = nullptr; + state->fatalAfterResponse = false; + state->deferredRequestWrite = nullptr; + state->deferredRequestClose = false; + state->deferredRequestMethod = L""; + state->cvState.WakeAllPendings(); + } + if (state->disconnectFinished) + { + state->lockState.Leave(); + return; + } + if (state->disconnectDelivering) + { + if (callbackDepth == 0) + { + while (!state->disconnectFinished) state->cvState.SleepWith(state->lockState); + } + state->lockState.Leave(); + return; + } + if (callbackDepth == 0) + { + while (state->activeCallbacks > 0 && !state->disconnectDelivering && !state->disconnectFinished) + { + state->cvState.SleepWith(state->lockState); + } + if (state->disconnectFinished) + { + state->lockState.Leave(); + return; + } + } + state->disconnectDelivering = true; + while (state->activeCallbacks > callbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + state->lockState.Leave(); + + try + { + InvokeHttpCallback(state, true, [](IHttpRequestCallback* installed) + { + installed->OnDisconnected(); + }); + } + catch (...) + { + Lifecycle::RetainedAdapterRelease releasing; + CS_LOCK(state->lockState) + { + state->callback = nullptr; + state->disconnectFinished = true; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + throw; + } + + Lifecycle::RetainedAdapterRelease releasing; + CS_LOCK(state->lockState) + { + state->callback = nullptr; + while (state->activeCallbacks > callbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + state->disconnectFinished = true; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + } + + void HttpRequestConnection::StopConnection(Ptr state, Ptr retainedAdapter) + { + auto callbackDepth = CurrentCallbackDepth(state); + auto socketCallbackDepth = CurrentSocketCallbackDepth(state); + auto timeoutCallbackDepth = CurrentTimeoutCallbackDepth(state); + auto nestedCallback = callbackDepth > 0 || socketCallbackDepth > 0; + IAsyncSocketConnection* connection = nullptr; + bool executeStop = false; + bool nestedFollower = false; + bool timeoutFollower = false; + Lifecycle::RetainedAdapterRelease releasing; + + state->lockState.Enter(); + if (retainedAdapter) state->retainedAdapter = retainedAdapter; + if (!state->stopStarted) + { + state->stopStarted = true; + state->timeoutArmed = false; + state->pendingWrite = nullptr; + state->writePending = false; + state->heldResponse = nullptr; + state->fatalAfterResponse = false; + state->deferredRequestWrite = nullptr; + state->deferredRequestClose = false; + state->deferredRequestMethod = L""; + if (!nestedCallback) + { + while (state->activeSocketCalls > 0) state->cvState.SleepWith(state->lockState); + } + connection = state->peerDisconnected ? nullptr : state->socketConnection; + executeStop = true; + } + else if (nestedCallback) + { + connection = state->socketConnection; + nestedFollower = true; + } + else if (timeoutCallbackDepth > 0) + { + timeoutFollower = true; + } + else + { + while (!state->stopFinished) state->cvState.SleepWith(state->lockState); + while (state->activeCallbacks > 0 || state->activeSocketCallbacks > 0 || state->activeSocketCalls > 0) + { + state->cvState.SleepWith(state->lockState); + } + state->TakeRetainedAdapterIfDrained(releasing); + state->lockState.Leave(); + return; + } + state->lockState.Leave(); + + if (nestedFollower) + { + if (connection && socketCallbackDepth > 0) connection->Stop(); + NotifyDisconnected(state); + return; + } + if (timeoutFollower) + { + return; + } + state->timeoutController->CancelAndWait(); + if (executeStop && connection) connection->Stop(); + NotifyDisconnected(state); + + CS_LOCK(state->lockState) + { + while (state->activeCallbacks > callbackDepth || state->activeSocketCallbacks > socketCallbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + state->stopFinished = true; + state->TakeRetainedAdapterIfDrained(releasing); + state->cvState.WakeAllPendings(); + } + } + + void HttpRequestConnection::ReportFatalError(Ptr state, const WString& error) + { + bool report = false; + CS_LOCK(state->lockState) + { + if (!state->terminal && !state->stopStarted) + { + state->terminal = true; + state->parserFailed = true; + state->timeoutArmed = false; + state->pendingWrite = nullptr; + state->writePending = false; + state->heldResponse = nullptr; + state->fatalAfterResponse = false; + state->deferredRequestWrite = nullptr; + state->deferredRequestClose = false; + state->deferredRequestMethod = L""; + report = true; + state->cvState.WakeAllPendings(); + } + } + if (report) + { + state->timeoutController->CancelAndWait(); + try + { + InvokeHttpCallback(state, true, [&](IHttpRequestCallback* installed) + { + installed->OnError(error, true); + }); + } + catch (...) + { + StopConnection(state); + throw; + } + StopConnection(state); + } + } + +/*********************************************************************** +HttpRequestConnection +***********************************************************************/ + + HttpRequestConnection::HttpRequestConnection( + IAsyncSocketConnection* connection, + HttpRequestConnectionDirection direction, + Ptr callbackDomain, + Ptr timeoutController, + bool responseNotFoundIsFatal + ) + : lifecycle(Ptr(new Lifecycle)) + { + CHECK_ERROR(connection, L"HttpRequestConnection requires a valid async socket connection."); + lifecycle->socketConnection = connection; + lifecycle->direction = direction; + lifecycle->responseNotFoundIsFatal = responseNotFoundIsFatal; + lifecycle->callbackDomain = callbackDomain ? callbackDomain : Ptr(new HttpRequestCallbackDomain); + lifecycle->timeoutController = timeoutController ? timeoutController : CreateHttpRequestTimeoutController(); + connection->InstallCallback(this); + } + + HttpRequestConnection::~HttpRequestConnection() + { + StopConnection(lifecycle); + } + + void HttpRequestConnection::RetainUntilStopped(Ptr retainedAdapter, const Func& drainedCallback) + { + CHECK_ERROR(retainedAdapter.Obj() == this, L"HttpRequestConnection::RetainUntilStopped requires this connection as its retained adapter."); + CHECK_ERROR(drainedCallback, L"HttpRequestConnection::RetainUntilStopped requires a drained callback."); + bool canRetain = false; + CS_LOCK(lifecycle->lockState) + { + if (!lifecycle->retainedAdapter && !lifecycle->drainedCallback && !lifecycle->stopStarted) + { + lifecycle->retainedAdapter = retainedAdapter; + lifecycle->drainedCallback = drainedCallback; + canRetain = true; + } + } + CHECK_ERROR(canRetain, L"HttpRequestConnection::RetainUntilStopped can only be called once before stopping."); + } + + void HttpRequestConnection::StopWithRetainedAdapter(Ptr retainedAdapter) + { + StopConnection(lifecycle, retainedAdapter); + } + + bool HttpRequestConnection::IsInsideCallback() + { + return CurrentCallbackDepth(lifecycle) > 0 || CurrentSocketCallbackDepth(lifecycle) > 0; + } + + void HttpRequestConnection::InstallCallback(IHttpRequestCallback* value) + { + auto state = lifecycle; + if (!value) + { + auto callbackDepth = CurrentCallbackDepth(state); + bool uninstallOwner = false; + CS_LOCK(state->lockState) + { + uninstallOwner = state->callback != nullptr; + state->callback = nullptr; + while ((callbackDepth == 0 || uninstallOwner) && state->activeCallbacks > callbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + } + return; + } + + bool canInstall = false; + CS_LOCK(state->lockState) + { + if (!state->callback && !state->callbackInstalling && !state->stopStarted && !state->terminal) + { + state->callback = value; + state->callbackInstalling = true; + state->activeCallbacks++; + canInstall = true; + } + } + CHECK_ERROR(canInstall, L"HttpRequestConnection::InstallCallback cannot replace a callback or install one on a stopped connection."); + + CallbackFrame frame(state); + try + { + value->OnInstalled(this); + } + catch (...) + { + CS_LOCK(state->lockState) + { + if (state->callback == value) state->callback = nullptr; + state->callbackInstalling = false; + state->cvState.WakeAllPendings(); + } + throw; + } + CS_LOCK(state->lockState) + { + state->callbackInstalling = false; + state->cvState.WakeAllPendings(); + } + } + + void HttpRequestConnection::BeginReadingLoopUnsafe() + { + auto state = lifecycle; + IAsyncSocketConnection* connection = nullptr; + Ptr callRetainer; + bool canBegin = false; + CS_LOCK(state->lockState) + { + if (!state->readingStarted && !state->stopStarted && !state->terminal && state->socketConnection) + { + state->readingStarted = true; + connection = state->socketConnection; + callRetainer = state->retainedAdapter; + state->activeSocketCalls++; + canBegin = true; + } + } + CHECK_ERROR(canBegin, L"HttpRequestConnection::BeginReadingLoopUnsafe can only be called once on an active connection."); + try + { + connection->BeginReadingLoopUnsafe(); + } + catch (...) + { + FinishSocketCall(state); + throw; + } + FinishSocketCall(state); + } + + void HttpRequestConnection::SendRequest(Ptr request, vint responseTimeout) + { + auto state = lifecycle; + CHECK_ERROR(state->direction == HttpRequestConnectionDirection::Client, L"HttpRequestConnection::SendRequest is only available on a client connection."); + CHECK_ERROR(request, L"HttpRequestConnection::SendRequest requires a request."); + bool requestClose = false; + auto buffer = SerializeHttpMessage(request.Obj(), nullptr, L"", requestClose); + IAsyncSocketConnection* connection = nullptr; + Ptr callRetainer; + bool canSend = false; + bool deferSend = false; + CS_LOCK(state->lockState) + { + auto commonStateAvailable = !state->stopStarted && !state->terminal && !state->peerDisconnected + && !state->exchangeActive && !state->writePending && !state->deferredRequestWrite + && !state->responseFinalizing && !state->fatalAfterResponse && !state->closeAfterExchange && state->socketConnection; + if (commonStateAvailable && state->responseDelivering) + { + state->deferredRequestWrite = buffer; + state->deferredRequestClose = requestClose; + state->deferredRequestMethod = request->method; + state->deferredResponseTimeout = responseTimeout; + deferSend = true; + canSend = true; + } + else if (commonStateAvailable && !state->responseDelivering) + { + state->exchangeActive = true; + state->closeAfterExchange = requestClose; + state->activeRequestMethod = request->method; + state->activeResponseTimeout = responseTimeout; + state->pendingWrite = buffer; + state->writePending = true; + connection = state->socketConnection; + callRetainer = state->retainedAdapter; + state->activeSocketCalls++; + canSend = true; + } + } + CHECK_ERROR(canSend, L"HttpRequestConnection::SendRequest requires an idle active client connection."); + if (deferSend) return; + SubmitWrite(state, connection, buffer); + ProcessBufferedInput(state); + } + + void HttpRequestConnection::SendResponse(Ptr response) + { + auto state = lifecycle; + CHECK_ERROR(state->direction == HttpRequestConnectionDirection::Server, L"HttpRequestConnection::SendResponse is only available on a server connection."); + CHECK_ERROR(response, L"HttpRequestConnection::SendResponse requires a response."); + WString responseToMethod; + CS_LOCK(state->lockState) + { + responseToMethod = state->activeRequestMethod; + } + bool responseClose = false; + auto buffer = SerializeHttpMessage(nullptr, response.Obj(), responseToMethod, responseClose); + IAsyncSocketConnection* connection = nullptr; + Ptr callRetainer; + bool canSend = false; + CS_LOCK(state->lockState) + { + if (!state->stopStarted && !state->terminal && state->awaitingResponse && !state->writePending && state->socketConnection) + { + state->closeAfterExchange |= responseClose; + state->pendingWrite = buffer; + state->writePending = true; + connection = state->socketConnection; + callRetainer = state->retainedAdapter; + state->activeSocketCalls++; + canSend = true; + } + } + CHECK_ERROR(canSend, L"HttpRequestConnection::SendResponse requires one delivered request without a response in progress."); + SubmitWrite(state, connection, buffer); + } + + void HttpRequestConnection::Stop() + { + StopConnection(lifecycle); + } + + void HttpRequestConnection::OnRead(const vuint8_t* buffer, vint size) + { + auto state = lifecycle; + SocketCallbackFrame frame(state); + if (!buffer || size <= 0) return; + bool tooLarge = false; + bool requestTooLarge = false; + bool unsolicited = false; + CS_LOCK(state->lockState) + { + if (state->stopStarted || state->terminal || state->parserFailed || state->peerDisconnected) return; + if (state->direction == HttpRequestConnectionDirection::Client && state->heldResponse) + { + state->fatalAfterResponse = true; + } + else if (state->direction == HttpRequestConnectionDirection::Client && (state->responseDelivering || state->responseFinalizing) && !state->exchangeActive) + { + state->fatalAfterResponse = true; + } + else if (state->direction == HttpRequestConnectionDirection::Client && !state->exchangeActive) + { + state->parserFailed = true; + unsolicited = true; + } + else if (size > HttpWireMessageSizeLimit - state->receiveBuffer.Count()) + { + if (state->direction == HttpRequestConnectionDirection::Server) + { + state->parserFailed = true; + state->awaitingResponse = true; + state->closeAfterExchange = true; + state->receiveBuffer.Clear(); + requestTooLarge = true; + } + else + { + state->parserFailed = true; + tooLarge = true; + } + } + else + { + for (vint i = 0; i < size; i++) state->receiveBuffer.Add(buffer[i]); + } + } + if (unsolicited) + { + ReportFatalError(state, L"The HTTP client received bytes without an active request exchange."); + return; + } + if (tooLarge) + { + ReportFatalError(state, L"The HTTP peer exceeded the configured wire-message size limit."); + return; + } + if (requestTooLarge) + { + ReportRequestFailure(state, HttpRequestFailure::PayloadTooLarge, false, true); + return; + } + ProcessBufferedInput(state); + } + + void HttpRequestConnection::OnWriteCompleted(Ptr buffer) + { + auto state = lifecycle; + SocketCallbackFrame frame(state); + bool mismatched = false; + bool ignored = false; + CS_LOCK(state->lockState) + { + if (!state->writePending || state->pendingWrite.Obj() != buffer.Obj()) + { + if (state->stopStarted || state->terminal || state->peerDisconnected) + { + ignored = true; + } + else + { + mismatched = true; + } + } + else + { + state->pendingWrite = nullptr; + } + } + if (ignored) return; + CHECK_ERROR(!mismatched, L"HttpRequestConnection received a completion for an unexpected async socket buffer."); + + InvokeHttpCallback(state, false, [](IHttpRequestCallback* installed) + { + installed->OnWriteCompleted(); + }); + + Ptr response; + bool serverSide = false; + bool closeAfterDelivery = false; + bool armTimeout = false; + vint responseTimeout = 0; + CS_LOCK(state->lockState) + { + if (state->stopStarted || state->terminal) return; + state->writePending = false; + serverSide = state->direction == HttpRequestConnectionDirection::Server; + if (serverSide) + { + state->awaitingResponse = false; + state->activeRequestMethod = L""; + closeAfterDelivery = state->closeAfterExchange; + } + else if (state->heldResponse) + { + response = std::move(state->heldResponse); + state->exchangeActive = false; + state->responseDelivering = true; + closeAfterDelivery = state->closeAfterExchange; + } + else if (!state->peerDisconnected && !state->timeoutArmed && state->activeResponseTimeout > 0) + { + state->timeoutArmed = true; + responseTimeout = state->activeResponseTimeout; + armTimeout = true; + } + } + + if (response) + { + DeliverResponse(state, response, closeAfterDelivery); + } + else if (closeAfterDelivery) + { + StopConnection(state); + } + else if (serverSide) + { + ProcessBufferedInput(state); + } + else if (armTimeout) + { + InstallTimeout(state, responseTimeout, L"The HTTP peer timed out before sending a response header."); + } + } + + void HttpRequestConnection::OnError(const WString& error, bool fatal) + { + auto state = lifecycle; + SocketCallbackFrame frame(state); + if (fatal) + { + ReportFatalError(state, error); + } + else + { + InvokeHttpCallback(state, false, [&](IHttpRequestCallback* installed) + { + installed->OnError(error, false); + }); + } + } + + void HttpRequestConnection::OnConnected() + { + auto state = lifecycle; + SocketCallbackFrame frame(state); + InvokeHttpCallback(state, false, [](IHttpRequestCallback* installed) + { + installed->OnConnected(); + }); + } + + void HttpRequestConnection::OnDisconnected() + { + auto state = lifecycle; + SocketCallbackFrame frame(state); + IAsyncSocketConnection* connection = nullptr; + bool incomplete = false; + bool surplusAfterHeldResponse = false; + bool deferDisconnected = false; + bool cancelTimeout = false; + CS_LOCK(state->lockState) + { + state->peerDisconnected = true; + state->timeoutArmed = false; + cancelTimeout = !state->stopStarted; + connection = state->socketConnection; + if (!state->terminal && !state->stopStarted && state->direction == HttpRequestConnectionDirection::Client && state->heldResponse) + { + surplusAfterHeldResponse = state->fatalAfterResponse; + incomplete = !surplusAfterHeldResponse; + state->terminal = true; + state->pendingWrite = nullptr; + state->writePending = false; + } + else if (state->direction == HttpRequestConnectionDirection::Client && (state->responseDelivering || state->responseFinalizing)) + { + deferDisconnected = true; + } + else + { + incomplete = !state->terminal && !state->stopStarted && ( + state->direction == HttpRequestConnectionDirection::Client + ? state->exchangeActive + : !state->awaitingResponse && state->receiveBuffer.Count() > 0 + ); + state->terminal = true; + } + } + if (connection) + { + connection->InstallCallback(nullptr); + } + CS_LOCK(state->lockState) + { + if (state->socketConnection == connection) state->socketConnection = nullptr; + state->cvState.WakeAllPendings(); + } + if (cancelTimeout) + { + state->timeoutController->CancelAndWait(); + } + if (deferDisconnected) + { + return; + } + if (surplusAfterHeldResponse) + { + InvokeHttpCallback(state, true, [](IHttpRequestCallback* installed) + { + installed->OnError(L"The HTTP client received an unsolicited response after its exchange completed.", true); + }); + } + else if (incomplete) + { + InvokeHttpCallback(state, true, [](IHttpRequestCallback* installed) + { + installed->OnError(L"The HTTP peer disconnected while a message was incomplete.", true); + }); + } + NotifyDisconnected(state); + StopConnection(state); + } + + void HttpRequestConnection::OnInstalled(IAsyncSocketConnection* connection) + { + auto state = lifecycle; + SocketCallbackFrame frame(state); + CHECK_ERROR(connection == state->socketConnection, L"HttpRequestConnection was installed on an unexpected async socket connection."); + } +} + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPREQUESTCLIENT.CPP +***********************************************************************/ + +namespace vl::inter_process::async_tcp_socket +{ +/*********************************************************************** +HttpRequestClient::Impl +***********************************************************************/ + + class HttpRequestClient::Impl : public Object + { + private: + class DeferredClientRelease : public Object + { + private: + CriticalSection lockState; + Ptr selfReference; + Ptr retainedClient; + + public: + DeferredClientRelease(Ptr client) + : retainedClient(client) + { + } + + void InitializeSelf(Ptr self) + { + CS_LOCK(lockState) + { + selfReference = self; + } + } + + void Run() + { + Ptr client; + CS_LOCK(lockState) + { + client = retainedClient; + } + try + { + client->GetConnection()->Stop(); + } + catch (...) + { + } + CS_LOCK(lockState) + { + retainedClient = nullptr; + selfReference = nullptr; + } + } + }; + + Ptr client; + Ptr connection; + + static void QueueDeferredRelease(Ptr deferredRelease) + { + auto finalize = Func([deferredRelease]() + { + deferredRelease->Run(); + }); + if (!ThreadPoolLite::Queue(finalize)) + { + // The holder self-reference keeps the native client alive if the + // operating system cannot start either asynchronous cleanup path. + if (!Thread::CreateAndStart(finalize)) return; + } + } + + public: + Impl(Ptr _client) + : client(_client) + { +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::HttpRequestClient::HttpRequestClient(Ptr)#" + CHECK_ERROR(client, ERROR_MESSAGE_PREFIX L"Requires a client."); + connection = Ptr(new HttpRequestConnection( + client->GetConnection(), + HttpRequestConnectionDirection::Client, + nullptr, + nullptr, + true + )); +#undef ERROR_MESSAGE_PREFIX + } + + ~Impl() + { + auto deferFinalization = connection->IsInsideCallback(); + Ptr deferredRelease; + if (deferFinalization) + { + deferredRelease = Ptr(new DeferredClientRelease(client)); + deferredRelease->InitializeSelf(deferredRelease); + } + connection->StopWithRetainedAdapter(connection); + if (deferFinalization) + { + QueueDeferredRelease(deferredRelease); + } + } + + IHttpRequestConnection* GetConnection() + { + return connection.Obj(); + } + + void WaitForServer() + { + client->WaitForServer(); + } + + ClientStatus GetStatus() + { + return client->GetStatus(); + } + }; + +/*********************************************************************** +HttpRequestClient +***********************************************************************/ + + HttpRequestClient::HttpRequestClient(Ptr client) + : impl(Ptr(new Impl(client))) + { + } + + HttpRequestClient::~HttpRequestClient() + { + } + + IHttpRequestConnection* HttpRequestClient::GetConnection() + { + return impl->GetConnection(); + } + + void HttpRequestClient::WaitForServer() + { + impl->WaitForServer(); + } + + ClientStatus HttpRequestClient::GetStatus() + { + return impl->GetStatus(); + } +} + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPREQUESTSERVER.CPP +***********************************************************************/ + +namespace vl::inter_process::async_tcp_socket +{ +/*********************************************************************** +HttpRequestServer::Impl +***********************************************************************/ + + class HttpRequestServer::Impl : public Object + { + private: + class Lifecycle : public Object + { + public: + HttpRequestServer* owner = nullptr; + Ptr server; + Ptr callbackDomain = Ptr(new HttpRequestCallbackDomain); + Func()> + timeoutControllerFactory; + + // covers owner, connections, and all lifecycle flags below + CriticalSection lockState; + ConditionVariable cvState; + collections::List> + connections; + bool startCalled = false; + bool stopStarted = false; + bool unexpectedStopNotified = false; + bool stopFinished = false; + bool nativeStopCalling = false; + bool destroyStarted = false; + bool destroyAdaptersRetained = false; + + Lifecycle( + HttpRequestServer* _owner, + Ptr _server, + const Func()>& _timeoutControllerFactory + ) + : owner(_owner) + , server(_server) + , timeoutControllerFactory(_timeoutControllerFactory) + { + } + }; + + class SocketServerCallback + : public Object + , public virtual IAsyncSocketServerCallback + { + private: + Ptr lifecycle; + CriticalSection lockSelf; + Ptr selfReference; + + public: + SocketServerCallback(Ptr _lifecycle) + : lifecycle(_lifecycle) + { + } + + void InitializeSelf(Ptr self) + { + CS_LOCK(lockSelf) + { + selfReference = self; + } + } + + void ReleaseSelfReference() + { + CS_LOCK(lockSelf) + { + selfReference = nullptr; + } + } + + WaitForClientResult OnClientConnected(IAsyncSocketConnection* connection) override + { + Ptr self; + CS_LOCK(lockSelf) + { + self = selfReference; + } + return self + ? Impl::OnSocketClientConnected(lifecycle, connection) + : WaitForClientResult::Reject; + } + + void OnServerStopped() override + { + Ptr self; + CS_LOCK(lockSelf) + { + self = selfReference; + } + if (self) + { + Impl::OnSocketServerStopped(lifecycle); + } + } + }; + + Ptr lifecycle; + Ptr callback; + + static void StopConnections(Ptr state, bool retainAdapters = false) + { + collections::List> stoppingConnections; + CS_LOCK(state->lockState) + { + for (auto connection : state->connections) + { + stoppingConnections.Add(connection); + } + } + for (auto connection : stoppingConnections) + { + if (retainAdapters) + { + connection->StopWithRetainedAdapter(connection); + } + else + { + connection->Stop(); + } + } + } + + static void FinalizeDestroyedOwner(Ptr state) + { + CS_LOCK(state->lockState) + { + if (state->destroyStarted && state->destroyAdaptersRetained) + { + state->owner = nullptr; + state->connections.Clear(); + } + } + } + + static void QueueDeferredStop(Ptr state, Ptr retainedCallback) + { + auto finalize = Func([state, retainedCallback]() + { + state->lockState.Enter(); + while (!state->stopFinished || state->nativeStopCalling) + { + state->cvState.SleepWith(state->lockState); + } + state->nativeStopCalling = true; + state->lockState.Leave(); + + try + { + state->server->Stop(); + } + catch (...) + { + } + try + { + StopConnections(state); + } + catch (...) + { + } + state->callbackDomain->WaitForCallbacks(0); + FinalizeDestroyedOwner(state); + + CS_LOCK(state->lockState) + { + state->nativeStopCalling = false; + state->cvState.WakeAllPendings(); + } + retainedCallback->ReleaseSelfReference(); + }); + if (!ThreadPoolLite::Queue(finalize)) + { + // The callback self-reference keeps all deferred state alive if the + // operating system cannot start either asynchronous cleanup path. + if (!Thread::CreateAndStart(finalize)) return; + } + } + + static WaitForClientResult OnSocketClientConnected(Ptr state, IAsyncSocketConnection* connection) + { + HttpRequestCallbackDomain::CallbackFrame callbackFrame(state->callbackDomain); + HttpRequestServer* owner = nullptr; + CS_LOCK(state->lockState) + { + if (!state->stopStarted) + { + owner = state->owner; + } + } + if (!owner) + { + return WaitForClientResult::Reject; + } + + Ptr timeoutController; + try + { + if (state->timeoutControllerFactory) + { + timeoutController = state->timeoutControllerFactory(); + CHECK_ERROR(timeoutController, L"The HTTP request timeout controller factory returned null."); + } + } + catch (...) + { + return WaitForClientResult::Reject; + } + + auto httpConnection = Ptr(new HttpRequestConnection( + connection, + HttpRequestConnectionDirection::Server, + state->callbackDomain, + timeoutController + )); + auto connectionObject = httpConnection.Obj(); + httpConnection->RetainUntilStopped(httpConnection, Func([state, connectionObject]() + { + CS_LOCK(state->lockState) + { + state->connections.Remove(connectionObject); + state->cvState.WakeAllPendings(); + } + })); + bool invoke = false; + CS_LOCK(state->lockState) + { + if (!state->stopStarted && state->owner == owner) + { + state->connections.Add(httpConnection); + invoke = true; + } + } + if (!invoke) + { + httpConnection->StopWithRetainedAdapter(httpConnection); + return WaitForClientResult::Reject; + } + + auto result = WaitForClientResult::Reject; + try + { + result = owner->OnClientConnected(httpConnection.Obj()); + } + catch (...) + { + result = WaitForClientResult::Reject; + } + + bool accepted = false; + CS_LOCK(state->lockState) + { + accepted = result == WaitForClientResult::Accept && !state->stopStarted; + if (!accepted) + { + state->connections.Remove(httpConnection.Obj()); + } + } + if (!accepted) + { + httpConnection->StopWithRetainedAdapter(httpConnection); + } + return accepted ? WaitForClientResult::Accept : WaitForClientResult::Reject; + } + + static void OnSocketServerStopped(Ptr state) + { + HttpRequestCallbackDomain::CallbackFrame callbackFrame(state->callbackDomain); + HttpRequestServer* owner = nullptr; + CS_LOCK(state->lockState) + { + if (!state->stopStarted && !state->unexpectedStopNotified) + { + state->unexpectedStopNotified = true; + owner = state->owner; + } + } + if (owner) + { + try + { + owner->OnServerStopped(); + } + catch (...) + { + } + } + } + + public: + Impl( + HttpRequestServer* owner, + Ptr server, + const Func()>& timeoutControllerFactory + ) + { +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::HttpRequestServer::HttpRequestServer(Ptr)#" + CHECK_ERROR(server, ERROR_MESSAGE_PREFIX L"Requires a server."); + lifecycle = Ptr(new Lifecycle(owner, server, timeoutControllerFactory)); + callback = Ptr(new SocketServerCallback(lifecycle)); +#undef ERROR_MESSAGE_PREFIX + } + + ~Impl() + { + Destroy(); + } + + void Start() + { +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::HttpRequestServer::Start()#" + auto state = lifecycle; + bool canStart = false; + CS_LOCK(state->lockState) + { + if (!state->startCalled && !state->stopStarted) + { + state->startCalled = true; + canStart = true; + } + } + CHECK_ERROR(canStart, ERROR_MESSAGE_PREFIX L"Can only be called once before stopping."); + + callback->InitializeSelf(callback); + try + { + state->server->Start(callback.Obj()); + } + catch (...) + { + try + { + Stop(); + } + catch (...) + { + } + throw; + } +#undef ERROR_MESSAGE_PREFIX + } + + void Stop() + { + auto state = lifecycle; + auto retainedCallback = callback; + auto callbackDepth = state->callbackDomain->CurrentCallbackDepth(); + bool firstStop = false; + bool nestedFollower = false; + bool deferFinalization = false; + state->lockState.Enter(); + if (!state->stopStarted) + { + state->stopStarted = true; + state->nativeStopCalling = true; + firstStop = true; + } + else if (callbackDepth > 0) + { + nestedFollower = true; + } + else + { + while (!state->stopFinished || state->nativeStopCalling) + { + state->cvState.SleepWith(state->lockState); + } + state->nativeStopCalling = true; + } + state->lockState.Leave(); + if (nestedFollower) + { + StopConnections(state); + return; + } + + try + { + state->server->Stop(); + StopConnections(state); + state->callbackDomain->WaitForCallbacks(firstStop ? callbackDepth : 0); + deferFinalization = firstStop && callbackDepth > 0; + if (!deferFinalization) + { + FinalizeDestroyedOwner(state); + retainedCallback->ReleaseSelfReference(); + } + } + catch (...) + { + if (!firstStop || callbackDepth == 0) + { + retainedCallback->ReleaseSelfReference(); + } + CS_LOCK(state->lockState) + { + state->nativeStopCalling = false; + if (firstStop) + { + state->stopFinished = true; + } + state->cvState.WakeAllPendings(); + } + throw; + } + + CS_LOCK(state->lockState) + { + state->nativeStopCalling = false; + if (firstStop) + { + state->stopFinished = true; + } + state->cvState.WakeAllPendings(); + } + if (deferFinalization) + { + QueueDeferredStop(state, retainedCallback); + } + } + + void Destroy() + { + auto state = lifecycle; + bool execute = false; + CS_LOCK(state->lockState) + { + if (!state->destroyStarted) + { + state->destroyStarted = true; + execute = true; + } + } + if (!execute) + { + return; + } + + Stop(); + StopConnections(state, true); + CS_LOCK(state->lockState) + { + state->destroyAdaptersRetained = true; + } + if (state->callbackDomain->CurrentCallbackDepth() == 0) + { + state->callbackDomain->WaitForCallbacks(0); + FinalizeDestroyedOwner(state); + } + } + + bool IsStopped() + { + return lifecycle->server->IsStopped(); + } + }; + +/*********************************************************************** +HttpRequestServer +***********************************************************************/ + + HttpRequestServer::HttpRequestServer(Ptr server) + : HttpRequestServer(server, {}) + { + } + + HttpRequestServer::HttpRequestServer( + Ptr server, + const Func()>& timeoutControllerFactory + ) + : impl(Ptr(new Impl(this, server, timeoutControllerFactory))) + { + } + + HttpRequestServer::~HttpRequestServer() + { + impl->Destroy(); + } + + WaitForClientResult HttpRequestServer::OnClientConnected(IHttpRequestConnection*) + { + return WaitForClientResult::Accept; + } + + void HttpRequestServer::OnServerStopped() + { + Stop(); + } + + void HttpRequestServer::Start() + { + impl->Start(); + } + + void HttpRequestServer::Stop() + { + impl->Stop(); + } + + bool HttpRequestServer::IsStopped() + { + return impl->IsStopped(); + } +} + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPSERVER.CPP +***********************************************************************/ + +#include + +namespace vl::inter_process::async_tcp_socket +{ + using namespace collections; + + namespace + { + constexpr vint GeneratedTokenLength = 36; + + WString ValidateServerUrlPrefix(const WString& urlPrefix) + { +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServer::SocketHttpServer(Ptr, const WString&)#" + auto normalizedUrlPrefix = urlPrefix; + while (normalizedUrlPrefix.Length() > 0 && normalizedUrlPrefix[normalizedUrlPrefix.Length() - 1] == L'/') + { + normalizedUrlPrefix = normalizedUrlPrefix.Left(normalizedUrlPrefix.Length() - 1); + } + CHECK_ERROR(ValidateHttpNetworkProtocolBaseUrl(normalizedUrlPrefix), ERROR_MESSAGE_PREFIX L"urlPrefix must be empty or a legal ASCII origin-form path prefix."); + CHECK_ERROR(ValidateHttpRequestLine(L"GET", normalizedUrlPrefix + HttpServerUrl_Connect) == HttpRequestLineValidationResult::Succeeded, ERROR_MESSAGE_PREFIX L"The /Connect target exceeds the HTTP request-line limit."); + const WString tokenPlaceholder = L"000000000000000000000000000000000000"; + CHECK_ERROR(ValidateHttpRequestLine(L"POST", normalizedUrlPrefix + HttpServerUrl_Request + L"/" + tokenPlaceholder) == HttpRequestLineValidationResult::Succeeded, ERROR_MESSAGE_PREFIX L"The /Request target plus a generated token exceeds the HTTP request-line limit."); + CHECK_ERROR(ValidateHttpRequestLine(L"POST", normalizedUrlPrefix + HttpServerUrl_Response + L"/" + tokenPlaceholder) == HttpRequestLineValidationResult::Succeeded, ERROR_MESSAGE_PREFIX L"The /Response target plus a generated token exceeds the HTTP request-line limit."); + return normalizedUrlPrefix; +#undef ERROR_MESSAGE_PREFIX + } + + bool HasEmptyBody(Ptr request) + { + if (!request || request->body.chunks.Count() != 0 || request->body.trailers.Count() != 0) return false; + HttpFraming framing; + if (AnalyzeHttpFraming(request->headers, framing) != HttpFramingAnalysisResult::Succeeded) return false; + if (framing.kind == HttpFramingKind::None) return true; + return + framing.kind == HttpFramingKind::ContentLength && + framing.contentLength == 0 && + framing.contentLengthFieldCount == 1 && + framing.contentLengthValueCount == 1 && + framing.contentLengthValuesPlainDecimal; + } + + bool DecodeSubmittedMessage(Ptr context, Ptr request, WString& message) + { + if (!request || request->body.trailers.Count() != 0) return false; + HttpFraming framing; + if (AnalyzeHttpFraming(request->headers, framing) != HttpFramingAnalysisResult::Succeeded) return false; + if ( + framing.kind != HttpFramingKind::ContentLength || + framing.contentLength == 0 || + framing.contentLengthFieldCount != 1 || + framing.contentLengthValueCount != 1 || + !framing.contentLengthValuesPlainDecimal || + CountHttpFields(request->headers, L"content-type") != 1 + ) + { + return false; + } + auto contentType = FindHttpField(request->headers, L"content-type"); + return + HttpFieldValueEqualsAscii(contentType->value, HttpNetworkProtocolContentType) && + context->TryGetBodyUtf8(message) && + IsValidHttpNetworkProtocolMessage(message); + } + + bool ExtractToken(const WString& path, const wchar_t* route, WString& token) + { + auto prefix = WString::Unmanaged(route) + L"/"; + if (path.Length() <= prefix.Length() || path.Left(prefix.Length()) != prefix) return false; + token = path.Right(path.Length() - prefix.Length()); + return token.Length() > 0; + } + + WString GenerateToken() + { + vuint8_t bytes[16]; + std::random_device random; + for (vint i = 0; i < 16; i++) bytes[i] = (vuint8_t)random(); + bytes[6] = (bytes[6] & 0x0F) | 0x40; + bytes[8] = (bytes[8] & 0x3F) | 0x80; + + const wchar_t* hex = L"0123456789abcdef"; + wchar_t text[GeneratedTokenLength]; + vint writing = 0; + for (vint i = 0; i < 16; i++) + { + if (i == 4 || i == 6 || i == 8 || i == 10) text[writing++] = L'-'; + text[writing++] = hex[bytes[i] >> 4]; + text[writing++] = hex[bytes[i] & 0x0F]; + } + return WString::CopyFrom(text, GeneratedTokenLength); + } + + BEGIN_GLOBAL_STORAGE_CLASS(SocketHttpServerTestHooks) + SpinLock lock; + Func claimed; + Func completed; + Func registered; + INITIALIZE_GLOBAL_STORAGE_CLASS + FINALIZE_GLOBAL_STORAGE_CLASS + SPIN_LOCK(lock) + { + claimed = {}; + completed = {}; + registered = {}; + } + END_GLOBAL_STORAGE_CLASS(SocketHttpServerTestHooks) + + void InvokePollClaimed(const WString& token) + { + Func callback; + auto& hooks = GetSocketHttpServerTestHooks(); + SPIN_LOCK(hooks.lock) { callback = hooks.claimed; } + if (callback) try { callback(token); } catch (...) {} + } + + void InvokePollCompleted(const WString& token, bool succeeded) + { + Func callback; + auto& hooks = GetSocketHttpServerTestHooks(); + SPIN_LOCK(hooks.lock) { callback = hooks.completed; } + if (callback) try { callback(token, succeeded); } catch (...) {} + } + + void InvokePollRegistered(const WString& token) + { + Func callback; + auto& hooks = GetSocketHttpServerTestHooks(); + SPIN_LOCK(hooks.lock) { callback = hooks.registered; } + if (callback) try { callback(token); } catch (...) {} + } + + class SocketHttpServerConnection; + class SocketHttpServerLifecycle; + class SocketHttpServerOutboundMessage : public Object + { + public: + Array body; + }; + + class SocketHttpServerConnectionLifecycle : public Object + { + public: + CriticalSection lockState; + ConditionVariable cvState; + SocketHttpServerConnection* owner = nullptr; + Ptr server; + WString token; + INetworkProtocolCallback* callback = nullptr; + List queuedInbound; + List> + queuedOutbound; + List> + queuedPollRegistrations; + Ptr pendingPoll; + Ptr inFlightPoll; + Ptr + inFlightMessage; + vint activeCallbacks = 0; + bool callbackInstalling = false; + bool pollRegistrationProcessing = false; + bool accepted = false; + bool stopStarted = false; + bool stopCancellationFinished = false; + bool stopFinished = false; + bool stopAssistProcessing = false; + bool disconnectDelivering = false; + bool disconnectFinished = false; + }; + + class SocketHttpServerConnection + : public Object + , public virtual INetworkProtocolConnection + { + struct CallbackFrame + { + Ptr state; + CallbackFrame* previous = nullptr; + + CallbackFrame(Ptr _state); + ~CallbackFrame(); + }; + + struct InboundFrame + { + Ptr state; + InboundFrame* previous = nullptr; + List> + generated; + + InboundFrame(Ptr _state); + ~InboundFrame(); + }; + + struct PollWork + { + Ptr context; + Ptr + message; + + operator bool() const { return context != nullptr; } + }; + + static thread_local CallbackFrame* currentCallbackFrame; + static thread_local InboundFrame* currentInboundFrame; + Ptr + lifecycle; + + static vint CurrentCallbackDepth(Ptr state); + static bool ClaimPollUnsafe(Ptr state, PollWork& work); + static void StartPollResponse(Ptr state, PollWork work); + static void FinishPollResponse(Ptr state, Ptr context, bool succeeded); + static void ProcessPollRegistrations(Ptr state); + void StopCore(bool removeFromServer, bool waitForPoll); + + public: + SocketHttpServerConnection(Ptr server, const WString& token); + + void DetachServer(SocketHttpServerLifecycle* server); + bool MarkAccepted(); + bool IsAccepted(); + bool HasCurrentCallback(); + WString GetToken(); + WaitForClientResult InvokeClientConnected(SocketHttpServer* server); + bool RegisterPoll(Ptr context); + bool DispatchInbound(const WString& message, Ptr& response); + void StopFromServer(); + void WaitForPollCompletion(); + + void InstallCallback(INetworkProtocolCallback* callback) override; + void BeginReadingLoopUnsafe() override; + void SendString(const WString& str) override; + void Stop() override; + }; + + class SocketHttpServerLifecycle : public Object + { + public: + SocketHttpServer* owner = nullptr; + CriticalSection lockState; + ConditionVariable cvState; + Dictionary> + connections; + List> + stoppingConnections; + bool startCalled = false; + bool started = false; + bool stopStarted = false; + bool stopProcessing = false; + bool stopProcessed = false; + bool stopAssistProcessing = false; + + SocketHttpServerLifecycle(SocketHttpServer* _owner) + : owner(_owner) + { + } + + void PrepareStart() + { + CS_LOCK(lockState) + { + CHECK_ERROR(!startCalled && !stopStarted, L"SocketHttpServer::Start can only be called once before stopping."); + startCalled = true; + started = true; + } + } + + Ptr CreateConnection(Ptr retainedSelf) + { + while (true) + { + auto token = GenerateToken(); + auto connection = Ptr(new SocketHttpServerConnection(retainedSelf, token)); + CS_LOCK(lockState) + { + if (!started || stopStarted) return nullptr; + if (!connections.Keys().Contains(token)) + { + connections.Add(token, connection); + return connection; + } + } + } + } + + Ptr FindConnection(const WString& token) + { + CS_LOCK(lockState) + { + if (stopStarted) return nullptr; + auto index = connections.Keys().IndexOf(token); + return index == -1 ? nullptr : connections.Values()[index]; + } + return nullptr; + } + + bool TryAccept(const WString& token, SocketHttpServerConnection* connection) + { + CS_LOCK(lockState) + { + if (stopStarted) return false; + auto index = connections.Keys().IndexOf(token); + if (index == -1 || connections.Values()[index].Obj() != connection) return false; + return connection->MarkAccepted(); + } + return false; + } + + bool IsStopped() + { + CS_LOCK(lockState) { return stopStarted; } + return true; + } + + void RemoveConnection(const WString& token, SocketHttpServerConnection* connection) + { + bool removed = false; + bool retained = false; + CS_LOCK(lockState) + { + auto index = connections.Keys().IndexOf(token); + if (index != -1 && connections.Values()[index].Obj() == connection) + { + if (connection->IsAccepted()) + { + stoppingConnections.Add(connections.Values()[index]); + retained = true; + } + connections.Remove(token); + removed = true; + } + } + if (removed && !retained) connection->DetachServer(this); + } + + void PrepareStop(List>& stopping) + { + CS_LOCK(lockState) + { + if (!stopStarted) + { + stopStarted = true; + started = false; + for (auto connection : connections.Values()) stoppingConnections.Add(connection); + connections.Clear(); + } + for (auto connection : stoppingConnections) stopping.Add(connection); + } + } + + void PrepareStopProcessing(bool callbackNested, bool inheritsAssist, bool& execute, bool& assist) + { + CS_LOCK(lockState) + { + if (!stopProcessing && !stopProcessed) + { + stopProcessing = true; + execute = true; + if (callbackNested) + { + stopAssistProcessing = true; + assist = true; + } + } + else if (callbackNested && !inheritsAssist && !stopProcessed && !stopAssistProcessing) + { + stopAssistProcessing = true; + assist = true; + } + } + } + + void FinishStop(bool ownsAssist) + { + lockState.Enter(); + if (ownsAssist) + { + stopAssistProcessing = false; + } + else + { + while (stopAssistProcessing) cvState.SleepWith(lockState); + } + stopProcessing = false; + stopProcessed = true; + cvState.WakeAllPendings(); + lockState.Leave(); + } + + void FinishStopAssist() + { + CS_LOCK(lockState) + { + stopAssistProcessing = false; + cvState.WakeAllPendings(); + } + } + + void WaitForStop() + { + CS_LOCK(lockState) + { + while (!stopProcessed) cvState.SleepWith(lockState); + } + } + + Ptr ReleaseStoppedConnection(SocketHttpServerConnection* connection) + { + Ptr releasing; + CS_LOCK(lockState) + { + for (vint i = 0; i < stoppingConnections.Count(); i++) + { + if (stoppingConnections[i].Obj() == connection) + { + releasing = stoppingConnections[i]; + stoppingConnections.RemoveAt(i); + break; + } + } + } + if (releasing) connection->DetachServer(this); + return releasing; + } + }; + + struct SocketHttpServerStopFrame + { + SocketHttpServerLifecycle* lifecycle = nullptr; + SocketHttpServerStopFrame* previous = nullptr; + bool ownsAssist = false; + }; + + thread_local SocketHttpServerStopFrame* currentSocketHttpServerStopFrame = nullptr; + + SocketHttpServerStopFrame* FindSocketHttpServerStopFrame(SocketHttpServerLifecycle* lifecycle) + { + for (auto frame = currentSocketHttpServerStopFrame; frame; frame = frame->previous) + { + if (frame->lifecycle == lifecycle) return frame; + } + return nullptr; + } + + struct SocketHttpServerStopScope + { + SocketHttpServerStopFrame frame; + + SocketHttpServerStopScope(SocketHttpServerLifecycle* lifecycle, bool ownsAssist) + { + frame.lifecycle = lifecycle; + frame.previous = currentSocketHttpServerStopFrame; + frame.ownsAssist = ownsAssist; + currentSocketHttpServerStopFrame = &frame; + } + + ~SocketHttpServerStopScope() + { + currentSocketHttpServerStopFrame = frame.previous; + } + }; + + thread_local SocketHttpServerConnection::CallbackFrame* SocketHttpServerConnection::currentCallbackFrame = nullptr; + thread_local SocketHttpServerConnection::InboundFrame* SocketHttpServerConnection::currentInboundFrame = nullptr; + + SocketHttpServerConnection::CallbackFrame::CallbackFrame(Ptr _state) + : state(_state) + , previous(currentCallbackFrame) + { + currentCallbackFrame = this; + } + + SocketHttpServerConnection::CallbackFrame::~CallbackFrame() + { + currentCallbackFrame = previous; + Ptr server; + SocketHttpServerConnection* owner = nullptr; + CS_LOCK(state->lockState) + { + state->activeCallbacks--; + if (state->activeCallbacks == 0 && state->stopFinished && state->server) + { + server = state->server; + owner = state->owner; + } + state->cvState.WakeAllPendings(); + } + Ptr releasing; + if (server && owner) releasing = server->ReleaseStoppedConnection(owner); + } + + SocketHttpServerConnection::InboundFrame::InboundFrame(Ptr _state) + : state(_state) + , previous(currentInboundFrame) + { + currentInboundFrame = this; + } + + SocketHttpServerConnection::InboundFrame::~InboundFrame() + { + currentInboundFrame = previous; + } + + vint SocketHttpServerConnection::CurrentCallbackDepth(Ptr state) + { + vint depth = 0; + for (auto frame = currentCallbackFrame; frame; frame = frame->previous) + { + if (frame->state == state) depth++; + } + return depth; + } + + bool SocketHttpServerConnection::ClaimPollUnsafe(Ptr state, PollWork& work) + { + if ( + state->stopStarted || + !state->accepted || + state->inFlightPoll || + !state->pendingPoll || + state->queuedOutbound.Count() == 0 + ) + { + return false; + } + + state->inFlightPoll = state->pendingPoll; + state->pendingPoll = nullptr; + state->inFlightMessage = state->queuedOutbound[0]; + state->queuedOutbound.RemoveAt(0); + work.context = state->inFlightPoll; + work.message = state->inFlightMessage; + return true; + } + + void SocketHttpServerConnection::StartPollResponse(Ptr state, PollWork work) + { + if (!work) return; + InvokePollClaimed(state->token); + bool submitted = false; + try + { + submitted = work.context->RespondBytes( + 200, + L"OK", + HttpNetworkProtocolContentType, + work.message->body, + Func([state, context = work.context](bool succeeded) + { + FinishPollResponse(state, context, succeeded); + }) + ); + } + catch (...) + { + } + if (!submitted) FinishPollResponse(state, work.context, false); + } + + void SocketHttpServerConnection::FinishPollResponse(Ptr state, Ptr context, bool succeeded) + { + PollWork next; + bool completed = false; + CS_LOCK(state->lockState) + { + if (state->inFlightPoll == context) + { + if (!succeeded && !state->stopStarted) + { + state->queuedOutbound.Insert(0, state->inFlightMessage); + } + state->inFlightPoll = nullptr; + state->inFlightMessage = nullptr; + ClaimPollUnsafe(state, next); + state->cvState.WakeAllPendings(); + completed = true; + } + } + if (!completed) return; + InvokePollCompleted(state->token, succeeded); + StartPollResponse(state, next); + } + + void SocketHttpServerConnection::ProcessPollRegistrations(Ptr state) + { + while (true) + { + Ptr context; + Ptr replaced; + PollWork work; + bool cancel = false; + state->lockState.Enter(); + if (state->queuedPollRegistrations.Count() == 0) + { + auto registered = state->pendingPoll && !state->stopStarted && state->accepted; + state->pollRegistrationProcessing = false; + state->cvState.WakeAllPendings(); + state->lockState.Leave(); + if (registered) InvokePollRegistered(state->token); + return; + } + context = state->queuedPollRegistrations[0]; + state->queuedPollRegistrations.RemoveAt(0); + replaced = state->pendingPoll; + state->pendingPoll = nullptr; + state->lockState.Leave(); + + if (replaced) replaced->Cancel(); + + CS_LOCK(state->lockState) + { + if (state->stopStarted || !state->accepted) + { + cancel = true; + } + else + { + state->pendingPoll = context; + ClaimPollUnsafe(state, work); + } + } + if (cancel) context->Cancel(); + StartPollResponse(state, work); + } + } + + SocketHttpServerConnection::SocketHttpServerConnection(Ptr server, const WString& token) + : lifecycle(Ptr(new SocketHttpServerConnectionLifecycle)) + { + lifecycle->owner = this; + lifecycle->server = server; + lifecycle->token = token; + } + + void SocketHttpServerConnection::DetachServer(SocketHttpServerLifecycle* server) + { + CS_LOCK(lifecycle->lockState) + { + if (lifecycle->server.Obj() == server) lifecycle->server = nullptr; + } + } + + bool SocketHttpServerConnection::MarkAccepted() + { + CS_LOCK(lifecycle->lockState) + { + if (!lifecycle->stopStarted) + { + lifecycle->accepted = true; + return true; + } + } + return false; + } + + bool SocketHttpServerConnection::IsAccepted() + { + CS_LOCK(lifecycle->lockState) { return lifecycle->accepted; } + return false; + } + + bool SocketHttpServerConnection::HasCurrentCallback() + { + return CurrentCallbackDepth(lifecycle) > 0; + } + + WString SocketHttpServerConnection::GetToken() + { + return lifecycle->token; + } + + WaitForClientResult SocketHttpServerConnection::InvokeClientConnected(SocketHttpServer* server) + { + auto state = lifecycle; + bool invoke = false; + CS_LOCK(state->lockState) + { + if (!state->stopStarted) + { + state->activeCallbacks++; + invoke = true; + } + } + if (!invoke) return WaitForClientResult::Reject; + + WaitForClientResult result; + { + CallbackFrame frame(state); + result = server->OnClientConnected(this); + } + if (result != WaitForClientResult::Accept) return WaitForClientResult::Reject; + + Ptr retainedServer; + CS_LOCK(state->lockState) { retainedServer = state->server; } + return retainedServer && retainedServer->TryAccept(state->token, this) + ? WaitForClientResult::Accept + : WaitForClientResult::Reject; + } + + bool SocketHttpServerConnection::RegisterPoll(Ptr context) + { + auto state = lifecycle; + bool process = false; + CS_LOCK(state->lockState) + { + if (state->stopStarted || !state->accepted) return false; + state->queuedPollRegistrations.Add(context); + if (!state->pollRegistrationProcessing) + { + state->pollRegistrationProcessing = true; + process = true; + } + } + if (process) ProcessPollRegistrations(state); + return true; + } + + bool SocketHttpServerConnection::DispatchInbound(const WString& message, Ptr& response) + { + auto state = lifecycle; + INetworkProtocolCallback* installed = nullptr; + PollWork work; + CS_LOCK(state->lockState) + { + if (state->stopStarted || !state->accepted) return false; + if (state->callback && !state->callbackInstalling) + { + installed = state->callback; + state->activeCallbacks++; + } + else + { + state->queuedInbound.Add(message); + if (state->queuedOutbound.Count() > 0) + { + response = state->queuedOutbound[0]; + state->queuedOutbound.RemoveAt(0); + } + ClaimPollUnsafe(state, work); + } + } + + if (!installed) + { + StartPollResponse(state, work); + return true; + } + + List> generated; + { + CallbackFrame callbackFrame(state); + InboundFrame inboundFrame(state); + installed->OnReadString(message); + generated = std::move(inboundFrame.generated); + } + + CS_LOCK(state->lockState) + { + if (state->stopStarted) return false; + if (generated.Count() > 0) + { + response = generated[0]; + for (vint i = 1; i < generated.Count(); i++) state->queuedOutbound.Add(generated[i]); + } + else if (state->queuedOutbound.Count() > 0) + { + response = state->queuedOutbound[0]; + state->queuedOutbound.RemoveAt(0); + } + ClaimPollUnsafe(state, work); + } + StartPollResponse(state, work); + return true; + } + + void SocketHttpServerConnection::StopCore(bool removeFromServer, bool waitForPoll) + { + auto state = lifecycle; + if (removeFromServer) + { + Ptr server; + CS_LOCK(state->lockState) { server = state->server; } + if (server) server->RemoveConnection(state->token, this); + } + + auto callbackDepth = CurrentCallbackDepth(state); + List> cancelling; + bool first = false; + bool ownsAssist = false; + state->lockState.Enter(); + if (!state->stopStarted) + { + first = true; + state->stopStarted = true; + if (callbackDepth > 0) + { + state->stopAssistProcessing = true; + ownsAssist = true; + } + if (state->pendingPoll) cancelling.Add(state->pendingPoll); + state->pendingPoll = nullptr; + for (auto context : state->queuedPollRegistrations) cancelling.Add(context); + state->queuedPollRegistrations.Clear(); + state->queuedInbound.Clear(); + state->queuedOutbound.Clear(); + } + else if (callbackDepth > 0) + { + if ( + state->stopFinished || + state->disconnectDelivering || + state->disconnectFinished || + state->stopAssistProcessing + ) + { + state->lockState.Leave(); + return; + } + state->stopAssistProcessing = true; + ownsAssist = true; + state->lockState.Leave(); + } + else + { + while (!state->stopFinished) state->cvState.SleepWith(state->lockState); + while (state->activeCallbacks > 0 || (waitForPoll && (state->inFlightPoll || state->pollRegistrationProcessing))) + { + state->cvState.SleepWith(state->lockState); + } + state->lockState.Leave(); + return; + } + if (first) state->lockState.Leave(); + + if (first) + { + for (auto context : cancelling) + { + try { context->Cancel(); } catch (...) {} + } + CS_LOCK(state->lockState) + { + state->stopCancellationFinished = true; + state->cvState.WakeAllPendings(); + } + } + + INetworkProtocolCallback* disconnected = nullptr; + state->lockState.Enter(); + while (!state->stopCancellationFinished || state->pollRegistrationProcessing) + { + state->cvState.SleepWith(state->lockState); + } + if (first && !ownsAssist) + { + while (state->stopAssistProcessing) state->cvState.SleepWith(state->lockState); + } + while (state->activeCallbacks > callbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + if (state->accepted && state->callback && !state->disconnectDelivering && !state->disconnectFinished) + { + disconnected = state->callback; + state->disconnectDelivering = true; + state->activeCallbacks++; + } + state->lockState.Leave(); + + if (disconnected) + { + try + { + CallbackFrame frame(state); + disconnected->OnDisconnected(); + } + catch (...) + { + } + } + + Ptr releasingServer; + CS_LOCK(state->lockState) + { + state->callback = nullptr; + state->callbackInstalling = false; + state->disconnectDelivering = false; + state->disconnectFinished = true; + if (ownsAssist) state->stopAssistProcessing = false; + if (first) + { + state->stopFinished = true; + if (state->activeCallbacks == 0) releasingServer = state->server; + } + state->cvState.WakeAllPendings(); + } + Ptr releasing; + if (releasingServer) releasing = releasingServer->ReleaseStoppedConnection(this); + if (first && waitForPoll) + { + CS_LOCK(state->lockState) + { + while (state->inFlightPoll) state->cvState.SleepWith(state->lockState); + } + } + } + + void SocketHttpServerConnection::StopFromServer() + { + StopCore(false, false); + } + + void SocketHttpServerConnection::WaitForPollCompletion() + { + CS_LOCK(lifecycle->lockState) + { + while (lifecycle->inFlightPoll || lifecycle->pollRegistrationProcessing) + { + lifecycle->cvState.SleepWith(lifecycle->lockState); + } + } + } + + void SocketHttpServerConnection::InstallCallback(INetworkProtocolCallback* callback) + { + auto state = lifecycle; + if (!callback) + { + auto callbackDepth = CurrentCallbackDepth(state); + CS_LOCK(state->lockState) + { + state->callback = nullptr; + while (state->activeCallbacks > callbackDepth) + { + state->cvState.SleepWith(state->lockState); + } + } + return; + } + + bool canInstall = false; + CS_LOCK(state->lockState) + { + if (!state->callback && !state->callbackInstalling && !state->stopStarted) + { + state->callback = callback; + state->callbackInstalling = true; + state->activeCallbacks++; + canInstall = true; + } + } + CHECK_ERROR(canInstall, L"SocketHttpServerConnection::InstallCallback cannot replace a callback or install one on a stopped connection."); + + try + { + CallbackFrame frame(state); + callback->OnInstalled(this); + while (true) + { + WString message; + bool replay = false; + CS_LOCK(state->lockState) + { + if (!state->stopStarted && state->callback == callback && state->queuedInbound.Count() > 0) + { + message = state->queuedInbound[0]; + state->queuedInbound.RemoveAt(0); + replay = true; + } + else + { + state->callbackInstalling = false; + state->cvState.WakeAllPendings(); + } + } + if (!replay) break; + callback->OnReadString(message); + } + } + catch (...) + { + CS_LOCK(state->lockState) + { + if (state->callback == callback) state->callback = nullptr; + state->callbackInstalling = false; + state->cvState.WakeAllPendings(); + } + throw; + } + } + + void SocketHttpServerConnection::BeginReadingLoopUnsafe() + { + } + + void SocketHttpServerConnection::SendString(const WString& str) + { +#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServerConnection::SendString(const WString&)#" + Array validated; + CHECK_ERROR(str.Length() > 0, ERROR_MESSAGE_PREFIX L"A logical HTTP message cannot be empty."); + CHECK_ERROR(IsValidHttpNetworkProtocolMessage(str), ERROR_MESSAGE_PREFIX L"A logical HTTP message must contain valid Unicode without NUL."); + CHECK_ERROR(EncodeStrictUtf8(str, validated), ERROR_MESSAGE_PREFIX L"A logical HTTP message must contain valid Unicode without NUL."); + CHECK_ERROR(validated.Count() <= HttpBodySizeLimit, ERROR_MESSAGE_PREFIX L"The UTF-8 message exceeds HttpBodySizeLimit."); +#undef ERROR_MESSAGE_PREFIX + auto message = Ptr(new SocketHttpServerOutboundMessage); + message->body = std::move(validated); + auto state = lifecycle; + PollWork work; + CS_LOCK(state->lockState) + { + CHECK_ERROR(!state->stopStarted, L"SocketHttpServerConnection::SendString cannot send on a stopped connection."); + if (currentInboundFrame && currentInboundFrame->state == state) + { + currentInboundFrame->generated.Add(message); + return; + } + state->queuedOutbound.Add(message); + ClaimPollUnsafe(state, work); + } + StartPollResponse(state, work); + } + + void SocketHttpServerConnection::Stop() + { + StopCore(true, true); + } + } + + class SocketHttpServer::Impl : public Object + { + public: + enum class BeginStopResult + { + Continue, + ReturnFollower, + }; + + Ptr lifecycle; + + Impl(SocketHttpServer* owner) + : lifecycle(Ptr(new SocketHttpServerLifecycle(owner))) + { + } + + static bool HasCurrentCallback(List>& stopping) + { + for (auto connection : stopping) + { + if (connection->HasCurrentCallback()) return true; + } + return false; + } + + static void DrainConnections(List>& stopping) + { + for (auto connection : stopping) + { + if (!connection->HasCurrentCallback()) connection->StopFromServer(); + } + for (auto connection : stopping) + { + if (connection->HasCurrentCallback()) connection->StopFromServer(); + } + } + + void ExecuteAssistant(List>& stopping) + { + try + { + SocketHttpServerStopScope scope(lifecycle.Obj(), true); + DrainConnections(stopping); + } + catch (...) + { + lifecycle->FinishStopAssist(); + throw; + } + lifecycle->FinishStopAssist(); + } + + BeginStopResult BeginStop(List>& stopping) + { + lifecycle->PrepareStop(stopping); + auto existingFrame = FindSocketHttpServerStopFrame(lifecycle.Obj()); + auto callbackNested = HasCurrentCallback(stopping); + bool execute = false; + bool assist = false; + lifecycle->PrepareStopProcessing( + callbackNested, + existingFrame && existingFrame->ownsAssist, + execute, + assist + ); + + if (execute) + { + try + { + SocketHttpServerStopScope scope(lifecycle.Obj(), assist); + DrainConnections(stopping); + } + catch (...) + { + lifecycle->FinishStop(assist); + throw; + } + lifecycle->FinishStop(assist); + return BeginStopResult::Continue; + } + + if (existingFrame) + { + if (existingFrame->ownsAssist) + { + DrainConnections(stopping); + } + else if (assist) + { + ExecuteAssistant(stopping); + } + return BeginStopResult::ReturnFollower; + } + + if (callbackNested) + { + if (assist) ExecuteAssistant(stopping); + return BeginStopResult::ReturnFollower; + } + + lifecycle->WaitForStop(); + DrainConnections(stopping); + return BeginStopResult::Continue; + } + + void OnRequest(SocketHttpServer* owner, Ptr context) + { + auto request = context->GetRequest(); + auto path = context->GetRelativePath(); + if (!request || context->GetQuery() != WString::Empty) + { + context->RespondStatus(404, L"Route not found"); + return; + } + + if (request->method == L"GET" && path == HttpServerUrl_Connect && HasEmptyBody(request)) + { + auto connection = lifecycle->CreateConnection(lifecycle); + if (!connection) + { + context->RespondStatus(404, L"Connection rejected"); + return; + } + + WaitForClientResult result = WaitForClientResult::Reject; + try { result = connection->InvokeClientConnected(owner); } + catch (...) { result = WaitForClientResult::Reject; } + if (result != WaitForClientResult::Accept) + { + connection->Stop(); + context->RespondStatus(404, L"Connection rejected"); + return; + } + + auto token = connection->GetToken(); + auto body = CreateHttpNetworkProtocolConnectBody( + WString::Unmanaged(HttpServerUrl_Request) + L"/" + token, + WString::Unmanaged(HttpServerUrl_Response) + L"/" + token + ); + context->RespondUtf8(200, L"OK", HttpNetworkProtocolContentType, body); + return; + } + + WString token; + if (request->method == L"POST" && ExtractToken(path, HttpServerUrl_Request, token) && HasEmptyBody(request)) + { + auto connection = lifecycle->FindConnection(token); + if (connection && connection->RegisterPoll(context)) return; + context->RespondStatus(404, L"Connection not found"); + return; + } + + WString message; + if (request->method == L"POST" && ExtractToken(path, HttpServerUrl_Response, token) && DecodeSubmittedMessage(context, request, message)) + { + auto connection = lifecycle->FindConnection(token); + Ptr response; + if (connection && connection->DispatchInbound(message, response)) + { + Array empty; + context->RespondBytes(200, L"OK", HttpNetworkProtocolContentType, response ? response->body : empty); + return; + } + } + + context->RespondStatus(404, L"Route not found"); + } + }; + + void SetSocketHttpServerPollCallbacksForTesting( + const Func& claimed, + const Func& completed, + const Func& registered + ) + { + auto& hooks = GetSocketHttpServerTestHooks(); + SPIN_LOCK(hooks.lock) + { + hooks.claimed = claimed; + hooks.completed = completed; + hooks.registered = registered; + } + } + + void ResetSocketHttpServerPollCallbacksForTesting() + { + SetSocketHttpServerPollCallbacksForTesting({}, {}, {}); + } + + SocketHttpServer::SocketHttpServer(Ptr server, const WString& urlPrefix) + : SocketHttpServerApi(server, ValidateServerUrlPrefix(urlPrefix)) + , impl(Ptr(new Impl(this))) + { + } + + SocketHttpServer::~SocketHttpServer() + { + try { Stop(); } catch (...) {} + CS_LOCK(impl->lifecycle->lockState) { impl->lifecycle->owner = nullptr; } + } + + WaitForClientResult SocketHttpServer::OnClientConnected(INetworkProtocolConnection*) + { + return WaitForClientResult::Accept; + } + + void SocketHttpServer::OnHttpRequestReceived(Ptr context) + { + impl->OnRequest(this, context); + } + + void SocketHttpServer::OnHttpServerStopping() + { + List> stopping; + impl->BeginStop(stopping); + } + + void SocketHttpServer::Start() + { + impl->lifecycle->PrepareStart(); + try + { + SocketHttpServerApi::Start(); + } + catch (...) + { + List> stopping; + impl->BeginStop(stopping); + throw; + } + } + + void SocketHttpServer::Stop() + { + List> stopping; + if (impl->BeginStop(stopping) == Impl::BeginStopResult::ReturnFollower) return; + SocketHttpServerApi::Stop(); + for (auto connection : stopping) connection->WaitForPollCompletion(); + } + + bool SocketHttpServer::IsStopped() + { + return impl->lifecycle->IsStopped() || SocketHttpServerApi::IsStopped(); + } +} + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPSERVERAPI.CPP +***********************************************************************/ + + +namespace vl::inter_process::async_tcp_socket +{ + using namespace collections; + + namespace + { + class ApiRegistration; + class RegistryEntry; + class SharedServer; + class ConnectionState; + + struct PrefixInfo + { + WString normalizedUrl; + WString decodedPath; + }; + + wchar_t Lower(wchar_t c) + { + return c >= L'A' && c <= L'Z' ? c - L'A' + L'a' : c; + } + + WString Lower(const WString& text) + { + if (text.Length() == 0) return {}; + auto buffer = new wchar_t[text.Length() + 1]; + for (vint i = 0; i < text.Length(); i++) buffer[i] = Lower(text[i]); + buffer[text.Length()] = 0; + return WString::TakeOver(buffer, text.Length()); + } + + bool Token(wchar_t c) + { + if ((c >= L'a' && c <= L'z') || (c >= L'A' && c <= L'Z') || (c >= L'0' && c <= L'9')) return true; + switch (c) + { + case L'!': case L'#': case L'$': case L'%': case L'&': case L'\'': case L'*': + case L'+': case L'-': case L'.': case L'^': case L'_': case L'`': case L'|': case L'~': + return true; + default: + return false; + } + } + + vint Hex(wchar_t c) + { + if (c >= L'0' && c <= L'9') return c - L'0'; + if (c >= L'a' && c <= L'f') return c - L'a' + 10; + if (c >= L'A' && c <= L'F') return c - L'A' + 10; + return -1; + } + + bool DecodePath(const WString& raw, WString& decoded) + { + List bytes; + for (vint i = 0; i < raw.Length(); i++) + { + auto c = raw[i]; + if (c == L'%') + { + if (i + 2 >= raw.Length()) return false; + auto h1 = Hex(raw[i + 1]); + auto h2 = Hex(raw[i + 2]); + if (h1 < 0 || h2 < 0) return false; + auto b = (vuint8_t)(h1 * 16 + h2); + if (b == 0 || b == '/' || b == '\\') return false; + bytes.Add(b); + i += 2; + } + else + { + if (c == 0 || c == L'\\' || c > 0x7F) return false; + bytes.Add((vuint8_t)c); + } + } + + Array encoded(bytes.Count()); + for (vint i = 0; i < bytes.Count(); i++) encoded[i] = bytes[i]; + return ::vl::inter_process::async_tcp_socket::DecodeStrictUtf8( + encoded.Count() == 0 ? nullptr : &encoded[0], + encoded.Count(), + decoded + ); + } + + bool ParseAuthority(const WString& text, vint& port) + { + vint colon = -1; + for (vint i = 0; i < text.Length(); i++) + { + if (text[i] == L':') + { + if (colon != -1) return false; + colon = i; + } + else if (text[i] == L'@' || text[i] == L'/' || text[i] == L'?' || text[i] == L'#') return false; + } + if (colon <= 0 || colon + 1 >= text.Length()) return false; + auto host = Lower(text.Left(colon)); + if (host != L"localhost" && host != L"127.0.0.1") return false; + vuint64_t number = 0; + for (vint i = colon + 1; i < text.Length(); i++) + { + if (text[i] < L'0' || text[i] > L'9') return false; + number = number * 10 + text[i] - L'0'; + if (number > 65535) return false; + } + if (number == 0) return false; + port = (vint)number; + return true; + } + + PrefixInfo ParsePrefix(vint port, const WString& urlPrefix) + { +#define ERROR_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServerApi::SocketHttpServerApi(Ptr, const WString&, bool)#" + CHECK_ERROR(1 <= port && port <= 65535, ERROR_PREFIX L"The injected server port must be in 1..65535."); + CHECK_ERROR(urlPrefix.Length() == 0 || urlPrefix[0] == L'/', ERROR_PREFIX L"The URL prefix must be empty or begin with /."); + CHECK_ERROR(urlPrefix.IndexOf(L'?') == -1 && urlPrefix.IndexOf(L'#') == -1, ERROR_PREFIX L"Query and fragment components are not supported."); + auto rawPath = urlPrefix; + PrefixInfo info; + while (rawPath.Length() > 0 && rawPath[rawPath.Length() - 1] == L'/') rawPath = rawPath.Left(rawPath.Length() - 1); + CHECK_ERROR(DecodePath(rawPath, info.decodedPath), ERROR_PREFIX L"The path contains invalid escaping, UTF-8, NUL, or an encoded separator."); + info.normalizedUrl = L"http://localhost:" + itow(port) + rawPath; + return info; +#undef ERROR_PREFIX + } + + WString TrimOws(const WString& text) + { + vint begin = 0, end = text.Length(); + while (begin < end && (text[begin] == L' ' || text[begin] == L'\t')) begin++; + while (begin < end && (text[end - 1] == L' ' || text[end - 1] == L'\t')) end--; + return text.Sub(begin, end - begin); + } + + bool AnalyzeCacheControl(const Array& bytes, bool& noStore) + { + WString text; + if (!DecodeAsciiHttpFieldValue(bytes, text)) return false; + noStore = false; + vint begin = 0; + bool quoted = false, escaped = false; + for (vint i = 0; i <= text.Length(); i++) + { + if (i < text.Length()) + { + auto c = text[i]; + if (escaped) { escaped = false; continue; } + if (quoted) + { + if (c == L'\\') escaped = true; + else if (c == L'"') quoted = false; + continue; + } + if (c == L'"') { quoted = true; continue; } + if (c != L',') continue; + } + else if (quoted || escaped) return false; + + auto item = TrimOws(text.Sub(begin, i - begin)); + if (item.Length() == 0) return false; + auto equals = item.IndexOf(L'='); + auto name = TrimOws(equals == -1 ? item : item.Left(equals)); + if (name.Length() == 0) return false; + for (vint j = 0; j < name.Length(); j++) if (!Token(name[j])) return false; + if (equals != -1 && TrimOws(item.Right(item.Length() - equals - 1)).Length() == 0) return false; + if (Lower(name) == L"no-store") + { + if (equals != -1) return false; + noStore = true; + } + begin = i + 1; + } + return true; + } + + bool ValidHttpDate(const Array& bytes) + { + WString text; + if (!DecodeAsciiHttpFieldValue(bytes, text)) return false; + text = TrimOws(text); + if (text.Length() != 29 + || text[3] != L',' || text[4] != L' ' || text[7] != L' ' || text[11] != L' ' + || text[16] != L' ' || text[19] != L':' || text[22] != L':' || text[25] != L' ' + || text.Sub(26, 3) != L"GMT") return false; + auto dayName = text.Sub(0, 3); + if (dayName != L"Sun" && dayName != L"Mon" && dayName != L"Tue" && dayName != L"Wed" && dayName != L"Thu" && dayName != L"Fri" && dayName != L"Sat") return false; + vint digitIndexes[] = { 5, 6, 12, 13, 14, 15, 17, 18, 20, 21, 23, 24 }; + for (auto index : digitIndexes) + { + if (text[index] < L'0' || text[index] > L'9') return false; + } + auto monthName = text.Sub(8, 3); + vint month = + monthName == L"Jan" ? 1 : monthName == L"Feb" ? 2 : monthName == L"Mar" ? 3 : + monthName == L"Apr" ? 4 : monthName == L"May" ? 5 : monthName == L"Jun" ? 6 : + monthName == L"Jul" ? 7 : monthName == L"Aug" ? 8 : monthName == L"Sep" ? 9 : + monthName == L"Oct" ? 10 : monthName == L"Nov" ? 11 : monthName == L"Dec" ? 12 : 0; + if (month == 0) return false; + auto day = (text[5] - L'0') * 10 + text[6] - L'0'; + auto year = (text[12] - L'0') * 1000 + (text[13] - L'0') * 100 + (text[14] - L'0') * 10 + text[15] - L'0'; + auto hour = (text[17] - L'0') * 10 + text[18] - L'0'; + auto minute = (text[20] - L'0') * 10 + text[21] - L'0'; + auto second = (text[23] - L'0') * 10 + text[24] - L'0'; + if (year == 0 || hour > 23 || minute > 59 || second > 60) return false; + vint monthDays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + if (month == 2 && (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0))) monthDays[1] = 29; + return day >= 1 && day <= monthDays[month - 1]; + } + + const wchar_t* Reason(vint code) + { + switch (code) + { + case 200: return L"OK"; case 204: return L"No Content"; case 304: return L"Not Modified"; + case 400: return L"Bad Request"; case 404: return L"Not Found"; case 405: return L"Method Not Allowed"; + case 408: return L"Request Timeout"; case 413: return L"Payload Too Large"; case 414: return L"URI Too Long"; + case 415: return L"Unsupported Media Type"; case 417: return L"Expectation Failed"; + case 431: return L"Request Header Fields Too Large"; case 500: return L"Internal Server Error"; + case 501: return L"Not Implemented"; case 505: return L"HTTP Version Not Supported"; + default: return L"Response"; + } + } + + void ValidateConvenienceResponseArguments(vint statusCode, const WString& reason, const WString& contentType, vint bodySize) + { + CHECK_ERROR(statusCode >= 200 && statusCode <= 599, L"Invalid HTTP response status."); + for (vint i = 0; i < reason.Length(); i++) + { + CHECK_ERROR((vuint32_t)reason[i] >= 0x20 && (vuint32_t)reason[i] <= 0x7E, L"Invalid HTTP response reason phrase."); + } + if (contentType != WString::Empty) + { + CreateAsciiHttpField(L"content-type", contentType); + } + CHECK_ERROR(bodySize <= HttpBodySizeLimit, L"The response body is too large."); + } + + Ptr CreateConvenienceResponse(vint statusCode, const WString& reason, const WString& contentType, Array&& body) + { + auto response = Ptr(new HttpResponse); + response->statusCode = statusCode; + response->reason = reason; + if (contentType != WString::Empty) + { + response->headers.Add(CreateAsciiHttpField(L"content-type", contentType)); + } + SetHttpBodyBytes(response->body, std::move(body)); + return response; + } + + WString Two(vint value) { return value < 10 ? L"0" + itow(value) : itow(value); } + + WString DateHeader() + { + const wchar_t* days[] = { L"Sun", L"Mon", L"Tue", L"Wed", L"Thu", L"Fri", L"Sat" }; + const wchar_t* months[] = { L"Jan", L"Feb", L"Mar", L"Apr", L"May", L"Jun", L"Jul", L"Aug", L"Sep", L"Oct", L"Nov", L"Dec" }; + auto now = DateTime::UtcTime(); + return WString::Unmanaged(days[now.dayOfWeek]) + L", " + Two(now.day) + L" " + months[now.month - 1] + L" " + itow(now.year) + L" " + Two(now.hour) + L":" + Two(now.minute) + L":" + Two(now.second) + L" GMT"; + } + + Ptr Normalize(Ptr input, const WString& method) + { + CHECK_ERROR(input, L"SocketHttpRequestContext::Respond requires a response."); + CHECK_ERROR(input->statusCode >= 200 && input->statusCode <= 599, L"Invalid HTTP response status."); + CHECK_ERROR(input->body.trailers.Count() == 0, L"Response trailers are not supported."); + Array body; + CHECK_ERROR(FlattenHttpBody(input->body, body), L"The response body is too large."); + auto bodySize = body.Count(); + auto bodyAllowed = method != L"HEAD" && input->statusCode != 204 && input->statusCode != 304; + auto length = bodyAllowed || method == L"HEAD" ? bodySize : 0; + auto output = Ptr(new HttpResponse); + output->statusCode = input->statusCode; + output->reason = input->reason == WString::Empty ? Reason(input->statusCode) : input->reason; + for (vint i = 0; i < output->reason.Length(); i++) + { + CHECK_ERROR(output->reason[i] >= 0x20 && output->reason[i] <= 0x7E, L"Invalid HTTP response reason phrase."); + } + + List normalizedHeaders; + for (auto&& source : input->headers) + { + auto copy = CreateAsciiHttpField(source.name, WString::Empty); + for (auto c : source.value) CHECK_ERROR(c == '\t' || (c >= 0x20 && c != 0x7F), L"Invalid response field value."); + copy.value.Resize(source.value.Count()); + for (vint i = 0; i < source.value.Count(); i++) copy.value[i] = source.value[i]; + normalizedHeaders.Add(std::move(copy)); + } + + HttpFraming framing; + CHECK_ERROR(AnalyzeHttpFraming(normalizedHeaders, framing) == HttpFramingAnalysisResult::Succeeded, L"Invalid or unsupported HTTP response framing."); + CHECK_ERROR(framing.kind != HttpFramingKind::Chunked, L"Response Transfer-Encoding is not supported."); + CHECK_ERROR(framing.contentLengthValuesPlainDecimal, L"Application Content-Length values must be plain decimal numbers."); + CHECK_ERROR(framing.contentLength <= (vuint64_t)std::numeric_limits::max(), L"The application Content-Length is too large."); + + bool date = false, noStore = false, cors = false; + for (auto&& source : normalizedHeaders) + { + if (source.name == L"content-length") continue; + if (source.name == L"date") + { + CHECK_ERROR(!date && ValidHttpDate(source.value), L"A response requires at most one valid IMF-fixdate Date field."); + date = true; + } + else if (source.name == L"cache-control") + { + bool sourceNoStore = false; + CHECK_ERROR(AnalyzeCacheControl(source.value, sourceNoStore), L"Invalid Cache-Control response field."); + noStore |= sourceNoStore; + } + else if (source.name == L"access-control-allow-origin") + { + WString value; + CHECK_ERROR(!cors && DecodeAsciiHttpFieldValue(source.value, value) && TrimOws(value) == L"*", L"The loopback CORS policy requires exactly one Access-Control-Allow-Origin: * field."); + cors = true; + } + HttpField copy; + copy.name = source.name; + copy.value.Resize(source.value.Count()); + if (source.value.Count() > 0) + { + memcpy(©.value[0], &source.value[0], source.value.Count()); + } + output->headers.Add(std::move(copy)); + } + auto contentLength = framing.kind == HttpFramingKind::ContentLength; + auto suppliedLength = (vint)framing.contentLength; + auto outputLength = length; + if (contentLength && input->statusCode == 304 && method != L"HEAD") outputLength = suppliedLength; + else CHECK_ERROR(!contentLength || suppliedLength == length, L"Contradictory response Content-Length."); + if (!date) output->headers.Add(CreateAsciiHttpField(L"date", DateHeader())); + if (!noStore) output->headers.Add(CreateAsciiHttpField(L"cache-control", L"no-store")); + if (!cors) output->headers.Add(CreateAsciiHttpField(L"access-control-allow-origin", L"*")); + if (input->statusCode != 204) output->headers.Add(CreateAsciiHttpField(L"content-length", itow(outputLength))); + if (bodyAllowed && bodySize > 0) + { + SetHttpBodyBytes(output->body, std::move(body)); + } + return output; + } + + Ptr Automatic(vint code, bool close, bool preflight = false) + { + auto response = Ptr(new HttpResponse); + response->statusCode = code; + response->reason = Reason(code); + if (close) response->headers.Add(CreateAsciiHttpField(L"connection", L"close")); + if (preflight) + { + response->headers.Add(CreateAsciiHttpField(L"access-control-allow-methods", L"GET, HEAD, POST, OPTIONS")); + response->headers.Add(CreateAsciiHttpField(L"access-control-allow-headers", L"Accept, Content-Type")); + response->headers.Add(CreateAsciiHttpField(L"allow", L"GET, HEAD, POST, OPTIONS")); + } + return response; + } + } +} + +namespace vl::inter_process::async_tcp_socket +{ + using namespace collections; + + namespace + { + class ApiRegistration : public Object + { + public: + struct Frame + { + ApiRegistration* registration; + Frame* previous; + Frame(ApiRegistration* value); + ~Frame(); + }; + static thread_local Frame* currentFrame; + + PrefixInfo prefix; + bool respondToOptions; + Ptr entry; + CriticalSection lock; + ConditionVariable cv; + List> contexts; + List> contextCancellations; + Func)> requestCallback; + Func stoppingCallback; + vint activeCallbacks = 0; + bool stopping = false; + bool stopped = false; + + ApiRegistration(const PrefixInfo& _prefix, bool _respondToOptions) + : prefix(_prefix), respondToOptions(_respondToOptions) + { + } + + static vint Depth(ApiRegistration* registration) + { + vint depth = 0; + for (auto frame = currentFrame; frame; frame = frame->previous) if (frame->registration == registration) depth++; + return depth; + } + + bool AddContext(Ptr context, const Func& cancellation) + { + bool added = false; + CS_LOCK(lock) + { + if (!stopping) + { + contexts.Add(context); + contextCancellations.Add(cancellation); + added = true; + } + } + return added; + } + + void RemoveContext(SocketHttpRequestContext* context) + { + CS_LOCK(lock) + { + vint index = -1; + for (vint i = 0; i < contexts.Count(); i++) + { + if (contexts[i].Obj() == context) { index = i; break; } + } + if (index != -1) + { + contexts.RemoveAt(index); + contextCancellations.RemoveAt(index); + } + cv.WakeAllPendings(); + } + } + + bool InvokeRequest(Ptr context) + { + Func)> callback; + CS_LOCK(lock) + { + if (!stopping && requestCallback) + { + callback = requestCallback; + activeCallbacks++; + } + } + if (!callback) return false; + Frame frame(this); + try + { + callback(context); + } + catch (...) + { + CS_LOCK(lock) { activeCallbacks--; cv.WakeAllPendings(); } + throw; + } + CS_LOCK(lock) { activeCallbacks--; cv.WakeAllPendings(); } + return true; + } + + bool ReserveCompletion(const Func& callback) + { + if (!callback) return false; + bool reserved = false; + CS_LOCK(lock) + { + if (!stopped) { activeCallbacks++; reserved = true; } + } + return reserved; + } + + void InvokeReservedCompletion(const Func& callback, bool succeeded) + { + Frame frame(this); + try { callback(succeeded); } catch (...) {} + CS_LOCK(lock) { activeCallbacks--; cv.WakeAllPendings(); } + } + + void Stop(bool notify) + { + List> cancelling; + Func callback; + auto depth = Depth(this); + lock.Enter(); + if (stopped) { lock.Leave(); return; } + if (stopping) + { + if (depth > 0) { lock.Leave(); return; } + while (!stopped) cv.SleepWith(lock); + lock.Leave(); + return; + } + stopping = true; + if (notify) callback = stoppingCallback; + for (auto cancellation : contextCancellations) cancelling.Add(cancellation); + lock.Leave(); + + if (callback) + { + CS_LOCK(lock) { activeCallbacks++; } + Frame frame(this); + try { callback(); } catch (...) {} + CS_LOCK(lock) { activeCallbacks--; cv.WakeAllPendings(); } + } + for (auto cancellation : cancelling) cancellation(); + lock.Enter(); + while (contexts.Count() > 0 || activeCallbacks > depth) cv.SleepWith(lock); + requestCallback = {}; + stoppingCallback = {}; + stopped = true; + cv.WakeAllPendings(); + lock.Leave(); + } + + bool IsStopped() + { + bool result = false; + CS_LOCK(lock) { result = stopping; } + return result; + } + }; + + thread_local ApiRegistration::Frame* ApiRegistration::currentFrame = nullptr; + ApiRegistration::Frame::Frame(ApiRegistration* value) : registration(value), previous(currentFrame) { currentFrame = this; } + ApiRegistration::Frame::~Frame() { currentFrame = previous; } + + class RegistryEntry : public Object + { + public: + Ptr socketServer; + EventObject readyEvent; + List> registrations; + Ptr server; + WString failureMessage; + AsyncSocketServerStartFailure failure = AsyncSocketServerStartFailure::Other; + bool succeeded = false, terminal = false; + + RegistryEntry(Ptr _socketServer) + : socketServer(_socketServer) + { + CHECK_ERROR(readyEvent.CreateManualUnsignal(false), L"Failed to create a socket HTTP registry event."); + } + }; + + class ConnectionState : public Object + { + public: + CriticalSection lock; + Ptr entry; + IHttpRequestConnection* connection = nullptr; + Ptr context; + bool automaticWrite = false; + bool closeAfterWrite = false; + bool terminal = false; + + ConnectionState(Ptr _entry) : entry(_entry) {} + }; + } + + class SocketHttpRequestContext::Impl : public Object + { + public: + enum class State { Pending, Sending, Completed, Cancelled }; + CriticalSection lock; + Ptr connectionState; + Ptr registration; + Ptr request; + WString relativePath, query; + SocketHttpRequestContext* owner = nullptr; + Func completion; + State state = State::Pending; + + Impl(Ptr cs, Ptr reg, Ptr req, const WString& relative, const WString& _query) + : connectionState(cs), registration(reg), request(req), relativePath(relative), query(_query) + { + } + + bool Finish(bool succeeded, bool includePending, bool includeSending, bool stopConnection) + { + Func callback; + bool won = false; + CS_LOCK(lock) + { + if ((includeSending && state == State::Sending) || (includePending && state == State::Pending)) + { + if (state == State::Sending) + { + callback = completion; + completion = {}; + } + state = succeeded ? State::Completed : State::Cancelled; + won = true; + } + } + if (!won) return false; + auto completionReserved = registration->ReserveCompletion(callback); + IHttpRequestConnection* connection = nullptr; + CS_LOCK(connectionState->lock) + { + if (connectionState->context.Obj() == owner) connectionState->context = nullptr; + if (stopConnection) { connectionState->terminal = true; connection = connectionState->connection; } + } + registration->RemoveContext(owner); + if (connection) try { connection->Stop(); } catch (...) {} + if (completionReserved) registration->InvokeReservedCompletion(callback, succeeded); + return true; + } + }; + + class SocketHttpServerApiDispatcher : public Object, public virtual IHttpRequestCallback + { + private: + CriticalSection lockSelf; + Ptr selfReference; + Ptr state; + Ptr Retain(); + void SendAutomatic(vint code, bool close, bool preflight = false); + void Process(Ptr request); + + public: + SocketHttpServerApiDispatcher(Ptr entry) : state(Ptr(new ConnectionState(entry))) {} + void InitializeSelf(Ptr self) { CS_LOCK(lockSelf) { selfReference = self; } } + void OnReadRequest(Ptr request) override; + void OnReadRequestFailure(HttpRequestFailure failure) override; + void OnWriteCompleted() override; + void OnError(const WString& error, bool fatal) override; + void OnDisconnected() override; + void OnInstalled(IHttpRequestConnection* connection) override; + }; + + namespace + { + void UnexpectedStop(Ptr entry); + + class SharedServer : public HttpRequestServer + { + private: + CriticalSection lock; + Ptr selfReference; + Ptr entry; + protected: + void OnServerStopped() override; + public: + SharedServer( + Ptr server, + Ptr _entry, + const Func()>& timeoutControllerFactory + ) + : HttpRequestServer(server, timeoutControllerFactory) + , entry(_entry) + { + } + void InitializeSelf(Ptr self) { CS_LOCK(lock) { selfReference = self; } } + WaitForClientResult OnClientConnected(IHttpRequestConnection* connection) override; + void StopAndRelease(); + }; + + BEGIN_GLOBAL_STORAGE_CLASS(SocketHttpRegistry) + SpinLock lock; + Dictionary> entries; + Func()> + timeoutControllerFactory; + INITIALIZE_GLOBAL_STORAGE_CLASS + FINALIZE_GLOBAL_STORAGE_CLASS + List> servers; + List> retainedEntries; + SPIN_LOCK(lock) + { + for (auto entry : entries.Values()) + { + retainedEntries.Add(entry); + if (entry->server) servers.Add(entry->server); + } + entries.Clear(); + timeoutControllerFactory = {}; + } + for (auto server : servers) + { + auto shared = server.Cast(); + if (shared) shared->StopAndRelease(); else server->Stop(); + } + END_GLOBAL_STORAGE_CLASS(SocketHttpRegistry) + } +} + +namespace vl::inter_process::async_tcp_socket +{ + using namespace collections; + + namespace + { + Func()> GetTimeoutControllerFactory() + { + Func()> factory; + auto& registry = GetSocketHttpRegistry(); + SPIN_LOCK(registry.lock) { factory = registry.timeoutControllerFactory; } + return factory; + } + + bool CurrentEntry(SocketHttpRegistry& registry, Ptr entry) + { + auto index = registry.entries.Keys().IndexOf(entry->socketServer.Obj()); + return index != -1 && registry.entries.Values()[index] == entry; + } + + bool Duplicate(Ptr entry, Ptr registration) + { + for (auto existing : entry->registrations) + { + if (existing != registration && existing->prefix.decodedPath == registration->prefix.decodedPath) return true; + } + return false; + } + + void RegisterApi(Ptr socketServer, Ptr registration) + { + auto& registry = GetSocketHttpRegistry(); + Ptr entry; + bool creator = false; + SPIN_LOCK(registry.lock) + { + auto index = registry.entries.Keys().IndexOf(socketServer.Obj()); + if (index != -1) entry = registry.entries.Values()[index]; + } + if (!entry) + { + auto candidate = Ptr(new RegistryEntry(socketServer)); + SPIN_LOCK(registry.lock) + { + auto index = registry.entries.Keys().IndexOf(socketServer.Obj()); + if (index == -1) + { + entry = candidate; + entry->registrations.Add(registration); + registration->entry = entry; + registry.entries.Add(socketServer.Obj(), entry); + creator = true; + } + else entry = registry.entries.Values()[index]; + } + } + if (!creator) + { + CHECK_ERROR(entry->readyEvent.Wait(), L"Failed to wait for a shared socket HTTP listener."); + bool joined = false, duplicate = false; + AsyncSocketServerStartFailure failure = AsyncSocketServerStartFailure::Other; + WString message; + SPIN_LOCK(registry.lock) + { + if (entry->succeeded && !entry->terminal && CurrentEntry(registry, entry)) + { + duplicate = Duplicate(entry, registration); + if (!duplicate) + { + entry->registrations.Add(registration); + registration->entry = entry; + joined = true; + } + } + else + { + failure = entry->failure; + message = entry->failureMessage; + } + } + CHECK_ERROR(!duplicate, L"A SocketHttpServerApi with the same normalized prefix has already started."); + if (joined) return; + if (message == WString::Empty) message = L"The injected async socket server stopped before the API could join it."; + throw AsyncSocketServerStartException(failure, message); + } + + Ptr server; + AsyncSocketServerStartFailure failure = AsyncSocketServerStartFailure::Other; + WString message; + bool started = false; + try + { + server = Ptr(new SharedServer(socketServer, entry, GetTimeoutControllerFactory())); + server->InitializeSelf(server); + server->Start(); + started = true; + } + catch (const AsyncSocketServerStartException& exception) { failure = exception.GetFailure(); message = exception.Message(); } + catch (const Exception& exception) { message = exception.Message(); } + catch (...) { message = L"The socket HTTP listener failed to start."; } + + bool published = false; + SPIN_LOCK(registry.lock) + { + if (started && !entry->terminal && CurrentEntry(registry, entry)) + { + entry->server = server; + entry->succeeded = true; + published = true; + } + else + { + if (started) + { + failure = AsyncSocketServerStartFailure::Other; + message = L"The listener stopped while starting."; + } + else if (message == WString::Empty) + { + message = L"The socket HTTP listener failed to start."; + } + if (CurrentEntry(registry, entry)) registry.entries.Remove(entry->socketServer.Obj()); + entry->registrations.Remove(registration.Obj()); + registration->entry = nullptr; + entry->failure = failure; + entry->failureMessage = message; + entry->terminal = true; + } + } + entry->readyEvent.Signal(); + if (published) return; + if (server) server->StopAndRelease(); + throw AsyncSocketServerStartException(failure, message); + } + + Ptr UnregisterApi(Ptr registration) + { + Ptr server; + Ptr entry; + auto& registry = GetSocketHttpRegistry(); + SPIN_LOCK(registry.lock) + { + entry = registration->entry; + if (entry) + { + entry->registrations.Remove(registration.Obj()); + registration->entry = nullptr; + if (entry->registrations.Count() == 0) + { + if (CurrentEntry(registry, entry)) registry.entries.Remove(entry->socketServer.Obj()); + entry->terminal = true; + server = entry->server; + entry->server = nullptr; + } + } + } + return server; + } + + List> Registrations(Ptr entry) + { + List> result; + auto& registry = GetSocketHttpRegistry(); + SPIN_LOCK(registry.lock) + { + if (!entry->terminal) for (auto item : entry->registrations) result.Add(item); + } + return result; + } + + void UnexpectedStop(Ptr entry) + { + List> stopping; + auto& registry = GetSocketHttpRegistry(); + SPIN_LOCK(registry.lock) + { + if (!entry->terminal) + { + entry->terminal = true; + if (CurrentEntry(registry, entry)) registry.entries.Remove(entry->socketServer.Obj()); + for (auto registration : entry->registrations) { registration->entry = nullptr; stopping.Add(registration); } + entry->registrations.Clear(); + entry->server = nullptr; + } + } + for (auto registration : stopping) registration->Stop(true); + } + + bool Match(const WString& prefix, const WString& path) + { + return prefix.Length() == 0 || path == prefix || (path.Length() > prefix.Length() && path.Left(prefix.Length()) == prefix && path[prefix.Length()] == L'/'); + } + + bool SingleHeader(Ptr request, const WString& name, WString& value, bool& exists) + { + auto count = CountHttpFields(request->headers, name); + if (count > 1) return false; + exists = count == 1; + if (!exists) return true; + return DecodeAsciiHttpFieldValue(FindHttpField(request->headers, name)->value, value); + } + + WString Trim(const WString& text) + { + vint begin = 0, end = text.Length(); + while (begin < end && (text[begin] == L' ' || text[begin] == L'\t')) begin++; + while (begin < end && (text[end - 1] == L' ' || text[end - 1] == L'\t')) end--; + return text.Sub(begin, end - begin); + } + + bool Preflight(Ptr request, bool& present, bool& supportedMethod) + { + present = false; + supportedMethod = false; + WString method; + bool exists = false; + if (!SingleHeader(request, L"access-control-request-method", method, exists)) return false; + if (!exists) return true; + present = true; + method = Trim(method); + supportedMethod = method == L"GET" || method == L"HEAD" || method == L"POST" || method == L"OPTIONS"; + if (method.Length() == 0) return false; + for (auto&& field : request->headers) + { + if (field.name != L"access-control-request-headers") continue; + WString headers; + if (!DecodeAsciiHttpFieldValue(field.value, headers)) return false; + vint reading = 0; + while (reading <= headers.Length()) + { + vint comma = reading; + while (comma < headers.Length() && headers[comma] != L',') comma++; + auto item = Lower(Trim(headers.Sub(reading, comma - reading))); + if (item.Length() == 0 || (item != L"accept" && item != L"content-type")) return false; + if (comma == headers.Length()) break; + reading = comma + 1; + } + } + return true; + } + } + + void SetSocketHttpServerTimeoutControllerFactoryForTesting(const Func()>& factory) + { + auto& registry = GetSocketHttpRegistry(); + SPIN_LOCK(registry.lock) + { + CHECK_ERROR(registry.entries.Count() == 0, L"The test timeout controller factory can only change while no API is started."); + registry.timeoutControllerFactory = factory; + } + } + + void ResetSocketHttpServerTimeoutControllerFactoryForTesting() { SetSocketHttpServerTimeoutControllerFactoryForTesting({}); } + + WaitForClientResult SharedServer::OnClientConnected(IHttpRequestConnection* connection) + { + Ptr retained; + CS_LOCK(lock) { retained = entry; } + if (!retained) return WaitForClientResult::Reject; + try + { + auto dispatcher = Ptr(new SocketHttpServerApiDispatcher(retained)); + dispatcher->InitializeSelf(dispatcher); + connection->InstallCallback(dispatcher.Obj()); + return WaitForClientResult::Accept; + } + catch (...) { return WaitForClientResult::Reject; } + } + + void SharedServer::StopAndRelease() + { + Ptr retained; + CS_LOCK(lock) { retained = selfReference; } + try { HttpRequestServer::Stop(); } catch (...) {} + CS_LOCK(lock) { entry = nullptr; selfReference = nullptr; } + } + + void SharedServer::OnServerStopped() + { + Ptr retained; + Ptr retainedEntry; + CS_LOCK(lock) { retained = selfReference; retainedEntry = entry; } + if (retainedEntry) UnexpectedStop(retainedEntry); + try { HttpRequestServer::OnServerStopped(); } catch (...) {} + CS_LOCK(lock) { entry = nullptr; selfReference = nullptr; } + } +} + +namespace vl::inter_process::async_tcp_socket +{ + using namespace collections; + + Ptr SocketHttpServerApiDispatcher::Retain() + { + Ptr retained; + CS_LOCK(lockSelf) { retained = selfReference; } + return retained; + } + + void SocketHttpServerApiDispatcher::SendAutomatic(vint code, bool close, bool preflight) + { + auto response = Normalize(Automatic(code, close, preflight), L"GET"); + IHttpRequestConnection* connection = nullptr; + CS_LOCK(state->lock) + { + if (state->terminal || state->automaticWrite || state->context) return; + state->automaticWrite = true; + state->closeAfterWrite = close; + connection = state->connection; + } + if (!connection) return; + try + { + connection->SendResponse(response); + } + catch (...) + { + CS_LOCK(state->lock) + { + state->automaticWrite = false; + state->terminal = true; + } + try { connection->Stop(); } catch (...) {} + } + } + + void SocketHttpServerApiDispatcher::Process(Ptr request) + { + if (!request || request->version.major != 1 || request->version.minor != 1) + { + SendAutomatic(505, true); + return; + } + + WString host; + bool hostExists = false; + if (!SingleHeader(request, L"host", host, hostExists) || !hostExists) + { + SendAutomatic(400, true); + return; + } + vint port = 0; + if (!ParseAuthority(host, port)) + { + SendAutomatic(400, true); + return; + } + + auto supportedMethod = request->method == L"GET" || request->method == L"HEAD" || request->method == L"POST" || request->method == L"OPTIONS"; + if (!supportedMethod) + { + SendAutomatic(501, false); + return; + } + + auto registrations = Registrations(state->entry); + auto matchesPort = state->entry->socketServer->GetPort() == port; + if (request->requestTarget == L"*") + { + if (request->method != L"OPTIONS") + { + SendAutomatic(400, true); + return; + } + bool automaticOptions = false; + for (auto registration : registrations) + { + if (matchesPort && registration->respondToOptions) + { + automaticOptions = true; + break; + } + } + if (!automaticOptions) + { + SendAutomatic(501, false); + return; + } + bool present = false, methodSupported = false; + if (!Preflight(request, present, methodSupported)) + { + SendAutomatic(400, false, true); + return; + } + SendAutomatic(present && !methodSupported ? 405 : 200, false, true); + return; + } + + auto target = request->requestTarget; + if (target.Length() == 0 || target[0] != L'/' || target.IndexOf(L'#') != -1) + { + SendAutomatic(400, true); + return; + } + vint question = target.IndexOf(L'?'); + auto rawPath = question == -1 ? target : target.Left(question); + auto query = question == -1 ? WString::Empty : target.Right(target.Length() - question - 1); + WString path; + if (!DecodePath(rawPath, path)) + { + SendAutomatic(400, true); + return; + } + + Ptr selected; + for (auto registration : registrations) + { + if (matchesPort && Match(registration->prefix.decodedPath, path)) + { + if (!selected || registration->prefix.decodedPath.Length() > selected->prefix.decodedPath.Length()) selected = registration; + } + } + if (!selected) + { + SendAutomatic(404, false); + return; + } + + if (request->method == L"OPTIONS" && selected->respondToOptions) + { + bool present = false, methodSupported = false; + if (!Preflight(request, present, methodSupported)) + { + SendAutomatic(400, false, true); + return; + } + if (present) + { + SendAutomatic(methodSupported ? 200 : 405, false, true); + return; + } + } + + WString relative; + if (selected->prefix.decodedPath.Length() == 0) relative = path; + else if (path == selected->prefix.decodedPath) relative = L"/"; + else relative = path.Right(path.Length() - selected->prefix.decodedPath.Length()); + if (relative.Length() == 0) relative = L"/"; + + auto contextImpl = Ptr(new SocketHttpRequestContext::Impl(state, selected, request, relative, query)); + auto context = Ptr(new SocketHttpRequestContext(contextImpl)); + contextImpl->owner = context.Obj(); + auto cancelForStop = Func([contextImpl]() + { + contextImpl->Finish(false, true, true, true); + }); + bool installed = false; + CS_LOCK(state->lock) + { + if (!state->terminal && !state->context && !state->automaticWrite) + { + state->context = context; + installed = true; + } + } + if (!installed || !selected->AddContext(context, cancelForStop)) + { + CS_LOCK(state->lock) { if (state->context == context) state->context = nullptr; } + SendAutomatic(404, false); + return; + } + + try + { + if (!selected->InvokeRequest(context)) context->Cancel(); + } + catch (...) + { + context->Respond(Automatic(500, false)); + } + } + + void SocketHttpServerApiDispatcher::OnReadRequest(Ptr request) + { + auto retained = Retain(); + if (!retained) return; + try { Process(request); } catch (...) { SendAutomatic(500, true); } + } + + void SocketHttpServerApiDispatcher::OnReadRequestFailure(HttpRequestFailure failure) + { + auto retained = Retain(); + if (!retained) return; + SendAutomatic((vint)failure, true); + } + + void SocketHttpServerApiDispatcher::OnWriteCompleted() + { + auto retained = Retain(); + if (!retained) return; + Ptr context; + bool automatic = false, close = false; + CS_LOCK(state->lock) + { + context = state->context; + if (!context && state->automaticWrite) + { + automatic = true; + close = state->closeAfterWrite; + state->automaticWrite = false; + state->closeAfterWrite = false; + } + } + if (context) context->impl->Finish(true, false, true, false); + if (!context && !automatic) return; + + if (close) + { + IHttpRequestConnection* stopping = nullptr; + CS_LOCK(state->lock) + { + state->terminal = true; + stopping = state->connection; + } + if (stopping) try { stopping->Stop(); } catch (...) {} + } + } + + void SocketHttpServerApiDispatcher::OnError(const WString&, bool fatal) + { + auto retained = Retain(); + if (!retained) return; + Ptr context; + IHttpRequestConnection* connection = nullptr; + CS_LOCK(state->lock) + { + context = state->context; + if (fatal || context || state->automaticWrite) + { + state->terminal = true; + connection = state->connection; + } + } + if (context) context->impl->Finish(false, true, true, true); + else if (connection) try { connection->Stop(); } catch (...) {} + } + + void SocketHttpServerApiDispatcher::OnDisconnected() + { + auto retained = Retain(); + if (!retained) return; + Ptr context; + CS_LOCK(state->lock) + { + state->terminal = true; + state->connection = nullptr; + state->automaticWrite = false; + context = state->context; + } + if (context) context->impl->Finish(false, true, true, false); + CS_LOCK(lockSelf) { selfReference = nullptr; } + } + + void SocketHttpServerApiDispatcher::OnInstalled(IHttpRequestConnection* connection) + { + CS_LOCK(state->lock) { state->connection = connection; } + connection->BeginReadingLoopUnsafe(); + } + + SocketHttpRequestContext::SocketHttpRequestContext(Ptr _impl) : impl(_impl) {} + SocketHttpRequestContext::~SocketHttpRequestContext() {} + Ptr SocketHttpRequestContext::GetRequest() { return impl->request; } + WString SocketHttpRequestContext::GetRelativePath() { return impl->relativePath; } + WString SocketHttpRequestContext::GetQuery() { return impl->query; } + bool SocketHttpRequestContext::TryGetBodyUtf8(WString& body) + { + Array bytes; + if (!FlattenHttpBody(impl->request->body, bytes)) return false; + return ::vl::inter_process::async_tcp_socket::DecodeStrictUtf8( + bytes.Count() == 0 ? nullptr : &bytes[0], + bytes.Count(), + body + ); + } + + bool SocketHttpRequestContext::Respond(Ptr response, Func completion) + { + Ptr normalized; + IHttpRequestConnection* connection = nullptr; + CS_LOCK(impl->lock) + { + if (impl->state != Impl::State::Pending) return false; + normalized = Normalize(response, impl->request->method); + impl->state = Impl::State::Sending; + impl->completion = completion; + } + CS_LOCK(impl->connectionState->lock) + { + if (!impl->connectionState->terminal && impl->connectionState->context.Obj() == this) connection = impl->connectionState->connection; + } + if (!connection) + { + impl->Finish(false, false, true, false); + return true; + } + try { connection->SendResponse(normalized); } + catch (...) { impl->Finish(false, false, true, true); } + return true; + } + + bool SocketHttpRequestContext::RespondStatus(vint statusCode, const WString& reason, Func completion) + { + ValidateConvenienceResponseArguments(statusCode, reason, WString::Empty, 0); + Array body; + return Respond(CreateConvenienceResponse(statusCode, reason, WString::Empty, std::move(body)), completion); + } + + bool SocketHttpRequestContext::RespondBytes(vint statusCode, const WString& reason, const WString& contentType, const Array& body, Func completion) + { + ValidateConvenienceResponseArguments(statusCode, reason, contentType, body.Count()); + Array copy(body.Count()); + if (body.Count() > 0) memcpy(©[0], &body[0], body.Count()); + return Respond(CreateConvenienceResponse(statusCode, reason, contentType, std::move(copy)), completion); + } + + bool SocketHttpRequestContext::RespondUtf8(vint statusCode, const WString& reason, const WString& contentType, const WString& body, Func completion) + { + ValidateConvenienceResponseArguments(statusCode, reason, contentType, 0); + Array encoded; + CHECK_ERROR(EncodeStrictUtf8(body, encoded), L"The response body contains invalid Unicode."); + CHECK_ERROR(encoded.Count() <= HttpBodySizeLimit, L"The response body is too large."); + return Respond(CreateConvenienceResponse(statusCode, reason, contentType, std::move(encoded)), completion); + } + + bool SocketHttpRequestContext::Cancel() + { + return impl->Finish(false, true, false, true); + } +} + +namespace vl::inter_process::async_tcp_socket +{ + class SocketHttpServerApi::Impl : public Object + { + private: + SocketHttpServerApi* owner; + Ptr socketServer; + Ptr registration; + CriticalSection lock; + ConditionVariable cv; + bool startCalled = false; + bool startInProgress = false; + bool startSucceeded = false; + bool stopRequested = false; + bool stopRequestedNotify = false; + bool stopStarted = false; + bool stopFinished = false; + + void Request(Ptr context) + { + SocketHttpServerApi* target = nullptr; + CS_LOCK(lock) { target = owner; } + if (target) target->OnHttpRequestReceived(context); + else context->Cancel(); + } + + void Stopping() + { + SocketHttpServerApi* target = nullptr; + CS_LOCK(lock) { target = owner; } + if (target) target->OnHttpServerStopping(); + } + + void StopInternal(bool notify) + { + bool first = false; + bool notifyRegistration = false; + auto callbackDepth = ApiRegistration::Depth(registration.Obj()); + lock.Enter(); + if (startInProgress && callbackDepth > 0) + { + stopRequested = true; + stopRequestedNotify |= notify; + lock.Leave(); + return; + } + while (startInProgress) cv.SleepWith(lock); + if (!stopStarted) + { + stopStarted = true; + first = true; + notifyRegistration = notify && startSucceeded; + } + else if (callbackDepth > 0) + { + lock.Leave(); + return; + } + else + { + while (!stopFinished) cv.SleepWith(lock); + } + lock.Leave(); + if (!first) return; + + auto server = UnregisterApi(registration); + registration->Stop(notifyRegistration); + if (server) + { + auto shared = server.Cast(); + if (shared) shared->StopAndRelease(); + else server->Stop(); + } + CS_LOCK(lock) + { + stopFinished = true; + cv.WakeAllPendings(); + } + } + + public: + Impl(SocketHttpServerApi* _owner, Ptr _socketServer, const WString& urlPrefix, bool respondToOptions) + : owner(_owner) + , socketServer(_socketServer) + { +#define ERROR_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServerApi::SocketHttpServerApi(Ptr, const WString&, bool)#" + CHECK_ERROR(socketServer, ERROR_PREFIX L"Requires a server."); + registration = Ptr(new ApiRegistration(ParsePrefix(socketServer->GetPort(), urlPrefix), respondToOptions)); + registration->requestCallback = Func)>([this](Ptr context) { Request(context); }); + registration->stoppingCallback = Func([this]() { Stopping(); }); +#undef ERROR_PREFIX + } + + void Start() + { +#define ERROR_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServerApi::Start()#" + CS_LOCK(lock) + { + CHECK_ERROR(!startCalled && !stopStarted, ERROR_PREFIX L"Can only be called once before stopping."); + startCalled = true; + startInProgress = true; + } + try + { + RegisterApi(socketServer, registration); + } + catch (...) + { + bool stopAfterStart = false, notifyAfterStart = false; + CS_LOCK(lock) + { + startInProgress = false; + stopAfterStart = stopRequested; + notifyAfterStart = stopRequestedNotify; + stopRequested = false; + stopRequestedNotify = false; + cv.WakeAllPendings(); + } + if (stopAfterStart) + { + try { StopInternal(notifyAfterStart); } catch (...) {} + } + throw; + } + bool stopAfterStart = false, notifyAfterStart = false; + CS_LOCK(lock) + { + startSucceeded = true; + startInProgress = false; + stopAfterStart = stopRequested; + notifyAfterStart = stopRequestedNotify; + stopRequested = false; + stopRequestedNotify = false; + cv.WakeAllPendings(); + } + if (stopAfterStart) StopInternal(notifyAfterStart); +#undef ERROR_PREFIX + } + + void Stop() { StopInternal(true); } + + void Destroy() + { + CS_LOCK(lock) { owner = nullptr; } + StopInternal(false); + } + + bool IsStopped() + { + CS_LOCK(lock) + { + if (stopStarted) return true; + } + return registration->IsStopped(); + } + + WString GetUrlPrefix() { return registration->prefix.normalizedUrl; } + }; + + SocketHttpServerApi::SocketHttpServerApi(Ptr server, const WString& urlPrefix, bool respondToOptions) + : impl(Ptr(new Impl(this, server, urlPrefix, respondToOptions))) + { + } + + SocketHttpServerApi::~SocketHttpServerApi() { impl->Destroy(); } + void SocketHttpServerApi::OnHttpServerStopping() {} + void SocketHttpServerApi::Start() { impl->Start(); } + void SocketHttpServerApi::Stop() { impl->Stop(); } + bool SocketHttpServerApi::IsStopped() { return impl->IsStopped(); } + WString SocketHttpServerApi::GetUrlPrefix() { return impl->GetUrlPrefix(); } +} + + +/*********************************************************************** +.\INTERPROCESS\NETWORKPROTOCOLHTTP.CPP +***********************************************************************/ + +namespace vl::inter_process +{ + using namespace vl::collections; + + namespace + { + void EncodeUtf8(const WString& text, Array& utf8) + { + stream::MemoryStream memoryStream; + { + stream::Utf8Encoder encoder; + stream::EncoderStream encoderStream(memoryStream, encoder); + stream::StreamWriter writer(encoderStream); + writer.WriteString(text); + } + + utf8.Resize((vint)memoryStream.Size()); + if (utf8.Count() > 0) + { + memoryStream.SeekFromBegin(0); + memoryStream.Read(&utf8[0], utf8.Count()); + } + } + + WString DecodeUtf8(const void* buffer, vint size) + { + if (size == 0) + { + return WString::Empty; + } + + stream::MemoryWrapperStream memoryStream((void*)buffer, size); + stream::Utf8Decoder decoder; + stream::DecoderStream decoderStream(memoryStream, decoder); + stream::StreamReader reader(decoderStream); + return reader.ReadToEnd(); + } + + vint HexValue(char c) + { + if ('0' <= c && c <= '9') return c - '0'; + if ('a' <= c && c <= 'f') return c - 'a' + 10; + if ('A' <= c && c <= 'F') return c - 'A' + 10; + return -1; + } + + bool IsHttpNetworkProtocolPathCharacter(wchar_t c) + { + if (L'a' <= c && c <= L'z') return true; + if (L'A' <= c && c <= L'Z') return true; + if (L'0' <= c && c <= L'9') return true; + switch (c) + { + case L'-': case L'.': case L'_': case L'~': + case L'!': case L'$': case L'&': case L'\'': case L'(': + case L')': case L'*': case L'+': case L',': case L';': case L'=': + case L':': case L'@': case L'/': + return true; + default: + return false; + } + } + + bool ValidateHttpNetworkProtocolPath(const WString& path, bool allowEmpty, bool rejectTrailingSlash) + { + if (path.Length() == 0) return allowEmpty; + if (path[0] != L'/') return false; + if (rejectTrailingSlash && path[path.Length() - 1] == L'/') return false; + + List decoded; + for (vint i = 0; i < path.Length(); i++) + { + wchar_t c = path[i]; + if (c == L'%') + { + if (i + 2 >= path.Length()) return false; + wchar_t highChar = path[i + 1]; + wchar_t lowChar = path[i + 2]; + if (highChar > 0x7F || lowChar > 0x7F) return false; + vint high = HexValue((char)highChar); + vint low = HexValue((char)lowChar); + if (high == -1 || low == -1) return false; + vuint8_t byte = (vuint8_t)(high * 16 + low); + if (byte == 0 || byte == '/' || byte == '\\') return false; + decoded.Add(byte); + i += 2; + } + else + { + if (c > 0x7F || !IsHttpNetworkProtocolPathCharacter(c)) return false; + decoded.Add((vuint8_t)c); + } + } + + WString ignored; + return async_tcp_socket::DecodeStrictUtf8( + decoded.Count() == 0 ? nullptr : &decoded[0], + decoded.Count(), + ignored + ); + } + } + + WString CreateHttpNetworkProtocolConnectBody(const WString& requestPath, const WString& responsePath) + { + CHECK_ERROR(requestPath.Length() > 0, L"CreateHttpNetworkProtocolConnectBody(const WString&, const WString&): The request path cannot be empty."); + CHECK_ERROR(responsePath.Length() > 0, L"CreateHttpNetworkProtocolConnectBody(const WString&, const WString&): The response path cannot be empty."); + CHECK_ERROR(requestPath.IndexOf(L';') == -1, L"CreateHttpNetworkProtocolConnectBody(const WString&, const WString&): The request path cannot contain a semicolon."); + CHECK_ERROR(responsePath.IndexOf(L';') == -1, L"CreateHttpNetworkProtocolConnectBody(const WString&, const WString&): The response path cannot contain a semicolon."); + return requestPath + L";" + responsePath; + } + + bool ParseHttpNetworkProtocolConnectBody(const WString& body, WString& requestPath, WString& responsePath) + { + vint delimiter = body.IndexOf(L';'); + if (delimiter <= 0 || delimiter == body.Length() - 1) return false; + if (body.Right(body.Length() - delimiter - 1).IndexOf(L';') != -1) return false; + + WString parsedRequestPath = body.Left(delimiter); + WString parsedResponsePath = body.Right(body.Length() - delimiter - 1); + requestPath = parsedRequestPath; + responsePath = parsedResponsePath; + return true; + } + + bool ValidateHttpNetworkProtocolBaseUrl(const WString& baseUrl) + { + return ValidateHttpNetworkProtocolPath(baseUrl, true, true); + } + + bool ValidateHttpNetworkProtocolEndpointPath(const WString& path) + { + return ValidateHttpNetworkProtocolPath(path, false, false); + } + + bool IsValidHttpNetworkProtocolMessage(const WString& message) + { + if (message.Length() == 0) return false; + for (vint i = 0; i < message.Length(); i++) + { + if (message[i] == 0) return false; + } + return true; + } + + WString HttpUrlEncodeQuery(const WString& query) + { + Array utf8; + EncodeUtf8(query, utf8); + + Array encoded(utf8.Count() * 3 + 1); + wchar_t* writing = &encoded[0]; + for (vint i = 0; i < utf8.Count(); i++) + { + vuint8_t x = (vuint8_t)utf8[i]; + if ((L'a' <= x && x <= L'z') || (L'A' <= x && x <= L'Z') || (L'0' <= x && x <= L'9')) + { + *writing++ = (wchar_t)x; + } + else + { + *writing++ = L'%'; + *writing++ = L"0123456789ABCDEF"[x / 16]; + *writing++ = L"0123456789ABCDEF"[x % 16]; + } + } + *writing = 0; + return &encoded[0]; + } + + WString HttpUrlDecodeQuery(const WString& query) + { + Array encoded; + EncodeUtf8(query, encoded); + + List utf8; + for (vint i = 0; i < encoded.Count(); i++) + { + char c = encoded[i]; + if (c == '%' && i + 2 < encoded.Count()) + { + vint high = HexValue(encoded[i + 1]); + vint low = HexValue(encoded[i + 2]); + if (high != -1 && low != -1) + { + utf8.Add((char)(high * 16 + low)); + i += 2; + continue; + } + } + + utf8.Add(c == '+' ? ' ' : c); + } + + return utf8.Count() == 0 + ? WString::Empty + : DecodeUtf8(&utf8[0], utf8.Count()); + } +} + +namespace vl::inter_process::windows_http +{ + void HttpRequest::SetBodyUtf8(const WString& bodyString) + { + EncodeUtf8(bodyString, body); + } + + bool HttpResponse::TryGetBodyUtf8(WString& bodyString) const + { + return ::vl::inter_process::async_tcp_socket::DecodeStrictUtf8( + body.Count() == 0 ? nullptr : reinterpret_cast(&body[0]), + body.Count(), + bodyString + ); + } + + WString HttpResponse::GetBodyUtf8() const + { + return body.Count() == 0 + ? WString::Empty + : DecodeUtf8(&body[0], body.Count()); + } +} + +namespace vl::inter_process +{ + windows_http::HttpRequest CreateHttpNetworkProtocolConnectRequest(const WString& target) + { + windows_http::HttpRequest request; + request.method = L"GET"; + request.query = target; + request.acceptTypes.Add(HttpNetworkProtocolContentType); + return request; + } + + windows_http::HttpRequest CreateHttpNetworkProtocolReceiveRequest(const WString& target) + { + windows_http::HttpRequest request; + request.method = L"POST"; + request.query = target; + request.acceptTypes.Add(HttpNetworkProtocolContentType); + request.extraHeaders.Add(L"Content-Length", L"0"); + return request; + } + + windows_http::HttpRequest CreateHttpNetworkProtocolSendRequest(const WString& target, const Array& body) + { + windows_http::HttpRequest request; + request.method = L"POST"; + request.query = target; + request.acceptTypes.Add(HttpNetworkProtocolContentType); + request.contentType = HttpNetworkProtocolContentType; + request.body.Resize(body.Count()); + for (vint i = 0; i < body.Count(); i++) + { + request.body[i] = body[i]; + } + return request; + } +} + diff --git a/Import/VlppOS.h b/Import/VlppOS.h index 62ad9299..6510ea60 100644 --- a/Import/VlppOS.h +++ b/Import/VlppOS.h @@ -1770,8 +1770,6 @@ Interfaces: #define VCZH_INTERPROCESS_ASYNCSOCKET #include -#include -#include #include #include @@ -1793,13 +1791,13 @@ namespace vl::inter_process::async_tcp_socket /// Called with one positive borrowed read block. virtual void OnRead(const vuint8_t* buffer, vint size) = 0; /// Called after the complete retained write buffer has been sent. - virtual void OnWriteCompleted(Ptr buffer) {} + virtual void OnWriteCompleted(Ptr buffer); /// Called when an asynchronous operation fails. - virtual void OnError(const WString& error, bool fatal) {} + virtual void OnError(const WString& error, bool fatal); /// Called for the client connection after it is established. - virtual void OnConnected() {} + virtual void OnConnected(); /// Called exactly once when the connection stops. - virtual void OnDisconnected() {} + virtual void OnDisconnected(); /// Called synchronously when this callback is installed. virtual void OnInstalled(IAsyncSocketConnection* connection) = 0; }; @@ -1818,17 +1816,49 @@ namespace vl::inter_process::async_tcp_socket class IAsyncSocketClient : public virtual Interface { public: + /// Returns the immutable loopback port selected during construction. + virtual vint GetPort() = 0; + /// Creates a fresh independent client with the same transport configuration and endpoint. + /// The returned client must be distinct, report the same port, and have status. This operation remains available while the source client is active or stopped. + virtual Ptr CreateSameEndpointClient() = 0; virtual IAsyncSocketConnection* GetConnection() = 0; virtual void WaitForServer() = 0; virtual ClientStatus GetStatus() = 0; }; + enum class AsyncSocketServerStartFailure + { + AddressInUse, + Other, + }; + + class AsyncSocketServerStartException : public Exception + { + private: + AsyncSocketServerStartFailure failure; + + public: + AsyncSocketServerStartException(AsyncSocketServerStartFailure _failure, const WString& message); + + AsyncSocketServerStartFailure GetFailure()const; + }; + + /// Callbacks for accepting asynchronous TCP connections. + class IAsyncSocketServerCallback : public virtual Interface + { + public: + virtual WaitForClientResult OnClientConnected(IAsyncSocketConnection* connection) = 0; + /// Called exactly once when a started listener stops unexpectedly. + virtual void OnServerStopped(); + }; + /// An asynchronous TCP server for the local machine. class IAsyncSocketServer : public virtual Interface { public: - virtual WaitForClientResult OnClientConnected(IAsyncSocketConnection* connection) = 0; - virtual void Start() = 0; + /// Returns the immutable loopback port selected during construction. + virtual vint GetPort() = 0; + virtual void Start(IAsyncSocketServerCallback* callback) = 0; virtual void Stop() = 0; virtual bool IsStopped() = 0; }; @@ -1838,6 +1868,9 @@ namespace vl::inter_process::async_tcp_socket constexpr vint AsyncSocketClientRetryCount = 50; constexpr vint AsyncSocketClientRetryDelay = 100; + extern Ptr CreateDefaultAsyncSocketServer(vint port); + extern Ptr CreateDefaultAsyncSocketClient(vint port); + /*********************************************************************** NetworkProtocolConnection ***********************************************************************/ @@ -1848,7 +1881,7 @@ NetworkProtocolConnection struct CallbackFrame; private: - inline static thread_local CallbackFrame* currentCallbackFrame = nullptr; + static thread_local CallbackFrame* currentCallbackFrame; CriticalSection lockState; ConditionVariable cvState; vint activeCallbacks = 0; @@ -1859,57 +1892,12 @@ NetworkProtocolConnection Ptr domain; CallbackFrame* previous = nullptr; - CallbackFrame(Ptr _domain) - : domain(_domain) - { - if (domain) - { - previous = currentCallbackFrame; - currentCallbackFrame = this; - CS_LOCK(domain->lockState) - { - domain->activeCallbacks++; - } - } - } - - ~CallbackFrame() - { - if (domain) - { - currentCallbackFrame = previous; - CS_LOCK(domain->lockState) - { - domain->activeCallbacks--; - domain->cvState.WakeAllPendings(); - } - } - } + CallbackFrame(Ptr _domain); + ~CallbackFrame(); }; - vint CurrentCallbackDepth() - { - vint depth = 0; - for (auto frame = currentCallbackFrame; frame; frame = frame->previous) - { - if (frame->domain.Obj() == this) - { - depth++; - } - } - return depth; - } - - void WaitForCallbacks(vint callbackDepth) - { - CS_LOCK(lockState) - { - while (activeCallbacks > callbackDepth) - { - cvState.SleepWith(lockState); - } - } - } + vint CurrentCallbackDepth(); + void WaitForCallbacks(vint callbackDepth); }; class NetworkProtocolConnectionLifecycle : public Object @@ -1945,13 +1933,7 @@ NetworkProtocolConnection vint characterBytesReceived = 0; bool parserFailed = false; - void TakeRetainedAdapterIfDrained(Ptr& releasing) - { - if (stopFinished && disconnectFinished && activeCallbacks == 0 && activeSocketCallbacks == 0 && activeSocketCalls == 0) - { - releasing = std::move(retainedAdapter); - } - } + void TakeRetainedAdapterIfDrained(Ptr& releasing); }; /// Adapts an asynchronous byte stream to framed network-protocol strings. @@ -1966,9 +1948,9 @@ NetworkProtocolConnection struct CallbackFrame; struct SocketCallbackFrame; - inline static thread_local CallbackFrame* currentCallbackFrame = nullptr; - inline static thread_local SocketCallbackFrame* - currentSocketCallbackFrame = nullptr; + static thread_local CallbackFrame* currentCallbackFrame; + static thread_local SocketCallbackFrame* + currentSocketCallbackFrame; struct CallbackFrame { @@ -1977,25 +1959,8 @@ NetworkProtocolConnection NetworkProtocolCallbackDomain::CallbackFrame domainFrame; - CallbackFrame(Ptr _state) - : state(_state) - , previous(currentCallbackFrame) - , domainFrame(state->callbackDomain) - { - currentCallbackFrame = this; - } - - ~CallbackFrame() - { - currentCallbackFrame = previous; - Ptr releasing; - CS_LOCK(state->lockState) - { - state->activeCallbacks--; - state->TakeRetainedAdapterIfDrained(releasing); - state->cvState.WakeAllPendings(); - } - } + CallbackFrame(Ptr _state); + ~CallbackFrame(); }; struct SocketCallbackFrame @@ -2003,347 +1968,24 @@ NetworkProtocolConnection Ptr state; SocketCallbackFrame* previous = nullptr; - SocketCallbackFrame(Ptr _state) - : state(_state) - , previous(currentSocketCallbackFrame) - { - currentSocketCallbackFrame = this; - CS_LOCK(state->lockState) - { - state->activeSocketCallbacks++; - } - } - - ~SocketCallbackFrame() - { - currentSocketCallbackFrame = previous; - Ptr releasing; - CS_LOCK(state->lockState) - { - state->activeSocketCallbacks--; - state->TakeRetainedAdapterIfDrained(releasing); - state->cvState.WakeAllPendings(); - } - } + SocketCallbackFrame(Ptr _state); + ~SocketCallbackFrame(); }; Ptr lifecycle; - static vint CurrentCallbackDepth(Ptr state) - { - vint depth = 0; - for (auto frame = currentCallbackFrame; frame; frame = frame->previous) - { - if (frame->state.Obj() == state.Obj()) - { - depth++; - } - } - return depth; - } - - static vint CurrentSocketCallbackDepth(Ptr state) - { - vint depth = 0; - for (auto frame = currentSocketCallbackFrame; frame; frame = frame->previous) - { - if (frame->state.Obj() == state.Obj()) - { - depth++; - } - } - return depth; - } - - static void FinishSocketCall(Ptr state) - { - CS_LOCK(state->lockState) - { - state->activeSocketCalls--; - state->cvState.WakeAllPendings(); - } - } + static vint CurrentCallbackDepth(Ptr state); + static vint CurrentSocketCallbackDepth(Ptr state); + static void FinishSocketCall(Ptr state); template - static void InvokeProtocolCallback(Ptr state, bool allowTerminal, TCallback&& invoke) - { - INetworkProtocolCallback* installed = nullptr; - auto callbackDepth = CurrentCallbackDepth(state); - state->lockState.Enter(); - while (state->callbackInstalling && callbackDepth == 0 && state->callback) - { - state->cvState.SleepWith(state->lockState); - } - if (state->callback && (allowTerminal || (!state->stopStarted && !state->terminal))) - { - installed = state->callback; - state->activeCallbacks++; - } - state->lockState.Leave(); + static void InvokeProtocolCallback(Ptr state, bool allowTerminal, TCallback&& invoke); - if (installed) - { - CallbackFrame frame(state); - invoke(installed); - } - } - - static void SubmitWrite(Ptr state, IAsyncSocketConnection* connection, Ptr buffer) - { - try - { - connection->WriteAsync(buffer); - } - catch (...) - { - CS_LOCK(state->lockState) - { - state->queuedWrites.Clear(); - state->writePending = false; - state->cvState.WakeAllPendings(); - } - FinishSocketCall(state); - throw; - } - FinishSocketCall(state); - } - - static void NotifyProtocolDisconnected(Ptr state) - { - auto callbackDepth = CurrentCallbackDepth(state); - state->lockState.Enter(); - if (!state->disconnectedNotified) - { - state->disconnectedNotified = true; - state->terminal = true; - state->queuedWrites.Clear(); - state->writePending = false; - state->cvState.WakeAllPendings(); - } - if (state->disconnectFinished) - { - state->lockState.Leave(); - return; - } - - if (state->disconnectDelivering) - { - if (callbackDepth == 0) - { - while (!state->disconnectFinished) - { - state->cvState.SleepWith(state->lockState); - } - } - state->lockState.Leave(); - return; - } - - if (callbackDepth == 0) - { - while (state->activeCallbacks > 0 && !state->disconnectDelivering && !state->disconnectFinished) - { - state->cvState.SleepWith(state->lockState); - } - if (state->disconnectFinished) - { - state->lockState.Leave(); - return; - } - if (state->disconnectDelivering) - { - while (!state->disconnectFinished) - { - state->cvState.SleepWith(state->lockState); - } - state->lockState.Leave(); - return; - } - } - - state->disconnectDelivering = true; - while (state->activeCallbacks > callbackDepth) - { - state->cvState.SleepWith(state->lockState); - } - state->lockState.Leave(); - - try - { - InvokeProtocolCallback(state, true, [](INetworkProtocolCallback* installed) - { - installed->OnDisconnected(); - }); - } - catch (...) - { - Ptr releasing; - CS_LOCK(state->lockState) - { - state->callback = nullptr; - while (state->activeCallbacks > callbackDepth) - { - state->cvState.SleepWith(state->lockState); - } - state->disconnectFinished = true; - state->TakeRetainedAdapterIfDrained(releasing); - state->cvState.WakeAllPendings(); - } - throw; - } - - Ptr releasing; - CS_LOCK(state->lockState) - { - state->callback = nullptr; - while (state->activeCallbacks > callbackDepth) - { - state->cvState.SleepWith(state->lockState); - } - state->disconnectFinished = true; - state->TakeRetainedAdapterIfDrained(releasing); - state->cvState.WakeAllPendings(); - } - } - - static void DetachSocketCallback(Ptr state, IAsyncSocketConnection* connection) - { - if (connection) - { - connection->InstallCallback(nullptr); - } - CS_LOCK(state->lockState) - { - if (state->socketConnection == connection) - { - state->socketConnection = nullptr; - } - state->cvState.WakeAllPendings(); - } - } - - static void StopConnection(Ptr state, Ptr retainedAdapter = nullptr) - { - auto callbackDepth = CurrentCallbackDepth(state); - auto socketCallbackDepth = CurrentSocketCallbackDepth(state); - auto nestedCallback = callbackDepth > 0 || socketCallbackDepth > 0; - IAsyncSocketConnection* connection = nullptr; - bool executeStop = false; - bool nestedFollower = false; - Ptr releasing; - - state->lockState.Enter(); - if (retainedAdapter) - { - state->retainedAdapter = retainedAdapter; - } - if (!state->stopStarted) - { - state->stopStarted = true; - if (!nestedCallback && !state->terminal && state->queuedWrites.Count() > 0) - { - state->drainWrites = true; - auto deadline = DateTime::LocalTime().osMilliseconds + WriteDrainTimeout; - while (state->queuedWrites.Count() > 0 && !state->terminal) - { - auto now = DateTime::LocalTime().osMilliseconds; - if (now >= deadline) - { - break; - } - state->cvState.SleepWithForTime(state->lockState, (vint)(deadline - now)); - } - state->drainWrites = false; - } - state->queuedWrites.Clear(); - state->writePending = false; - while (state->activeSocketCalls > 0) - { - state->cvState.SleepWith(state->lockState); - } - connection = state->socketConnection; - executeStop = true; - } - else if (nestedCallback) - { - connection = state->socketConnection; - nestedFollower = true; - } - else - { - while (!state->stopFinished) - { - state->cvState.SleepWith(state->lockState); - } - while (state->activeCallbacks > 0 || state->activeSocketCallbacks > 0 || state->activeSocketCalls > 0) - { - state->cvState.SleepWith(state->lockState); - } - state->TakeRetainedAdapterIfDrained(releasing); - state->lockState.Leave(); - return; - } - state->lockState.Leave(); - - if (nestedFollower) - { - if (connection && socketCallbackDepth > 0) - { - connection->Stop(); - } - NotifyProtocolDisconnected(state); - return; - } - - if (executeStop && connection) - { - connection->Stop(); - } - NotifyProtocolDisconnected(state); - - CS_LOCK(state->lockState) - { - while (state->activeCallbacks > callbackDepth || state->activeSocketCallbacks > socketCallbackDepth) - { - state->cvState.SleepWith(state->lockState); - } - state->stopFinished = true; - state->TakeRetainedAdapterIfDrained(releasing); - state->cvState.WakeAllPendings(); - } - } - - static void ReportFatalError(Ptr state, const WString& error) - { - bool report = false; - CS_LOCK(state->lockState) - { - if (!state->terminal && !state->stopStarted) - { - state->terminal = true; - state->queuedWrites.Clear(); - state->writePending = false; - state->cvState.WakeAllPendings(); - report = true; - } - } - if (report) - { - try - { - InvokeProtocolCallback(state, true, [&](INetworkProtocolCallback* installed) - { - installed->OnLocalError(error, true); - }); - } - catch (...) - { - StopConnection(state); - throw; - } - StopConnection(state); - } - } + static void SubmitWrite(Ptr state, IAsyncSocketConnection* connection, Ptr buffer); + static void NotifyProtocolDisconnected(Ptr state); + static void DetachSocketCallback(Ptr state, IAsyncSocketConnection* connection); + static void StopConnection(Ptr state, Ptr retainedAdapter = nullptr); + static void ReportFatalError(Ptr state, const WString& error); template friend class NetworkProtocolServer; @@ -2351,354 +1993,22 @@ NetworkProtocolConnection template friend class NetworkProtocolClient; - void StopWithRetainedAdapter(Ptr retainedAdapter) - { - StopConnection(lifecycle, retainedAdapter); - } + void StopWithRetainedAdapter(Ptr retainedAdapter); public: - explicit NetworkProtocolConnection(IAsyncSocketConnection* connection, Ptr callbackDomain = nullptr) - : lifecycle(Ptr(new Lifecycle)) - { - CHECK_ERROR(connection, L"NetworkProtocolConnection requires a valid async socket connection."); - lifecycle->socketConnection = connection; - lifecycle->callbackDomain = callbackDomain; - connection->InstallCallback(this); - } + explicit NetworkProtocolConnection(IAsyncSocketConnection* connection, Ptr callbackDomain = nullptr); + ~NetworkProtocolConnection(); - ~NetworkProtocolConnection() - { - StopConnection(lifecycle); - } - - void InstallCallback(INetworkProtocolCallback* value) override - { - auto state = lifecycle; - if (!value) - { - auto callbackDepth = CurrentCallbackDepth(state); - bool uninstallOwner = false; - CS_LOCK(state->lockState) - { - uninstallOwner = state->callback != nullptr; - state->callback = nullptr; - while ((callbackDepth == 0 || uninstallOwner) && state->activeCallbacks > callbackDepth) - { - state->cvState.SleepWith(state->lockState); - } - } - return; - } - - bool canInstall = false; - CS_LOCK(state->lockState) - { - if (!state->callback && !state->callbackInstalling && !state->stopStarted && !state->terminal) - { - state->callback = value; - state->callbackInstalling = true; - state->activeCallbacks++; - canInstall = true; - } - } - CHECK_ERROR(canInstall, L"NetworkProtocolConnection::InstallCallback cannot replace a callback or install one on a stopped connection."); - - CallbackFrame frame(state); - try - { - value->OnInstalled(this); - } - catch (...) - { - CS_LOCK(state->lockState) - { - if (state->callback == value) - { - state->callback = nullptr; - } - state->callbackInstalling = false; - state->cvState.WakeAllPendings(); - } - throw; - } - - CS_LOCK(state->lockState) - { - state->callbackInstalling = false; - state->cvState.WakeAllPendings(); - } - } - - void BeginReadingLoopUnsafe() override - { - auto state = lifecycle; - IAsyncSocketConnection* connection = nullptr; - CS_LOCK(state->lockState) - { - if (!state->stopStarted && !state->terminal && state->socketConnection) - { - connection = state->socketConnection; - state->activeSocketCalls++; - } - } - if (!connection) - { - return; - } - - try - { - connection->BeginReadingLoopUnsafe(); - } - catch (...) - { - FinishSocketCall(state); - throw; - } - FinishSocketCall(state); - } - - void SendString(const WString& str) override - { - auto state = lifecycle; - auto length = str.Length(); - CHECK_ERROR(length >= 0 && length <= (std::numeric_limits::max)(), L"NetworkProtocolConnection::SendString cannot encode a string longer than vint32_t."); - CHECK_ERROR((size_t)length <= ((size_t)(std::numeric_limits::max)() - sizeof(vint32_t)) / sizeof(wchar_t), L"NetworkProtocolConnection::SendString frame size overflow."); - - auto buffer = Ptr(new AsyncSocketBuffer); - auto characterBytes = (size_t)length * sizeof(wchar_t); - auto frameBytes = sizeof(vint32_t) + characterBytes; - buffer->data.Resize((vint)frameBytes); - auto encodedLength = (vint32_t)length; - std::memcpy(&buffer->data[0], &encodedLength, sizeof(encodedLength)); - if (characterBytes > 0) - { - std::memcpy(&buffer->data[sizeof(encodedLength)], str.Buffer(), characterBytes); - } - - IAsyncSocketConnection* connection = nullptr; - Ptr submitting; - CS_LOCK(state->lockState) - { - if (!state->stopStarted && !state->terminal && state->socketConnection) - { - state->queuedWrites.Add(buffer); - if (!state->writePending) - { - state->writePending = true; - connection = state->socketConnection; - submitting = state->queuedWrites[0]; - state->activeSocketCalls++; - } - } - } - if (submitting) - { - SubmitWrite(state, connection, submitting); - } - } - - void Stop() override - { - StopConnection(lifecycle); - } - - void OnRead(const vuint8_t* buffer, vint size) override - { - auto state = lifecycle; - SocketCallbackFrame socketCallbackFrame(state); - if (!buffer || size <= 0) - { - return; - } - - collections::List completedStrings; - bool malformed = false; - CS_LOCK(state->lockParser) - { - if (state->parserFailed) - { - return; - } - - auto reading = buffer; - auto available = size; - while (available > 0) - { - if (state->expectedCharacters == -1) - { - auto required = (vint)sizeof(vint32_t) - state->lengthBytesReceived; - auto copied = required < available ? required : available; - std::memcpy(state->lengthBytes + state->lengthBytesReceived, reading, (size_t)copied); - state->lengthBytesReceived += copied; - reading += copied; - available -= copied; - if (state->lengthBytesReceived < (vint)sizeof(vint32_t)) - { - continue; - } - - std::memcpy(&state->expectedCharacters, state->lengthBytes, sizeof(state->expectedCharacters)); - state->lengthBytesReceived = 0; - if (state->expectedCharacters < 0 || (size_t)state->expectedCharacters > ((size_t)(std::numeric_limits::max)() - sizeof(vint32_t)) / sizeof(wchar_t)) - { - state->parserFailed = true; - malformed = true; - break; - } - - state->characterBuffer.Resize((vint)state->expectedCharacters); - state->characterBytesReceived = 0; - if (state->expectedCharacters == 0) - { - completedStrings.Add(WString()); - state->expectedCharacters = -1; - } - } - - if (state->expectedCharacters >= 0) - { - auto characterBytes = (vint)((size_t)state->expectedCharacters * sizeof(wchar_t)); - auto required = characterBytes - state->characterBytesReceived; - auto copied = required < available ? required : available; - std::memcpy((vuint8_t*)&state->characterBuffer[0] + state->characterBytesReceived, reading, (size_t)copied); - state->characterBytesReceived += copied; - reading += copied; - available -= copied; - if (state->characterBytesReceived == characterBytes) - { - completedStrings.Add(WString::CopyFrom(&state->characterBuffer[0], state->expectedCharacters)); - state->characterBuffer.Resize(0); - state->characterBytesReceived = 0; - state->expectedCharacters = -1; - } - } - } - } - - for (auto&& str : completedStrings) - { - InvokeProtocolCallback(state, false, [&](INetworkProtocolCallback* installed) - { - installed->OnReadString(str); - }); - } - if (malformed) - { - ReportFatalError(state, L"The async socket protocol received an invalid string length."); - } - } - - void OnWriteCompleted(Ptr buffer) override - { - auto state = lifecycle; - SocketCallbackFrame socketCallbackFrame(state); - IAsyncSocketConnection* connection = nullptr; - Ptr submitting; - bool mismatched = false; - CS_LOCK(state->lockState) - { - if (state->queuedWrites.Count() == 0) - { - return; - } - if (state->queuedWrites[0].Obj() != buffer.Obj()) - { - state->queuedWrites.Clear(); - state->writePending = false; - mismatched = true; - state->cvState.WakeAllPendings(); - } - else - { - state->queuedWrites.RemoveAt(0); - if ((!state->stopStarted || state->drainWrites) && !state->terminal && state->socketConnection && state->queuedWrites.Count() > 0) - { - connection = state->socketConnection; - submitting = state->queuedWrites[0]; - state->activeSocketCalls++; - } - else - { - state->writePending = false; - } - state->cvState.WakeAllPendings(); - } - } - CHECK_ERROR(!mismatched, L"NetworkProtocolConnection received a completion for an unexpected async socket buffer."); - if (submitting) - { - SubmitWrite(state, connection, submitting); - } - } - - void OnError(const WString& error, bool fatal) override - { - auto state = lifecycle; - SocketCallbackFrame socketCallbackFrame(state); - if (fatal) - { - ReportFatalError(state, error); - } - else - { - InvokeProtocolCallback(state, false, [&](INetworkProtocolCallback* installed) - { - installed->OnLocalError(error, false); - }); - } - } - - void OnConnected() override - { - auto state = lifecycle; - SocketCallbackFrame socketCallbackFrame(state); - InvokeProtocolCallback(state, false, [](INetworkProtocolCallback* installed) - { - installed->OnConnected(); - }); - } - - void OnDisconnected() override - { - auto state = lifecycle; - SocketCallbackFrame socketCallbackFrame(state); - IAsyncSocketConnection* connection = nullptr; - CS_LOCK(state->lockState) - { - while (state->activeSocketCalls > 0) - { - state->cvState.SleepWith(state->lockState); - } - connection = state->socketConnection; - } - - try - { - DetachSocketCallback(state, connection); - } - catch (...) - { - CS_LOCK(state->lockState) - { - if (state->socketConnection == connection) - { - state->socketConnection = nullptr; - } - state->cvState.WakeAllPendings(); - } - NotifyProtocolDisconnected(state); - throw; - } - NotifyProtocolDisconnected(state); - } - - void OnInstalled(IAsyncSocketConnection* connection) override - { - auto state = lifecycle; - SocketCallbackFrame socketCallbackFrame(state); - CHECK_ERROR(connection == state->socketConnection, L"NetworkProtocolConnection was installed on an unexpected async socket connection."); - } + void InstallCallback(INetworkProtocolCallback* value) override; + void BeginReadingLoopUnsafe() override; + void SendString(const WString& str) override; + void Stop() override; + void OnRead(const vuint8_t* buffer, vint size) override; + void OnWriteCompleted(Ptr buffer) override; + void OnError(const WString& error, bool fatal) override; + void OnConnected() override; + void OnDisconnected() override; + void OnInstalled(IAsyncSocketConnection* connection) override; }; /*********************************************************************** @@ -2733,7 +2043,9 @@ NetworkProtocolServer } }; - class SocketServerBridge : public TAsyncSocketServer + class SocketServerBridge + : public TAsyncSocketServer + , public virtual IAsyncSocketServerCallback { private: Ptr lifecycle; @@ -2889,7 +2201,7 @@ NetworkProtocolServer void Start() override { - asyncSocketServer->Start(); + asyncSocketServer->Start(asyncSocketServer.Obj()); } void Stop() override @@ -7063,3 +6375,807 @@ Serialization (macros) #endif + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\HTTPREQUEST.H +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Interfaces: + IHttpRequest(Connection|Callback) + +***********************************************************************/ + +#ifndef VCZH_INTERPROCESS_ASYNCSOCKET_HTTPREQUEST +#define VCZH_INTERPROCESS_ASYNCSOCKET_HTTPREQUEST + + +namespace vl::inter_process::async_tcp_socket +{ + constexpr vint HttpIncompleteMessageTimeout = 30 * 1000; + constexpr vint HttpRequestLineSizeLimit = 8 * 1024; + constexpr vint HttpHeaderBlockSizeLimit = 64 * 1024; + constexpr vint HttpBodySizeLimit = 16 * 1024 * 1024; + constexpr vint HttpChunkSizeLineLimit = 4 * 1024; + constexpr vint HttpTrailerBlockSizeLimit = 64 * 1024; + + struct HttpVersion + { + vint major = 1; + vint minor = 1; + }; + + struct HttpField + { + WString name; + collections::Array value; + }; + + enum class HttpFramingKind + { + None, + ContentLength, + Chunked, + }; + + enum class HttpFramingAnalysisResult + { + Succeeded, + Invalid, + UnsupportedTransferCoding, + }; + + struct HttpFraming + { + HttpFramingKind kind = HttpFramingKind::None; + vuint64_t contentLength = 0; + vint contentLengthFieldCount = 0; + vint contentLengthValueCount = 0; + bool contentLengthValuesPlainDecimal = true; + bool connectionClose = false; + }; + + struct HttpBodyChunk + { + collections::Array data; + }; + + struct HttpBody + { + collections::List chunks; + collections::List trailers; + }; + + extern HttpFramingAnalysisResult AnalyzeHttpFraming(const collections::List& fields, HttpFraming& framing); + extern const HttpField* FindHttpField(const collections::List& fields, const WString& normalizedName); + extern vint CountHttpFields(const collections::List& fields, const WString& normalizedName); + extern HttpField CreateAsciiHttpField(const WString& name, const WString& value); + extern bool DecodeAsciiHttpFieldValue(const collections::Array& value, WString& text); + extern bool HttpFieldValueEqualsAscii(const collections::Array& value, const WString& expected); + extern bool TryGetHttpBodySize(const HttpBody& body, vint& size); + extern bool FlattenHttpBody(const HttpBody& body, collections::Array& bytes); + extern void SetHttpBodyBytes(HttpBody& body, collections::Array&& bytes); + extern bool EncodeStrictUtf8(const WString& text, collections::Array& bytes); + extern bool DecodeStrictUtf8(const vuint8_t* bytes, vint count, WString& text); + + enum class HttpRequestLineValidationResult + { + Succeeded, + InvalidMethod, + InvalidRequestTarget, + TooLong, + }; + + extern HttpRequestLineValidationResult ValidateHttpRequestLine(const WString& method, const WString& requestTarget); + + class HttpRequest : public Object + { + public: + HttpVersion version; + WString method; + WString requestTarget; + collections::List headers; + HttpBody body; + }; + + class HttpResponse : public Object + { + public: + HttpVersion version; + vint statusCode = 200; + WString reason; + collections::List headers; + HttpBody body; + }; + + enum class HttpResponseFailure + { + NotFound = 404, + }; + + enum class HttpRequestBodyParsingResult + { + Succeeded, + Incomplete, + Invalid, + }; + + enum class HttpRequestFailure + { + BadRequest = 400, + RequestTimeout = 408, + PayloadTooLarge = 413, + UriTooLong = 414, + ExpectationFailed = 417, + RequestHeaderFieldsTooLarge = 431, + NotImplemented = 501, + HttpVersionNotSupported = 505, + }; + + extern HttpRequestBodyParsingResult ParseHttpRequestBodyToChunks( + const vuint8_t* buffer, + vint availableBytes, + HttpBody& output, + vint& consumedBytes + ); + + class IHttpRequestConnection; + + class IHttpRequestCallback : public virtual Interface + { + public: + virtual void OnReadRequest(Ptr request); + virtual void OnReadRequestFailure(HttpRequestFailure failure); + virtual void OnReadResponse(Ptr response); + virtual void OnReadResponseFailure(HttpResponseFailure failure); + virtual void OnWriteCompleted(); + virtual void OnError(const WString& error, bool fatal); + virtual void OnConnected(); + virtual void OnDisconnected(); + virtual void OnInstalled(IHttpRequestConnection* connection) = 0; + }; + + class IHttpRequestConnection : public virtual Interface + { + public: + virtual void InstallCallback(IHttpRequestCallback* callback) = 0; + virtual void BeginReadingLoopUnsafe() = 0; + virtual void SendRequest(Ptr request, vint responseTimeout = HttpIncompleteMessageTimeout) = 0; + virtual void SendResponse(Ptr response) = 0; + virtual void Stop() = 0; + }; +} + +#endif + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPREQUEST.H +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Async Socket HTTP/1.1 Connection + +***********************************************************************/ + +#ifndef VCZH_INTERPROCESS_ASYNCSOCKET_HTTPREQUESTIMPL +#define VCZH_INTERPROCESS_ASYNCSOCKET_HTTPREQUESTIMPL + + +namespace vl::inter_process::async_tcp_socket +{ + enum class HttpRequestConnectionDirection + { + Server, + Client, + }; + + class IHttpRequestTimeoutController : public virtual Interface + { + public: + virtual void Arm(vint milliseconds, const Func& callback) = 0; + virtual void Refresh() = 0; + virtual void CancelAndWait() = 0; + }; + + extern Ptr CreateHttpRequestTimeoutController(); + + class HttpRequestCallbackDomain : public Object + { + public: + struct CallbackFrame; + + private: + static thread_local CallbackFrame* currentCallbackFrame; + CriticalSection lockState; + ConditionVariable cvState; + vint activeCallbacks = 0; + + public: + struct CallbackFrame + { + Ptr domain; + CallbackFrame* previous = nullptr; + + CallbackFrame(Ptr _domain); + ~CallbackFrame(); + }; + + vint CurrentCallbackDepth(); + void WaitForCallbacks(vint callbackDepth); + }; + + class HttpRequestConnectionLifecycle; + + class HttpRequestConnection final + : public Object + , public virtual IHttpRequestConnection + , public virtual IAsyncSocketCallback + { + private: + using Lifecycle = HttpRequestConnectionLifecycle; + struct CallbackFrame; + struct SocketCallbackFrame; + struct TimeoutCallbackFrame; + static thread_local CallbackFrame* currentCallbackFrame; + static thread_local SocketCallbackFrame* + currentSocketCallbackFrame; + static thread_local TimeoutCallbackFrame* + currentTimeoutCallbackFrame; + + Ptr lifecycle; + + static vint CurrentCallbackDepth(Ptr state); + static vint CurrentSocketCallbackDepth(Ptr state); + static vint CurrentTimeoutCallbackDepth(Ptr state); + static void FinishSocketCall(Ptr state); + + template + static void InvokeHttpCallback(Ptr state, bool allowTerminal, TCallback&& invoke); + + static void SubmitWrite(Ptr state, IAsyncSocketConnection* connection, Ptr buffer); + static void InstallTimeout(Ptr state, vint milliseconds, const WString& error); + static void RefreshTimeout(Ptr state); + static void ReportRequestFailure(Ptr state, HttpRequestFailure failure, bool timeoutOnly = false, bool reserved = false); + static void ReportResponseFailure(Ptr state, HttpResponseFailure failure); + static void DeliverResponse(Ptr state, Ptr response, bool closeAfterDelivery); + static void ProcessBufferedInput(Ptr state); + static void NotifyDisconnected(Ptr state); + static void StopConnection(Ptr state, Ptr retainedAdapter = nullptr); + static void ReportFatalError(Ptr state, const WString& error); + + public: + HttpRequestConnection( + IAsyncSocketConnection* connection, + HttpRequestConnectionDirection direction, + Ptr callbackDomain = nullptr, + Ptr timeoutController = nullptr, + bool responseNotFoundIsFatal = false + ); + ~HttpRequestConnection(); + + void RetainUntilStopped(Ptr retainedAdapter, const Func& drainedCallback); + void StopWithRetainedAdapter(Ptr retainedAdapter); + bool IsInsideCallback(); + + void InstallCallback(IHttpRequestCallback* callback) override; + void BeginReadingLoopUnsafe() override; + void SendRequest(Ptr request, vint responseTimeout = HttpIncompleteMessageTimeout) override; + void SendResponse(Ptr response) override; + void Stop() override; + + void OnRead(const vuint8_t* buffer, vint size) override; + void OnWriteCompleted(Ptr buffer) override; + void OnError(const WString& error, bool fatal) override; + void OnConnected() override; + void OnDisconnected() override; + void OnInstalled(IAsyncSocketConnection* connection) override; + }; +} + +#endif + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPREQUESTCLIENT.H +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Interfaces: + HttpRequestClient + +***********************************************************************/ + +#ifndef VCZH_INTERPROCESS_ASYNCSOCKET_HTTPREQUESTCLIENT +#define VCZH_INTERPROCESS_ASYNCSOCKET_HTTPREQUESTCLIENT + + +namespace vl::inter_process::async_tcp_socket +{ + /// Adapts an asynchronous TCP client to one HTTP/1.1 request connection. + class HttpRequestClient : public Object + { + private: + class Impl; + Ptr impl; + + public: + /// Requiring an asynchronous socket client is intentional. The caller selects and owns the transport composition, and this request adapter never creates or replaces the supplied client. Keep this dependency explicit; do not add internal client creation. + explicit HttpRequestClient(Ptr client); + virtual ~HttpRequestClient(); + + virtual IHttpRequestConnection* GetConnection(); + virtual void WaitForServer(); + virtual ClientStatus GetStatus(); + }; +} + +#endif + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPREQUESTSERVER.H +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Interfaces: + HttpRequestServer + +***********************************************************************/ + +#ifndef VCZH_INTERPROCESS_ASYNCSOCKET_HTTPREQUESTSERVER +#define VCZH_INTERPROCESS_ASYNCSOCKET_HTTPREQUESTSERVER + + +namespace vl::inter_process::async_tcp_socket +{ + /// Adapts an asynchronous TCP server to HTTP/1.1 request connections. + class HttpRequestServer : public Object + { + private: + class Impl; + Ptr impl; + + protected: + virtual void OnServerStopped(); + HttpRequestServer( + Ptr server, + const Func()>& timeoutControllerFactory + ); + + public: + /// Requiring an asynchronous socket server is intentional. The caller selects and owns the transport composition, and this request adapter never creates or replaces the supplied server. Keep this dependency explicit; do not add internal server creation. + explicit HttpRequestServer(Ptr server); + /// A derived destructor must call before destroying any state accessed by . + virtual ~HttpRequestServer(); + + virtual WaitForClientResult OnClientConnected(IHttpRequestConnection* connection); + void Start(); + void Stop(); + bool IsStopped(); + }; +} + +#endif + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPSERVERAPI.H +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Interfaces: + SocketHttp(ServerApi|RequestContext) + +***********************************************************************/ + +#ifndef VCZH_INTERPROCESS_ASYNCSOCKET_HTTPSERVERAPI +#define VCZH_INTERPROCESS_ASYNCSOCKET_HTTPSERVERAPI + + +namespace vl::inter_process::async_tcp_socket +{ + class SocketHttpServerApi; + class SocketHttpServerApiDispatcher; + + class SocketHttpRequestContext : public Object + { + friend class SocketHttpServerApiDispatcher; + + class Impl; + Ptr impl; + + SocketHttpRequestContext(Ptr _impl); + + public: + ~SocketHttpRequestContext(); + + SocketHttpRequestContext(const SocketHttpRequestContext&) = delete; + SocketHttpRequestContext(SocketHttpRequestContext&&) = delete; + SocketHttpRequestContext& operator=(const SocketHttpRequestContext&) = delete; + SocketHttpRequestContext& operator=(SocketHttpRequestContext&&) = delete; + + Ptr GetRequest(); + WString GetRelativePath(); + WString GetQuery(); + bool TryGetBodyUtf8(WString& body); + + bool Respond( + Ptr response, + Func completion = {} + ); + bool RespondStatus( + vint statusCode, + const WString& reason, + Func completion = {} + ); + bool RespondBytes( + vint statusCode, + const WString& reason, + const WString& contentType, + const collections::Array& body, + Func completion = {} + ); + bool RespondUtf8( + vint statusCode, + const WString& reason, + const WString& contentType, + const WString& body, + Func completion = {} + ); + bool Cancel(); + }; + + class SocketHttpServerApi : public Object + { + friend class SocketHttpServerApiDispatcher; + + class Impl; + Ptr impl; + + protected: + virtual void OnHttpRequestReceived( + Ptr context + ) = 0; + virtual void OnHttpServerStopping(); + + public: + /// Requiring an asynchronous socket server is intentional. The caller selects and owns the transport composition, the port comes from the supplied server, and multiple APIs share one listener only by receiving the same server. Keep this dependency explicit; do not add internal server creation. + SocketHttpServerApi( + Ptr server, + const WString& urlPrefix, + bool respondToOptions = true + ); + virtual ~SocketHttpServerApi(); + + SocketHttpServerApi(const SocketHttpServerApi&) = delete; + SocketHttpServerApi(SocketHttpServerApi&&) = delete; + SocketHttpServerApi& operator=(const SocketHttpServerApi&) = delete; + SocketHttpServerApi& operator=(SocketHttpServerApi&&) = delete; + + void Start(); + void Stop(); + bool IsStopped(); + WString GetUrlPrefix(); + }; +} + +#endif + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPSERVER.H +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Interfaces: + SocketHttpServer + +***********************************************************************/ + +#ifndef VCZH_INTERPROCESS_ASYNCSOCKET_HTTPSERVER +#define VCZH_INTERPROCESS_ASYNCSOCKET_HTTPSERVER + + +namespace vl::inter_process::async_tcp_socket +{ + class SocketHttpServer + : public SocketHttpServerApi + , public virtual INetworkProtocolServer + { + class Impl; + Ptr impl; + + protected: + void OnHttpRequestReceived(Ptr context) override; + void OnHttpServerStopping() override; + + public: + /// Requiring an asynchronous socket server is intentional. This protocol adapter takes its port from the server, forwards the caller-selected transport to , and never creates another server. Keep this dependency explicit; do not add an overload that selects a platform server internally. + SocketHttpServer(Ptr server, const WString& urlPrefix); + ~SocketHttpServer(); + + SocketHttpServer(const SocketHttpServer&) = delete; + SocketHttpServer(SocketHttpServer&&) = delete; + SocketHttpServer& operator=(const SocketHttpServer&) = delete; + SocketHttpServer& operator=(SocketHttpServer&&) = delete; + + virtual WaitForClientResult OnClientConnected(INetworkProtocolConnection* connection) override; + void Start() override; + void Stop() override; + bool IsStopped() override; + }; +} + +#endif + + +/*********************************************************************** +.\INTERPROCESS\NETWORKPROTOCOLHTTP.H +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Interfaces: + HttpRequest + HttpResponse + HttpError + +***********************************************************************/ + +#ifndef VCZH_INTERPROCESS_NETWORKPROTOCOLHTTP +#define VCZH_INTERPROCESS_NETWORKPROTOCOLHTTP + + +namespace vl::inter_process +{ + /* + * GET: /Connect + * Creates a new logical connection and returns its request and response URLs. + * Repeated calls create separate logical connections. + */ + constexpr const wchar_t* HttpServerUrl_Connect = L"/VlppInterProcess/Connect"; + + /* + * POST: /Request/GUID + * Client should always maintain a living request on the server. + * + * Returns only when a request is issued. + * It will be pending or timeout if no request is issued. + * If a request is issued but no living request available, it waits. + */ + constexpr const wchar_t* HttpServerUrl_Request = L"/VlppInterProcess/Request"; + + /* + * POST: /Response/GUID + * To send responses or events to the server. + * May return one queued server message in the same HTTP response. + */ + constexpr const wchar_t* HttpServerUrl_Response = L"/VlppInterProcess/Response"; + + constexpr const wchar_t* HttpNetworkProtocolContentType = L"application/json; charset=utf8"; + + extern WString HttpUrlEncodeQuery(const WString& query); + extern WString HttpUrlDecodeQuery(const WString& query); + extern WString CreateHttpNetworkProtocolConnectBody(const WString& requestPath, const WString& responsePath); + extern bool ParseHttpNetworkProtocolConnectBody(const WString& body, WString& requestPath, WString& responsePath); + extern bool ValidateHttpNetworkProtocolBaseUrl(const WString& baseUrl); + extern bool ValidateHttpNetworkProtocolEndpointPath(const WString& path); + extern bool IsValidHttpNetworkProtocolMessage(const WString& message); +} + +namespace vl::inter_process::windows_http +{ + /// An http request. + class HttpRequest + { + typedef collections::Array BodyBuffer; + typedef collections::List StringList; + typedef collections::Dictionary HeaderMap; + public: + /// Query of the request, like "/index.html". + WString query; + /// Set to true if the request uses SSL, or https. + bool secure = false; + /// User name to authorize. Set to empty if authorization is not needed. + WString username; + /// Password to authorize. Set to empty if authorization is not needed. + WString password; + /// HTTP method, like "GET", "POST", "PUT", "DELETE", etc. + WString method; + /// Cookie. Set to empty if cookie is not needed. + WString cookie; + /// Request body. This is a byte array. + BodyBuffer body; + /// Content type, like "text/xml". + WString contentType; + /// Accept type list, elements like "text/xml". + StringList acceptTypes; + /// A dictionary to contain extra headers. + HeaderMap extraHeaders; + /// Set to true to let this request finish when is called. + bool keepAliveOnStop = false; + /// Timeout for resolving the host name. 0 or -1 means infinite. + vint resolveTimeout = 0; + /// Timeout for connecting to the server. 0 or -1 means infinite. + vint connectTimeout = 60000; + /// Timeout for sending the request. 0 or -1 means infinite. + vint sendTimeout = 30000; + /// Timeout for receiving the response. 0 or -1 means infinite. + vint receiveTimeout = 30000; + + HttpRequest() = default; + void SetBodyUtf8(const WString& bodyString); + }; + + /// A type representing an http response. + class HttpResponse + { + typedef collections::Array BodyBuffer; + public: + /// Status code, like 200. + vint statusCode = 0; + /// Response body. This is a byte array. + BodyBuffer body; + /// Returned cookie from the server. + WString cookie; + /// Returned content type from the server. + WString contentType; + + HttpResponse() = default; + bool TryGetBodyUtf8(WString& bodyString) const; + WString GetBodyUtf8() const; + }; + + /// A transport error reported by the underlying HTTP implementation. + class HttpError + { + public: + vuint32_t errorCode = 0; + WString operation; + WString message; + }; +} + +namespace vl::inter_process +{ + extern windows_http::HttpRequest CreateHttpNetworkProtocolConnectRequest(const WString& target); + extern windows_http::HttpRequest CreateHttpNetworkProtocolReceiveRequest(const WString& target); + extern windows_http::HttpRequest CreateHttpNetworkProtocolSendRequest(const WString& target, const collections::Array& body); +} + +#endif + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPCLIENTAPI.H +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Interfaces: + SocketHttpClientApi + +***********************************************************************/ + +#ifndef VCZH_INTERPROCESS_ASYNCSOCKET_HTTPCLIENTAPI +#define VCZH_INTERPROCESS_ASYNCSOCKET_HTTPCLIENTAPI + + +namespace vl::inter_process::async_tcp_socket +{ + enum class SocketHttpClientErrorCode : vuint32_t + { + InvalidRequest = 1, + Stopped = 2, + Transport = 3, + UnsupportedCoding = 4, + ResponseNotFound = 5, + }; + + class SocketHttpClientApi : public Object + { + class Impl; + Ptr impl; + + public: + /// Requiring an asynchronous socket client is intentional. The caller selects and owns the transport composition, the client supplies its locked-in port, and this API never creates or replaces it. Keep this dependency explicit; do not add internal client creation. + SocketHttpClientApi( + Ptr client, + const WString& server + ); + ~SocketHttpClientApi(); + + SocketHttpClientApi(const SocketHttpClientApi&) = delete; + SocketHttpClientApi(SocketHttpClientApi&&) = delete; + SocketHttpClientApi& operator=(const SocketHttpClientApi&) = delete; + SocketHttpClientApi& operator=(SocketHttpClientApi&&) = delete; + + void WaitForServer(); + ClientStatus GetStatus(); + /// Send an HTTP request on the injected socket connection. + /// The injected socket owns name-resolution, connection, and send-phase timing. Only controls the response deadline for this exchange. + void HttpQuery( + const windows_http::HttpRequest& request, + Func)> callback + ); + void Stop(); + + static WString UrlEncodeQuery(const WString& query); + static WString UrlDecodeQuery(const WString& query); + }; +} + +#endif + + +/*********************************************************************** +.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPCLIENT.H +***********************************************************************/ +/*********************************************************************** +Vczh Library++ 3.0 +Developer: Zihan Chen(vczh) + +Interfaces: + SocketHttpClient + +***********************************************************************/ + +#ifndef VCZH_INTERPROCESS_ASYNCSOCKET_HTTPCLIENT +#define VCZH_INTERPROCESS_ASYNCSOCKET_HTTPCLIENT + + +namespace vl::inter_process::async_tcp_socket +{ + class SocketHttpClient + : public Object + , public virtual INetworkProtocolClient + , public virtual INetworkProtocolConnection + { + class Impl; + Ptr impl; + + public: + /// Requiring an asynchronous socket client is intentional. The supplied client is used directly for the first physical lane and creates fresh same-endpoint clients for the second lane and transport recovery. Keep this dependency explicit; do not add a factory parameter or select a platform socket internally. + SocketHttpClient( + Ptr client, + const WString& server, + const WString& urlPrefix + ); + ~SocketHttpClient(); + + SocketHttpClient(const SocketHttpClient&) = delete; + SocketHttpClient(SocketHttpClient&&) = delete; + SocketHttpClient& operator=(const SocketHttpClient&) = delete; + SocketHttpClient& operator=(SocketHttpClient&&) = delete; + + INetworkProtocolConnection* GetConnection() override; + void WaitForServer() override; + ClientStatus GetStatus() override; + void InstallCallback(INetworkProtocolCallback* callback) override; + void BeginReadingLoopUnsafe() override; + void SendString(const WString& str) override; + void Stop() override; + }; +} + +#endif + +