diff --git a/examples/static_server.cc b/examples/static_server.cc index adaf537..5b1a38f 100644 --- a/examples/static_server.cc +++ b/examples/static_server.cc @@ -11,7 +11,7 @@ void Help(const char* argv0) { std::cout << "Usage: " << argv0 << " " << std::endl; std::cout << " E.g.," << std::endl; - std::cout << " " << argv0 << " 8080 D:/www" << std::endl; + std::cout << " " << argv0 << " 8080 D:\\www" << std::endl; } int main(int argc, char* argv[]) { diff --git a/unittest/body_unittest.cc b/unittest/body_unittest.cc index ce1bc6a..979da6d 100644 --- a/unittest/body_unittest.cc +++ b/unittest/body_unittest.cc @@ -12,8 +12,8 @@ TEST(FormBodyTest, Payload) { form_body.InitPayload(); auto payload = form_body.NextPayload(); - EXPECT_EQ(false, payload.empty()); + EXPECT_TRUE(!payload.empty()); payload = form_body.NextPayload(); - EXPECT_EQ(true, payload.empty()); + EXPECT_TRUE(payload.empty()); } diff --git a/webcc/body.cc b/webcc/body.cc index 8433cc8..3a7a30b 100644 --- a/webcc/body.cc +++ b/webcc/body.cc @@ -10,6 +10,8 @@ #include "webcc/gzip.h" #endif +namespace bfs = boost::filesystem; + namespace webcc { // ----------------------------------------------------------------------------- @@ -40,7 +42,7 @@ Payload StringBody::NextPayload(bool free_previous) { if (index_ == 0) { index_ = 1; - return Payload{ boost::asio::buffer(data_) }; + return { boost::asio::buffer(data_) }; } return {}; } @@ -133,4 +135,44 @@ void FormBody::Free(std::size_t index) { } } +// ----------------------------------------------------------------------------- + +FileBody::FileBody(const Path& path, std::size_t chunk_size) + : path_(path), chunk_size_(chunk_size) { + size_ = utility::TellSize(path_); + if (size_ == kInvalidLength) { + throw Error{ Error::kFileError, "Cannot read the file" }; + } +} + +void FileBody::InitPayload() { + assert(chunk_size_ > 0); + chunk_.resize(chunk_size_); + + if (stream_.is_open()) { + stream_.close(); + } + + stream_.open(path_, std::ios::binary); + + if (stream_.fail()) { + throw Error{ Error::kFileError, "Cannot read the file" }; + } +} + +Payload FileBody::NextPayload(bool free_previous) { + boost::ignore_unused(free_previous); + + if (stream_.read(&chunk_[0], chunk_.size()).gcount() > 0) { + return { + boost::asio::buffer(chunk_.data(), (std::size_t)stream_.gcount()) + }; + } + return {}; +} + +void FileBody::Dump(std::ostream& os, const std::string& prefix) const { + os << prefix << "" << std::endl; +} + } // namespace webcc diff --git a/webcc/body.h b/webcc/body.h index 6401e25..35994d1 100644 --- a/webcc/body.h +++ b/webcc/body.h @@ -5,6 +5,8 @@ #include #include +#include "boost/filesystem/fstream.hpp" + #include "webcc/common.h" namespace webcc { @@ -122,6 +124,34 @@ private: std::size_t index_ = 0; }; +// ----------------------------------------------------------------------------- + +// File body for server to serve a file without loading the whole of it into +// the memory. +class FileBody : public Body { +public: + explicit FileBody(const Path& path, std::size_t chunk_size = 1024); + + std::size_t GetSize() const override { + return size_; + } + + void InitPayload() override; + + Payload NextPayload(bool free_previous = false) override; + + void Dump(std::ostream& os, const std::string& prefix) const override; + +private: + Path path_; + std::size_t chunk_size_; + + std::size_t size_; // File size in bytes + + boost::filesystem::ifstream stream_; + std::string chunk_; +}; + } // namespace webcc #endif // WEBCC_BODY_H_ diff --git a/webcc/connection.cc b/webcc/connection.cc index f2934d3..35e4d3e 100644 --- a/webcc/connection.cc +++ b/webcc/connection.cc @@ -6,18 +6,18 @@ #include "webcc/connection_pool.h" #include "webcc/logger.h" -#include "webcc/request_handler.h" +#include "webcc/server.h" using boost::asio::ip::tcp; namespace webcc { Connection::Connection(tcp::socket socket, ConnectionPool* pool, - RequestHandler* handler) + Server* server) : socket_(std::move(socket)), pool_(pool), buffer_(kBufferSize), - request_handler_(handler) { + server_(server) { } void Connection::Start() { @@ -53,7 +53,14 @@ void Connection::SendResponse(ResponsePtr response) { } void Connection::SendResponse(Status status) { - SendResponse(std::make_shared(status)); + auto response = std::make_shared(status); + + // According to the testing based on HTTPie (and Chrome), the `Content-Length` + // header is expected for a response with status like 404 even when the body + // is empty. + response->SetBody(std::make_shared(), true); + + SendResponse(response); } void Connection::DoRead() { @@ -65,8 +72,13 @@ void Connection::DoRead() { void Connection::OnRead(boost::system::error_code ec, std::size_t length) { if (ec) { + // TODO if (ec == boost::asio::error::eof) { LOG_WARN("Socket read EOF."); + //} else if (ec == boost::asio::error::operation_aborted) { + // LOG_WARN("Socket read aborted."); + //} else if (ec == boost::asio::error::connection_aborted) { + // LOG_WARN("Socket connection aborted."); } else { LOG_ERRO("Socket read error (%s).", ec.message().c_str()); } @@ -80,6 +92,7 @@ void Connection::OnRead(boost::system::error_code ec, std::size_t length) { if (!request_parser_.Parse(buffer_.data(), length)) { // Bad request. + // TODO: Always close the connection? LOG_ERRO("Failed to parse HTTP request."); SendResponse(Status::kBadRequest); return; @@ -95,7 +108,7 @@ void Connection::OnRead(boost::system::error_code ec, std::size_t length) { // Enqueue this connection. // Some worker thread will handle it later. - request_handler_->Enqueue(shared_from_this()); + server_->Enqueue(shared_from_this()); } void Connection::DoWrite() { @@ -124,10 +137,10 @@ void Connection::DoWriteBody() { if (!payload.empty()) { boost::asio::async_write(socket_, payload, - std::bind(&Connection::OnWriteBody, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2)); + std::bind(&Connection::OnWriteBody, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2)); } else { // No more body payload left, we're done. OnWriteOK(); diff --git a/webcc/connection.h b/webcc/connection.h index 15d8862..4dde259 100644 --- a/webcc/connection.h +++ b/webcc/connection.h @@ -5,7 +5,7 @@ #include #include -#include "boost/asio/ip/tcp.hpp" // for ip::tcp::socket +#include "boost/asio/ip/tcp.hpp" #include "webcc/globals.h" #include "webcc/request.h" @@ -14,16 +14,13 @@ namespace webcc { -class Connection; class ConnectionPool; -class RequestHandler; - -using ConnectionPtr = std::shared_ptr; +class Server; class Connection : public std::enable_shared_from_this { public: Connection(boost::asio::ip::tcp::socket socket, ConnectionPool* pool, - RequestHandler* handler); + Server* server); ~Connection() = default; @@ -40,10 +37,10 @@ public: // Close the socket. void Close(); - // Send response to client. + // Send a response to the client. void SendResponse(ResponsePtr response); - // TODO: Remove + // Send a response with the given status and an empty body to the client. void SendResponse(Status status); private: @@ -60,17 +57,17 @@ private: // Shutdown the socket. void Shutdown(); - // Socket for the connection. + // The socket for the connection. boost::asio::ip::tcp::socket socket_; // The pool for this connection. ConnectionPool* pool_; - // Buffer for incoming data. + // The buffer for incoming data. std::vector buffer_; - // The handler used to process the incoming request. - RequestHandler* request_handler_; + // The server. + Server* server_; // The incoming request. RequestPtr request_; @@ -82,6 +79,8 @@ private: ResponsePtr response_; }; +using ConnectionPtr = std::shared_ptr; + } // namespace webcc #endif // WEBCC_CONNECTION_H_ diff --git a/webcc/request_handler.cc b/webcc/request_handler.cc deleted file mode 100644 index d2d089d..0000000 --- a/webcc/request_handler.cc +++ /dev/null @@ -1,210 +0,0 @@ -#include "webcc/request_handler.h" - -#include -#include -#include - -#include "boost/algorithm/string.hpp" -#include "boost/filesystem/fstream.hpp" - -#include "webcc/logger.h" -#include "webcc/request.h" -#include "webcc/response.h" -#include "webcc/url.h" -#include "webcc/utility.h" - -#if WEBCC_ENABLE_GZIP -#include "webcc/gzip.h" -#endif - -namespace bfs = boost::filesystem; - -namespace webcc { - -RequestHandler::RequestHandler(const Path& doc_root) : doc_root_(doc_root) { -} - -bool RequestHandler::Route(const std::string& url, ViewPtr view, - const Strings& methods) { - assert(view); - - // TODO: More error check - - routes_.push_back({ url, {}, view, methods }); - - return true; -} - -bool RequestHandler::Route(const RegexUrl& regex_url, ViewPtr view, - const Strings& methods) { - assert(view); - - // TODO: More error check - - try { - - routes_.push_back({ "", regex_url(), view, methods }); - - } catch (const std::regex_error& e) { - LOG_ERRO("Not a valid regular expression: %s", e.what()); - return false; - } - - return true; -} - -void RequestHandler::Enqueue(ConnectionPtr connection) { - queue_.Push(connection); -} - -void RequestHandler::Start(std::size_t count) { - assert(count > 0 && workers_.size() == 0); - - for (std::size_t i = 0; i < count; ++i) { - workers_.emplace_back(std::bind(&RequestHandler::WorkerRoutine, this)); - } -} - -void RequestHandler::Stop() { - LOG_INFO("Stopping workers..."); - - // Clear pending connections. - // The connections will be closed later (see Server::DoAwaitStop). - LOG_INFO("Clear pending connections..."); - queue_.Clear(); - - // Enqueue a null connection to trigger the first worker to stop. - queue_.Push(ConnectionPtr()); - - for (auto& worker : workers_) { - if (worker.joinable()) { - worker.join(); - } - } - - LOG_INFO("All workers have been stopped."); -} - -void RequestHandler::WorkerRoutine() { - LOG_INFO("Worker is running."); - - for (;;) { - auto connection = queue_.PopOrWait(); - - if (!connection) { - LOG_INFO("Worker is going to stop."); - - // For stopping next worker. - queue_.Push(ConnectionPtr()); - - // Stop the worker. - break; - } - - Handle(connection); - } -} - -void RequestHandler::Handle(ConnectionPtr connection) { - auto request = connection->request(); - - const Url& url = request->url(); - UrlArgs args; - - LOG_INFO("Request URL path: %s", url.path().c_str()); - - // Find view - auto view = FindView(request->method(), url.path(), &args); - - if (!view) { - LOG_WARN("No view matches the URL path: %s", url.path().c_str()); - - if (!ServeStatic(connection)) { - connection->SendResponse(Status::kNotFound); - } - - return; - } - - // Save the (regex matched) URL args to request object. - request->set_args(args); - - // Ask the matched view to process the request. - ResponsePtr response = view->Handle(request); - - // Send the response back. - if (response) { - connection->SendResponse(response); - } else { - connection->SendResponse(Status::kNotImplemented); - } -} - -ViewPtr RequestHandler::FindView(const std::string& method, - const std::string& url, UrlArgs* args) { - assert(args != nullptr); - - for (auto& route : routes_) { - if (std::find(route.methods.begin(), route.methods.end(), method) == - route.methods.end()) { - continue; - } - - if (route.url.empty()) { - std::smatch match; - - if (std::regex_match(url, match, route.url_regex)) { - // Any sub-matches? - // Start from 1 because match[0] is the whole string itself. - for (size_t i = 1; i < match.size(); ++i) { - args->push_back(match[i].str()); - } - - return route.view; - } - } else { - if (boost::iequals(route.url, url)) { - return route.view; - } - } - } - - return ViewPtr(); -} - -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"; - } - - Path p = doc_root_ / path; - - std::string data; - if (!utility::ReadFile(p, &data)) { - connection->SendResponse(Status::kNotFound); - return false; - } - - auto response = std::make_shared(Status::kOK); - - if (!data.empty()) { - std::string extension = p.extension().string(); - response->SetContentType(media_types::FromExtension(extension), ""); - - // TODO: Use FileBody instead for streaming. - // TODO: gzip - auto body = std::make_shared(std::move(data)); - response->SetBody(body, true); - } - - // Send response back to client. - connection->SendResponse(response); - - return true; -} - -} // namespace webcc diff --git a/webcc/request_handler.h b/webcc/request_handler.h deleted file mode 100644 index b6d5a66..0000000 --- a/webcc/request_handler.h +++ /dev/null @@ -1,97 +0,0 @@ -#ifndef WEBCC_REQUEST_HANDLER_H_ -#define WEBCC_REQUEST_HANDLER_H_ - -#include -#include -#include -#include - -#include "webcc/connection.h" -#include "webcc/queue.h" -#include "webcc/view.h" - -namespace webcc { - -// ----------------------------------------------------------------------------- - -// Wrapper for regular expression URL. -class RegexUrl { -public: - explicit RegexUrl(const std::string& url) : url_(url) { - } - - std::regex operator()() const { - std::regex::flag_type flags = std::regex::ECMAScript | std::regex::icase; - - return std::regex(url_, flags); - } - -private: - std::string url_; -}; - -using R = RegexUrl; // A shortcut - -// ----------------------------------------------------------------------------- - -class RequestHandler { -public: - explicit RequestHandler(const Path& doc_root); - - virtual ~RequestHandler() = default; - - RequestHandler(const RequestHandler&) = delete; - RequestHandler& operator=(const RequestHandler&) = delete; - - bool Route(const std::string& url, ViewPtr view, const Strings& methods); - - bool Route(const RegexUrl& regex_url, ViewPtr view, const Strings& methods); - - // Put the connection into the queue. - void Enqueue(ConnectionPtr connection); - - // Start worker threads. - void Start(std::size_t count); - - // Clear pending connections from the queue and stop worker threads. - void Stop(); - -private: - void WorkerRoutine(); - - // 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 Handle(ConnectionPtr connection); - - // Find the view by HTTP method and URL. - ViewPtr FindView(const std::string& method, const std::string& url, - UrlArgs* args); - - // TODO - bool ServeStatic(ConnectionPtr connection); - -private: - struct RouteInfo { - std::string url; - std::regex url_regex; - ViewPtr view; - Strings methods; - }; - -private: - // The directory containing the files to be served. - Path doc_root_; - - Queue queue_; - - std::vector workers_; - - std::vector routes_; -}; - -} // namespace webcc - -#endif // WEBCC_REQUEST_HANDLER_H_ diff --git a/webcc/server.cc b/webcc/server.cc index 8d6eef2..d777bca 100644 --- a/webcc/server.cc +++ b/webcc/server.cc @@ -1,19 +1,28 @@ #include "webcc/server.h" +#include #include #include -#include "webcc/request_handler.h" +#include "boost/algorithm/string.hpp" +#include "boost/filesystem/fstream.hpp" + +#include "webcc/body.h" #include "webcc/logger.h" +#include "webcc/request.h" +#include "webcc/response.h" +#include "webcc/url.h" #include "webcc/utility.h" +namespace bfs = boost::filesystem; + using tcp = boost::asio::ip::tcp; namespace webcc { Server::Server(std::uint16_t port, std::size_t workers, const Path& doc_root) : acceptor_(io_context_), signals_(io_context_), workers_(workers), - request_handler_(doc_root) { + doc_root_(doc_root) { RegisterSignals(); boost::system::error_code ec; @@ -51,6 +60,35 @@ Server::Server(std::uint16_t port, std::size_t workers, const Path& doc_root) } } +bool Server::Route(const std::string& url, ViewPtr view, + const Strings& methods) { + assert(view); + + // TODO: More error check + + routes_.push_back({ url, {}, view, methods }); + + return true; +} + +bool Server::Route(const UrlRegex& regex_url, ViewPtr view, + const Strings& methods) { + assert(view); + + // TODO: More error check + + try { + + routes_.push_back({ "", regex_url(), view, methods }); + + } catch (const std::regex_error& e) { + LOG_ERRO("Not a valid regular expression: %s", e.what()); + return false; + } + + return true; +} + void Server::Run() { if (!acceptor_.is_open()) { LOG_ERRO("Server is NOT going to run."); @@ -64,7 +102,11 @@ void Server::Run() { DoAccept(); // Start worker threads. - request_handler_.Start(workers_); + assert(workers_ > 0 && worker_threads_.empty()); + + for (std::size_t i = 0; i < workers_; ++i) { + worker_threads_.emplace_back(std::bind(&Server::WorkerRoutine, this)); + } // The io_context::run() call will block until all asynchronous operations // have finished. While the server is running, there is always at least one @@ -73,6 +115,30 @@ void Server::Run() { io_context_.run(); } +void Server::Stop() { + LOG_INFO("Stopping workers..."); + + // Clear pending connections. + // The connections will be closed later (see Server::DoAwaitStop). + LOG_INFO("Clear pending connections..."); + queue_.Clear(); + + // Enqueue a null connection to trigger the first worker to stop. + queue_.Push(ConnectionPtr()); + + for (auto& thread : worker_threads_) { + if (thread.joinable()) { + thread.join(); + } + } + + LOG_INFO("All workers have been stopped."); +} + +void Server::Enqueue(ConnectionPtr connection) { + queue_.Push(connection); +} + void Server::RegisterSignals() { signals_.add(SIGINT); // Ctrl+C signals_.add(SIGTERM); @@ -95,7 +161,7 @@ void Server::DoAccept() { LOG_INFO("Accepted a connection."); auto connection = std::make_shared( - std::move(socket), &pool_, &request_handler_); + std::move(socket), &pool_, this); pool_.Start(connection); } @@ -115,11 +181,133 @@ void Server::DoAwaitStop() { acceptor_.close(); // Stop worker threads. - request_handler_.Stop(); + Stop(); // Close all connections. pool_.CloseAll(); }); } +void Server::WorkerRoutine() { + LOG_INFO("Worker is running."); + + for (;;) { + auto connection = queue_.PopOrWait(); + + if (!connection) { + LOG_INFO("Worker is going to stop."); + + // For stopping next worker. + queue_.Push(ConnectionPtr()); + + // Stop the worker. + break; + } + + Handle(connection); + } +} + +void Server::Handle(ConnectionPtr connection) { + auto request = connection->request(); + + const Url& url = request->url(); + UrlArgs args; + + LOG_INFO("Request URL path: %s", url.path().c_str()); + + auto view = FindView(request->method(), url.path(), &args); + + if (!view) { + LOG_WARN("No view matches the URL path: %s", url.path().c_str()); + if (!ServeStatic(connection)) { + connection->SendResponse(Status::kNotFound); + } + return; + } + + // Save the (regex matched) URL args to request object. + request->set_args(args); + + // Ask the matched view to process the request. + ResponsePtr response = view->Handle(request); + + // Send the response back. + if (response) { + connection->SendResponse(response); + } else { + connection->SendResponse(Status::kNotImplemented); + } +} + +ViewPtr Server::FindView(const std::string& method, const std::string& url, + UrlArgs* args) { + assert(args != nullptr); + + for (auto& route : routes_) { + if (std::find(route.methods.begin(), route.methods.end(), method) == + route.methods.end()) { + continue; + } + + if (route.url.empty()) { + std::smatch match; + + if (std::regex_match(url, match, route.url_regex)) { + // Any sub-matches? + // Start from 1 because match[0] is the whole string itself. + for (size_t i = 1; i < match.size(); ++i) { + args->push_back(match[i].str()); + } + + return route.view; + } + } else { + if (boost::iequals(route.url, url)) { + return route.view; + } + } + } + + return ViewPtr(); +} + +bool Server::ServeStatic(ConnectionPtr connection) { + if (doc_root_.empty()) { + LOG_INFO("The doc root was not specified."); + return false; + } + + 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"; // TODO + } + + Path p = doc_root_ / path; + + try { + auto body = std::make_shared(p); + + auto response = std::make_shared(Status::kOK); + + std::string extension = p.extension().string(); + response->SetContentType(media_types::FromExtension(extension), ""); + + // NOTE: Gzip compression is not supported. + response->SetBody(body, true); + + // Send response back to client. + connection->SendResponse(response); + + return true; + + } catch (const Error& error) { + LOG_ERRO("File error: %s.", error.message().c_str()); + return false; + } +} + } // namespace webcc diff --git a/webcc/server.h b/webcc/server.h index 6765d62..352f6da 100644 --- a/webcc/server.h +++ b/webcc/server.h @@ -1,8 +1,9 @@ #ifndef WEBCC_SERVER_H_ #define WEBCC_SERVER_H_ -#include #include +#include +#include #include "boost/asio/io_context.hpp" #include "boost/asio/ip/tcp.hpp" @@ -10,7 +11,8 @@ #include "webcc/connection.h" #include "webcc/connection_pool.h" -#include "webcc/request_handler.h" +#include "webcc/queue.h" +#include "webcc/url.h" #include "webcc/view.h" namespace webcc { @@ -28,21 +30,23 @@ public: // Route a URL to a view. // The URL should start with "/". E.g., "/instances". bool Route(const std::string& url, ViewPtr view, - const Strings& methods = { "GET" }) { - return request_handler_.Route(url, view, methods); - } + const Strings& methods = { "GET" }); - // Route a regular expression URL to a view. + // Route a URL (as regular expression) to a view. // The URL should start with "/" and be a regular expression. // E.g., "/instances/(\\d+)". - bool Route(const RegexUrl& regex_url, ViewPtr view, - const Strings& methods = { "GET" }) { - return request_handler_.Route(regex_url, view, methods); - } + bool Route(const UrlRegex& regex_url, ViewPtr view, + const Strings& methods = { "GET" }); // Run the loop. void Run(); + // Clear pending connections from the queue and stop worker threads. + void Stop(); + + // Put the connection into the queue. + void Enqueue(ConnectionPtr connection); + private: // Register to handle the signals that indicate when the server should exit. void RegisterSignals(); @@ -53,6 +57,30 @@ private: // Wait for a request to stop the server. void DoAwaitStop(); + void WorkerRoutine(); + + // 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 Handle(ConnectionPtr connection); + + // Find the view by HTTP method and URL. + ViewPtr FindView(const std::string& method, const std::string& url, + UrlArgs* args); + + // Serve static files from the doc root. + bool ServeStatic(ConnectionPtr connection); + +private: + struct RouteInfo { + std::string url; + std::regex url_regex; + ViewPtr view; + Strings methods; + }; + // The io_context used to perform asynchronous operations. boost::asio::io_context io_context_; @@ -68,8 +96,16 @@ private: // The number of worker threads. std::size_t workers_; - // The handler for incoming requests. - RequestHandler request_handler_; + // Worker threads. + std::vector worker_threads_; + + // The directory with the static files to be served. + Path doc_root_; + + // The queue with connection waiting for the workers to process. + Queue queue_; + + std::vector routes_; }; } // namespace webcc diff --git a/webcc/url.cc b/webcc/url.cc index af55284..cdd1bef 100644 --- a/webcc/url.cc +++ b/webcc/url.cc @@ -168,83 +168,6 @@ bool SplitKeyValue(const std::string& kv, std::string* key, // ----------------------------------------------------------------------------- -UrlQuery::UrlQuery(const std::string& str) { - if (!str.empty()) { - // Split into key value pairs separated by '&'. - for (std::size_t i = 0; i != std::string::npos;) { - std::size_t j = str.find_first_of('&', i); - - std::string kv; - if (j == std::string::npos) { - kv = str.substr(i); - i = std::string::npos; - } else { - kv = str.substr(i, j - i); - i = j + 1; - } - - std::string key; - std::string value; - if (SplitKeyValue(kv, &key, &value)) { - Add(std::move(key), std::move(value)); - } - } - } -} - -void UrlQuery::Add(std::string&& key, std::string&& value) { - if (!Has(key)) { - parameters_.push_back({ std::move(key), std::move(value) }); - } -} - -void UrlQuery::Add(const std::string& key, const std::string& value) { - if (!Has(key)) { - parameters_.push_back({ key, value }); - } -} - -void UrlQuery::Remove(const std::string& key) { - auto it = Find(key); - if (it != parameters_.end()) { - parameters_.erase(it); - } -} - -const std::string& UrlQuery::Get(const std::string& key) const { - auto it = Find(key); - if (it != parameters_.end()) { - return it->second; - } - - static const std::string kEmptyValue; - return kEmptyValue; -} - -std::string UrlQuery::ToString() const { - if (parameters_.empty()) { - return ""; - } - - std::string str = parameters_[0].first + "=" + parameters_[0].second; - - for (std::size_t i = 1; i < parameters_.size(); ++i) { - str += "&"; - str += parameters_[i].first + "=" + parameters_[i].second; - } - - str = EncodeQuery(str); - return str; -} - -UrlQuery::ConstIterator UrlQuery::Find(const std::string& key) const { - return std::find_if(parameters_.begin(), - parameters_.end(), - [&key](const Parameter& p) { return p.first == key; }); -} - -// ----------------------------------------------------------------------------- - Url::Url(const std::string& str, bool decode) { Init(str, decode); } @@ -328,4 +251,80 @@ void Url::Clear() { query_.clear(); } +// ----------------------------------------------------------------------------- + +UrlQuery::UrlQuery(const std::string& str) { + if (!str.empty()) { + // Split into key value pairs separated by '&'. + for (std::size_t i = 0; i != std::string::npos;) { + std::size_t j = str.find_first_of('&', i); + + std::string kv; + if (j == std::string::npos) { + kv = str.substr(i); + i = std::string::npos; + } else { + kv = str.substr(i, j - i); + i = j + 1; + } + + std::string key; + std::string value; + if (SplitKeyValue(kv, &key, &value)) { + Add(std::move(key), std::move(value)); + } + } + } +} + +void UrlQuery::Add(std::string&& key, std::string&& value) { + if (!Has(key)) { + parameters_.push_back({ std::move(key), std::move(value) }); + } +} + +void UrlQuery::Add(const std::string& key, const std::string& value) { + if (!Has(key)) { + parameters_.push_back({ key, value }); + } +} + +void UrlQuery::Remove(const std::string& key) { + auto it = Find(key); + if (it != parameters_.end()) { + parameters_.erase(it); + } +} + +const std::string& UrlQuery::Get(const std::string& key) const { + auto it = Find(key); + if (it != parameters_.end()) { + return it->second; + } + + static const std::string kEmptyValue; + return kEmptyValue; +} + +std::string UrlQuery::ToString() const { + if (parameters_.empty()) { + return ""; + } + + std::string str = parameters_[0].first + "=" + parameters_[0].second; + + for (std::size_t i = 1; i < parameters_.size(); ++i) { + str += "&"; + str += parameters_[i].first + "=" + parameters_[i].second; + } + + str = EncodeQuery(str); + return str; +} + +UrlQuery::ConstIterator UrlQuery::Find(const std::string& key) const { + return std::find_if(parameters_.begin(), parameters_.end(), + [&key](const Parameter& p) { return p.first == key; }); +} + } // namespace webcc diff --git a/webcc/url.h b/webcc/url.h index 310b11d..ce9f930 100644 --- a/webcc/url.h +++ b/webcc/url.h @@ -1,8 +1,7 @@ #ifndef WEBCC_URL_H_ #define WEBCC_URL_H_ -// A simplified implementation of URL (or URI). - +#include #include #include #include @@ -13,48 +12,7 @@ namespace webcc { // ----------------------------------------------------------------------------- -// URL query parameters. -class UrlQuery { -public: - using Parameter = std::pair; - using Parameters = std::vector; - - UrlQuery() = default; - - // The query string should be key value pairs separated by '&'. - explicit UrlQuery(const std::string& str); - - void Add(const std::string& key, const std::string& value); - - void Add(std::string&& key, std::string&& value); - - void Remove(const std::string& key); - - // Get a value by key. - // Return empty string if the key doesn't exist. - const std::string& Get(const std::string& key) const; - - bool Has(const std::string& key) const { - return Find(key) != parameters_.end(); - } - - bool IsEmpty() const { - return parameters_.empty(); - } - - // Return key-value pairs concatenated by '&'. - // E.g., "item=12731&color=blue&size=large". - std::string ToString() const; - -private: - using ConstIterator = Parameters::const_iterator; - ConstIterator Find(const std::string& key) const; - - Parameters parameters_; -}; - -// ----------------------------------------------------------------------------- - +// A simplified implementation of URL (or URI). class Url { public: Url() = default; @@ -127,6 +85,71 @@ private: std::string query_; }; +// ----------------------------------------------------------------------------- + +// URL query parameters. +class UrlQuery { +public: + using Parameter = std::pair; + + UrlQuery() = default; + + // The query string should be key value pairs separated by '&'. + explicit UrlQuery(const std::string& str); + + void Add(const std::string& key, const std::string& value); + + void Add(std::string&& key, std::string&& value); + + void Remove(const std::string& key); + + // Get a value by key. + // Return empty string if the key doesn't exist. + const std::string& Get(const std::string& key) const; + + bool Has(const std::string& key) const { + return Find(key) != parameters_.end(); + } + + bool IsEmpty() const { + return parameters_.empty(); + } + + // Return key-value pairs concatenated by '&'. + // E.g., "item=12731&color=blue&size=large". + std::string ToString() const; + +private: + using ConstIterator = std::vector::const_iterator; + + ConstIterator Find(const std::string& key) const; + +private: + std::vector parameters_; +}; + +// ----------------------------------------------------------------------------- + +// Wrapper for URL as regular expression. +// Used by Server::Route(). +class UrlRegex { +public: + explicit UrlRegex(const std::string& url) : url_(url) { + } + + std::regex operator()() const { + std::regex::flag_type flags = std::regex::ECMAScript | std::regex::icase; + + return std::regex(url_, flags); + } + +private: + std::string url_; +}; + +// Shortcut +using R = UrlRegex; + } // namespace webcc #endif // WEBCC_URL_H_