From 1abd8b3506438e4fd5f424dfa17d5c5e4d3e570a Mon Sep 17 00:00:00 2001 From: ivanka2012 Date: Fri, 11 Dec 2020 07:02:47 +0100 Subject: [PATCH] Add implicit page resolving capability (a-directory -> a-directory/index.html) (#647) Co-authored-by: an-tao --- config.example.json | 5 + drogon_ctl/templates/config.csp | 5 + examples/CMakeLists.txt | 11 +- examples/simple_example/a-directory/page.html | 187 ++++++++++++++++++ examples/simple_example/main.cc | 2 + examples/simple_example_test/main.cc | 81 +++++++- lib/inc/drogon/HttpAppFramework.h | 39 ++++ lib/src/ConfigLoader.cc | 4 + lib/src/HttpAppFrameworkImpl.cc | 20 ++ lib/src/HttpAppFrameworkImpl.h | 6 + lib/src/StaticFileRouter.cc | 99 +++++++--- lib/src/StaticFileRouter.h | 18 ++ 12 files changed, 446 insertions(+), 31 deletions(-) create mode 100644 examples/simple_example/a-directory/page.html diff --git a/config.example.json b/config.example.json index 17847be9..268ab0bb 100644 --- a/config.example.json +++ b/config.example.json @@ -70,6 +70,11 @@ //If there isn't any handler registered to the path "/", the home page file in the "document_root" is send to clients as a response //to the request for "/". "home_page": "index.html", + //use_implicit_page: enable implicit pages if true, true by default + "use_implicit_page": true, + //implicit_page: Set the file which would the server access in a directory that a user accessed. + //For example, by default, http://localhost/a-directory resolves to http://localhost/a-directory/index.html. + "implicit_page": "index.html", //static_file_headers: Headers for static files /*"static_file_headers": [ { diff --git a/drogon_ctl/templates/config.csp b/drogon_ctl/templates/config.csp index 4ace2a98..1f7fa383 100644 --- a/drogon_ctl/templates/config.csp +++ b/drogon_ctl/templates/config.csp @@ -70,6 +70,11 @@ //If there isn't any handler registered to the path "/", the home page file in the "document_root" is send to clients as a response //to the request for "/". "home_page": "index.html", + //use_implicit_page: enable implicit pages if true, true by default + "use_implicit_page": true, + //implicit_page: Set the file which would the server access in a directory that a user accessed. + //For example, by default, http://localhost/a-directory resolves to http://localhost/a-directory/index.html. + "implicit_page": "index.html", //static_file_headers: Headers for static files /*"static_file_headers": [ { diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 3095724d..31babf21 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -19,7 +19,8 @@ set(simple_example_sources simple_example/main.cc) add_executable(webapp ${simple_example_sources}) -drogon_create_views(webapp ${CMAKE_CURRENT_SOURCE_DIR}/simple_example ${CMAKE_CURRENT_BINARY_DIR}) +drogon_create_views(webapp ${CMAKE_CURRENT_SOURCE_DIR}/simple_example + ${CMAKE_CURRENT_BINARY_DIR}) add_dependencies(webapp drogon_ctl) set(client_example_sources client_example/main.cc) @@ -45,7 +46,13 @@ add_custom_command( ${CMAKE_CURRENT_SOURCE_DIR}/simple_example/index.html.gz ${PROJECT_SOURCE_DIR}/trantor/trantor/tests/server.pem $) - +add_custom_command( + TARGET webapp POST_BUILD + COMMAND ${CMAKE_COMMAND} + -E + copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/simple_example/a-directory + $/a-directory) set(example_targets webapp webapp_test diff --git a/examples/simple_example/a-directory/page.html b/examples/simple_example/a-directory/page.html new file mode 100644 index 00000000..e1b1fc52 --- /dev/null +++ b/examples/simple_example/a-directory/page.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 (from an implicit page)

+

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:

+ +
    +
  • Use a non-blocking I/O network lib based on epoll (kqueue under MacOS/FreeBSD) to provide high-concurrency, high-performance network IO, please visit the benchmarks page for more details;
  • +
  • Provide a completely asynchronous programming mode;
  • +
  • Support Http1.0/1.1 (server side and client side);
  • +
  • Based on template, a simple reflection mechanism is implemented to completely decouple the main program framework, controllers and views.
  • +
  • Support cookies and built-in sessions;
  • +
  • Support back-end rendering, the controller generates the data to the view to generate the Html page, the view is described by a “JSP-like” CSP file, the C++ code is embedded into the Html page by the CSP tag, and the drogon command-line tool automatically generates the C++ code file for compilation;
  • +
  • Support view page dynamic loading (dynamic compilation and loading at runtime);
  • +
  • Provide a convenient and flexible routing solution from the path to the controller handler;
  • +
  • Support filter chains to facilitate the execution of unified logic (such as login verification, Http Method constraint verification, etc.) before controllers;
  • +
  • Support https (based on OpenSSL);
  • +
  • Support WebSocket (server side and client side);
  • +
  • Support JSON format request and response, very friendly to the Restful API application development;
  • +
  • Support file download and upload;
  • +
  • Support gzip compression transmission;
  • +
  • Support pipelining;
  • +
  • Provide a lightweight command line tool, drogon_ctl, to simplify the creation of various classes in Drogon and the generation of view code;
  • +
  • Support non-blocking I/O based asynchronously reading and writing database (PostgreSQL and MySQL(MariaDB) database);
  • +
  • Support asynchronously reading and writing sqlite3 database based on thread pool;
  • +
  • Support ARM Architecture;
  • +
  • Provide a convenient lightweight ORM implementation that supports for regular object-to-database bidirectional mapping;
  • +
  • Support plugins which can be installed by the configuration file at load time;
  • +
  • Support AOP with build-in joinpoints.
  • +
+ +

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::kWarn); + 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/main.cc b/examples/simple_example/main.cc index 8bc95140..5fe0869e 100644 --- a/examples/simple_example/main.cc +++ b/examples/simple_example/main.cc @@ -214,6 +214,8 @@ int main() std::string opaque("drogonOpaque"); // Load configuration app().loadConfigFile("config.example.json"); + app().setImplicitPageEnable(true); + app().setImplicitPage("page.html"); auto &json = app().getCustomConfig(); if (json.empty()) { diff --git a/examples/simple_example_test/main.cc b/examples/simple_example_test/main.cc index ae9ad513..72774ada 100644 --- a/examples/simple_example_test/main.cc +++ b/examples/simple_example_test/main.cc @@ -20,6 +20,7 @@ #include #include +#include #ifndef _WIN32 #include #endif @@ -29,7 +30,8 @@ #define GREEN "\033[32m" /* Green */ #define JPG_LEN 44618 -#define INDEX_LEN 10606 +size_t indexLen; +size_t indexImplicitLen; using namespace drogon; @@ -860,7 +862,7 @@ void doTest(const HttpClientPtr &client, const HttpResponsePtr &resp) { if (result == ReqResult::Ok) { - if (resp->getBody().length() == INDEX_LEN) + if (resp->getBody().length() == indexLen) { outputGood(req, isHttps); } @@ -887,7 +889,7 @@ void doTest(const HttpClientPtr &client, const HttpResponsePtr &resp) { if (result == ReqResult::Ok) { - if (resp->getBody().length() == INDEX_LEN) + if (resp->getBody().length() == indexLen) { outputGood(req, isHttps); } @@ -1151,6 +1153,64 @@ void doTest(const HttpClientPtr &client, exit(1); } }); + // Test implicit pages + std::string body; + req = HttpRequest::newHttpRequest(); + req->setMethod(drogon::Get); + req->setPath("/a-directory"); + client->sendRequest(req, + [req, isHttps, &body](ReqResult result, + const HttpResponsePtr &resp) { + if (result == ReqResult::Ok) + { + if (resp->getBody().length() == + indexImplicitLen) + { + body = resp->getBody(); + outputGood(req, isHttps); + } + else + { + LOG_DEBUG << resp->getBody().length(); + LOG_ERROR << "Error!"; + LOG_ERROR << resp->getBody(); + exit(1); + } + } + else + { + LOG_ERROR << "Error!"; + exit(1); + } + }); + req = HttpRequest::newHttpRequest(); + req->setMethod(drogon::Get); + req->setPath("/a-directory/page.html"); + client->sendRequest(req, + [req, isHttps, &body](ReqResult result, + const HttpResponsePtr &resp) { + if (result == ReqResult::Ok) + { + if (resp->getBody().length() == + indexImplicitLen && + body == resp->getBody()) + { + outputGood(req, isHttps); + } + else + { + LOG_DEBUG << resp->getBody().length(); + LOG_ERROR << "Error!"; + LOG_ERROR << resp->getBody(); + exit(1); + } + } + else + { + LOG_ERROR << "Error!"; + exit(1); + } + }); // return; // Test file upload UploadFile file1("./drogon.jpg"); @@ -1186,6 +1246,19 @@ void doTest(const HttpClientPtr &client, } }); } +void loadFileLengths() +{ + try + { + indexLen = std::filesystem::file_size("index.html"); + indexImplicitLen = std::filesystem::file_size("a-directory/page.html"); + } + catch (std::exception e) + { + LOG_ERROR << "Unable to retrieve HTML file sizes: " << e.what(); + exit(1); + } +} int main(int argc, char *argv[]) { trantor::EventLoopThread loop[2]; @@ -1195,7 +1268,7 @@ int main(int argc, char *argv[]) ever = true; loop[0].run(); loop[1].run(); - + loadFileLengths(); do { std::promise pro1; diff --git a/lib/inc/drogon/HttpAppFramework.h b/lib/inc/drogon/HttpAppFramework.h index d7723ac8..ff7eefbc 100644 --- a/lib/inc/drogon/HttpAppFramework.h +++ b/lib/inc/drogon/HttpAppFramework.h @@ -1067,6 +1067,45 @@ class HttpAppFramework : public trantor::NonCopyable */ virtual const std::string &getHomePage() const = 0; + /// Set to enable implicit pages, enabled by default + /** + * @brief Implicit pages are used when the server detects if the user + * requested a directory. By default, it will try to append index.html to + * the path, see setImplicitPage() if you want to customize this + * (http://localhost/a-directory resolves to + * http://localhost/a-directory/index.html by default). + * + * @note + * This operation can be performed by an option in the configuration file. + */ + virtual HttpAppFramework &setImplicitPageEnable(bool useImplicitPage) = 0; + + /// Return true if implicit pages are enabled + /** + * @note + * This method must be called after the framework has been run. + */ + virtual bool isImplicitPageEnabled() const = 0; + + /// Set the HTML file that a directory would resolve to by default, default + /// is "index.html" + /** + * @brief Sets the page which would the server load in if it detects that + * the user requested a directory + * + * @note + * This operation can be performed by an option in the configuration file. + */ + virtual HttpAppFramework &setImplicitPage( + const std::string &implicitPageFile) = 0; + + /// Get the implicit HTML page + /** + * @note + * This method must be called after the framework has been run. + */ + virtual const std::string &getImplicitPage() const = 0; + /// Get a database client by name /** * @note diff --git a/lib/src/ConfigLoader.cc b/lib/src/ConfigLoader.cc index d4d85e27..c68f3c85 100644 --- a/lib/src/ConfigLoader.cc +++ b/lib/src/ConfigLoader.cc @@ -454,6 +454,10 @@ static void loadApp(const Json::Value &app) } drogon::app().enableReusePort(app.get("reuse_port", false).asBool()); drogon::app().setHomePage(app.get("home_page", "index.html").asString()); + drogon::app().setImplicitPageEnable( + app.get("use_implicit_page", true).asBool()); + drogon::app().setImplicitPage( + app.get("implicit_page", "index.html").asString()); } static void loadDbClients(const Json::Value &dbClients) { diff --git a/lib/src/HttpAppFrameworkImpl.cc b/lib/src/HttpAppFrameworkImpl.cc index 903065c0..de0856ad 100644 --- a/lib/src/HttpAppFrameworkImpl.cc +++ b/lib/src/HttpAppFrameworkImpl.cc @@ -192,6 +192,26 @@ HttpAppFramework &HttpAppFrameworkImpl::setBrStatic(bool useGzipStatic) staticFileRouterPtr_->setBrStatic(useGzipStatic); return *this; } +HttpAppFramework &HttpAppFrameworkImpl::setImplicitPageEnable( + bool useImplicitPage) +{ + staticFileRouterPtr_->setImplicitPageEnable(useImplicitPage); + return *this; +} +bool HttpAppFrameworkImpl::isImplicitPageEnabled() const +{ + return staticFileRouterPtr_->isImplicitPageEnabled(); +} +HttpAppFramework &HttpAppFrameworkImpl::setImplicitPage( + const std::string &implicitPageFile) +{ + staticFileRouterPtr_->setImplicitPage(implicitPageFile); + return *this; +} +const std::string &HttpAppFrameworkImpl::getImplicitPage() const +{ + return staticFileRouterPtr_->getImplicitPage(); +} #ifndef _WIN32 HttpAppFramework &HttpAppFrameworkImpl::enableDynamicViewsLoading( const std::vector &libPaths, diff --git a/lib/src/HttpAppFrameworkImpl.h b/lib/src/HttpAppFrameworkImpl.h index 06da8c23..bf1afaf0 100644 --- a/lib/src/HttpAppFrameworkImpl.h +++ b/lib/src/HttpAppFrameworkImpl.h @@ -348,6 +348,12 @@ class HttpAppFrameworkImpl : public HttpAppFramework { return termSignalHandler_; } + virtual HttpAppFramework &setImplicitPageEnable( + bool useImplicitPage) override; + bool isImplicitPageEnabled() const override; + virtual HttpAppFramework &setImplicitPage( + const std::string &implicitPageFile) override; + const std::string &getImplicitPage() const override; size_t getClientMaxBodySize() const { return clientMaxBodySize_; diff --git a/lib/src/StaticFileRouter.cc b/lib/src/StaticFileRouter.cc index b2824f5f..f749a277 100644 --- a/lib/src/StaticFileRouter.cc +++ b/lib/src/StaticFileRouter.cc @@ -121,29 +121,52 @@ void StaticFileRouter::route( callback(app().getCustomErrorHandler()(k403Forbidden)); return; } - if (!location.allowAll_) + std::string filePath = + location.realLocation_ + + std::string{restOfThePath.data(), restOfThePath.length()}; + struct stat fileStat; + if (stat(filePath.c_str(), &fileStat) != 0) { - pos = restOfThePath.rfind('.'); - if (pos == string_view::npos) + callback(HttpResponse::newNotFoundResponse()); + return; + } + if (S_ISDIR(fileStat.st_mode)) + { + // Check if path is eligible for an implicit index.html + if (implicitPageEnable_) { - callback(app().getCustomErrorHandler()(k403Forbidden)); - return; + filePath = filePath + "/" + implicitPage_; } - std::string extension{restOfThePath.data() + pos + 1, - restOfThePath.length() - pos - 1}; - std::transform(extension.begin(), - extension.end(), - extension.begin(), - tolower); - if (fileTypeSet_.find(extension) == fileTypeSet_.end()) + else { callback(app().getCustomErrorHandler()(k403Forbidden)); return; } } - std::string filePath = - location.realLocation_ + - std::string{restOfThePath.data(), restOfThePath.length()}; + else + { + if (!location.allowAll_) + { + pos = restOfThePath.rfind('.'); + if (pos == string_view::npos) + { + callback(app().getCustomErrorHandler()(k403Forbidden)); + return; + } + std::string extension{restOfThePath.data() + pos + 1, + restOfThePath.length() - pos - 1}; + std::transform(extension.begin(), + extension.end(), + extension.begin(), + tolower); + if (fileTypeSet_.find(extension) == fileTypeSet_.end()) + { + callback(app().getCustomErrorHandler()(k403Forbidden)); + return; + } + } + } + if (location.filters_.empty()) { sendStaticFileResponse(filePath, @@ -176,20 +199,46 @@ void StaticFileRouter::route( return; } } - auto pos = lPath.rfind('.'); - if (pos != std::string::npos) + + std::string directoryPath = + HttpAppFrameworkImpl::instance().getDocumentRoot() + path; + struct stat fileStat; + if (stat(directoryPath.c_str(), &fileStat) == 0) { - std::string filetype = lPath.substr(pos + 1); - if (fileTypeSet_.find(filetype) != fileTypeSet_.end()) + if (S_ISDIR(fileStat.st_mode)) { - // LOG_INFO << "file query!" << path; - std::string filePath = - HttpAppFrameworkImpl::instance().getDocumentRoot() + path; - sendStaticFileResponse(filePath, req, callback, ""); - return; + // Check if path is eligible for an implicit index.html + if (implicitPageEnable_) + { + std::string filePath = directoryPath + "/" + implicitPage_; + sendStaticFileResponse(filePath, req, callback, ""); + return; + } + else + { + callback(app().getCustomErrorHandler()(k403Forbidden)); + return; + } + } + else + { + // This is a normal page + auto pos = path.rfind('.'); + if (pos == std::string::npos) + { + callback(app().getCustomErrorHandler()(k403Forbidden)); + return; + } + std::string filetype = lPath.substr(pos + 1); + if (fileTypeSet_.find(filetype) != fileTypeSet_.end()) + { + // LOG_INFO << "file query!" << path; + std::string filePath = directoryPath; + sendStaticFileResponse(filePath, req, callback, ""); + return; + } } } - callback(HttpResponse::newNotFoundResponse()); } diff --git a/lib/src/StaticFileRouter.h b/lib/src/StaticFileRouter.h index acf3cddf..4e315940 100644 --- a/lib/src/StaticFileRouter.h +++ b/lib/src/StaticFileRouter.h @@ -77,6 +77,22 @@ class StaticFileRouter { headers_ = headers; } + void setImplicitPageEnable(bool useImplicitPage) + { + implicitPageEnable_ = useImplicitPage; + } + bool isImplicitPageEnabled() const + { + return implicitPageEnable_; + } + void setImplicitPage(const std::string &implicitPageFile) + { + implicitPage_ = implicitPageFile; + } + const std::string &getImplicitPage() const + { + return implicitPage_; + } private: std::set fileTypeSet_{"html", @@ -110,6 +126,8 @@ class StaticFileRouter IOThreadStorage>> staticFilesCache_; std::vector> headers_; + bool implicitPageEnable_{true}; + std::string implicitPage_{"index.html"}; struct Location { std::string uriPrefix_;