NAWA  0.8
Web Application Framework for C++
FastcgiRequestHandler.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 <fastcgi++/log.hpp>
25 #include <fastcgi++/manager.hpp>
26 #include <fastcgi++/request.hpp>
27 #include <nawa/Exception.h>
32 #include <nawa/logging/Log.h>
33 #include <nawa/util/utils.h>
34 
35 using namespace nawa;
36 using namespace std;
37 
38 namespace {
39  Log logger("fastcgi");
40 
44  enum class RawPostAccess {
45  NEVER,
46  NONSTANDARD,
47  ALWAYS
48  };
49 
50  class FastcgippRequestAdapter : public Fastcgipp::Request<char> {
51  shared_ptr<string> rawPost;
52 
53  public:
58  FastcgippRequestAdapter() : Fastcgipp::Request<char>() {}
59 
64  bool response() override;
65 
71  bool inProcessor() override;
72  };
73 }// namespace
74 
75 bool FastcgippRequestAdapter::response() {
76  RequestInitContainer requestInit;
77  auto requestHandler = any_cast<RequestHandler*>(m_externalObject);
78 
79  // fill environment
80  {
81  auto const& renv = environment();
82  auto renvp = [&](string const& k) {
83  return renv.parameters.count(k) ? renv.parameters.at(k) : string();
84  };
85  requestInit.environment = {
86  {"content-type", renvp("CONTENT_TYPE")},
87  };
88 
89  {
90  // the base URL is the URL without the request URI, e.g., https://www.example.com
91  stringstream baseUrl;
92  auto https = renv.parameters.count("HTTPS");
93  baseUrl << (https ? "https://" : "http://")
94  << renvp("HTTP_HOST");
95  auto baseUrlStr = baseUrl.str();
96  requestInit.environment["BASE_URL"] = baseUrlStr;
97  auto requestUri = renvp("REQUEST_URI");
98 
99  // fullUrlWithQS is the full URL, e.g., https://www.example.com/test?a=b&c=d
100  requestInit.environment["FULL_URL_WITH_QS"] = baseUrlStr + requestUri;
101 
102  // fullUrlWithoutQS is the full URL without query string, e.g., https://www.example.com/test
103  baseUrl << requestUri.substr(0, requestUri.find_first_of('?'));
104  requestInit.environment["FULL_URL_WITHOUT_QS"] = baseUrl.str();
105  }
106 
107  for (auto const& [k, v] : renv.parameters) {
108  string envKey;
109  // parameters starting with HTTP_ are usually HTTP headers
110  // convert them so they should match their original name (lowercase), e.g., HTTP_USER_AGENT => user-agent
111  if (k.substr(0, 5) == "HTTP_") {
112  envKey = utils::stringReplace(utils::toLowercase(k.substr(5)), {{'_', '-'}});
113  } else {
114  // other FastCGI parameters are used as they are
115  envKey = k;
116  }
117  if (requestInit.environment.count(envKey) == 0) {
118  requestInit.environment[envKey] = v;
119  }
120  }
121 
122  // GET, POST, COOKIE vars, raw POST, POST content type
123  requestInit.getVars = utils::toUnorderedMultimap(renv.gets);
124  requestInit.postVars = utils::toUnorderedMultimap(renv.posts);
125  requestInit.cookieVars = utils::toUnorderedMultimap(renv.cookies);
126  requestInit.rawPost = rawPost;
127  requestInit.postContentType = renv.contentType;
128 
129  // POST files
130  for (auto const& [k, fcgiFile] : renv.files) {
131  requestInit.postFiles.insert({k, File(fcgiFile.data, fcgiFile.size).filename(fcgiFile.filename).contentType(fcgiFile.contentType)});
132  }
133  }
134 
135  ConnectionInitContainer connectionInit;
136  connectionInit.requestInit = move(requestInit);
137  connectionInit.config = *requestHandler->getConfig();
138 
139  connectionInit.flushCallback = [this](FlushCallbackContainer flushInfo) {
140  string response;
141  if (!flushInfo.flushedBefore) {
142  response = "status: " + flushInfo.getStatusString() + "\r\n";
143  }
144  response += flushInfo.getFullHttp();
145  dump(response.c_str(), response.size());
146  };
147 
148  Connection connection(connectionInit);
149  requestHandler->handleRequest(connection);
150  connection.flushResponse();
151 
152  return true;
153 }
154 
155 bool FastcgippRequestAdapter::inProcessor() {
156  auto requestHandler = any_cast<RequestHandler*>(m_externalObject);
157  auto postContentType = environment().contentType;
158  auto configPtr = requestHandler->getConfig();
159 
160  string rawPostAccess = (*configPtr)[{"post", "raw_access"}];
161  if (postContentType.empty() || rawPostAccess == "never" ||
162  (rawPostAccess != "always" && (postContentType == "multipart/form-data" || postContentType == "application/x-www-form-urlencoded"))) {
163  return false;
164  }
165 
166  auto postBuffer = environment().postBuffer();
167  rawPost = make_shared<string>(postBuffer.data(), postBuffer.size());
168  return false;
169 }
170 
171 struct FastcgiRequestHandler::Data {
172  unique_ptr<Fastcgipp::Manager<FastcgippRequestAdapter>> fastcgippManager;
173  bool requestHandlingActive = false;
174  bool joined = false;
175 };
176 
177 FastcgiRequestHandler::FastcgiRequestHandler(shared_ptr<HandleRequestFunctionWrapper> handleRequestFunction,
178  Config config, int concurrency) {
179  data = make_unique<Data>();
180 
181  setAppRequestHandler(move(handleRequestFunction));
182  setConfig(move(config));
183  auto configPtr = getConfig();
184 
185  size_t postMax = 0;
186  try {
187  postMax = configPtr->isSet({"post", "max_size"})
188  ? static_cast<size_t>(stoul((*configPtr)[{"post", "max_size"}])) * 1024
189  : 0;
190  } catch (invalid_argument& e) {
191  NLOG_WARNING(logger, "WARNING: Invalid value given for post/max_size given in the config file.")
192  }
193 
194  // set up fastcgilite logging
195  Fastcgipp::Logging::addHeader = false;
196  Fastcgipp::Logging::logFunction = [&](string const& msg, Fastcgipp::Logging::Level level) {
197  Log::Level nawaLevel;
198  switch (level) {
199  case Fastcgipp::Logging::INFO:
200  nawaLevel = Log::Level::INFORMATIONAL;
201  break;
202  case Fastcgipp::Logging::FAIL:
203  case Fastcgipp::Logging::ERROR:
204  nawaLevel = Log::Level::ERROR;
205  break;
206  case Fastcgipp::Logging::WARNING:
207  nawaLevel = Log::Level::WARNING;
208  break;
209  case Fastcgipp::Logging::DEBUG:
210  case Fastcgipp::Logging::DIAG:
211  nawaLevel = Log::Level::DEBUG;
212  break;
213  default:
214  nawaLevel = Log::Level::INFORMATIONAL;
215  }
216  logger.write(msg, nawaLevel);
217  };
218 
219  data->fastcgippManager = make_unique<Fastcgipp::Manager<FastcgippRequestAdapter>>(concurrency, postMax,
220  static_cast<RequestHandler*>(this));
221 
222  // socket handling
223  string mode = (*configPtr)[{"fastcgi", "mode"}];
224  if (mode == "tcp") {
225  auto fastcgiListen = (*configPtr)[{"fastcgi", "listen"}];
226  auto fastcgiPort = (*configPtr)[{"fastcgi", "port"}];
227  if (fastcgiListen.empty())
228  fastcgiListen = "127.0.0.1";
229  char const* fastcgiListenC = fastcgiListen.c_str();
230  if (fastcgiListen == "all")
231  fastcgiListenC = nullptr;
232  if (fastcgiPort.empty())
233  fastcgiPort = "8000";
234  if (!data->fastcgippManager->listen(fastcgiListenC, fastcgiPort.c_str())) {
235  throw Exception(__PRETTY_FUNCTION__, 1,
236  "Could not create TCP socket for FastCGI.");
237  }
238  } else if (mode == "unix") {
239  uint32_t permissions = 0xffffffffUL;
240  string permStr = (*configPtr)[{"fastcgi", "permissions"}];
241  // convert the value from the config file
242  if (!permStr.empty()) {
243  char const* psptr = permStr.c_str();
244  char* endptr;
245  long perm = strtol(psptr, &endptr, 8);
246  if (*endptr == '\0') {
247  permissions = (uint32_t) perm;
248  }
249  }
250 
251  auto fastcgiSocketPath = (*configPtr)[{"fastcgi", "path"}];
252  if (fastcgiSocketPath.empty()) {
253  fastcgiSocketPath = "/etc/nawarun/sock.d/nawarun.sock";
254  }
255  auto fastcgiOwner = (*configPtr)[{"fastcgi", "owner"}];
256  auto fastcgiGroup = (*configPtr)[{"fastcgi", "group"}];
257 
258  if (!data->fastcgippManager->listen(fastcgiSocketPath.c_str(), permissions,
259  fastcgiOwner.empty() ? nullptr : fastcgiOwner.c_str(),
260  fastcgiGroup.empty() ? nullptr : fastcgiGroup.c_str())) {
261  throw Exception(__PRETTY_FUNCTION__, 2,
262  "Could not create UNIX socket for FastCGI.");
263  }
264  } else {
265  throw Exception(__PRETTY_FUNCTION__, 3,
266  "Unknown FastCGI socket mode in configuration.");
267  }
268 
269  // tell fastcgi to use SO_REUSEADDR if enabled in config
270  if ((*configPtr)[{"fastcgi", "reuseaddr"}] != "off") {
271  data->fastcgippManager->reuseAddress(true);
272  }
273 }
274 
276  if (data->requestHandlingActive && !data->joined) {
277  data->fastcgippManager->terminate();
278  }
279  if (!data->joined) {
280  data->fastcgippManager->join();
281  data->fastcgippManager.reset(nullptr);
282  }
283 }
284 
286  if (data->requestHandlingActive) {
287  return;
288  }
289  if (data->joined) {
290  throw Exception(__PRETTY_FUNCTION__, 10, "FastcgiRequestHandler was already joined.");
291  }
292  if (data->fastcgippManager) {
293  try {
294  data->fastcgippManager->start();
295  data->requestHandlingActive = true;
296  } catch (...) {
297  throw Exception(__PRETTY_FUNCTION__, 1,
298  "An unknown error occurred during start of request handling.");
299  }
300  } else {
301  throw Exception(__PRETTY_FUNCTION__, 2, "FastCGI manager is not available.");
302  }
303 }
304 
306  if (data->joined) {
307  return;
308  }
309  if (data->fastcgippManager) {
310  data->fastcgippManager->stop();
311  }
312 }
313 
315  if (data->joined) {
316  return;
317  }
318  if (data->fastcgippManager) {
319  data->fastcgippManager->terminate();
320  }
321 }
322 
324  if (data->joined) {
325  return;
326  }
327  if (data->fastcgippManager) {
328  data->fastcgippManager->join();
329  data->joined = true;
330  data->fastcgippManager.reset(nullptr);
331  }
332 }
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.
Class which connects NAWA to the fastcgi++ library.
Simple class for (not (yet) thread-safe) logging to stderr or to any other output stream.
#define NLOG_WARNING(Logger, Message)
Definition: Log.h:189
Handles and serves incoming requests via the NAWA app.
FastcgiRequestHandler(std::shared_ptr< HandleRequestFunctionWrapper > handleRequestFunction, Config config, int concurrency)
void terminate() noexcept override
std::string & filename() noexcept
Definition: Log.h:38
Level
Definition: Log.h:47
std::shared_ptr< Config const > getConfig() const noexcept
void setConfig(Config config) noexcept
void setAppRequestHandler(std::shared_ptr< HandleRequestFunctionWrapper > handleRequestFunction) noexcept
std::unordered_multimap< KeyType, ValueType > toUnorderedMultimap(MapType< KeyType, ValueType, Args... > inputMap)
Definition: utils.h:214
std::string stringReplace(std::string input, std::unordered_map< char, char > const &patterns)
Definition: utils.cpp:514
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::unordered_multimap< std::string, File > postFiles
std::unordered_multimap< std::string, std::string > postVars
std::shared_ptr< std::string > rawPost
Contains useful functions that improve the readability and facilitate maintenance of the NAWA code.