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>
30 #include <nawa/logging/Log.h>
32 #include <nawa/util/utils.h>
33 
34 using namespace nawa;
35 using namespace std;
36 namespace http = boost::network::http;
37 struct HttpHandler;
38 using HttpServer = http::server<HttpHandler>;
39 
40 namespace {
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 
68 struct 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 
148 struct 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 = 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  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 
243 struct 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 
252 HttpRequestHandler::HttpRequestHandler(shared_ptr<HandleRequestFunctionWrapper> handleRequestFunction,
253  Config config,
254  int concurrency) {
255  data = make_unique<Data>();
256 
257  setAppRequestHandler(move(handleRequestFunction));
258  setConfig(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 
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 
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 
321 void HttpRequestHandler::stop() noexcept {
322  if (data->joined) {
323  return;
324  }
325  if (data->server) {
326  data->server->stop();
327  }
328 }
329 
331  if (data->joined) {
332  return;
333  }
334  if (data->server) {
335  data->server->stop();
336  }
337 }
338 
339 void 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
void terminate() noexcept override
HttpRequestHandler(std::shared_ptr< HandleRequestFunctionWrapper > handleRequestFunction, Config config, int concurrency)
void stop() noexcept override
void join() noexcept override
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:531
std::unordered_multimap< std::string, std::string > parseCookies(std::string const &rawCookies)
Definition: utils.cpp:569
std::string generateErrorPage(unsigned int httpStatus)
Definition: utils.cpp:266
std::string toLowercase(std::string s)
Definition: utils.cpp:256
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.