NAWA 0.9
Web Application Framework for C++
Connection.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2019-2022 Tobias Flaig.
3 *
4 * This file is part of nawa.
5 *
6 * nawa is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License,
8 * version 3, as published by the Free Software Foundation.
9 *
10 * nawa is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with nawa. If not, see <https://www.gnu.org/licenses/>.
17 */
18
24#include <fstream>
25#include <nawa/Exception.h>
29#include <nawa/logging/Log.h>
30#include <nawa/oss.h>
31#include <nawa/util/encoding.h>
32#include <nawa/util/utils.h>
33#include <regex>
34#include <sstream>
35#include <sys/stat.h>
36
37using namespace nawa;
38using namespace std;
39
40namespace {
41 Log logger;
42
43 unordered_map<unsigned int, string> const httpStatusCodes = {
44 {200, "OK"},
45 {201, "Created"},
46 {202, "Accepted"},
47 {203, "Non-Authoritative Information"},
48 {204, "No Content"},
49 {205, "Reset Content"},
50 {206, "Partial Content"},
51 {207, "Multi-Status"},
52 {208, "Already Reported"},
53 {226, "IM Used"},
54 {300, "Multiple Choices"},
55 {301, "Moved Permanently"},
56 {302, "Found"},
57 {303, "See Other"},
58 {304, "Not Modified"},
59 {305, "Use Proxy"},
60 {307, "Temporary Redirect"},
61 {308, "Permanent Redirect"},
62 {400, "Bad Request"},
63 {401, "Unauthorized"},
64 {402, "Payment Required"},
65 {403, "Forbidden"},
66 {404, "Not Found"},
67 {405, "Method Not Allowed"},
68 {406, "Not Acceptable"},
69 {407, "Proxy Authentication Required"},
70 {408, "Request Timeout"},
71 {409, "Conflict"},
72 {410, "Gone"},
73 {411, "Length Required"},
74 {412, "Precondition Failed"},
75 {413, "Payload Too Large"},
76 {414, "URI Too Long"},
77 {415, "Unsupported Media Type"},
78 {416, "Range Not Satisfiable"},
79 {417, "Expectation Failed"},
80 {418, "I'm a teapot"},
81 {421, "Misdirected Request"},
82 {422, "Unprocessable Entity"},
83 {423, "Locked"},
84 {424, "Failed Dependency"},
85 {426, "Upgrade Required"},
86 {428, "Precondition Required"},
87 {429, "Too Many Requests"},
88 {431, "Request Header Fields Too Large"},
89 {451, "Unavailable For Legal Reasons"},
90 {500, "Internal Server Error"},
91 {501, "Not Implemented"},
92 {502, "Bad Gateway"},
93 {503, "Service Unavailable"},
94 {504, "Gateway Timeout"},
95 {505, "HTTP Version Not Supported"},
96 {506, "Variant Also Negotiates"},
97 {507, "Insufficient Storage"},
98 {508, "Loop Detected"},
99 {510, "Not Extended"},
100 {511, "Network Authentication Required"}};
101}// namespace
102
103struct Connection::Data {
104 string bodyString;
105 unsigned int responseStatus = 200;
106 unordered_map<string, vector<string>> headers;
107 unordered_map<string, Cookie> cookies;
108 Cookie cookiePolicy;
109 bool isFlushed = false;
110 FlushCallbackFunction flushCallback;
111
112 Request request;
113 Session session;
114 Config config;
115 stringstream responseStream;
116
117 void clearStream() {
118 responseStream.str(string());
119 responseStream.clear();
120 }
121
122 void mergeStream() {
123 bodyString += responseStream.str();
124 clearStream();
125 }
126
127 Data(Connection* base, ConnectionInitContainer const& connectionInit) : request(connectionInit.requestInit),
128 config(connectionInit.config),
129 session(*base) {}
130};
131
133
134void Connection::setResponseBody(std::string content) {
135 data->bodyString = std::move(content);
136 data->clearStream();
137}
138
139void Connection::sendFile(std::string const& path, std::string const& contentType, bool forceDownload,
140 std::string const& downloadFilename, bool checkIfModifiedSince) {
141
142 // open file as binary
143 ifstream f(path, ifstream::binary);
144
145 // throw exception if file cannot be opened
146 if (!f) {
147 throw Exception(__PRETTY_FUNCTION__, 1, "Cannot open file for reading");
148 }
149
150 // get time of last modification
151 struct stat fileStat {};
152 time_t lastModified = 0;
153 if (stat(path.c_str(), &fileStat) == 0) {
154 lastModified = oss::getLastModifiedTimeOfFile(fileStat);
155 }
156
157 // check if-modified if requested
158 time_t ifModifiedSince = 0;
159 try {
160 ifModifiedSince = stol(data->request.env()["if-modified-since"]);
161 } catch (invalid_argument const&) {
162 } catch (out_of_range const&) {}
163 if (checkIfModifiedSince && ifModifiedSince >= lastModified) {
164 setStatus(304);
165 setResponseBody(string());
166 return;
167 }
168
169 // set content-type header
170 if (!contentType.empty()) {
171 setHeader("content-type", contentType);
172 } else {
173 // use the function from utils.h to guess the content type
175 }
176
177 // set the content-disposition header
178 if (forceDownload) {
179 if (!downloadFilename.empty()) {
180 stringstream hval;
181 hval << "attachment; filename=\"" << downloadFilename << '"';
182 setHeader("content-disposition", hval.str());
183 } else {
184 setHeader("content-disposition", "attachment");
185 }
186 } else if (!downloadFilename.empty()) {
187 stringstream hval;
188 hval << "inline; filename=\"" << downloadFilename << '"';
189 setHeader("content-disposition", hval.str());
190 }
191
192 // set the content-length header
193 // get file size
194 f.seekg(0, ios::end);
195 long fs = f.tellg();
196 f.seekg(0);
197 setHeader("content-length", to_string(fs));
198
199 // set the last-modified header (if possible)
200 if (lastModified > 0) {
201 try {
202 setHeader("last-modified", utils::makeHttpTime(lastModified));
203 } catch (Exception const& e) {
204 NLOG_ERROR(logger, e.getMessage())
205 NLOG_DEBUG(logger, e.getDebugMessage())
206 }
207 }
208
209 // resize the bodyString, fill it with \0 chars if needed, make sure char fs [(fs+1)th] is \0, and insert file contents
210 data->bodyString.resize(static_cast<unsigned long>(fs) + 1, '\0');
211 data->bodyString[fs] = '\0';
212 f.read(&data->bodyString[0], fs);
213
214 // also clear the stream so that it doesn't mess with our file
215 data->clearStream();
216}
217
218void Connection::setHeader(std::string key, std::string value) {
219 // convert to lowercase
220 transform(key.begin(), key.end(), key.begin(), ::tolower);
221 data->headers[key] = {std::move(value)};
222}
223
224void Connection::addHeader(std::string key, std::string value) {
225 // convert to lowercase
226 transform(key.begin(), key.end(), key.begin(), ::tolower);
227 data->headers[key].push_back(std::move(value));
228}
229
230void Connection::unsetHeader(std::string key) {
231 // convert to lowercase
232 transform(key.begin(), key.end(), key.begin(), ::tolower);
233 data->headers.erase(key);
234}
235
236std::unordered_multimap<std::string, std::string> Connection::getHeaders(bool includeCookies) const {
237 unordered_multimap<string, string> ret;
238 for (auto const& [key, values] : data->headers) {
239 for (auto const& value : values) {
240 ret.insert({key, value});
241 }
242 }
243
244 // include cookies if desired
245 if (includeCookies)
246 for (auto const& e : data->cookies) {
247 stringstream headerVal;
248 headerVal << e.first << "=" << e.second.content();
249 // Domain option
250 optional<string> domain = e.second.domain() ? e.second.domain() : data->cookiePolicy.domain();
251 if (domain && !domain->empty()) {
252 headerVal << "; Domain=" << *domain;
253 }
254 // Path option
255 optional<string> path = e.second.path() ? e.second.path() : data->cookiePolicy.path();
256 if (path && !path->empty()) {
257 headerVal << "; Path=" << *path;
258 }
259 // Expires option
260 optional<time_t> expiry = e.second.expires() ? e.second.expires() : data->cookiePolicy.expires();
261 if (expiry) {
262 try {
263 headerVal << "; Expires=" << utils::makeHttpTime(*expiry);
264 } catch (Exception const& e) {
265 NLOG_ERROR(logger, e.getMessage())
266 NLOG_DEBUG(logger, e.getDebugMessage())
267 }
268 }
269 // Max-Age option
270 optional<unsigned long> maxAge = e.second.maxAge() ? e.second.maxAge()
271 : data->cookiePolicy.maxAge();
272 if (maxAge) {
273 headerVal << "; Max-Age=" << *maxAge;
274 }
275 // Secure option
276 if (e.second.secure() || data->cookiePolicy.secure()) {
277 headerVal << "; Secure";
278 }
279 // HttpOnly option
280 if (e.second.httpOnly() || data->cookiePolicy.httpOnly()) {
281 headerVal << "; HttpOnly";
282 }
283 // SameSite option
284 Cookie::SameSite sameSite = (e.second.sameSite() != Cookie::SameSite::OFF) ? e.second.sameSite()
285 : data->cookiePolicy.sameSite();
286 if (sameSite == Cookie::SameSite::LAX) {
287 headerVal << "; SameSite=lax";
288 } else if (sameSite == Cookie::SameSite::STRICT) {
289 headerVal << "; SameSite=strict";
290 }
291 ret.insert({"set-cookie", headerVal.str()});
292 }
293
294 return ret;
295}
296
298 data->mergeStream();
299 return data->bodyString;
300}
301
303 data = make_unique<Data>(this, connectionInit);
304 data->flushCallback = connectionInit.flushCallback;
305
306 data->headers["content-type"] = {"text/html; charset=utf-8"};
307 // autostart of session must happen here (as config is not yet accessible in Session constructor)
308 // check if autostart is enabled in config and if yes, directly call ::start
309 if (data->config[{"session", "autostart"}] == "on") {
310 data->session.start();
311 }
312}
313
314void Connection::setCookie(std::string const& key, Cookie cookie) {
315 // check key and value using regex, according to ietf rfc 6265
316 regex matchKey(R"([A-Za-z0-9!#$%&'*+\-.^_`|~]*)");
317 regex matchContent(R"([A-Za-z0-9!#$%&'()*+\-.\/:<=>?@[\]^_`{|}~]*)");
318 if (!regex_match(key, matchKey) || !regex_match(cookie.content(), matchContent)) {
319 throw Exception(__PRETTY_FUNCTION__, 1, "Invalid characters in key or value");
320 }
321 data->cookies[key] = std::move(cookie);
322}
323
324void Connection::setCookie(std::string const& key, std::string cookieContent) {
325 setCookie(key, Cookie(std::move(cookieContent)));
326}
327
328void Connection::unsetCookie(std::string const& key) {
329 data->cookies.erase(key);
330}
331
333 // use callback to flush response
334 data->flushCallback(FlushCallbackContainer{
335 .status = data->responseStatus,
336 .headers = getHeaders(true),
337 .body = getResponseBody(),
338 .flushedBefore = data->isFlushed});
339 // response has been flushed now
340 data->isFlushed = true;
341 // also, empty the Connection object, so that content will not be sent more than once
342 setResponseBody("");
343}
344
345void Connection::setStatus(unsigned int status) {
346 data->responseStatus = status;
347}
348
350 data->cookiePolicy = std::move(policy);
351}
352
353unsigned int Connection::getStatus() const {
354 return data->responseStatus;
355}
356
357Request const& Connection::request() const noexcept {
358 return data->request;
359}
360
362 return data->session;
363}
364
365Session const& Connection::session() const noexcept {
366 return data->session;
367}
368
370 return data->config;
371}
372
373Config const& Connection::config() const noexcept {
374 return data->config;
375}
376
377ostream& Connection::responseStream() noexcept {
378 return data->responseStream;
379}
380
381bool Connection::applyFilters(AccessFilterList const& accessFilters) {
382 // if filters are disabled, do not even check
383 if (!accessFilters.filtersEnabled())
384 return false;
385
386 auto requestPath = data->request.env().getRequestPath();
387
388 // check block filters
389 for (auto const& flt : accessFilters.blockFilters()) {
390 // if the filter does not apply (or does in case of an inverted filter), go to the next
391 bool matches = flt.matches(requestPath);
392 if ((!matches && !flt.invert()) || (matches && flt.invert())) {
393 continue;
394 }
395
396 // filter matches -> apply block
397 setStatus(flt.status());
398 if (!flt.response().empty()) {
399 setResponseBody(flt.response());
400 } else {
402 }
403 // the request has been blocked, so no more filters have to be applied
404 // returning true means: the request has been filtered
405 return true;
406 }
407
408 // the ID is used to identify the exact filter for session cookie creation
409 int authFilterID = -1;
410 for (auto const& flt : accessFilters.authFilters()) {
411 ++authFilterID;
412
413 bool matches = flt.matches(requestPath);
414 if ((!matches && !flt.invert()) || (matches && flt.invert())) {
415 continue;
416 }
417
418 bool isAuthenticated = false;
419 string sessionVarKey;
420
421 // check session variable for this filter, if session usage is on
422 if (flt.useSessions()) {
423 data->session.start();
424 sessionVarKey = "_nawa_authfilter" + to_string(authFilterID);
425 if (data->session.isSet(sessionVarKey)) {
426 isAuthenticated = true;
427 }
428 }
429
430 // if this did not work, request authentication or invoke auth function if credentials have already been sent
431 if (!isAuthenticated) {
432 // case 1: no authorization header sent by client -> send a 401 without body
433 if (data->request.env()["authorization"].empty()) {
434 setStatus(401);
435 stringstream hval;
436 hval << "Basic";
437 if (!flt.authName().empty()) {
438 hval << " realm=\"" << flt.authName() << '"';
439 }
440 setHeader("www-authenticate", hval.str());
441
442 // that's it, the response must be sent to the client directly so it can authenticate
443 return true;
444 }
445 // case 2: credentials already sent
446 else {
447 // split the authorization string, only the last part should contain base64
448 auto authResponse = utils::splitString(data->request.env()["authorization"], ' ', true);
449 // here, we should have a vector with size 2 and [0]=="Basic", otherwise sth is wrong
450 if (authResponse.size() == 2 || authResponse.at(0) == "Basic") {
451 auto credentials = utils::splitString(encoding::base64Decode(authResponse.at(1)), ':', true);
452 // credentials must also have 2 elements, a username and a password,
453 // and the auth function must be callable
454 if (credentials.size() == 2 && flt.authFunction()) {
455 // now we can actually check the credentials with our function (if it is set)
456 if (flt.authFunction()(credentials.at(0), credentials.at(1))) {
457 isAuthenticated = true;
458 // now, if sessions are used, set the session variable to the username
459 if (flt.useSessions()) {
460 data->session.set(sessionVarKey, any(credentials.at(0)));
461 }
462 }
463 }
464 }
465 }
466 }
467
468 // now, if the user is still not authenticated, send a 403 Forbidden
469 if (!isAuthenticated) {
470 setStatus(403);
471 if (!flt.response().empty()) {
472 setResponseBody(flt.response());
473 } else {
475 }
476
477 // request blocked
478 return true;
479 }
480
481 // if the user is authenticated, we can continue to process forward filters
482 break;
483 }
484
485 // check forward filters
486 for (auto const& flt : accessFilters.forwardFilters()) {
487 bool matches = flt.matches(requestPath);
488 if ((!matches && !flt.invert()) || (matches && flt.invert())) {
489 continue;
490 }
491
492 stringstream filePath;
493 filePath << flt.basePath();
494 if (flt.basePathExtension() == ForwardFilter::BasePathExtension::BY_PATH) {
495 for (auto const& e : requestPath) {
496 filePath << '/' << e;
497 }
498 } else {
499 filePath << '/' << requestPath.back();
500 }
501
502 // send file if it exists, catch the "file does not exist" nawa::Exception and send 404 document if not
503 auto filePathStr = filePath.str();
504 try {
505 sendFile(filePathStr, "", false, "", true);
506 } catch (Exception&) {
507 // file does not exist, send 404
508 setStatus(404);
509 if (!flt.response().empty()) {
510 setResponseBody(flt.response());
511 } else {
513 }
514 }
515
516 // return true as the request has been filtered
517 return true;
518 }
519
520 // if no filters were triggered (and therefore returned true), return false so that the request can be handled by
521 // the app
522 return false;
523}
524
526 stringstream hval;
527 hval << status;
528 if (httpStatusCodes.count(status) == 1) {
529 hval << " " << httpStatusCodes.at(status);
530 }
531 return hval.str();
532}
533
535 stringstream raw;
536 // include headers and cookies, but only when flushing for the first time
537 if (!flushedBefore) {
538 // Add headers, incl. cookies, to the raw HTTP source
539 for (auto const& e : headers) {
540 raw << e.first << ": " << e.second << "\r\n";
541 }
542 raw << "\r\n";
543 }
544 raw << body;
545 return raw.str();
546}
Options to check the path and invoke certain actions before forwarding the request to the app.
Container used by request handlers to initiate the nawa::Connection object.
Response object to be passed back to NAWA and accessor to the request.
Exception class that can be used by apps to catch errors resulting from nawa function calls.
Simple class for (not (yet) thread-safe) logging to stderr or to any other output stream.
#define NLOG_DEBUG(Logger, Message)
Definition: Log.h:201
#define NLOG_ERROR(Logger, Message)
Definition: Log.h:183
std::vector< BlockFilter > & blockFilters() noexcept
bool & filtersEnabled() noexcept
std::vector< AuthFilter > & authFilters() noexcept
std::vector< ForwardFilter > & forwardFilters() noexcept
nawa::Session & session() noexcept
Definition: Connection.cpp:361
void addHeader(std::string key, std::string value)
Definition: Connection.cpp:224
void setCookiePolicy(Cookie policy)
Definition: Connection.cpp:349
void unsetHeader(std::string key)
Definition: Connection.cpp:230
nawa::Config & config() noexcept
Definition: Connection.cpp:369
void setStatus(unsigned int status)
Definition: Connection.cpp:345
unsigned int getStatus() const
Definition: Connection.cpp:353
void setCookie(std::string const &key, Cookie cookie)
Definition: Connection.cpp:314
void setHeader(std::string key, std::string value)
Definition: Connection.cpp:218
std::ostream & responseStream() noexcept
Definition: Connection.cpp:377
Connection(ConnectionInitContainer const &connectionInit)
Definition: Connection.cpp:302
std::unordered_multimap< std::string, std::string > getHeaders(bool includeCookies=true) const
Definition: Connection.cpp:236
bool applyFilters(AccessFilterList const &accessFilters)
Definition: Connection.cpp:381
std::string getResponseBody()
Definition: Connection.cpp:297
nawa::Request const & request() const noexcept
Definition: Connection.cpp:357
void sendFile(std::string const &path, std::string const &contentType="", bool forceDownload=false, std::string const &downloadFilename="", bool checkIfModifiedSince=false)
Definition: Connection.cpp:139
void unsetCookie(const std::string &key)
Definition: Connection.cpp:328
void setResponseBody(std::string content)
Definition: Connection.cpp:134
std::string & content() noexcept
virtual std::string getMessage() const noexcept
Definition: Exception.h:71
virtual std::string getDebugMessage() const noexcept
Definition: Exception.h:79
Definition: Log.h:38
Namespace containing functions for text encoding and decoding.
#define NAWA_DEFAULT_DESTRUCTOR_IMPL(Class)
Definition: macros.h:36
std::string base64Decode(std::string const &input)
Definition: encoding.cpp:286
time_t getLastModifiedTimeOfFile(struct stat const &fileStat)
Definition: oss.h:34
std::string getFileExtension(std::string const &filename)
Definition: utils.cpp:343
std::string generateErrorPage(unsigned int httpStatus)
Definition: utils.cpp:266
std::string makeHttpTime(time_t time)
Definition: utils.cpp:359
std::string contentTypeByExtension(std::string extension)
Definition: utils.cpp:351
std::vector< std::string > splitString(std::string str, char delimiter, bool ignoreEmpty=false)
Definition: utils.cpp:448
Definition: AppInit.h:31
std::function< void(FlushCallbackContainer)> FlushCallbackFunction
This file contains helpers for operating-system specific stuff.
std::string getFullHttp() const
Definition: Connection.cpp:534
std::unordered_multimap< std::string, std::string > headers
std::string getStatusString() const
Definition: Connection.cpp:525
Contains useful functions that improve the readability and facilitate maintenance of the NAWA code.