COROIO: coroio/http/httpd.hpp Source File
COROIO
 
Loading...
Searching...
No Matches
httpd.hpp
1#pragma once
2
3#include <coroio/socket.hpp>
5#include <coroio/sockutils.hpp>
6
7#include <chrono>
8#include <ctime>
9#include <iomanip>
10#include <sstream>
11#include <cstdio>
12#include <cstdlib>
13
14namespace NNet
15{
16
23// /path?arg1=value1&arg2=value2#fragment
24class TUri {
25public:
26 TUri() = default;
28 TUri(const std::string& uriStr);
30 const std::string& Path() const;
32 const std::map<std::string, std::string>& QueryParameters() const;
34 const std::string& Fragment() const;
35
36private:
37 void Parse(const std::string& uriStr);
38 std::string Path_;
39 std::map<std::string, std::string> QueryParameters_;
40 std::string Fragment_;
41};
42
53class TRequest {
54public:
55 TRequest(std::string&& header,
56 std::function<TFuture<ssize_t>(char*, size_t)> bodyReader,
57 std::function<TFuture<std::string>()> chunkHeaderReader = {});
59 std::string_view Method() const;
61 const TUri& Uri() const;
63 std::string_view Version() const { return Version_; }
64
66 bool HasBody() const;
80 TFuture<ssize_t> ReadBodySome(char* buffer, size_t size);
82 bool BodyConsumed() const;
84 bool RequireConnectionClose() const;
86 const std::map<std::string_view, std::string_view>& Headers() const;
87
88private:
89 void ParseRequestLine();
90 void ParseHeaders();
91
92 TFuture<ssize_t> ReadBodySomeContentLength(char* buffer, size_t size);
93 TFuture<ssize_t> ReadBodySomeChunked(char* buffer, size_t size);
94
95 std::string Header_;
96 size_t HeaderStartPos_ = 0;
97 std::map<std::string_view, std::string_view> Headers_;
98 size_t ContentLength_ = 0;
99 bool HasBody_ = false;
100 bool Chunked_ = false;
101 bool BodyConsumed_ = false;
102 size_t CurrentChunkSize_ = 0;
103 std::function<TFuture<ssize_t>(char*, size_t)> BodyReader_;
104 std::function<TFuture<std::string>()> ChunkHeaderReader_;
105 std::string_view Method_;
106 TUri Uri_;
107 std::string_view Version_;
108};
109
127public:
128 TResponse(std::function<TFuture<ssize_t>(const void*, size_t)> writer)
129 : Writer_(std::move(writer))
130 {}
132 void SetStatus(int statusCode);
134 void SetHeader(const std::string& name, const std::string& value);
150 TFuture<void> WriteBodyChunk(const char* data, size_t size);
156 TFuture<void> WriteBodyFull(const std::string& data);
158 bool IsClosed() const;
160 int StatusCode() const {
161 return StatusCode_;
162 }
163
164private:
165 TFuture<void> CompleteWrite(const char* data, size_t size);
166
167 int StatusCode_ = 200;
168 std::map<std::string, std::string> Headers_;
169 bool HeadersSent_ = false;
170 bool Chunked_ = false;
171 bool IsClosed_ = false;
172 std::function<TFuture<ssize_t>(const void*, size_t)> Writer_;
173};
174
194struct IRouter {
195 virtual TFuture<void> HandleRequest(TRequest& request, TResponse& response) = 0;
196};
197
199public:
200 TFuture<void> HandleRequest(TRequest& request, TResponse& response) override {
201 if (request.Uri().Path() == "/") {
202 response.SetStatus(200);
203 response.SetHeader("Content-Type", "text/plain");
204 response.SetHeader("Connection", "close");
205 co_await response.SendHeaders();
206 co_await response.WriteBodyFull("Hello, World!");
207 } else {
208 response.SetStatus(404);
209 response.SetHeader("Content-Type", "text/plain");
210 response.SetHeader("Connection", "close");
211 co_await response.SendHeaders();
212 co_await response.WriteBodyFull("Not Found");
213 }
214 }
215};
216
236template<typename TSocket>
238public:
246 TWebServer(TSocket&& serverSocket, IRouter& router, std::function<void(const std::string&)> logger = {})
247 : ServerSocket(std::move(serverSocket))
248 , Router(router)
249 , Logger(std::move(logger))
250 {}
251
259 while (true) {
260 auto clientSocket = co_await ServerSocket.Accept();
261 HandleClient(std::move(clientSocket));
262 }
263 }
264
265private:
266 TVoidTask HandleClient(TSocket clientSocket) {
267 auto byteReader = TByteReader<TSocket>(clientSocket);
268
269 auto bodyReader = [&](char* buffer, size_t size) -> TFuture<ssize_t> {
270 co_return co_await byteReader.ReadSome(buffer, size);
271 };
272
273 auto chunkHeaderReader = [&]() -> TFuture<std::string> {
274 co_return co_await byteReader.ReadUntil("\r\n");
275 };
276
277 auto bodyWriter = [&](const void* data, size_t size) -> TFuture<ssize_t> {
278 co_return co_await clientSocket.WriteSome(data, size);
279 };
280
281 std::string clientString = clientSocket.RemoteAddr() ? clientSocket.RemoteAddr()->ToString() : "unknown";
282
283 try {
284 while (true) {
285 auto header = co_await byteReader.ReadUntil("\r\n\r\n");
286 TRequest request(std::move(header), bodyReader, chunkHeaderReader);
287 TResponse response(bodyWriter);
288 co_await Router.HandleRequest(request, response);
289 Log(request, response, clientString);
290 if (response.IsClosed() || request.RequireConnectionClose()) {
291 break;
292 }
293 }
294 } catch (const std::exception& ex) {
295 if (Logger) {
296 Logger(std::string("Client handler exception: ") + ex.what());
297 }
298 }
299 co_return;
300 }
301
302 void Log(const TRequest& request, const TResponse& response, const std::string& clientString) {
303 if (!Logger) {
304 return;
305 }
306 LogStream.str("");
307 LogStream.clear();
308
309 // Build full path with query parameters if any
310 std::string fullPath = request.Uri().Path();
311 const auto& qp = request.Uri().QueryParameters();
312 if (!qp.empty()) {
313 fullPath.push_back('?');
314 bool first = true;
315 for (const auto& [k,v] : qp) {
316 if (!first) fullPath.push_back('&');
317 first = false;
318 fullPath.append(k);
319 fullPath.push_back('=');
320 fullPath.append(v);
321 }
322 }
323
324 // Timestamp in nginx style: [08/Nov/2025:15:23:10 +0100]
325 auto now = std::chrono::system_clock::now();
326 std::time_t raw = std::chrono::system_clock::to_time_t(now);
327 std::tm localTm{};
328 std::tm gmTm{};
329#if defined(_WIN32)
330 localtime_s(&localTm, &raw);
331 gmtime_s(&gmTm, &raw);
332#else
333 localtime_r(&raw, &localTm);
334 gmtime_r(&raw, &gmTm);
335#endif
336 // Compute timezone offset
337 // mktime converts tm in local time to time_t; difference gives offset vs UTC
338 std::time_t localTime = std::mktime(&localTm);
339 std::time_t gmTime = std::mktime(&gmTm);
340 long tzOffsetSec = static_cast<long>(difftime(localTime, gmTime));
341 int tzSign = tzOffsetSec >= 0 ? 1 : -1;
342 tzOffsetSec = std::labs(tzOffsetSec);
343 int tzHours = static_cast<int>(tzOffsetSec / 3600);
344 int tzMins = static_cast<int>((tzOffsetSec % 3600) / 60);
345 char tzBuf[8];
346 std::snprintf(tzBuf, sizeof(tzBuf), "%c%02d%02d", tzSign > 0 ? '+' : '-', tzHours, tzMins);
347
348 // Prepare HTTP version string
349 std::string_view ver = request.Version();
350 bool hasHttpPrefix = ver.size() >= 5 && ver.substr(0,5) == "HTTP/";
351
352 LogStream << clientString << " - - [" << std::put_time(&localTm, "%d/%b/%Y:%H:%M:%S ") << tzBuf << "] \""
353 << request.Method() << ' ' << fullPath << ' ' << (hasHttpPrefix ? std::string(ver) : (std::string("HTTP/") + std::string(ver))) << "\" "
354 << response.StatusCode() << ' ' << '-' << ' ' << "\"-\" \"-\"";
355 Logger(LogStream.str());
356 }
357
358 TSocket ServerSocket;
359 IRouter& Router;
360 std::function<void(const std::string&)> Logger;
361 std::ostringstream LogStream;
362};
363
364} // namespace NNet {
Definition httpd.hpp:198
Represents an incoming HTTP/1.1 request.
Definition httpd.hpp:53
std::string_view Version() const
HTTP version string (e.g. "HTTP/1.1").
Definition httpd.hpp:63
bool BodyConsumed() const
Returns true once the full body has been read.
Definition httpd.cpp:190
bool HasBody() const
Returns true if the request has a body (non-zero Content-Length or chunked).
Definition httpd.cpp:186
const std::map< std::string_view, std::string_view > & Headers() const
All request headers as a name → value map (views into the header buffer).
Definition httpd.cpp:161
TFuture< std::string > ReadBodyFull()
Reads and returns the entire request body as a string.
Definition httpd.cpp:194
bool RequireConnectionClose() const
Returns true if the client sent Connection: close or is HTTP/1.0.
Definition httpd.cpp:165
std::string_view Method() const
HTTP method string (e.g. "GET", "POST").
Definition httpd.cpp:178
TFuture< ssize_t > ReadBodySome(char *buffer, size_t size)
Reads up to size bytes of the request body into buffer.
Definition httpd.cpp:222
const TUri & Uri() const
Parsed request URI.
Definition httpd.cpp:182
Builds and sends an HTTP/1.1 response.
Definition httpd.hpp:126
TFuture< void > WriteBodyChunk(const char *data, size_t size)
Writes data as one chunk using Transfer-Encoding: chunked.
Definition httpd.cpp:339
void SetStatus(int statusCode)
Sets the HTTP status code (default is 200). Must be called before SendHeaders.
Definition httpd.cpp:283
TFuture< void > WriteBodyFull(const std::string &data)
Sends headers (with Content-Length) and the full body in one call.
Definition httpd.cpp:356
int StatusCode() const
Returns the status code set via SetStatus (default 200).
Definition httpd.hpp:160
TFuture< void > SendHeaders()
Sends the status line and all accumulated headers to the client.
Definition httpd.cpp:305
bool IsClosed() const
Returns true if the connection has been closed or an error occurred.
Definition httpd.cpp:335
void SetHeader(const std::string &name, const std::string &value)
Adds or replaces a response header. Must be called before SendHeaders.
Definition httpd.cpp:287
auto WriteSome(const void *buf, size_t size)
Asynchronously writes data from the provided buffer to the socket.
Definition socket.hpp:189
High-level asynchronous socket for network communication.
Definition socket.hpp:367
const std::optional< TAddress > & RemoteAddr() const
Returns the remote address of the connected peer.
Definition socket.cpp:109
auto Accept()
Asynchronously accepts an incoming connection.
Definition socket.hpp:456
Parsed HTTP request URI: /path?key=value&...#fragment.
Definition httpd.hpp:24
const std::string & Fragment() const
Returns the fragment (the part after #), or empty string if absent.
Definition httpd.cpp:72
const std::map< std::string, std::string > & QueryParameters() const
Returns query parameters as a key→value map (e.g. {"page": "2"}).
Definition httpd.cpp:76
const std::string & Path() const
Returns the decoded path component (e.g. "/api/v1/users").
Definition httpd.cpp:80
HTTP/1.1 server that accepts connections and dispatches to a router.
Definition httpd.hpp:237
TWebServer(TSocket &&serverSocket, IRouter &router, std::function< void(const std::string &)> logger={})
Constructs the server.
Definition httpd.hpp:246
TVoidTask Start()
Starts the accept loop as a detached TVoidTask.
Definition httpd.hpp:258
Implementation of a promise/future system for coroutines.
Interface for HTTP request handlers.
Definition httpd.hpp:194
A utility for reading data from a socket-like object, either a fixed number of bytes or until a speci...
Definition sockutils.hpp:76
Owned coroutine handle that carries a result of type T.
Definition corochain.hpp:185
Fire-and-forget coroutine handle.
Definition promises.hpp:26