diff --git a/config.example.json b/config.example.json index 1c9640ff..73a361f2 100644 --- a/config.example.json +++ b/config.example.json @@ -147,7 +147,11 @@ //pipelining_requests: Set the maximum number of unhandled requests that can be cached in pipelining buffer. //After the maximum number of requests are made, the connection is closed. //The default value of 0 means no limit. - "pipelining_requests": 0 + "pipelining_requests": 0, + //gzip_static: If it is set to true, when the client requests a static file, drogon first finds the compressed + //file with the extension ".gz" in the same path and send the compressed file to the client. + //The default value of gzip_static is true. + "gzip_static": true }, //plugins: Define all plugins running in the application "plugins": [{ diff --git a/drogon_ctl/templates/config.csp b/drogon_ctl/templates/config.csp index 1c9640ff..73a361f2 100644 --- a/drogon_ctl/templates/config.csp +++ b/drogon_ctl/templates/config.csp @@ -147,7 +147,11 @@ //pipelining_requests: Set the maximum number of unhandled requests that can be cached in pipelining buffer. //After the maximum number of requests are made, the connection is closed. //The default value of 0 means no limit. - "pipelining_requests": 0 + "pipelining_requests": 0, + //gzip_static: If it is set to true, when the client requests a static file, drogon first finds the compressed + //file with the extension ".gz" in the same path and send the compressed file to the client. + //The default value of gzip_static is true. + "gzip_static": true }, //plugins: Define all plugins running in the application "plugins": [{ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 9f53f1ef..1a95a0fc 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -29,7 +29,12 @@ add_executable(webapp_test simple_example_test/main.cc) add_executable(pipelining_test simple_example_test/HttpPipeliningTest.cc) add_executable(websocket_test simple_example_test/WebSocketTest.cc) +add_custom_command(TARGET webapp POST_BUILD + COMMAND gzip + ARGS -c ${CMAKE_CURRENT_SOURCE_DIR}/simple_example/index.html > ${CMAKE_CURRENT_BINARY_DIR}/index.html.gz + VERBATIM) add_custom_command(TARGET webapp POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/config.example.json ${PROJECT_SOURCE_DIR}/drogon.jpg + ${CMAKE_CURRENT_SOURCE_DIR}/simple_example/index.html ${PROJECT_SOURCE_DIR}/trantor/trantor/tests/server.pem $) \ No newline at end of file diff --git a/examples/simple_example/index.html b/examples/simple_example/index.html new file mode 100755 index 00000000..67b276db --- /dev/null +++ b/examples/simple_example/index.html @@ -0,0 +1,187 @@ +

+ +

Build Status +Codacy Badge +Total alerts +Language grade: C/C++ +Join the chat at https://gitter.im/drogon-web/community +Docker image

+ +

Overview

+

Drogon is a C++14/17-based HTTP application framework. Drogon can be used to easily build various types of web application server programs using C++. Drogon is the name of a dragon in the American TV series “Game of Thrones” that I really like.

+ +

Drogon’s main application platform is Linux. It also supports Mac OS and FreeBSD. Currently, it does not support windows. Its main features are as follows:

+ + + +

A very simple example

+ +

Unlike most C++ frameworks, the main program of the drogon application can be kept clean and simple. Drogon uses a few tricks to decouple controllers from the main program. The routing settings of controllers can be done through macros or configuration file.

+ +

Below is the main program of a typical drogon application:

+ +

c++ +#include <drogon/drogon.h> +using namespace drogon; +int main() +{ + app().setLogPath("./"); + app().setLogLevel(trantor::Logger::WARN); + app().addListener("0.0.0.0", 80); + app().setThreadNum(16); + app().enableRunAsDaemon(); + app().run(); +} +

+ +

It can be further simplified by using configuration file as follows:

+ +

c++ +#include <drogon/drogon.h> +using namespace drogon; +int main() +{ + app().loadConfigFile("./config.json"); + app().run(); +} +

+ +

Drogon provides some interfaces for adding controller logic directly in the main() function, for example, user can register a handler like this in Drogon:

+ +

