NAWA 0.8
Web Application Framework for C++
HttpRequestHandler.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 <boost/network/protocol/http/server.hpp>
25#include <nawa/Exception.h>
27#include <nawa/RequestHandler/impl/HttpRequestHandler.h>
29#include <nawa/connection/ConnectionInitContainer.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(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"], 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(), 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>(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 // TODO accept languages (split), split acceptContentTypes?, acceptCharsets (where to find?)
167 // - consistent names for other elements in req handlers?
168 for (auto const& h : request.headers) {
169 if (requestInit.environment.count(utils::toLowercase(h.name)) == 0) {
170 requestInit.environment[utils::toLowercase(h.name)] = h.value;
171 }
172 }
173
174 {
175 // the base URL is the URL without the request URI, e.g., https://www.example.com
176 stringstream baseUrl;
177
178 // change following section if HTTPS should ever be implemented (copy from fastcgi)
179 baseUrl << "http://" << requestInit.environment["host"];
180
181 auto baseUrlStr = baseUrl.str();
182 requestInit.environment["BASE_URL"] = baseUrlStr;
183
184 // fullUrlWithQS is the full URL, e.g., https://www.example.com/test?a=b&c=d
185 requestInit.environment["FULL_URL_WITH_QS"] = baseUrlStr + request.destination;
186
187 // fullUrlWithoutQS is the full URL without query string, e.g., https://www.example.com/test
188 baseUrl << request.destination.substr(0, request.destination.find_first_of('?'));
189 requestInit.environment["FULL_URL_WITHOUT_QS"] = baseUrl.str();
190 }
191
192 if (request.destination.find_first_of('?') != string::npos) {
193 requestInit.getVars = utils::splitQueryString(request.destination);
194 }
195 requestInit.cookieVars = utils::parseCookies(requestInit.environment["cookie"]);
196
197 ConnectionInitContainer connectionInit;
198 connectionInit.requestInit = move(requestInit);
199 connectionInit.config = (*configPtr);
200
201 connectionInit.flushCallback = [httpConn](FlushCallbackContainer flushInfo) {
202 if (!flushInfo.flushedBefore) {
203 httpConn->set_status(HttpServer::connection::status_t(flushInfo.status));
204 httpConn->set_headers(flushInfo.headers);
205 }
206 httpConn->write(flushInfo.body);
207 };
208
209 // is there POST data to be handled?
210 if (request.method == "POST" && connectionInit.requestInit.environment.count("content-length")) {
211 try {
212 string rawPostStr = (*configPtr)[{"post", "raw_access"}];
213 auto rawPostAccess = (rawPostStr == "never")
214 ? RawPostAccess::NEVER
215 : ((rawPostStr == "always") ? RawPostAccess::ALWAYS
216 : RawPostAccess::NONSTANDARD);
217
218 auto contentLength = stoul(connectionInit.requestInit.environment.at("content-length"));
219 ssize_t maxPostSize = stol((*configPtr)[{"post", "max_size"}]) * 1024;
220
221 if (contentLength > maxPostSize) {
222 sendServerError(httpConn);
223 return;
224 }
225
226 auto inputConsumingHandler = make_shared<InputConsumingHttpHandler>(requestHandler,
227 move(connectionInit), maxPostSize,
228 contentLength, rawPostAccess);
229 httpConn->read([inputConsumingHandler](HttpServer::connection::input_range input,
230 boost::system::error_code ec, size_t bytesTransferred,
231 HttpServer::connection_ptr httpConn) {
232 (*inputConsumingHandler)(input, ec, bytesTransferred, httpConn);
233 });
234 } catch (invalid_argument const&) {
235 } catch (out_of_range const&) {}
236 return;
237 }
238
239 Connection connection(connectionInit);
240 requestHandler->handleRequest(connection);
241 connection.flushResponse();
242 }
243};
244
245struct HttpRequestHandler::Data {
246 unique_ptr<HttpHandler> handler;
247 unique_ptr<HttpServer> server;
248 int concurrency = 1;
249 vector<thread> threadPool;
250 bool requestHandlingActive = false;
251 bool joined = false;
252};
253
254HttpRequestHandler::HttpRequestHandler(shared_ptr<HandleRequestFunctionWrapper> handleRequestFunction,
255 Config config,
256 int concurrency) {
257 data = make_unique<Data>();
258
259 setAppRequestHandler(move(handleRequestFunction));
260 setConfig(move(config));
261 auto configPtr = getConfig();
262
263 logger.setAppname("HttpRequestHandler");
264
265 data->handler = make_unique<HttpHandler>();
266 data->handler->requestHandler = this;
267 HttpServer::options httpServerOptions(*data->handler);
268
269 // set options from config
270 string listenAddr = getListenAddr(configPtr);
271 string listenPort = getListenPort(configPtr);
272 bool reuseAddr = (*configPtr)[{"http", "reuseaddr"}] != "off";
273 data->server = make_unique<HttpServer>(
274 httpServerOptions.address(listenAddr).port(listenPort).reuse_address(reuseAddr));
275
276 if (concurrency > 0) {
277 data->concurrency = concurrency;
278 }
279
280 try {
281 data->server->listen();
282 } catch (exception const& e) {
283 throw Exception(__PRETTY_FUNCTION__, 1,
284 "Could not listen to host/port.", e.what());
285 }
286}
287
288HttpRequestHandler::~HttpRequestHandler() {
289 if (data->requestHandlingActive && !data->joined) {
290 data->server->stop();
291 }
292 if (!data->joined) {
293 for (auto& t : data->threadPool) {
294 t.join();
295 }
296 data->threadPool.clear();
297 }
298}
299
300void HttpRequestHandler::start() {
301 if (data->requestHandlingActive) {
302 return;
303 }
304 if (data->joined) {
305 throw Exception(__PRETTY_FUNCTION__, 10, "HttpRequestHandler was already joined.");
306 }
307 if (data->server) {
308 try {
309 for (int i = 0; i < data->concurrency; ++i) {
310 data->threadPool.emplace_back([this] { data->server->run(); });
311 }
312 data->requestHandlingActive = true;
313 } catch (exception const& e) {
314 throw Exception(__PRETTY_FUNCTION__, 1,
315 string("An error occurred during start of request handling."),
316 e.what());
317 }
318 } else {
319 throw Exception(__PRETTY_FUNCTION__, 2, "HTTP handler is not available.");
320 }
321}
322
323void HttpRequestHandler::stop() noexcept {
324 if (data->joined) {
325 return;
326 }
327 if (data->server) {
328 data->server->stop();
329 }
330}
331
332void HttpRequestHandler::terminate() noexcept {
333 // TODO find (implement in fork of cpp-netlib) a way to forcefully terminate
334 if (data->joined) {
335 return;
336 }
337 if (data->server) {
338 data->server->stop();
339 }
340}
341
342void HttpRequestHandler::join() noexcept {
343 if (data->joined) {
344 return;
345 }
346 for (auto& t : data->threadPool) {
347 t.join();
348 }
349 data->joined = true;
350 data->threadPool.clear();
351}
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
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::unordered_multimap< std::string, std::string > splitQueryString(std::string const &queryString)
Definition: utils.cpp:501
std::unordered_multimap< std::string, std::string > parseCookies(std::string const &rawCookies)
Definition: utils.cpp:539
std::string generateErrorPage(unsigned int httpStatus)
Definition: utils.cpp:266
std::string toLowercase(std::string s)
Definition: utils.cpp:256
Definition: AppInit.h:31
Contains useful functions that improve the readability and facilitate maintenance of the NAWA code.