diff --git a/examples/file_upload_server.cc b/examples/file_upload_server.cc index 5c090d9..dfb1da8 100644 --- a/examples/file_upload_server.cc +++ b/examples/file_upload_server.cc @@ -2,12 +2,11 @@ #include #include "webcc/logger.h" -#include "webcc/rest_server.h" -#include "webcc/rest_service.h" +#include "webcc/server.h" // ----------------------------------------------------------------------------- -class FileUploadService : public webcc::RestService { +class FileUploadService : public webcc::Service { public: void Handle(const webcc::RestRequest& request, webcc::RestResponse* response) override { @@ -48,7 +47,8 @@ int main(int argc, char* argv[]) { std::size_t workers = 2; try { - webcc::RestServer server(port, workers); + // TODO: doc root + webcc::Server server(port, workers); server.Bind(std::make_shared(), "/upload", false); diff --git a/examples/rest_book_server.cc b/examples/rest_book_server.cc index b9f5575..fa1fbbb 100644 --- a/examples/rest_book_server.cc +++ b/examples/rest_book_server.cc @@ -7,8 +7,7 @@ #include "json/json.h" #include "webcc/logger.h" -#include "webcc/rest_server.h" -#include "webcc/rest_service.h" +#include "webcc/server.h" #include "examples/common/book.h" #include "examples/common/book_json.h" @@ -34,7 +33,7 @@ static void Sleep(int seconds) { // ----------------------------------------------------------------------------- -class BookListService : public webcc::RestListService { +class BookListService : public webcc::ListService { public: explicit BookListService(int sleep_seconds) : sleep_seconds_(sleep_seconds) { @@ -58,7 +57,7 @@ private: // The URL is like '/books/{BookID}', and the 'url_matches' parameter // contains the matched book ID. -class BookDetailService : public webcc::RestDetailService { +class BookDetailService : public webcc::DetailService { public: explicit BookDetailService(int sleep_seconds) : sleep_seconds_(sleep_seconds) { @@ -227,7 +226,8 @@ int main(int argc, char* argv[]) { std::size_t workers = 2; try { - webcc::RestServer server(port, workers); + // TODO: doc root + webcc::Server server(port, workers); server.Bind(std::make_shared(sleep_seconds), "/books", false); diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 742cdc4..ce9a551 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -3,7 +3,7 @@ set(UT_SRCS base64_unittest.cc request_parser_unittest.cc - rest_service_manager_unittest.cc + service_manager_unittest.cc url_unittest.cc utility_unittest.cc ) diff --git a/unittest/rest_service_manager_unittest.cc b/unittest/service_manager_unittest.cc similarity index 67% rename from unittest/rest_service_manager_unittest.cc rename to unittest/service_manager_unittest.cc index 6d657d2..1655a9a 100644 --- a/unittest/rest_service_manager_unittest.cc +++ b/unittest/service_manager_unittest.cc @@ -1,10 +1,10 @@ #include "gtest/gtest.h" -#include "webcc/rest_service_manager.h" +#include "webcc/service_manager.h" // ----------------------------------------------------------------------------- -class MyRestService : public webcc::RestService { +class MyService : public webcc::Service { public: void Handle(const webcc::RestRequest& request, webcc::RestResponse* response) override { @@ -14,16 +14,16 @@ public: // ----------------------------------------------------------------------------- -TEST(RestServiceManagerTest, URL_RegexBasic) { - webcc::RestServiceManager service_manager; +TEST(ServiceManagerTest, URL_RegexBasic) { + webcc::ServiceManager service_manager; - service_manager.AddService(std::make_shared(), + service_manager.AddService(std::make_shared(), "/instance/(\\d+)", true); std::vector matches; std::string url = "/instance/12345"; - webcc::RestServicePtr service = service_manager.GetService(url, &matches); + webcc::ServicePtr service = service_manager.GetService(url, &matches); EXPECT_TRUE(!!service); @@ -38,16 +38,16 @@ TEST(RestServiceManagerTest, URL_RegexBasic) { } TEST(RestServiceManagerTest, URL_RegexMultiple) { - webcc::RestServiceManager service_manager; + webcc::ServiceManager service_manager; - service_manager.AddService(std::make_shared(), + service_manager.AddService(std::make_shared(), "/study/(\\d+)/series/(\\d+)/instance/(\\d+)", true); std::vector matches; std::string url = "/study/1/series/2/instance/3"; - webcc::RestServicePtr service = service_manager.GetService(url, &matches); + webcc::ServicePtr service = service_manager.GetService(url, &matches); EXPECT_TRUE(!!service); @@ -64,14 +64,14 @@ TEST(RestServiceManagerTest, URL_RegexMultiple) { } TEST(RestServiceManagerTest, URL_NonRegex) { - webcc::RestServiceManager service_manager; + webcc::ServiceManager service_manager; - service_manager.AddService(std::make_shared(), "/instances", + service_manager.AddService(std::make_shared(), "/instances", false); std::vector matches; std::string url = "/instances"; - webcc::RestServicePtr service = service_manager.GetService(url, &matches); + webcc::ServicePtr service = service_manager.GetService(url, &matches); EXPECT_TRUE(!!service); EXPECT_EQ(0, matches.size()); diff --git a/webcc/common.cc b/webcc/common.cc index 324413b..d0d1161 100644 --- a/webcc/common.cc +++ b/webcc/common.cc @@ -214,7 +214,8 @@ FormPart::FormPart(const std::string& name, const Path& path, // Determine media type from file extension. if (media_type_.empty()) { std::string extension = path.extension().string(); - media_type_ = media_types::FromExtension(extension, false); + // TODO: Default to "application/text"? + media_type_ = media_types::FromExtension(extension); } } diff --git a/webcc/connection_pool.cc b/webcc/connection_pool.cc index dc0595b..6f3fead 100644 --- a/webcc/connection_pool.cc +++ b/webcc/connection_pool.cc @@ -4,9 +4,6 @@ namespace webcc { -ConnectionPool::ConnectionPool() { -} - void ConnectionPool::Start(ConnectionPtr c) { LOG_VERB("Starting connection..."); connections_.insert(c); diff --git a/webcc/connection_pool.h b/webcc/connection_pool.h index c35cc7a..a6ed1ca 100644 --- a/webcc/connection_pool.h +++ b/webcc/connection_pool.h @@ -9,11 +9,11 @@ namespace webcc { class ConnectionPool { public: + ConnectionPool() = default; + ConnectionPool(const ConnectionPool&) = delete; ConnectionPool& operator=(const ConnectionPool&) = delete; - ConnectionPool(); - // Add a connection to the pool and start it. void Start(ConnectionPtr c); @@ -24,7 +24,6 @@ public: void CloseAll(); private: - /// The managed connections. std::set connections_; }; diff --git a/webcc/globals.cc b/webcc/globals.cc index 55a5e1c..45ecb44 100644 --- a/webcc/globals.cc +++ b/webcc/globals.cc @@ -1,7 +1,8 @@ #include "webcc/globals.h" #include -#include + +#include "boost/algorithm/string.hpp" namespace webcc { @@ -9,37 +10,32 @@ namespace webcc { namespace media_types { -// TODO: Add more. -static void InitMap(std::map& map) { - map["gif"] = "image/gif"; - map["htm"] = "text/html"; - map["html"] = "text/html"; - map["jpg"] = "image/jpeg"; - map["jpeg"] = "image/jpeg"; - map["png"] = "image/png"; - map["txt"] = "text/plain"; - map[""] = ""; -} - -// TODO: Ignore case on Windows. -std::string FromExtension(const std::string& extension, - bool default_to_plain_text) { - static std::map s_map; - - if (s_map.empty()) { - InitMap(s_map); - } - - auto it = s_map.find(extension); - if (it != s_map.end()) { - return it->second; - } - - if (default_to_plain_text) { - return "text/plain"; - } else { - return ""; - } +std::string FromExtension(const std::string& ext) { + using boost::iequals; + + if (iequals(ext, ".htm")) { return "text/html"; } + if (iequals(ext, ".html")) { return "text/html"; } + if (iequals(ext, ".php")) { return "text/html"; } + if (iequals(ext, ".css")) { return "text/css"; } + if (iequals(ext, ".txt")) { return "text/plain"; } + if (iequals(ext, ".js")) { return "application/javascript"; } + if (iequals(ext, ".json")) { return "application/json"; } + if (iequals(ext, ".xml")) { return "application/xml"; } + if (iequals(ext, ".swf")) { return "application/x-shockwave-flash"; } + if (iequals(ext, ".flv")) { return "video/x-flv"; } + if (iequals(ext, ".png")) { return "image/png"; } + if (iequals(ext, ".jpe")) { return "image/jpeg"; } + if (iequals(ext, ".jpeg")) { return "image/jpeg"; } + if (iequals(ext, ".jpg")) { return "image/jpeg"; } + if (iequals(ext, ".gif")) { return "image/gif"; } + if (iequals(ext, ".bmp")) { return "image/bmp"; } + if (iequals(ext, ".ico")) { return "image/vnd.microsoft.icon"; } + if (iequals(ext, ".tiff")) { return "image/tiff"; } + if (iequals(ext, ".tif")) { return "image/tiff"; } + if (iequals(ext, ".svg")) { return "image/svg+xml"; } + if (iequals(ext, ".svgz")) { return "image/svg+xml"; } + + return "application/text"; } } // namespace media_types diff --git a/webcc/globals.h b/webcc/globals.h index aee6ccc..35786d5 100644 --- a/webcc/globals.h +++ b/webcc/globals.h @@ -131,8 +131,7 @@ const char* const kTextPlain = "text/plain"; const char* const kTextXml = "text/xml"; // Get media type from file extension. -std::string FromExtension(const std::string& extension, - bool default_to_plain_text = true); +std::string FromExtension(const std::string& ext); } // namespace media_types diff --git a/webcc/logger.cc b/webcc/logger.cc index 533fb5a..69fb275 100644 --- a/webcc/logger.cc +++ b/webcc/logger.cc @@ -248,7 +248,7 @@ void Log(int level, const char* file, int line, const char* format, ...) { va_list args; va_start(args, format); - fprintf(g_logger.file, "%s, %s, %7s, %24s, %4d, ", + fprintf(g_logger.file, "%s, %s, %7s, %20s, %4d, ", timestamp.c_str(), kLevelNames[level], thread_id.c_str(), file, line); @@ -271,12 +271,12 @@ void Log(int level, const char* file, int line, const char* format, ...) { if (g_colorlogtostderr && g_terminal_has_color) { if (level < WEBCC_WARN) { - fprintf(stderr, "%s%s, %s, %7s, %25s, %4d, ", + fprintf(stderr, "%s%s, %s, %7s, %20s, %4d, ", TerminalReset(), timestamp.c_str(), kLevelNames[level], thread_id.c_str(), file, line); } else { - fprintf(stderr, "%s%s%s, %s, %7s, %25s, %4d, ", + fprintf(stderr, "%s%s%s, %s, %7s, %20s, %4d, ", TerminalReset(), level == WEBCC_WARN ? TerminalYellow() : TerminalRed(), timestamp.c_str(), kLevelNames[level], thread_id.c_str(), @@ -287,7 +287,7 @@ void Log(int level, const char* file, int line, const char* format, ...) { fprintf(stderr, "%s\n", TerminalReset()); } else { - fprintf(stderr, "%s, %s, %7s, %25s, %4d, ", + fprintf(stderr, "%s, %s, %7s, %20s, %4d, ", timestamp.c_str(), kLevelNames[level], thread_id.c_str(), file, line); diff --git a/webcc/request.cc b/webcc/request.cc index 3a735b2..b689d12 100644 --- a/webcc/request.cc +++ b/webcc/request.cc @@ -44,22 +44,11 @@ void Request::Prepare() { Payload data_payload; - using boost::asio::buffer; - for (auto& part : form_parts_) { - // Boundary - data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); - data_payload.push_back(buffer(boundary_)); - data_payload.push_back(buffer(misc_strings::CRLF)); - + AddBoundary(data_payload); part->Prepare(&data_payload); } - - // Boundary end - data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); - data_payload.push_back(buffer(boundary_)); - data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); - data_payload.push_back(buffer(misc_strings::CRLF)); + AddBoundary(data_payload, true); // Update Content-Length header. std::size_t content_length = 0; @@ -84,7 +73,7 @@ void Request::CreateStartLine() { throw Error{ Error::kSyntaxError, "Host is missing" }; } - std::string target = "/" + url_.path(); + std::string target = url_.path(); if (!url_.query().empty()) { target += "?"; target += url_.query(); @@ -96,4 +85,15 @@ void Request::CreateStartLine() { start_line_ += " HTTP/1.1"; } +void Request::AddBoundary(Payload& payload, bool end) { + using boost::asio::buffer; + + payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); + payload.push_back(buffer(boundary_)); + if (end) { + payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); + } + payload.push_back(buffer(misc_strings::CRLF)); +} + } // namespace webcc diff --git a/webcc/request.h b/webcc/request.h index 543842c..5c0a3c7 100644 --- a/webcc/request.h +++ b/webcc/request.h @@ -73,6 +73,9 @@ public: private: void CreateStartLine(); + // Add boundary to the payload for multipart form data. + void AddBoundary(Payload& payload, bool end = false); + private: std::string method_; diff --git a/webcc/request_handler.cc b/webcc/request_handler.cc index e26e088..5bbdbb8 100644 --- a/webcc/request_handler.cc +++ b/webcc/request_handler.cc @@ -1,14 +1,29 @@ #include "webcc/request_handler.h" -#include +#include +#include // for move() +#include -#include "webcc/globals.h" +#include "webcc/logger.h" #include "webcc/request.h" #include "webcc/response.h" -#include "webcc/logger.h" +#include "webcc/url.h" + +#if WEBCC_ENABLE_GZIP +#include "webcc/gzip.h" +#endif namespace webcc { +RequestHandler::RequestHandler(const std::string& doc_root) + : doc_root_(doc_root) { +} + +bool RequestHandler::Bind(ServicePtr service, const std::string& url, + bool is_regex) { + return service_manager_.AddService(service, url, is_regex); +} + void RequestHandler::Enqueue(ConnectionPtr connection) { queue_.Push(connection); } @@ -45,7 +60,7 @@ void RequestHandler::WorkerRoutine() { LOG_INFO("Worker is running."); for (;;) { - ConnectionPtr connection = queue_.PopOrWait(); + auto connection = queue_.PopOrWait(); if (!connection) { LOG_INFO("Worker is going to stop."); @@ -61,4 +76,106 @@ void RequestHandler::WorkerRoutine() { } } +void RequestHandler::HandleConnection(ConnectionPtr connection) { + auto request = connection->request(); + + const Url& url = request->url(); + + RestRequest rest_request{ request }; + + LOG_INFO("Request URL path: %s", url.path().c_str()); + + // Get service by URL path. + auto service = service_manager_.GetService(url.path(), + &rest_request.url_matches); + + if (!service) { + LOG_WARN("No service matches the URL path: %s", url.path().c_str()); + + if (!ServeStatic(connection)) { + connection->SendResponse(Status::kNotFound); + } + + return; + } + + RestResponse rest_response; + service->Handle(rest_request, &rest_response); + + auto response = std::make_shared(rest_response.status); + + if (!rest_response.content.empty()) { + if (!rest_response.media_type.empty()) { + response->SetContentType(rest_response.media_type, rest_response.charset); + } + SetContent(request, response, std::move(rest_response.content)); + } + + // Send response back to client. + connection->SendResponse(response); +} + +bool RequestHandler::ServeStatic(ConnectionPtr connection) { + auto request = connection->request(); + std::string path = request->url().path(); + + // If path ends in slash (i.e. is a directory) then add "index.html". + if (path[path.size() - 1] == '/') { + path += "index.html"; + } + + // Determine the file extension. + std::string extension; + std::size_t last_slash_pos = path.find_last_of("/"); + std::size_t last_dot_pos = path.find_last_of("."); + if (last_dot_pos != std::string::npos && last_dot_pos > last_slash_pos) { + extension = path.substr(last_dot_pos + 1); + } + + // Open the file to send back. + std::string full_path = doc_root_ + path; + std::ifstream ifs(full_path.c_str(), std::ios::in | std::ios::binary); + if (!ifs) { + // The file doesn't exist. + connection->SendResponse(Status::kNotFound); + return false; + } + + // Fill out the content to be sent to the client. + std::string content; + char buf[512]; + while (ifs.read(buf, sizeof(buf)).gcount() > 0) { + content.append(buf, ifs.gcount()); + } + + auto response = std::make_shared(Status::kOK); + + if (!content.empty()) { + response->SetContentType(media_types::FromExtension(extension), ""); + response->SetContent(std::move(content), true); + } + + // Send response back to client. + connection->SendResponse(response); + + return true; +} + +void RequestHandler::SetContent(RequestPtr request, ResponsePtr response, + std::string&& content) { +#if WEBCC_ENABLE_GZIP + // Only support gzip (no deflate) for response compression. + if (content.size() > kGzipThreshold && request->AcceptEncodingGzip()) { + std::string compressed; + if (gzip::Compress(content, &compressed)) { + response->SetHeader(headers::kContentEncoding, "gzip"); + response->SetContent(std::move(compressed), true); + return; + } + } +#endif // WEBCC_ENABLE_GZIP + + response->SetContent(std::move(content), true); +} + } // namespace webcc diff --git a/webcc/request_handler.h b/webcc/request_handler.h index f409489..aa63de0 100644 --- a/webcc/request_handler.h +++ b/webcc/request_handler.h @@ -7,21 +7,22 @@ #include "webcc/connection.h" #include "webcc/queue.h" +#include "webcc/service_manager.h" namespace webcc { -class Request; -class Response; - // The common handler for all incoming requests. class RequestHandler { public: - RequestHandler() = default; + explicit RequestHandler(const std::string& doc_root); + virtual ~RequestHandler() = default; RequestHandler(const RequestHandler&) = delete; RequestHandler& operator=(const RequestHandler&) = delete; + bool Bind(ServicePtr service, const std::string& url, bool is_regex); + // Put the connection into the queue. void Enqueue(ConnectionPtr connection); @@ -34,13 +35,28 @@ public: private: void WorkerRoutine(); - // Called by the worker routine. - virtual void HandleConnection(ConnectionPtr connection) = 0; + // Handle a connection (or more precisely, the request inside it). + // Get the request from the connection, process it, prepare the response, + // then send the response back to the client. + // The connection will keep alive if it's a persistent connection. When next + // request comes, this connection will be put back to the queue again. + virtual void HandleConnection(ConnectionPtr connection); + + // TODO + bool ServeStatic(ConnectionPtr connection); + + void SetContent(RequestPtr request, ResponsePtr response, + std::string&& content); private: + // The directory containing the files to be served. + std::string doc_root_; + Queue queue_; std::vector workers_; + + ServiceManager service_manager_; }; } // namespace webcc diff --git a/webcc/rest_request_handler.cc b/webcc/rest_request_handler.cc deleted file mode 100644 index b3d3331..0000000 --- a/webcc/rest_request_handler.cc +++ /dev/null @@ -1,70 +0,0 @@ -#include "webcc/rest_request_handler.h" - -#include // for move() -#include - -#include "webcc/logger.h" -#include "webcc/url.h" - -#if WEBCC_ENABLE_GZIP -#include "webcc/gzip.h" -#endif - -namespace webcc { - -bool RestRequestHandler::Bind(RestServicePtr service, const std::string& url, - bool is_regex) { - return service_manager_.AddService(service, url, is_regex); -} - -void RestRequestHandler::HandleConnection(ConnectionPtr connection) { - RequestPtr request = connection->request(); - - const Url& url = request->url(); - - RestRequest rest_request{ request }; - - // Get service by URL path. - std::string path = "/" + url.path(); - auto service = service_manager_.GetService(path, &rest_request.url_matches); - - if (!service) { - LOG_WARN("No service matches the URL path: %s", url.path().c_str()); - connection->SendResponse(Status::kNotFound); - return; - } - - RestResponse rest_response; - service->Handle(rest_request, &rest_response); - - auto response = std::make_shared(rest_response.status); - - if (!rest_response.content.empty()) { - if (!rest_response.media_type.empty()) { - response->SetContentType(rest_response.media_type, rest_response.charset); - } - SetContent(request, response, std::move(rest_response.content)); - } - - // Send response back to client. - connection->SendResponse(response); -} - -void RestRequestHandler::SetContent(RequestPtr request, ResponsePtr response, - std::string&& content) { -#if WEBCC_ENABLE_GZIP - // Only support gzip (no deflate) for response compression. - if (content.size() > kGzipThreshold && request->AcceptEncodingGzip()) { - std::string compressed; - if (gzip::Compress(content, &compressed)) { - response->SetHeader(headers::kContentEncoding, "gzip"); - response->SetContent(std::move(compressed), true); - return; - } - } -#endif // WEBCC_ENABLE_GZIP - - response->SetContent(std::move(content), true); -} - -} // namespace webcc diff --git a/webcc/rest_request_handler.h b/webcc/rest_request_handler.h deleted file mode 100644 index 5291c74..0000000 --- a/webcc/rest_request_handler.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef WEBCC_REST_REQUEST_HANDLER_H_ -#define WEBCC_REST_REQUEST_HANDLER_H_ - -// HTTP server handling REST requests. - -#include - -#include "webcc/request_handler.h" -#include "webcc/rest_service_manager.h" - -namespace webcc { - -class RestRequestHandler : public RequestHandler { -public: - RestRequestHandler() = default; - - ~RestRequestHandler() override = default; - - bool Bind(RestServicePtr service, const std::string& url, bool is_regex); - -private: - void HandleConnection(ConnectionPtr connection) override; - - void SetContent(RequestPtr request, ResponsePtr response, - std::string&& content); - -private: - RestServiceManager service_manager_; -}; - -} // namespace webcc - -#endif // WEBCC_REST_REQUEST_HANDLER_H_ diff --git a/webcc/rest_server.h b/webcc/rest_server.h deleted file mode 100644 index 91321a8..0000000 --- a/webcc/rest_server.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef WEBCC_REST_SERVER_H_ -#define WEBCC_REST_SERVER_H_ - -// HTTP server handling REST requests. - -#include - -#include "webcc/server.h" -#include "webcc/rest_request_handler.h" -#include "webcc/rest_service.h" - -namespace webcc { - -class RestServer : public Server { -public: - RestServer(std::uint16_t port, std::size_t workers) - : Server(port, workers) { - } - - ~RestServer() override = default; - - // Bind a REST service to the given URL path. - // The URL should start with "/" and it will be treated as a regular - // expression if |is_regex| is true. - // Examples: - // - "/instances" - // - "/instances/(\\d+)" - // Binding to the same URL multiple times is allowed, but only the last one - // takes effect. - bool Bind(RestServicePtr service, const std::string& url, bool is_regex) { - return request_handler_.Bind(service, url, is_regex); - } - -private: - RequestHandler* GetRequestHandler() override { - return &request_handler_; - } - -private: - RestRequestHandler request_handler_; -}; - -} // namespace webcc - -#endif // WEBCC_REST_SERVER_H_ diff --git a/webcc/server.cc b/webcc/server.cc index 00e79a0..063e86b 100644 --- a/webcc/server.cc +++ b/webcc/server.cc @@ -11,8 +11,10 @@ using tcp = boost::asio::ip::tcp; namespace webcc { -Server::Server(std::uint16_t port, std::size_t workers) - : acceptor_(io_context_), signals_(io_context_), workers_(workers) { +Server::Server(std::uint16_t port, std::size_t workers, + const std::string& doc_root) + : acceptor_(io_context_), signals_(io_context_), workers_(workers), + request_handler_(doc_root) { RegisterSignals(); boost::system::error_code ec; @@ -50,9 +52,11 @@ Server::Server(std::uint16_t port, std::size_t workers) } } -void Server::Run() { - assert(GetRequestHandler() != nullptr); +bool Server::Bind(ServicePtr service, const std::string& url, bool is_regex) { + return request_handler_.Bind(service, url, is_regex); +} +void Server::Run() { if (!acceptor_.is_open()) { LOG_ERRO("Server is NOT going to run."); return; @@ -65,7 +69,7 @@ void Server::Run() { DoAccept(); // Start worker threads. - GetRequestHandler()->Start(workers_); + request_handler_.Start(workers_); // The io_context::run() call will block until all asynchronous operations // have finished. While the server is running, there is always at least one @@ -96,7 +100,7 @@ void Server::DoAccept() { LOG_INFO("Accepted a connection."); auto connection = std::make_shared( - std::move(socket), &pool_, GetRequestHandler()); + std::move(socket), &pool_, &request_handler_); pool_.Start(connection); } @@ -116,7 +120,7 @@ void Server::DoAwaitStop() { acceptor_.close(); // Stop worker threads. - GetRequestHandler()->Stop(); + request_handler_.Stop(); // Close all connections. pool_.CloseAll(); diff --git a/webcc/server.h b/webcc/server.h index 1170333..cc7444d 100644 --- a/webcc/server.h +++ b/webcc/server.h @@ -7,25 +7,35 @@ #include "boost/asio/ip/tcp.hpp" #include "boost/asio/signal_set.hpp" -#include "webcc/globals.h" #include "webcc/connection.h" #include "webcc/connection_pool.h" +#include "webcc/request_handler.h" +#include "webcc/service.h" namespace webcc { -class RequestHandler; - // HTTP server accepts TCP connections from TCP clients. // NOTE: Only support IPv4. class Server { public: - Server(std::uint16_t port, std::size_t workers); + Server(std::uint16_t port, std::size_t workers, + const std::string& doc_root = ""); virtual ~Server() = default; Server(const Server&) = delete; Server& operator=(const Server&) = delete; + // Bind a service to the given URL path. + // The URL should start with "/" and it will be treated as a regular + // expression if |is_regex| is true. + // Examples: + // - "/instances" + // - "/instances/(\\d+)" + // Binding to the same URL multiple times is allowed, but only the last one + // takes effect. + bool Bind(ServicePtr service, const std::string& url, bool is_regex); + // Run the server's io_service loop. void Run(); @@ -40,7 +50,7 @@ private: void DoAwaitStop(); // Get the handler for incoming requests. - virtual RequestHandler* GetRequestHandler() = 0; + //virtual RequestHandler* GetRequestHandler(); // The io_context used to perform asynchronous operations. boost::asio::io_context io_context_; @@ -51,11 +61,14 @@ private: // The connection pool which owns all live connections. ConnectionPool pool_; - // The signal_set is used to register for process termination notifications. + // The signals for processing termination notifications. boost::asio::signal_set signals_; // The number of worker threads. std::size_t workers_; + + // The handler for incoming requests. + RequestHandler request_handler_; }; } // namespace webcc diff --git a/webcc/rest_service.cc b/webcc/service.cc similarity index 69% rename from webcc/rest_service.cc rename to webcc/service.cc index 09199f8..b64645a 100644 --- a/webcc/rest_service.cc +++ b/webcc/service.cc @@ -1,4 +1,4 @@ -#include "webcc/rest_service.h" +#include "webcc/service.h" #include "webcc/logger.h" @@ -6,8 +6,7 @@ namespace webcc { // ----------------------------------------------------------------------------- -void RestListService::Handle(const RestRequest& request, - RestResponse* response) { +void ListService::Handle(const RestRequest& request, RestResponse* response) { const std::string& method = request.http->method(); if (method == methods::kGet) { @@ -17,14 +16,13 @@ void RestListService::Handle(const RestRequest& request, Post(request.http->content(), response); } else { - LOG_ERRO("RestListService doesn't support '%s' method.", method.c_str()); + LOG_ERRO("ListService doesn't support '%s' method.", method.c_str()); } } // ----------------------------------------------------------------------------- -void RestDetailService::Handle(const RestRequest& request, - RestResponse* response) { +void DetailService::Handle(const RestRequest& request, RestResponse* response) { const std::string& method = request.http->method(); if (method == methods::kGet) { @@ -40,7 +38,7 @@ void RestDetailService::Handle(const RestRequest& request, Delete(request.url_matches, response); } else { - LOG_ERRO("RestDetailService doesn't support '%s' method.", method.c_str()); + LOG_ERRO("DetailService doesn't support '%s' method.", method.c_str()); } } diff --git a/webcc/rest_service.h b/webcc/service.h similarity index 85% rename from webcc/rest_service.h rename to webcc/service.h index 5160cbf..0e08454 100644 --- a/webcc/rest_service.h +++ b/webcc/service.h @@ -1,5 +1,5 @@ -#ifndef WEBCC_REST_SERVICE_H_ -#define WEBCC_REST_SERVICE_H_ +#ifndef WEBCC_SERVICE_H_ +#define WEBCC_SERVICE_H_ // NOTE: // The design of RestListService and RestDetailService is very similar to @@ -32,6 +32,7 @@ struct RestRequest { UrlMatches url_matches; }; +// TODO: Add ResponseBuilder instead. struct RestResponse { Status status; @@ -43,20 +44,20 @@ struct RestResponse { // ----------------------------------------------------------------------------- -// Base class for your REST service. -class RestService { +// Base class for your service. +class Service { public: - virtual ~RestService() = default; + virtual ~Service() = default; - // Handle REST request, output response. + // Handle request, output response. virtual void Handle(const RestRequest& request, RestResponse* response) = 0; }; -using RestServicePtr = std::shared_ptr; +using ServicePtr = std::shared_ptr; // ----------------------------------------------------------------------------- -class RestListService : public RestService { +class ListService : public Service { public: void Handle(const RestRequest& request, RestResponse* response) override; @@ -71,7 +72,7 @@ protected: // ----------------------------------------------------------------------------- -class RestDetailService : public RestService { +class DetailService : public Service { public: void Handle(const RestRequest& request, RestResponse* response) override; @@ -98,4 +99,4 @@ protected: } // namespace webcc -#endif // WEBCC_REST_SERVICE_H_ +#endif // WEBCC_SERVICE_H_ diff --git a/webcc/rest_service_manager.cc b/webcc/service_manager.cc similarity index 62% rename from webcc/rest_service_manager.cc rename to webcc/service_manager.cc index aac4a29..8231fcd 100644 --- a/webcc/rest_service_manager.cc +++ b/webcc/service_manager.cc @@ -1,4 +1,4 @@ -#include "webcc/rest_service_manager.h" +#include "webcc/service_manager.h" #include @@ -6,15 +6,14 @@ namespace webcc { -bool RestServiceManager::AddService(RestServicePtr service, - const std::string& url, - bool is_regex) { +bool ServiceManager::AddService(ServicePtr service, const std::string& url, + bool is_regex) { assert(service); - ServiceItem item(service, url, is_regex); + Item item(service, url, is_regex); if (!is_regex) { - service_items_.push_back(std::move(item)); + items_.push_back(std::move(item)); return true; } @@ -23,7 +22,7 @@ bool RestServiceManager::AddService(RestServicePtr service, try { // Compile the regex. item.url_regex.assign(url, flags); - service_items_.push_back(std::move(item)); + items_.push_back(std::move(item)); return true; } catch (const std::regex_error& e) { LOG_ERRO("URL is not a valid regular expression: %s", e.what()); @@ -31,11 +30,11 @@ bool RestServiceManager::AddService(RestServicePtr service, } } -RestServicePtr RestServiceManager::GetService(const std::string& url, - UrlMatches* matches) { +ServicePtr ServiceManager::GetService(const std::string& url, + UrlMatches* matches) { assert(matches != nullptr); - for (ServiceItem& item : service_items_) { + for (Item& item : items_) { if (item.is_regex) { std::smatch match; @@ -55,7 +54,7 @@ RestServicePtr RestServiceManager::GetService(const std::string& url, } } - return RestServicePtr(); + return ServicePtr(); } } // namespace webcc diff --git a/webcc/rest_service_manager.h b/webcc/service_manager.h similarity index 56% rename from webcc/rest_service_manager.h rename to webcc/service_manager.h index 37c0b17..8e5315f 100644 --- a/webcc/rest_service_manager.h +++ b/webcc/service_manager.h @@ -1,54 +1,52 @@ -#ifndef WEBCC_REST_SERVICE_MANAGER_H_ -#define WEBCC_REST_SERVICE_MANAGER_H_ +#ifndef WEBCC_SERVICE_MANAGER_H_ +#define WEBCC_SERVICE_MANAGER_H_ #include // NOLINT #include #include // for move() #include -#include "webcc/rest_service.h" +#include "webcc/service.h" namespace webcc { -class RestServiceManager { +class ServiceManager { public: - RestServiceManager() = default; + ServiceManager() = default; - RestServiceManager(const RestServiceManager&) = delete; - RestServiceManager& operator=(const RestServiceManager&) = delete; + ServiceManager(const ServiceManager&) = delete; + ServiceManager& operator=(const ServiceManager&) = delete; // Add a service and bind it with the given URL. // The |url| should start with "/" and will be treated as a regular expression // if |regex| is true. // Examples: "/instances", "/instances/(\\d+)". - bool AddService(RestServicePtr service, const std::string& url, - bool is_regex); + bool AddService(ServicePtr service, const std::string& url, bool is_regex); // The |matches| is only available when the |url| bound to the service is a // regular expression and has sub-expressions. // E.g., the URL bound to the service is "/instances/(\\d+)", now match // "/instances/12345" against it, you will get one match of "12345". - RestServicePtr GetService(const std::string& url, UrlMatches* matches); + ServicePtr GetService(const std::string& url, UrlMatches* matches); private: - class ServiceItem { + class Item { public: - ServiceItem(RestServicePtr _service, const std::string& _url, - bool _is_regex) + Item(ServicePtr _service, const std::string& _url, bool _is_regex) : service(_service), url(_url), is_regex(_is_regex) { } - ServiceItem(const ServiceItem&) = default; - ServiceItem& operator=(const ServiceItem&) = default; + Item(const Item&) = default; + Item& operator=(const Item&) = default; - ServiceItem(ServiceItem&& rhs) + Item(Item&& rhs) : service(rhs.service), url(std::move(rhs.url)), is_regex(rhs.is_regex), url_regex(std::move(rhs.url_regex)) { } - RestServicePtr service; + ServicePtr service; // URL string, e.g., "/instances/(\\d+)". std::string url; @@ -60,9 +58,9 @@ private: std::regex url_regex; }; - std::vector service_items_; + std::vector items_; }; } // namespace webcc -#endif // WEBCC_REST_SERVICE_MANAGER_H_ +#endif // WEBCC_SERVICE_MANAGER_H_ diff --git a/webcc/url.cc b/webcc/url.cc index fcb0be8..af55284 100644 --- a/webcc/url.cc +++ b/webcc/url.cc @@ -278,44 +278,44 @@ void Url::AddQuery(const std::string& key, const std::string& value) { void Url::Parse(const std::string& str) { std::string tmp = boost::trim_left_copy(str); - std::size_t pos = std::string::npos; + std::size_t p = std::string::npos; - pos = tmp.find("://"); - if (pos != std::string::npos) { - scheme_ = tmp.substr(0, pos); - tmp = tmp.substr(pos + 3); + p = tmp.find("://"); + if (p != std::string::npos) { + scheme_ = tmp.substr(0, p); + tmp = tmp.substr(p + 3); } - pos = tmp.find('/'); - if (pos != std::string::npos) { - host_ = tmp.substr(0, pos); + p = tmp.find('/'); + if (p != std::string::npos) { + host_ = tmp.substr(0, p); - tmp = tmp.substr(pos + 1); + tmp = tmp.substr(p); - pos = tmp.find('?'); - if (pos != std::string::npos) { - path_ = tmp.substr(0, pos); - query_ = tmp.substr(pos + 1); + p = tmp.find('?'); + if (p != std::string::npos) { + path_ = tmp.substr(0, p); + query_ = tmp.substr(p + 1); } else { path_ = tmp; } } else { path_ = ""; - pos = tmp.find('?'); - if (pos != std::string::npos) { - host_ = tmp.substr(0, pos); - query_ = tmp.substr(pos + 1); + p = tmp.find('?'); + if (p != std::string::npos) { + host_ = tmp.substr(0, p); + query_ = tmp.substr(p + 1); } else { host_ = tmp; } } if (!host_.empty()) { - pos = host_.find(':'); - if (pos != std::string::npos) { - port_ = host_.substr(pos + 1); - host_ = host_.substr(0, pos); + p = host_.find(':'); + if (p != std::string::npos) { + port_ = host_.substr(p + 1); + host_ = host_.substr(0, p); } } }