c++ +app.registerHandler("/test?username={1}", + [](const HttpRequestPtr& req, + const std::function<void (const HttpResponsePtr &)> & callback, + const std::string &name) + { + Json::Value json; + json["result"]="ok"; + json["message"]=std::string("hello,")+name; + auto resp=HttpResponse::newHttpJsonResponse(json); + callback(resp); + }, + {Get,"LoginFilter"}); +

+ +

While such interfaces look intuitive, they are not suitable for complex business logic scenarios. Assuming there are tens or even hundreds of handlers that need to be registered in the framework, isn’t it a better practice to implement them separately in their respective classes? So unless your logic is very simple, we don’t recommend using above interfaces. Instead, we can create an HttpSimpleController as follows:

+ +

```c++ +/// The TestCtrl.h file +#pragma once +#include <drogon/HttpSimpleController.h> +using namespace drogon; +class TestCtrl:public drogon::HttpSimpleController +{ +public: + virtual void asyncHandleHttpRequest(const HttpRequestPtr& req,const std::function<void (const HttpResponsePtr &)> & callback)override; + PATH_LIST_BEGIN + PATH_ADD("/test",Get); + PATH_LIST_END +};

+ +

/// The TestCtrl.cc file +#include “TestCtrl.h” +void TestCtrl::asyncHandleHttpRequest(const HttpRequestPtr& req, + const std::function<void (const HttpResponsePtr &)> & callback) +{ + //write your application logic here + auto resp = HttpResponse::newHttpResponse(); + resp->setBody(“<p>Hello, world!</p>”); + resp->setExpiredTime(0); + callback(resp); +} +```

+ +

Most of the above programs can be automatically generated by the command line tool drogon_ctl provided by drogon (The cammand is drogon_ctl create controller TestCtrl). All the user needs to do is add their own business logic. In the example, the controller returns a Hello, world! string when the client accesses the http://ip/test URL.

+ +

For JSON format response, we create the controller as follows:

+ +

```c++ +/// The header file +#pragma once +#include <drogon/HttpSimpleController.h> +using namespace drogon; +class JsonCtrl : public drogon::HttpSimpleController +{ + public: + virtual void asyncHandleHttpRequest(const HttpRequestPtr &req, const std::function<void(const HttpResponsePtr &)> &callback) override; + PATH_LIST_BEGIN + //list path definitions here; + PATH_ADD("/json", Get); + PATH_LIST_END +};

+ +

/// The source file +#include “JsonCtrl.h” +void JsonCtrl::asyncHandleHttpRequest(const HttpRequestPtr &req, + const std::function<void(const HttpResponsePtr &)> &callback) +{ + Json::Value ret; + ret[“message”] = “Hello, World!”; + auto resp = HttpResponse::newHttpJsonResponse(ret); + callback(resp); +} +```

+ +

Let’s go a step further and create a demo RESTful API with the HttpController class, as shown below (Omit the source file):

+ +

c++ +/// The header file +#pragma once +#include <drogon/HttpController.h> +using namespace drogon; +namespace api +{ +namespace v1 +{ +class User : public drogon::HttpController<User> +{ + public: + METHOD_LIST_BEGIN + //use METHOD_ADD to add your custom processing function here; + METHOD_ADD(User::getInfo, "/{1}", Get); //path is /api/v1/User/{arg1} + METHOD_ADD(User::getDetailInfo, "/{1}/detailinfo", Get); //path is /api/v1/User/{arg1}/detailinfo + METHOD_ADD(User::newUser, "/{1}", Post); //path is /api/v1/User/{arg1} + METHOD_LIST_END + //your declaration of processing function maybe like this: + void getInfo(const HttpRequestPtr &req, const std::function<void(const HttpResponsePtr &)> &callback, int userId) const; + void getDetailInfo(const HttpRequestPtr &req, const std::function<void(const HttpResponsePtr &)> &callback, int userId) const; + void newUser(const HttpRequestPtr &req, const std::function<void(const HttpResponsePtr &)> &callback, std::string &&userName); + public: + User() + { + LOG_DEBUG << "User constructor!"; + } +}; +} // namespace v1 +} // namespace api +

+ +

As you can see, users can use the HttpController to map paths and parameters at the same time. This is a very convenient way to create a RESTful API application.

+ +

In addition, you can also find that all handler interfaces are in asynchronous mode, where the response is returned by a callback object. This design is for performance reasons because in asynchronous mode the drogon application can handle a large number of concurrent requests with a small number of threads.

