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