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
17// /path?arg1=value1&arg2=value2#fragment
18class TUri {
19public:
20 TUri() = default;
21 TUri(const std::string& uriStr);
22 const std::string& Path() const;
23 const std::map<std::string, std::string>& QueryParameters() const;
24 const std::string& Fragment() const;
25
26private:
27 void Parse(const std::string& uriStr);
28 std::string Path_;
29 std::map<std::string, std::string> QueryParameters_;
30 std::string Fragment_;
31};
32
33class TRequest {
34public:
35 TRequest(std::string&& header,
36 std::function<TFuture<ssize_t>(char*, size_t)> bodyReader,
37 std::function<TFuture<std::string>()> chunkHeaderReader = {});
38 std::string_view Method() const;
39 const TUri& Uri() const;
40 std::string_view Version() const { return Version_; }
41
42 bool HasBody() const;
43 TFuture<std::string> ReadBodyFull();
44 TFuture<ssize_t> ReadBodySome(char* buffer, size_t size); // read up to Content-Length
45 bool BodyConsumed() const;
46 bool RequireConnectionClose() const;
47 const std::map<std::string_view, std::string_view>& Headers() const;
48
49private:
50 void ParseRequestLine();
51 void ParseHeaders();
52
53 TFuture<ssize_t> ReadBodySomeContentLength(char* buffer, size_t size);
54 TFuture<ssize_t> ReadBodySomeChunked(char* buffer, size_t size);
55
56 std::string Header_;
57 size_t HeaderStartPos_ = 0;
58 std::map<std::string_view, std::string_view> Headers_;
59 size_t ContentLength_ = 0;
60 bool HasBody_ = false;
61 bool Chunked_ = false;
62 bool BodyConsumed_ = false;
63 size_t CurrentChunkSize_ = 0;
64 std::function<TFuture<ssize_t>(char*, size_t)> BodyReader_;
65 std::function<TFuture<std::string>()> ChunkHeaderReader_;
66 std::string_view Method_;
67 TUri Uri_;
68 std::string_view Version_;
69};
70
71class TResponse {
72public:
73 TResponse(std::function<TFuture<ssize_t>(const void*, size_t)> writer)
74 : Writer_(std::move(writer))
75 {}
76 void SetStatus(int statusCode);
77 void SetHeader(const std::string& name, const std::string& value);
78 TFuture<void> SendHeaders();
79 TFuture<void> WriteBodyChunk(const char* data, size_t size); // Chunked transfer encoding
80 TFuture<void> WriteBodyFull(const std::string& data); // Content-Length + body
81 bool IsClosed() const;
82 int StatusCode() const {
83 return StatusCode_;
84 }
85
86private:
87 TFuture<void> CompleteWrite(const char* data, size_t size);
88
89 int StatusCode_ = 200;
90 std::map<std::string, std::string> Headers_;
91 bool HeadersSent_ = false;
92 bool Chunked_ = false;
93 bool IsClosed_ = false;
94 std::function<TFuture<ssize_t>(const void*, size_t)> Writer_;
95};
96
97struct IRouter {
98 virtual TFuture<void> HandleRequest(TRequest& request, TResponse& response) = 0;
99};
100
102public:
103 TFuture<void> HandleRequest(TRequest& request, TResponse& response) override {
104 if (request.Uri().Path() == "/") {
105 response.SetStatus(200);
106 response.SetHeader("Content-Type", "text/plain");
107 response.SetHeader("Connection", "close");
108 co_await response.SendHeaders();
109 co_await response.WriteBodyFull("Hello, World!");
110 } else {
111 response.SetStatus(404);
112 response.SetHeader("Content-Type", "text/plain");
113 response.SetHeader("Connection", "close");
114 co_await response.SendHeaders();
115 co_await response.WriteBodyFull("Not Found");
116 }
117 }
118};
119
120template<typename TSocket>
122public:
123 TWebServer(TSocket&& serverSocket, IRouter& router, std::function<void(const std::string&)> logger = {})
124 : ServerSocket(std::move(serverSocket))
125 , Router(router)
126 , Logger(std::move(logger))
127 {}
128
129 TVoidTask Start() {
130 while (true) {
131 auto clientSocket = co_await ServerSocket.Accept();
132 HandleClient(std::move(clientSocket));
133 }
134 }
135
136private:
137 TVoidTask HandleClient(TSocket clientSocket) {
138 auto byteReader = TByteReader<TSocket>(clientSocket);
139
140 auto bodyReader = [&](char* buffer, size_t size) -> TFuture<ssize_t> {
141 co_return co_await byteReader.ReadSome(buffer, size);
142 };
143
144 auto chunkHeaderReader = [&]() -> TFuture<std::string> {
145 co_return co_await byteReader.ReadUntil("\r\n");
146 };
147
148 auto bodyWriter = [&](const void* data, size_t size) -> TFuture<ssize_t> {
149 co_return co_await clientSocket.WriteSome(data, size);
150 };
151
152 std::string clientString = clientSocket.RemoteAddr() ? clientSocket.RemoteAddr()->ToString() : "unknown";
153
154 try {
155 while (true) {
156 auto header = co_await byteReader.ReadUntil("\r\n\r\n");
157 TRequest request(std::move(header), bodyReader, chunkHeaderReader);
158 TResponse response(bodyWriter);
159 co_await Router.HandleRequest(request, response);
160 Log(request, response, clientString);
161 if (response.IsClosed() || request.RequireConnectionClose()) {
162 break;
163 }
164 }
165 } catch (const std::exception& ex) {
166 if (Logger) {
167 Logger(std::string("Client handler exception: ") + ex.what());
168 }
169 }
170 co_return;
171 }
172
173 void Log(const TRequest& request, const TResponse& response, const std::string& clientString) {
174 if (!Logger) {
175 return;
176 }
177 LogStream.str("");
178 LogStream.clear();
179
180 // Build full path with query parameters if any
181 std::string fullPath = request.Uri().Path();
182 const auto& qp = request.Uri().QueryParameters();
183 if (!qp.empty()) {
184 fullPath.push_back('?');
185 bool first = true;
186 for (const auto& [k,v] : qp) {
187 if (!first) fullPath.push_back('&');
188 first = false;
189 fullPath.append(k);
190 fullPath.push_back('=');
191 fullPath.append(v);
192 }
193 }
194
195 // Timestamp in nginx style: [08/Nov/2025:15:23:10 +0100]
196 auto now = std::chrono::system_clock::now();
197 std::time_t raw = std::chrono::system_clock::to_time_t(now);
198 std::tm localTm{};
199 std::tm gmTm{};
200#if defined(_WIN32)
201 localtime_s(&localTm, &raw);
202 gmtime_s(&gmTm, &raw);
203#else
204 localtime_r(&raw, &localTm);
205 gmtime_r(&raw, &gmTm);
206#endif
207 // Compute timezone offset
208 // mktime converts tm in local time to time_t; difference gives offset vs UTC
209 std::time_t localTime = std::mktime(&localTm);
210 std::time_t gmTime = std::mktime(&gmTm);
211 long tzOffsetSec = static_cast<long>(difftime(localTime, gmTime));
212 int tzSign = tzOffsetSec >= 0 ? 1 : -1;
213 tzOffsetSec = std::labs(tzOffsetSec);
214 int tzHours = static_cast<int>(tzOffsetSec / 3600);
215 int tzMins = static_cast<int>((tzOffsetSec % 3600) / 60);
216 char tzBuf[8];
217 std::snprintf(tzBuf, sizeof(tzBuf), "%c%02d%02d", tzSign > 0 ? '+' : '-', tzHours, tzMins);
218
219 // Prepare HTTP version string
220 std::string_view ver = request.Version();
221 bool hasHttpPrefix = ver.size() >= 5 && ver.substr(0,5) == "HTTP/";
222
223 LogStream << clientString << " - - [" << std::put_time(&localTm, "%d/%b/%Y:%H:%M:%S ") << tzBuf << "] \""
224 << request.Method() << ' ' << fullPath << ' ' << (hasHttpPrefix ? std::string(ver) : (std::string("HTTP/") + std::string(ver))) << "\" "
225 << response.StatusCode() << ' ' << '-' << ' ' << "\"-\" \"-\"";
226 Logger(LogStream.str());
227 }
228
229 TSocket ServerSocket;
230 IRouter& Router;
231 std::function<void(const std::string&)> Logger;
232 std::ostringstream LogStream;
233};
234
235} // namespace NNet {
Definition httpd.hpp:101
Definition httpd.hpp:33
Definition httpd.hpp:71
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
Definition httpd.hpp:18
Definition httpd.hpp:121
Implementation of a promise/future system for coroutines.
Definition httpd.hpp:97
A utility for reading data from a socket-like object, either a fixed number of bytes or until a speci...
Definition sockutils.hpp:64
Future type for coroutines returning a value of type T.
Definition corochain.hpp:182
Definition promises.hpp:9