SNode.C
Loading...
Searching...
No Matches
Response.cpp
Go to the documentation of this file.
1/*
2 * SNode.C - A Slim Toolkit for Network Communication
3 * Copyright (C) Volker Christian <me@vchrist.at>
4 * 2020, 2021, 2022, 2023, 2024, 2025
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published
8 * by the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20/*
21 * MIT License
22 *
23 * Permission is hereby granted, free of charge, to any person obtaining a copy
24 * of this software and associated documentation files (the "Software"), to deal
25 * in the Software without restriction, including without limitation the rights
26 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
27 * copies of the Software, and to permit persons to whom the Software is
28 * furnished to do so, subject to the following conditions:
29 *
30 * The above copyright notice and this permission notice shall be included in
31 * all copies or substantial portions of the Software.
32 *
33 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
39 * THE SOFTWARE.
40 */
41
42#include "web/http/server/Response.h"
43
44#include "SocketContext.h"
45#include "core/file/FileReader.h"
46#include "core/socket/stream/SocketConnection.h"
47#include "web/http/MimeTypes.h"
48#include "web/http/StatusCodes.h"
49#include "web/http/server/SocketContextUpgradeFactorySelector.h"
50
51#ifndef DOXYGEN_SHOULD_SKIP_THIS
52
53#include "log/Logger.h"
54#include "utils/system/time.h"
55#include "web/http/CiStringMap.h"
56#include "web/http/http_utils.h"
57
58#include <cerrno>
59#include <filesystem>
60#include <iterator>
61#include <numeric>
62#include <sstream>
63#include <system_error>
64#include <utility>
65
66#endif /* DOXYGEN_SHOULD_SKIP_THIS */
67
68#define to_hex_str(int_val) (static_cast<std::ostringstream const&>(std::ostringstream() << std::uppercase << std::hex << int_val)).str()
69
70namespace web::http::server {
71
73 : socketContext(socketContext) {
74 }
75
79 }
80 }
81
83 stop();
84 socketContext = nullptr;
85 }
86
87 bool Response::isConnected() const {
88 return socketContext != nullptr;
89 }
90
91 void Response::init() {
92 statusCode = 200;
93 httpMajor = 1;
94 httpMinor = 1;
95 headers.clear();
96 cookies.clear();
97 trailer.clear();
98 contentLength = 0;
99 contentSent = 0;
102 }
103
104 Response& Response::status(int statusCode) {
105 this->statusCode = statusCode;
106
107 return *this;
108 }
109
110 Response& Response::append(const std::string& field, const std::string& value) {
111 const std::map<std::string, std::string>::iterator it = headers.find(field);
112
113 if (it != headers.end()) {
114 set(field, it->second.append(", ").append(value));
115 } else {
116 set(field, value);
117 }
118
119 return *this;
120 }
121
122 Response& Response::set(const std::map<std::string, std::string>& headers, bool overwrite) {
123 for (const auto& [field, value] : headers) {
124 set(field, value, overwrite);
125 }
126
127 return *this;
128 }
129
130 Response& Response::set(const std::string& field, const std::string& value, bool overwrite) {
131 if (!value.empty()) {
132 if (overwrite) {
133 headers.insert_or_assign(field, value);
134 } else {
135 headers.insert({field, value});
136 }
137
138 if (web::http::ciEquals(field, "Connection")) {
139 if (web::http::ciContains(headers[field], "keep-alive")) {
141 } else if (web::http::ciContains(headers[field], "close")) {
143 }
144 } else if (web::http::ciEquals(field, "Content-Length")) {
145 contentLength = std::stoul(value);
147 headers.erase("Transfer-Encoding");
148 } else if (web::http::ciEquals(field, "Transfer-Encoding")) {
149 if (web::http::ciContains(headers[field], "chunked")) {
151 headers.erase("Content-Length");
152 }
153 if (web::http::ciContains(headers[field], "compressed")) {
154 }
155 if (web::http::ciContains(headers[field], "deflate")) {
156 }
157 if (web::http::ciContains(headers[field], "gzip")) {
158 }
159 } else if (web::http::ciEquals(field, "Content-Encoding")) {
160 if (web::http::ciContains(headers[field], "compressed")) {
161 }
162 if (web::http::ciContains(headers[field], "deflate")) {
163 }
164 if (web::http::ciContains(headers[field], "gzip")) {
165 }
166 if (web::http::ciContains(headers[field], "br")) {
167 }
168 }
169 } else {
170 headers.erase(field);
171 }
172
173 return *this;
174 }
175
176 Response& Response::type(const std::string& type) {
177 return set("Content-Type", type);
178 }
179
180 Response& Response::cookie(const std::string& name, const std::string& value, const std::map<std::string, std::string>& options) {
181 cookies.insert({name, CookieOptions(value, options)});
182
183 return *this;
184 }
185
186 Response& Response::clearCookie(const std::string& name, const std::map<std::string, std::string>& options) {
187 std::map<std::string, std::string> opts = options;
188
189 opts.erase("Max-Age");
190 const time_t time = 0;
191 opts["Expires"] = httputils::to_http_date(utils::system::gmtime(&time));
192
193 return cookie(name, "", opts);
194 }
195
196 Response& Response::setTrailer(const std::string& field, const std::string& value, bool overwrite) {
197 if (!value.empty()) {
198 if (overwrite) {
199 trailer.insert_or_assign(field, value);
200 } else {
201 trailer.insert({field, value});
202 }
203 if (!headers.contains("Trailer")) {
204 set("Trailer", field);
205 } else {
206 headers["Trailer"].append("," + field);
207 }
208 } else {
209 trailer.erase(field);
210 }
211
212 return *this;
213 }
214
215 void Response::send(const char* chunk, std::size_t chunkLen) {
216 if (isConnected()) {
217 if (chunkLen > 0) {
218 set("Content-Type", "application/octet-stream", false);
219 }
220 set("Content-Length", std::to_string(chunkLen));
221
223 sendFragment(chunk, chunkLen);
225 }
226 }
227
228 void Response::send(const std::string& chunk) {
229 if (!chunk.empty()) {
230 set("Content-Type", "text/html; charset=utf-8", false);
231 }
232
233 send(chunk.data(), chunk.size());
234 }
235
236 void Response::sendStatus(int statusCode) {
237 status(statusCode);
238
240 }
241
242 /* Just an UML-Sequence diagram test */
243 /** Sequence diagram of res.upgrade(req).
244@startuml
245!include web/http/server/pu/response_upgrade.pu
246@enduml
247 */
248 void Response::upgrade(const std::shared_ptr<Request>& request, const std::function<void(const std::string&)>& status) {
249 if (isConnected()) {
250 const std::string connectionName = socketContext->getSocketConnection()->getConnectionName();
251
252 std::string name;
253
254 if (request != nullptr) {
255 LOG(DEBUG) << connectionName << " HTTP: Initiating upgrade: " << request->method << " " << request->url
256 << " HTTP/" + std::to_string(httpMajor) + "." + std::to_string(httpMinor) << "\n"
258 request->url,
259 "HTTP/" + std::to_string(request->httpMajor) + "." + std::to_string(request->httpMinor),
260 request->queries,
261 request->headers,
262 {},
263 request->cookies,
264 std::vector<char>());
265 if (web::http::ciContains(request->get("connection"), "Upgrade")) {
266 SocketContextUpgradeFactory* socketContextUpgradeFactory =
268
269 if (socketContextUpgradeFactory != nullptr) {
270 name = socketContextUpgradeFactory->name();
271
272 LOG(DEBUG) << connectionName << " HTTP upgrade: SocketContextUpgradeFactory create success for: " << name;
273
274 core::socket::stream::SocketContext* socketContextUpgrade =
275 socketContextUpgradeFactory->create(socketContext->getSocketConnection());
276
277 if (socketContextUpgrade != nullptr) {
278 LOG(DEBUG) << connectionName << " HTTP upgrade: SocketContextUpgrade create success for: " << name;
279
280 LOG(DEBUG) << connectionName << " HTTP upgrade: Response to upgrade request: " << request->method << " "
281 << request->url << " " << "HTTP/" << request->httpMajor << "." << request->httpMinor << "\n"
282 << httputils::toString("HTTP/" + std::to_string(httpMajor) + "." + std::to_string(httpMinor),
283 std::to_string(statusCode),
287 {});
288
289 socketContext->switchSocketContext(socketContextUpgrade);
290 } else {
291 LOG(DEBUG) << connectionName << " HTTP upgrade: SocketContextUpgrade create failed for: " << name;
292
293 set("Connection", "close").status(404);
294 }
295 } else {
296 LOG(DEBUG) << connectionName
297 << " SocketContextUpgradeFactory create failed for all of: " << request->get("upgrade");
298
299 set("Connection", "close").status(404);
300 }
301 } else {
302 LOG(DEBUG) << connectionName << " HTTP upgrade: No upgrade requested";
303
304 set("Connection", "close").status(400);
305 }
306 } else {
307 LOG(ERROR) << connectionName << " HTTP upgrade: Request has gone away";
308
309 set("Connection", "close").status(500);
310 }
311
312 LOG(DEBUG) << connectionName << " HTTP: Upgrade bootstrap " << (!name.empty() ? "success" : "failed");
313 LOG(DEBUG) << " Protocol selected: " << name;
314 LOG(DEBUG) << " requested: " << request->get("upgrade");
315 LOG(DEBUG) << " Subprotocol selected: " << header("upgrade");
316 LOG(DEBUG) << " requested: " << request->get("Sec-WebSocket-Protocol");
317
318 status(name);
319 } else {
320 LOG(ERROR) << "HTTP upgrade: Unexpected disconnect";
321 }
322 }
323
324 void Response::sendFile(const std::string& file, const std::function<void(int)>& onStatus) {
325 if (isConnected()) {
326 std::string absolutFileName = file;
327
328 if (std::filesystem::exists(absolutFileName)) {
329 std::error_code ec;
330 absolutFileName = std::filesystem::canonical(absolutFileName);
331
332 if (std::filesystem::is_regular_file(absolutFileName, ec) && !ec) {
333 core::file::FileReader::open(absolutFileName, [this, &absolutFileName, &onStatus](int fd) {
334 onStatus(errno);
335
336 if (fd >= 0) {
337 set("Content-Type", web::http::MimeTypes::contentType(absolutFileName), false);
338 set("Last-Modified", httputils::file_mod_http_date(absolutFileName), false);
339 if (httpMajor == 1) {
340 if (httpMinor == 1) {
341 set("Transfer-Encoding", "chunked");
342 } else {
343 set("Content-Length", std::to_string(std::filesystem::file_size(absolutFileName)));
344 }
345 }
346 } else {
347 status(404);
348 }
349 })->pipe(this);
350 } else {
351 status(404);
352 errno = EEXIST;
353 onStatus(errno);
354 }
355 } else {
356 status(404);
357 errno = ENOENT;
358 onStatus(errno);
359 }
360 }
361 }
362
363 void Response::end() {
364 send("");
365 }
366
368 if (isConnected()) {
370
371 socketContext->sendToPeer("HTTP/" + std::to_string(httpMajor)
372 .append(".")
373 .append(std::to_string(httpMinor))
374 .append(" ")
375 .append(std::to_string(statusCode))
376 .append(" ")
378 .append("\r\n"));
380
381 set("X-Powered-By", "snode.c");
382
383 for (const auto& [field, value] : headers) {
384 socketContext->sendToPeer(std::string(field).append(": ").append(value).append("\r\n"));
385 }
386
387 for (const auto& [cookie, cookieValue] : cookies) {
388 const std::string cookieString = std::accumulate(
389 cookieValue.getOptions().begin(),
390 cookieValue.getOptions().end(),
391 cookie + "=" + cookieValue.getValue(),
392 [](const std::string& str, const std::pair<const std::string&, const std::string&> option) -> std::string {
393 return str + "; " + option.first + (!option.second.empty() ? "=" + option.second : "");
394 });
395 socketContext->sendToPeer("Set-Cookie: " + cookieString + "\r\n");
396 }
397
399 }
400
401 return *this;
402 }
403
404 Response& Response::sendFragment(const char* chunk, std::size_t chunkLen) {
405 if (isConnected()) {
407 socketContext->sendToPeer(to_hex_str(chunkLen).append("\r\n"));
408 }
409
410 socketContext->sendToPeer(chunk, chunkLen);
411 contentSent += chunkLen;
412
413 if (transferEncoding == TransferEncoding::Chunked || web::http::ciContains(headers["Content-Type"], "text/event-stream")) {
415 contentLength += chunkLen;
416 }
417 }
418
419 return *this;
420 }
421
422 Response& Response::sendFragment(const std::string& chunk) {
423 return sendFragment(chunk.data(), chunk.size());
424 }
425
427 if (isConnected()) {
429 sendFragment(""); // For transfer encoding chunked. Terminate the chunk sequence.
430
431 if (!trailer.empty()) {
432 for (auto& [field, value] : trailer) {
433 socketContext->sendToPeer(std::string(field).append(":").append(value).append("\r\n"));
434 }
435
437 }
438 }
439
441 }
442 }
443
444 void Response::onSourceConnect(core::pipe::Source* source) {
445 if (isConnected()) {
448
449 source->start();
450 } else {
451 source->stop();
452 }
453 } else {
454 source->stop();
455 }
456 }
457
458 void Response::onSourceData(const char* chunk, std::size_t chunkLen) {
459 sendFragment(chunk, chunkLen);
460 }
461
463 if (isConnected()) {
465 }
466
468 }
469
470 void Response::onSourceError(int errnum) {
471 errno = errnum;
472
473 if (isConnected()) {
476 }
477
479 }
480
481 const std::string& Response::header(const std::string& field) {
482 return headers[field];
483 }
484
486 return socketContext;
487 }
488
489} // namespace web::http::server
static FileReader * open(const std::string &path, const std::function< void(int)> &callback)
void stop()
Definition Sink.cpp:69
bool isStreaming()
Definition Sink.cpp:65
virtual void stop()=0
bool pipe(Sink *sink)
Definition Source.cpp:60
virtual void start()=0
void sendToPeer(const std::string &data) const
const std::string & getConnectionName() const
SocketConnection * getSocketConnection() const
void switchSocketContext(SocketContext *newSocketContext)
void sendToPeer(const char *chunk, std::size_t chunkLen) const final
bool streamToPeer(core::pipe::Source *source) const
const std::map< std::string, std::string > & getOptions() const
CookieOptions(const std::string &value, const std::map< std::string, std::string > &options)
const std::string & getValue() const
static std::string contentType(const std::string &file)
core::socket::stream::SocketContext * create(core::socket::stream::SocketConnection *socketConnection) final
static std::string reason(int status)
CiStringMap< std::string > cookies
Definition Request.h:87
CiStringMap< std::string > headers
Definition Request.h:86
CiStringMap< std::string > queries
Definition Request.h:85
const std::string & get(const std::string &key, int i=0) const
Definition Request.cpp:60
void onSourceData(const char *chunk, std::size_t chunkLen) override
Definition Response.cpp:458
Response(SocketContext *socketContext)
Definition Response.cpp:72
SocketContext * getSocketContext() const
Definition Response.cpp:485
void onSourceConnect(core::pipe::Source *source) override
Definition Response.cpp:444
web::http::CiStringMap< web::http::CookieOptions > cookies
Definition Response.h:128
Response & status(int statusCode)
Definition Response.cpp:104
void send(const std::string &chunk)
Definition Response.cpp:228
void send(const char *chunk, std::size_t chunkLen)
Definition Response.cpp:215
Response & setTrailer(const std::string &field, const std::string &value, bool overwrite=true)
Definition Response.cpp:196
Response & set(const std::map< std::string, std::string > &headers, bool overwrite=true)
Definition Response.cpp:122
web::http::CiStringMap< std::string > trailer
Definition Response.h:129
web::http::CiStringMap< std::string > headers
Definition Response.h:127
Response & set(const std::string &field, const std::string &value, bool overwrite=true)
Definition Response.cpp:130
TransferEncoding transferEncoding
Definition Response.h:138
void upgrade(const std::shared_ptr< Request > &request, const std::function< void(const std::string &)> &status)
Definition Response.cpp:248
Response & type(const std::string &type)
Definition Response.cpp:176
void onSourceError(int errnum) override
Definition Response.cpp:470
Response & sendFragment(const std::string &chunk)
Definition Response.cpp:422
Response & cookie(const std::string &name, const std::string &value, const std::map< std::string, std::string > &options={})
Definition Response.cpp:180
web::http::server::SocketContext * socketContext
Definition Response.h:135
Response & append(const std::string &field, const std::string &value)
Definition Response.cpp:110
void sendStatus(int statusCode)
Definition Response.cpp:236
Response & sendFragment(const char *chunk, std::size_t chunkLen)
Definition Response.cpp:404
void onSourceEof() override
Definition Response.cpp:462
void sendFile(const std::string &file, const std::function< void(int)> &callback)
Definition Response.cpp:324
Response & clearCookie(const std::string &name, const std::map< std::string, std::string > &options={})
Definition Response.cpp:186
ConnectionState connectionState
Definition Response.h:137
const std::string & header(const std::string &field)
Definition Response.cpp:481
SocketContextUpgradeFactory * select(Request &req, Response &res) override
void responseCompleted(const Response &response, bool success)
void responseStarted(const Response &response)
std::string to_http_date(struct tm *tm)
std::string toString(const std::string &version, const std::string &statusCode, const std::string &reason, const web::http::CiStringMap< std::string > &header, const web::http::CiStringMap< web::http::CookieOptions > &cookies, const std::vector< char > &body)
std::string file_mod_http_date(const std::string &filePath)
std::string toString(const std::string &method, const std::string &url, const std::string &version, const web::http::CiStringMap< std::string > &queries, const web::http::CiStringMap< std::string > &header, const web::http::CiStringMap< std::string > &trailer, const web::http::CiStringMap< std::string > &cookies, const std::vector< char > &body)
struct tm * gmtime(const time_t *timep)
Definition time.cpp:62
bool ciEquals(const std::string &str1, const std::string &str2)
bool ciContains(const std::string &str1, const std::string &str2)
#define to_hex_str(int_val)
Definition Request.cpp:73