+ +

After compiling all of the above source files, we get a very simple web application. This is a good start. for more information, please visit the wiki site

diff --git a/examples/simple_example_test/main.cc b/examples/simple_example_test/main.cc index 894b3ac8..aca9553d 100644 --- a/examples/simple_example_test/main.cc +++ b/examples/simple_example_test/main.cc @@ -27,6 +27,8 @@ #define GREEN "\033[32m" /* Green */ #define JPG_LEN 44618 +#define INDEX_LEN 10605 + using namespace drogon; void outputGood(const HttpRequestPtr &req, bool isHttps) @@ -579,7 +581,54 @@ void doTest(const HttpClientPtr &client, std::promise &pro, bool isHttps = exit(1); } }); - + /// Test gzip_static + req = HttpRequest::newHttpRequest(); + req->setMethod(drogon::Get); + req->setPath("/index.html"); + client->sendRequest(req, [=](ReqResult result, const HttpResponsePtr &resp) { + if (result == ReqResult::Ok) + { + if (resp->getBody().length() == INDEX_LEN) + { + outputGood(req, isHttps); + } + else + { + LOG_DEBUG << resp->getBody().length(); + LOG_ERROR << "Error!"; + exit(1); + } + } + else + { + LOG_ERROR << "Error!"; + exit(1); + } + }); + req = HttpRequest::newHttpRequest(); + req->setMethod(drogon::Get); + req->setPath("/index.html"); + req->addHeader("accept-encoding", "gzip"); + client->sendRequest(req, [=](ReqResult result, const HttpResponsePtr &resp) { + if (result == ReqResult::Ok) + { + if (resp->getBody().length() == INDEX_LEN) + { + outputGood(req, isHttps); + } + else + { + LOG_DEBUG << resp->getBody().length(); + LOG_ERROR << "Error!"; + exit(1); + } + } + else + { + LOG_ERROR << "Error!"; + exit(1); + } + }); /// Test file download req = HttpRequest::newHttpRequest(); req->setMethod(drogon::Get); diff --git a/lib/inc/drogon/HttpAppFramework.h b/lib/inc/drogon/HttpAppFramework.h index d6540a92..a24d8b7a 100755 --- a/lib/inc/drogon/HttpAppFramework.h +++ b/lib/inc/drogon/HttpAppFramework.h @@ -641,6 +641,17 @@ public: */ virtual void setPipeliningRequestsNumber(const size_t number) = 0; + ///Set the gzip_static option. + /** + * If it is set to true, when the client requests a static file, drogon first finds the compressed + * file with the extension ".gz" in the same path and send the compressed file to the client. + * The default value is true. + * + * NOTE: + * This operation can be performed by an option in the configuration file. + */ + virtual void setGzipStatic(bool useGzipStatic) = 0; + #if USE_ORM ///Get a database client by @param name /** diff --git a/lib/src/ConfigLoader.cc b/lib/src/ConfigLoader.cc index c1e919f2..7130a905 100644 --- a/lib/src/ConfigLoader.cc +++ b/lib/src/ConfigLoader.cc @@ -235,6 +235,8 @@ static void loadApp(const Json::Value &app) drogon::app().setKeepaliveRequestsNumber(keepaliveReqs); auto pipeliningReqs = app.get("pipelining_requests", 0).asUInt64(); drogon::app().setPipeliningRequestsNumber(pipeliningReqs); + auto useGzipStatic = app.get("gzip_static", true).asBool(); + drogon::app().setGzipStatic(useGzipStatic); } static void loadDbClients(const Json::Value &dbClients) { diff --git a/lib/src/HttpAppFrameworkImpl.cc b/lib/src/HttpAppFrameworkImpl.cc index 96e5a758..bfda9657 100755 --- a/lib/src/HttpAppFrameworkImpl.cc +++ b/lib/src/HttpAppFrameworkImpl.cc @@ -57,7 +57,7 @@ namespace drogon class DrogonFileLocker : public trantor::NonCopyable { - public: +public: DrogonFileLocker() { _fd = open("/tmp/drogon.lock", O_TRUNC | O_CREAT, 0755); @@ -68,7 +68,7 @@ class DrogonFileLocker : public trantor::NonCopyable close(_fd); } - private: +private: int _fd = 0; }; @@ -743,38 +743,54 @@ void HttpAppFrameworkImpl::onAsyncRequest(const HttpRequestImplPtr &req, std::fu callback(cachedResp); return; } - auto resp = HttpResponse::newFileResponse(filePath); - if (!timeStr.empty()) + HttpResponsePtr resp; + if (_gzipStaticFlag && req->getHeaderBy("accept-encoding").find("gzip") != std::string::npos) { - resp->addHeader("Last-Modified", timeStr); - resp->addHeader("Expires", "Thu, 01 Jan 1970 00:00:00 GMT"); - } - //cache the response for 5 seconds by default - if (_staticFilesCacheTime >= 0) - { - resp->setExpiredTime(_staticFilesCacheTime); - _responseCachingMap->insert(filePath, resp, resp->expiredTime(), [=]() { - std::lock_guard guard(_staticFilesCacheMutex); - _staticFilesCache.erase(filePath); - }); + //Find compressed file first. + auto gzipFileName = filePath + ".gz"; + std::ifstream infile(gzipFileName, std::ifstream::binary); + if (infile) { - std::lock_guard guard(_staticFilesCacheMutex); - _staticFilesCache[filePath] = resp; + resp = HttpResponse::newFileResponse(gzipFileName, "", drogon::getContentType(filePath)); + resp->addHeader("Content-Encoding", "gzip"); } } + if (!resp) + resp = HttpResponse::newFileResponse(filePath); + if (resp->statusCode() != k404NotFound) + { + if (!timeStr.empty()) + { + resp->addHeader("Last-Modified", timeStr); + resp->addHeader("Expires", "Thu, 01 Jan 1970 00:00:00 GMT"); + } + //cache the response for 5 seconds by default + if (_staticFilesCacheTime >= 0) + { + resp->setExpiredTime(_staticFilesCacheTime); + _responseCachingMap->insert(filePath, resp, resp->expiredTime(), [=]() { + std::lock_guard guard(_staticFilesCacheMutex); + _staticFilesCache.erase(filePath); + }); + { + std::lock_guard guard(_staticFilesCacheMutex); + _staticFilesCache[filePath] = resp; + } + } - if (needSetJsessionid) - { - auto newCachedResp = resp; - if (resp->expiredTime() >= 0) + if (needSetJsessionid) { - //make a copy - newCachedResp = std::make_shared(*std::dynamic_pointer_cast(resp)); - newCachedResp->setExpiredTime(-1); + auto newCachedResp = resp; + if (resp->expiredTime() >= 0) + { + //make a copy + newCachedResp = std::make_shared(*std::dynamic_pointer_cast(resp)); + newCachedResp->setExpiredTime(-1); + } + newCachedResp->addCookie("JSESSIONID", sessionId); + callback(newCachedResp); + return; } - newCachedResp->addCookie("JSESSIONID", sessionId); - callback(newCachedResp); - return; } callback(resp); return; diff --git a/lib/src/HttpAppFrameworkImpl.h b/lib/src/HttpAppFrameworkImpl.h index eb0d89e0..7a826ce7 100644 --- a/lib/src/HttpAppFrameworkImpl.h +++ b/lib/src/HttpAppFrameworkImpl.h @@ -170,6 +170,7 @@ public: virtual void setIdleConnectionTimeout(size_t timeout) override { _idleConnectionTimeout = timeout; } virtual void setKeepaliveRequestsNumber(const size_t number) override { _keepaliveRequestsNumber = number; } virtual void setPipeliningRequestsNumber(const size_t number) override { _pipeliningRequestsNumber = number; } + virtual void setGzipStatic(bool useGzipStatic) override { _gzipStaticFlag = useGzipStatic; } virtual std::vector> getHandlersInfo() const override; size_t keepaliveRequestsNumber() const { return _keepaliveRequestsNumber; } @@ -293,6 +294,7 @@ private: size_t _pipeliningRequestsNumber = 0; bool _useSendfile = true; bool _useGzip = true; + bool _gzipStaticFlag = true; int _staticFilesCacheTime = 5; std::unordered_map> _staticFilesCache; std::mutex _staticFilesCacheMutex; diff --git a/lib/src/HttpResponseImpl.cc b/lib/src/HttpResponseImpl.cc index c13ca6e2..c27d5199 100755 --- a/lib/src/HttpResponseImpl.cc +++ b/lib/src/HttpResponseImpl.cc @@ -14,6 +14,7 @@ #include "HttpAppFrameworkImpl.h" #include "HttpResponseImpl.h" +#include "HttpUtils.h" #include #include #include @@ -80,15 +81,15 @@ HttpResponsePtr HttpResponse::newHttpViewResponse(const std::string &viewName, c HttpResponsePtr HttpResponse::newFileResponse(const std::string &fullPath, const std::string &attachmentFileName, ContentType type) { - auto resp = std::make_shared(); + std::ifstream infile(fullPath, std::ifstream::binary); LOG_TRACE << "send http file:" << fullPath; if (!infile) { - resp->setStatusCode(k404NotFound); - resp->setCloseConnection(true); + auto resp = HttpResponse::newNotFoundResponse(); return resp; } + auto resp = std::make_shared(); std::streambuf *pbuf = infile.rdbuf(); std::streamsize filesize = pbuf->pubseekoff(0, infile.end); pbuf->pubseekoff(0, infile.beg); // rewind @@ -97,7 +98,7 @@ HttpResponsePtr HttpResponse::newFileResponse(const std::string &fullPath, const //TODO : Is 200k an appropriate value? Or set it to be configurable { //The advantages of sendfile() can only be reflected in sending large files. - std::dynamic_pointer_cast(resp)->setSendfile(fullPath); + resp->setSendfile(fullPath); } else { @@ -110,71 +111,13 @@ HttpResponsePtr HttpResponse::newFileResponse(const std::string &fullPath, const if (type == CT_NONE) { - std::string filename; if (!attachmentFileName.empty()) { - filename = attachmentFileName; + resp->setContentTypeCode(drogon::getContentType(attachmentFileName)); } else { - auto pos = fullPath.rfind("/"); - if (pos != std::string::npos) - { - filename = fullPath.substr(pos + 1); - } - else - { - filename = fullPath; - } - } - std::string filetype; - auto pos = filename.rfind("."); - if (pos != std::string::npos) - { - filetype = filename.substr(pos + 1); - transform(filetype.begin(), filetype.end(), filetype.begin(), tolower); - } - if (filetype == "html") - resp->setContentTypeCode(CT_TEXT_HTML); - else if (filetype == "js") - resp->setContentTypeCode(CT_APPLICATION_X_JAVASCRIPT); - else if (filetype == "css") - resp->setContentTypeCode(CT_TEXT_CSS); - else if (filetype == "xml") - resp->setContentTypeCode(CT_TEXT_XML); - else if (filetype == "xsl") - resp->setContentTypeCode(CT_TEXT_XSL); - else if (filetype == "txt") - resp->setContentTypeCode(CT_TEXT_PLAIN); - else if (filetype == "svg") - resp->setContentTypeCode(CT_IMAGE_SVG_XML); - else if (filetype == "ttf") - resp->setContentTypeCode(CT_APPLICATION_X_FONT_TRUETYPE); - else if (filetype == "otf") - resp->setContentTypeCode(CT_APPLICATION_X_FONT_OPENTYPE); - else if (filetype == "woff2") - resp->setContentTypeCode(CT_APPLICATION_FONT_WOFF2); - else if (filetype == "woff") - resp->setContentTypeCode(CT_APPLICATION_FONT_WOFF); - else if (filetype == "eot") - resp->setContentTypeCode(CT_APPLICATION_VND_MS_FONTOBJ); - else if (filetype == "png") - resp->setContentTypeCode(CT_IMAGE_PNG); - else if (filetype == "jpg") - resp->setContentTypeCode(CT_IMAGE_JPG); - else if (filetype == "jpeg") - resp->setContentTypeCode(CT_IMAGE_JPG); - else if (filetype == "gif") - resp->setContentTypeCode(CT_IMAGE_GIF); - else if (filetype == "bmp") - resp->setContentTypeCode(CT_IMAGE_BMP); - else if (filetype == "ico") - resp->setContentTypeCode(CT_IMAGE_XICON); - else if (filetype == "icns") - resp->setContentTypeCode(CT_IMAGE_ICNS); - else - { - resp->setContentTypeCode(CT_APPLICATION_OCTET_STREAM); + resp->setContentTypeCode(drogon::getContentType(fullPath)); } } else diff --git a/lib/src/HttpServer.cc b/lib/src/HttpServer.cc index 674bf660..960005a8 100755 --- a/lib/src/HttpServer.cc +++ b/lib/src/HttpServer.cc @@ -213,7 +213,7 @@ void HttpServer::onRequest(const TcpConnectionPtr &conn, const HttpRequestImplPt if (HttpAppFramework::instance().isGzipEnabled() && sendfileName.empty() && req->getHeaderBy("accept-encoding").find("gzip") != std::string::npos && - std::dynamic_pointer_cast(response)->getHeaderBy("content-encoding") == "" && + std::dynamic_pointer_cast(response)->getHeaderBy("content-encoding").empty() && response->getContentType() < CT_APPLICATION_OCTET_STREAM && response->getBody().length() > 1024) { diff --git a/lib/src/HttpUtils.cc b/lib/src/HttpUtils.cc index 916fe090..e3e9c273 100644 --- a/lib/src/HttpUtils.cc +++ b/lib/src/HttpUtils.cc @@ -375,4 +375,103 @@ const string_view &statusCodeToString(int code) } } +ContentType getContentType(const std::string &filename) +{ + std::string extName; + auto pos = filename.rfind("."); + if (pos != std::string::npos) + { + extName = filename.substr(pos + 1); + transform(extName.begin(), extName.end(), extName.begin(), tolower); + } + switch (extName.length()) + { + case 0: + return CT_APPLICATION_OCTET_STREAM; + case 2: + { + if (extName == "js") + return CT_APPLICATION_X_JAVASCRIPT; + return CT_APPLICATION_OCTET_STREAM; + } + case 3: + { + switch (extName[0]) + { + case 'b': + if (extName == "bmp") + return CT_IMAGE_BMP; + break; + case 'c': + if (extName == "css") + return CT_TEXT_CSS; + break; + case 'e': + if (extName == "eot") + return CT_APPLICATION_VND_MS_FONTOBJ; + break; + case 'g': + if (extName == "gif") + return CT_IMAGE_GIF; + break; + case 'i': + if (extName == "ico") + return CT_IMAGE_XICON; + break; + case 'j': + if (extName == "jpg") + return CT_IMAGE_JPG; + break; + case 'o': + if (extName == "otf") + return CT_APPLICATION_X_FONT_OPENTYPE; + break; + case 'p': + if (extName == "png") + return CT_IMAGE_PNG; + break; + case 's': + if (extName == "svg") + return CT_IMAGE_SVG_XML; + break; + case 't': + if (extName == "txt") + return CT_TEXT_PLAIN; + else if (extName == "ttf") + return CT_APPLICATION_X_FONT_TRUETYPE; + break; + case 'x': + if (extName == "xml") + return CT_TEXT_XML; + else if (extName == "xsl") + return CT_TEXT_XSL; + break; + default: + break; + } + return CT_APPLICATION_OCTET_STREAM; + } + case 4: + { + if (extName == "html") + return CT_TEXT_HTML; + else if (extName == "jpeg") + return CT_IMAGE_JPG; + else if (extName == "icns") + return CT_IMAGE_ICNS; + else if (extName == "woff") + return CT_APPLICATION_FONT_WOFF; + return CT_APPLICATION_OCTET_STREAM; + } + case 5: + { + if (extName == "woff2") + return CT_APPLICATION_FONT_WOFF2; + return CT_APPLICATION_OCTET_STREAM; + } + default: + return CT_APPLICATION_OCTET_STREAM; + } +} + } // namespace drogon \ No newline at end of file diff --git a/lib/src/HttpUtils.h b/lib/src/HttpUtils.h index 58478ee4..26cf2cb8 100644 --- a/lib/src/HttpUtils.h +++ b/lib/src/HttpUtils.h @@ -19,17 +19,11 @@ #include #include -#if CXX_STD >= 17 -#include -typedef std::string_view string_view; -#else -#include -typedef std::experimental::basic_string_view string_view; -#endif namespace drogon { const string_view &webContentTypeToString(ContentType contenttype); const string_view &statusCodeToString(int code); +ContentType getContentType(const std::string &extName); } // namespace drogon