NAWA 0.9
Web Application Framework for C++
HttpRequestHandler.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 <boost/network/protocol/http/server.hpp>
25#include <nawa/Exception.h>
30#include <nawa/logging/Log.h>
32#include <nawa/util/utils.h>
33
34using namespace nawa;
35using namespace std;
36namespace http = boost::network::http;
37struct HttpHandler;
38using HttpServer = http::server<HttpHandler>;
39
40namespace {
41 Log logger;
42
46 enum class RawPostAccess {
47 NEVER,
48 NONSTANDARD,
49 ALWAYS
50 };
51
52 auto sendServerError = [](HttpServer::connection_ptr& httpConn) {
53 httpConn->set_status(HttpServer::connection::internal_server_error);
54 httpConn->set_headers(unordered_multimap<string, string>({{"content-type", "text/html; charset=utf-8"}}));
55 httpConn->write(utils::generateErrorPage(500));
56 };
57
58 inline string getListenAddr(shared_ptr<Config const> const& configPtr) {
59 return (*configPtr)[{"http", "listen"}].empty() ? "127.0.0.1" : (*configPtr)[{"http", "listen"}];
60 }
61
62 inline string getListenPort(shared_ptr<Config const> const& configPtr) {
63 return (*configPtr)[{"http", "port"}].empty() ? "8080" : (*configPtr)[{"http", "port"}];
64 ;
65 }
66}// namespace
67
68struct InputConsumingHttpHandler : public enable_shared_from_this<InputConsumingHttpHandler> {
69 RequestHandler* requestHandler = nullptr;
70 ConnectionInitContainer connectionInit;
71 ssize_t maxPostSize;
72 size_t expectedSize;
73 string postBody;
74 RawPostAccess rawPostAccess;
75
76 InputConsumingHttpHandler(RequestHandler* requestHandler, ConnectionInitContainer connectionInit,
77 ssize_t maxPostSize, size_t expectedSize, RawPostAccess rawPostAccess)
78 : requestHandler(requestHandler), connectionInit(std::move(connectionInit)), maxPostSize(maxPostSize),
79 expectedSize(expectedSize), rawPostAccess(rawPostAccess) {}
80
81 void operator()(HttpServer::connection::input_range input, boost::system::error_code ec,
82 size_t bytesTransferred, HttpServer::connection_ptr httpConn) {
83 if (ec == boost::asio::error::eof) {
84 NLOG_ERROR(logger, "Request with POST data could not be handled.")
85 NLOG_DEBUG(logger, "Debug info: boost::asio::error::eof in cpp-netlib while processing POST data")
86 sendServerError(httpConn);
87 return;
88 }
89
90 // too large?
91 if (postBody.size() + bytesTransferred > maxPostSize) {
92 sendServerError(httpConn);
93 return;
94 }
95
96 // fill POST body
97 postBody.insert(postBody.end(), boost::begin(input), boost::end(input));
98
99 // check whether even more data has to be read
100 if (postBody.size() < expectedSize) {
101 auto self = this->shared_from_this();
102 httpConn->read([self](HttpServer::connection::input_range input,
103 boost::system::error_code ec, size_t bytes_transferred,
104 HttpServer::connection_ptr httpConn) {
105 (*self)(input, ec, bytes_transferred, httpConn);
106 });
107 return;
108 }
109
110 string const multipartContentType = "multipart/form-data";
111 string const plainTextContentType = "text/plain";
112 auto postContentType = utils::toLowercase(connectionInit.requestInit.environment["content-type"]);
113 auto& requestInit = connectionInit.requestInit;
114
115 if (rawPostAccess == RawPostAccess::ALWAYS) {
116 requestInit.rawPost = make_shared<string>(postBody);
117 }
118
119 if (postContentType == "application/x-www-form-urlencoded") {
120 requestInit.postContentType = postContentType;
121 requestInit.postVars = utils::splitQueryString(postBody);
122 } else if (postContentType.substr(0, multipartContentType.length()) == multipartContentType) {
123 try {
124 MimeMultipart postData(connectionInit.requestInit.environment["content-type"], std::move(postBody));
125 for (auto const& p : postData.parts()) {
126 // find out whether the part is a file
127 if (!p.filename().empty() || (!p.contentType().empty() &&
128 p.contentType().substr(0, plainTextContentType.length()) !=
129 plainTextContentType)) {
130 File pf = File(p.content()).contentType(p.contentType()).filename(p.filename());
131 requestInit.postFiles.insert({p.partName(), std::move(pf)});
132 } else {
133 requestInit.postVars.insert({p.partName(), p.content()});
134 }
135 }
136 } catch (Exception const&) {}
137 } else if (rawPostAccess == RawPostAccess::NONSTANDARD) {
138 requestInit.rawPost = make_shared<string>(std::move(postBody));
139 }
140
141 // finally handle the request
142 Connection connection(connectionInit);
143 requestHandler->handleRequest(connection);
144 connection.flushResponse();
145 }
146};
147
148struct HttpHandler {
149 RequestHandler* requestHandler = nullptr;
150
151 void operator()(HttpServer::request const& request, HttpServer::connection_ptr httpConn) {
152 auto configPtr = requestHandler->getConfig();
153
154 RequestInitContainer requestInit;
155 requestInit.environment = {
156 {"REMOTE_ADDR", request.source.substr(0, request.source.find_first_of(':'))},
157 {"REQUEST_URI", request.destination},
158 {"REMOTE_PORT", to_string(request.source_port)},
159 {"REQUEST_METHOD", request.method},
160 {"SERVER_ADDR", getListenAddr(configPtr)},
161 {"SERVER_PORT", getListenPort(configPtr)},
162 {"SERVER_SOFTWARE", "NAWA Development Web Server"},
163 };
164
165 // evaluate request headers
166 for (auto const& h : request.headers) {
167 if (requestInit.environment.count(utils::toLowercase(h.name)) == 0) {
168 requestInit.environment[utils::toLowercase(h.name)] = h.value;
169 }
170 }
171
172 {
173 // the base URL is the URL without the request URI, e.g., https://www.example.com
174 stringstream baseUrl;
175
176 // change following section if HTTPS should ever be implemented (copy from fastcgi)
177 baseUrl << "http://" << requestInit.environment["host"];
178
179 auto baseUrlStr = baseUrl.str();
180 requestInit.environment["BASE_URL"] = baseUrlStr;
181
182 // fullUrlWithQS is the full URL, e.g., https://www.example.com/test?a=b&c=d
183 requestInit.environment["FULL_URL_WITH_QS"] = baseUrlStr + request.destination;
184
185 // fullUrlWithoutQS is the full URL without query string, e.g., https://www.example.com/test
186 baseUrl << request.destination.substr(0, request.destination.find_first_of('?'));
187 requestInit.environment["FULL_URL_WITHOUT_QS"] = baseUrl.str();
188 }
189
190 if (request.destination.find_first_of('?') != string::npos) {
191 requestInit.getVars = utils::splitQueryString(request.destination);
192 }
193 requestInit.cookieVars = utils::parseCookies(requestInit.environment["cookie"]);
194
195 ConnectionInitContainer connectionInit;
196 connectionInit.requestInit = std::move(requestInit);
197 connectionInit.config = (*configPtr);
198
199 connectionInit.flushCallback = [httpConn](FlushCallbackContainer flushInfo) {
200 if (!flushInfo.flushedBefore) {
201 httpConn->set_status(HttpServer::connection::status_t(flushInfo.status));
202 httpConn->set_headers(flushInfo.headers);
203 }
204 httpConn->write(flushInfo.body);
205 };
206
207 // is there POST data to be handled?
208 if (request.method == "POST" && connectionInit.requestInit.environment.count("content-length")) {
209 try {
210 string rawPostStr = (*configPtr)[{"post", "raw_access"}];
211 auto rawPostAccess = (rawPostStr == "never")
212 ? RawPostAccess::NEVER
213 : ((rawPostStr == "always") ? RawPostAccess::ALWAYS
214 : RawPostAccess::NONSTANDARD);
215
216 auto contentLength = stoul(connectionInit.requestInit.environment.at("content-length"));
217 ssize_t maxPostSize = stol((*configPtr)[{"post", "max_size"}]) * 1024;
218
219 if (contentLength > maxPostSize) {
220 sendServerError(httpConn);
221 return;
222 }
223
224 auto inputConsumingHandler = make_shared<InputConsumingHttpHandler>(requestHandler,
225 std::move(connectionInit), maxPostSize,
226 contentLength, rawPostAccess);
227 httpConn->read([inputConsumingHandler](HttpServer::connection::input_range input,
228 boost::system::error_code ec, size_t bytesTransferred,
229 HttpServer::connection_ptr httpConn) {
230 (*inputConsumingHandler)(input, ec, bytesTransferred, httpConn);
231 });
232 } catch (invalid_argument const&) {
233 } catch (out_of_range const&) {}
234 return;
235 }
236
237 Connection connection(connectionInit);
238 requestHandler->handleRequest(connection);
239 connection.flushResponse();
240 }
241};
242
243struct HttpRequestHandler::Data {
244 unique_ptr<HttpHandler> handler;
245 unique_ptr<HttpServer> server;
246 int concurrency = 1;
247 vector<thread> threadPool;
248 bool requestHandlingActive = false;
249 bool joined = false;
250};
251
252HttpRequestHandler::HttpRequestHandler(std::shared_ptr<HandleRequestFunctionWrapper> handleRequestFunction,
253 Config config,
254 int concurrency) {
255 data = make_unique<Data>();
256
257 setAppRequestHandler(std::move(handleRequestFunction));
258 setConfig(std::move(config));
259 auto configPtr = getConfig();
260
261 logger.setAppname("HttpRequestHandler");
262
263 data->handler = make_unique<HttpHandler>();
264 data->handler->requestHandler = this;
265 HttpServer::options httpServerOptions(*data->handler);
266
267 // set options from config
268 string listenAddr = getListenAddr(configPtr);
269 string listenPort = getListenPort(configPtr);
270 bool reuseAddr = (*configPtr)[{"http", "reuseaddr"}] != "off";
271 data->server = make_unique<HttpServer>(
272 httpServerOptions.address(listenAddr).port(listenPort).reuse_address(reuseAddr));
273
274 if (concurrency > 0) {
275 data->concurrency = concurrency;
276 }
277
278 try {
279 data->server->listen();
280 } catch (exception const& e) {
281 throw Exception(__PRETTY_FUNCTION__, 1,
282 "Could not listen to host/port.", e.what());
283 }
284}
285
286HttpRequestHandler::~HttpRequestHandler() {
287 if (data->requestHandlingActive && !data->joined) {
288 data->server->stop();
289 }
290 if (!data->joined) {
291 for (auto& t : data->threadPool) {
292 t.join();
293 }
294 data->threadPool.clear();
295 }
296}
297
298void HttpRequestHandler::start() {
299 if (data->requestHandlingActive) {
300 return;
301 }
302 if (data->joined) {
303 throw Exception(__PRETTY_FUNCTION__, 10, "HttpRequestHandler was already joined.");
304 }
305 if (data->server) {
306 try {
307 for (int i = 0; i < data->concurrency; ++i) {
308 data->threadPool.emplace_back([this] { data->server->run(); });
309 }
310 data->requestHandlingActive = true;
311 } catch (exception const& e) {
312 throw Exception(__PRETTY_FUNCTION__, 1,
313 string("An error occurred during start of request handling."),
314 e.what());
315 }
316 } else {
317 throw Exception(__PRETTY_FUNCTION__, 2, "HTTP handler is not available.");
318 }
319}
320
321void HttpRequestHandler::stop() noexcept {
322 if (data->joined) {
323 return;
324 }
325 if (data->server) {
326 data->server->stop();
327 }
328}
329
330void HttpRequestHandler::terminate() noexcept {
331 if (data->joined) {
332 return;
333 }
334 if (data->server) {
335 data->server->stop();
336 }
337}
338
339void HttpRequestHandler::join() noexcept {
340 if (data->joined) {
341 return;
342 }
343 for (auto& t : data->threadPool) {
344 t.join();
345 }
346 data->joined = true;
347 data->threadPool.clear();
348}
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.
http::server< HttpHandler > HttpServer
A request handler which creates a development web server.
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
Parser for MIME multipart, especially in POST form data.
Handles and serves incoming requests via the NAWA app.
std::string & contentType() noexcept
Definition: Log.h:38
std::shared_ptr< Config const > getConfig() const noexcept
void handleRequest(Connection &connection)
std::string toLowercase(std::string s)
Definition: utils.cpp:256
std::string generateErrorPage(unsigned int httpStatus)
Definition: utils.cpp:266
std::unordered_multimap< std::string, std::string > splitQueryString(std::string const &queryString)
Definition: utils.cpp:531
std::unordered_multimap< std::string, std::string > parseCookies(std::string const &rawCookies)
Definition: utils.cpp:569
Definition: AppInit.h:31
std::unordered_multimap< std::string, std::string > getVars
std::unordered_map< std::string, std::string > environment
std::unordered_multimap< std::string, std::string > cookieVars
std::shared_ptr< std::string > rawPost
Contains useful functions that improve the readability and facilitate maintenance of the NAWA code.