27#include <nawa/connection/ConnectionInitContainer.h>
40 const unordered_map<unsigned int, string> httpStatusCodes = {
44 {203,
"Non-Authoritative Information"},
46 {205,
"Reset Content"},
47 {206,
"Partial Content"},
48 {207,
"Multi-Status"},
49 {208,
"Already Reported"},
51 {300,
"Multiple Choices"},
52 {301,
"Moved Permanently"},
55 {304,
"Not Modified"},
57 {307,
"Temporary Redirect"},
58 {308,
"Permanent Redirect"},
60 {401,
"Unauthorized"},
61 {402,
"Payment Required"},
64 {405,
"Method Not Allowed"},
65 {406,
"Not Acceptable"},
66 {407,
"Proxy Authentication Required"},
67 {408,
"Request Timeout"},
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"},
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"},
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"}};
100struct Connection::Data {
102 unsigned int responseStatus = 200;
103 unordered_map<string, vector<string>> headers;
104 unordered_map<string, Cookie> cookies;
106 bool isFlushed =
false;
107 FlushCallbackFunction flushCallback;
112 stringstream responseStream;
115 responseStream.str(
string());
116 responseStream.clear();
120 bodyString += responseStream.str();
124 Data(
Connection* base, ConnectionInitContainer
const& connectionInit) : request(connectionInit.requestInit),
125 config(connectionInit.config),
132 data->bodyString = move(content);
136void Connection::sendFile(
string const& path,
string const& contentType,
bool forceDownload,
137 string const& downloadFilename,
bool checkIfModifiedSince) {
140 ifstream f(path, ifstream::binary);
144 throw Exception(__PRETTY_FUNCTION__, 1,
"Cannot open file for reading");
148 struct stat fileStat;
149 time_t lastModified = 0;
150 if (stat(path.c_str(), &fileStat) == 0) {
151 lastModified = oss::getLastModifiedTimeOfFile(fileStat);
155 time_t ifModifiedSince = 0;
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) {
167 if (!contentType.empty()) {
176 if (!downloadFilename.empty()) {
178 hval <<
"attachment; filename=\"" << downloadFilename <<
'"';
179 setHeader(
"content-disposition", hval.str());
181 setHeader(
"content-disposition",
"attachment");
183 }
else if (!downloadFilename.empty()) {
185 hval <<
"inline; filename=\"" << downloadFilename <<
'"';
186 setHeader(
"content-disposition", hval.str());
191 f.seekg(0, ios::end);
194 setHeader(
"content-length", to_string(fs));
197 if (lastModified > 0) {
202 data->bodyString.resize(
static_cast<unsigned long>(fs) + 1,
'\0');
203 data->bodyString[fs] =
'\0';
204 f.read(&data->bodyString[0], fs);
212 transform(key.begin(), key.end(), key.begin(), ::tolower);
213 data->headers[key] = {move(value)};
218 transform(key.begin(), key.end(), key.begin(), ::tolower);
219 data->headers[key].push_back(move(value));
224 transform(key.begin(), key.end(), key.begin(), ::tolower);
225 data->headers.erase(key);
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});
238 for (
auto const& e : data->cookies) {
239 stringstream headerVal;
240 headerVal << e.first <<
"=" << e.second.content();
242 optional<string> domain = e.second.domain() ? e.second.domain() : data->cookiePolicy.domain();
243 if (domain && !domain->empty()) {
244 headerVal <<
"; Domain=" << *domain;
247 optional<string> path = e.second.path() ? e.second.path() : data->cookiePolicy.path();
248 if (path && !path->empty()) {
249 headerVal <<
"; Path=" << *path;
252 optional<time_t> expiry = e.second.expires() ? e.second.expires() : data->cookiePolicy.expires();
257 optional<unsigned long> maxAge = e.second.maxAge() ? e.second.maxAge()
258 : data->cookiePolicy.maxAge();
260 headerVal <<
"; Max-Age=" << *maxAge;
263 if (e.second.secure() || data->cookiePolicy.secure()) {
264 headerVal <<
"; Secure";
267 if (e.second.httpOnly() || data->cookiePolicy.httpOnly()) {
268 headerVal <<
"; HttpOnly";
272 : data->cookiePolicy.sameSite();
274 headerVal <<
"; SameSite=lax";
276 headerVal <<
"; SameSite=strict";
278 ret.insert({
"set-cookie", headerVal.str()});
286 return data->bodyString;
290 data = make_unique<Data>(
this, connectionInit);
291 data->flushCallback = connectionInit.flushCallback;
293 data->headers[
"content-type"] = {
"text/html; charset=utf-8"};
296 if (data->config[{
"session",
"autostart"}] ==
"on") {
297 data->session.start();
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");
308 data->cookies[key] = move(cookie);
316 data->cookies.erase(key);
321 data->flushCallback(FlushCallbackContainer{
322 .status = data->responseStatus,
325 .flushedBefore = data->isFlushed});
327 data->isFlushed =
true;
333 data->responseStatus = status;
337 data->cookiePolicy = move(policy);
341 return data->responseStatus;
345 return data->request;
349 return data->session;
353 return data->session;
365 return data->responseStream;
373 auto requestPath = data->request.env().getRequestPath();
378 bool matches = flt.matches(requestPath);
379 if ((!matches && !flt.invert()) || (matches && flt.invert())) {
385 if (!flt.response().empty()) {
396 int authFilterID = -1;
397 for (
auto const& flt : accessFilters.
authFilters()) {
400 bool matches = flt.matches(requestPath);
401 if ((!matches && !flt.invert()) || (matches && flt.invert())) {
405 bool isAuthenticated =
false;
406 string sessionVarKey;
409 if (flt.useSessions()) {
410 data->session.start();
411 sessionVarKey =
"_nawa_authfilter" + to_string(authFilterID);
412 if (data->session.isSet(sessionVarKey)) {
413 isAuthenticated =
true;
418 if (!isAuthenticated) {
420 if (data->request.env()[
"authorization"].empty()) {
424 if (!flt.authName().empty()) {
425 hval <<
" realm=\"" << flt.authName() <<
'"';
427 setHeader(
"www-authenticate", hval.str());
435 auto authResponse =
utils::splitString(data->request.env()[
"authorization"],
' ',
true);
437 if (authResponse.size() == 2 || authResponse.at(0) ==
"Basic") {
441 if (credentials.size() == 2 && flt.authFunction()) {
443 if (flt.authFunction()(credentials.at(0), credentials.at(1))) {
444 isAuthenticated =
true;
446 if (flt.useSessions()) {
447 data->session.set(sessionVarKey, any(credentials.at(0)));
456 if (!isAuthenticated) {
458 if (!flt.response().empty()) {
474 bool matches = flt.matches(requestPath);
475 if ((!matches && !flt.invert()) || (matches && flt.invert())) {
479 stringstream filePath;
480 filePath << flt.basePath();
482 for (
auto const& e : requestPath) {
483 filePath <<
'/' << e;
486 filePath <<
'/' << requestPath.back();
490 auto filePathStr = filePath.str();
492 sendFile(filePathStr,
"",
false,
"",
true);
496 if (!flt.response().empty()) {
512string FlushCallbackContainer::getStatusString()
const {
515 if (httpStatusCodes.count(status) == 1) {
516 hval <<
" " << httpStatusCodes.at(status);
521string FlushCallbackContainer::getFullHttp()
const {
524 if (!flushedBefore) {
526 for (
auto const& e : headers) {
527 raw << e.first <<
": " << e.second <<
"\r\n";
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
void addHeader(std::string key, std::string value)
void setCookiePolicy(Cookie policy)
void unsetHeader(std::string key)
std::unordered_multimap< std::string, std::string > getHeaders(bool includeCookies=true) const
nawa::Config & config() noexcept
void setStatus(unsigned int status)
unsigned int getStatus() const
void setCookie(std::string const &key, Cookie cookie)
void setHeader(std::string key, std::string value)
std::ostream & responseStream() noexcept
Connection(ConnectionInitContainer const &connectionInit)
bool applyFilters(AccessFilterList const &accessFilters)
std::string getResponseBody()
nawa::Request const & request() const noexcept
void sendFile(std::string const &path, std::string const &contentType="", bool forceDownload=false, std::string const &downloadFilename="", bool checkIfModifiedSince=false)
void unsetCookie(const std::string &key)
void setResponseBody(std::string content)
std::string & content() noexcept
Namespace containing functions for text encoding and decoding.
#define NAWA_DEFAULT_DESTRUCTOR_IMPL(Class)
std::string base64Decode(std::string const &input)
std::string generateErrorPage(unsigned int httpStatus)
std::string contentTypeByExtension(std::string extension)
std::string getFileExtension(std::string const &filename)
std::string makeHttpTime(time_t time)
std::vector< std::string > splitString(std::string str, char delimiter, bool ignoreEmpty=false)
Contains useful functions that improve the readability and facilitate maintenance of the NAWA code.