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>
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 
37 using namespace nawa;
38 using namespace std;
39 
40 namespace {
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 
103 struct 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 
134 void Connection::setResponseBody(string content) {
135  data->bodyString = move(content);
136  data->clearStream();
137 }
138 
139 void Connection::sendFile(string const& path, string const& contentType, bool forceDownload,
140  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
174  setHeader("content-type", utils::contentTypeByExtension(utils::getFileExtension(path)));
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 
218 void Connection::setHeader(string key, string value) {
219  // convert to lowercase
220  transform(key.begin(), key.end(), key.begin(), ::tolower);
221  data->headers[key] = {move(value)};
222 }
223 
224 void Connection::addHeader(string key, string value) {
225  // convert to lowercase
226  transform(key.begin(), key.end(), key.begin(), ::tolower);
227  data->headers[key].push_back(move(value));
228 }
229 
230 void Connection::unsetHeader(string key) {
231  // convert to lowercase
232  transform(key.begin(), key.end(), key.begin(), ::tolower);
233  data->headers.erase(key);
234 }
235 
236 unordered_multimap<string, 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 
314 void Connection::setCookie(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] = move(cookie);
322 }
323 
324 void Connection::setCookie(string const& key, string cookieContent) {
325  setCookie(key, Cookie(move(cookieContent)));
326 }
327 
328 void Connection::unsetCookie(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 
345 void Connection::setStatus(unsigned int status) {
346  data->responseStatus = status;
347 }
348 
350  data->cookiePolicy = move(policy);
351 }
352 
353 unsigned int Connection::getStatus() const {
354  return data->responseStatus;
355 }
356 
357 Request const& Connection::request() const noexcept {
358  return data->request;
359 }
360 
362  return data->session;
363 }
364 
365 Session const& Connection::session() const noexcept {
366  return data->session;
367 }
368 
370  return data->config;
371 }
372 
373 Config const& Connection::config() const noexcept {
374  return data->config;
375 }
376 
377 ostream& Connection::responseStream() noexcept {
378  return data->responseStream;
379 }
380 
381 bool 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 {
401  setResponseBody(utils::generateErrorPage(flt.status()));
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 {
474  setResponseBody(utils::generateErrorPage(403));
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 {
512  setResponseBody(utils::generateErrorPage(404));
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< ForwardFilter > & forwardFilters() noexcept
std::vector< AuthFilter > & authFilters() noexcept
bool & filtersEnabled() noexcept
std::vector< BlockFilter > & blockFilters() 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
std::unordered_multimap< std::string, std::string > getHeaders(bool includeCookies=true) const
Definition: Connection.cpp:236
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
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
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 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: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::string getStatusString() const
Definition: Connection.cpp:525
Contains useful functions that improve the readability and facilitate maintenance of the NAWA code.