diff --git a/.gitignore b/.gitignore index 29daf6a..b3f7949 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,10 @@ cmake_install.cmake install_manifest.txt *.cmake #Additions to https://github.com/github/gitignore/blob/master/CMake.gitignore +build Testing compile_commands.json +.cache .usages_clang *.crt diff --git a/CMakeLists.txt b/CMakeLists.txt index fb8ca62..8c16916 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,15 +24,37 @@ target_link_libraries(simple-web-server INTERFACE Threads::Threads) if(USE_STANDALONE_ASIO) target_compile_definitions(simple-web-server INTERFACE ASIO_STANDALONE) - find_path(ASIO_PATH asio.hpp) - if(NOT ASIO_PATH) - message(FATAL_ERROR "Standalone Asio not found") + # There is no canonical way to use Asio from CMake. + # In particular, Asio does not support CMake natively. + # However, Conan and Vcpkg do provide CMake support on their own. + # Prefer the CMake target and fall back to finding asio.hpp. + if(NOT TARGET asio::asio) + find_package(asio) + endif() + if(TARGET asio::asio) + target_link_libraries(simple-web-server INTERFACE asio::asio) else() + find_path(ASIO_PATH asio.hpp) + if(NOT ASIO_PATH) + message(FATAL_ERROR "Standalone Asio not found") + endif() target_include_directories(simple-web-server SYSTEM INTERFACE ${ASIO_PATH}) endif() else() - find_package(Boost 1.53.0 COMPONENTS system REQUIRED) - target_link_libraries(simple-web-server INTERFACE Boost::boost Boost::system) + # In Boost 1.89.0+, system is header-only, so we try to find it but make it optional + find_package(Boost 1.53.0 COMPONENTS system) + if(NOT Boost_FOUND) + # Fallback: try without system component for newer Boost versions where it's header-only + find_package(Boost 1.53.0 REQUIRED) + endif() + + target_link_libraries(simple-web-server INTERFACE Boost::boost) + + # Only link Boost::system if the target exists (for older Boost < 1.89) + if(TARGET Boost::system) + target_link_libraries(simple-web-server INTERFACE Boost::system) + endif() + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) target_compile_definitions(simple-web-server INTERFACE USE_BOOST_REGEX) find_package(Boost 1.53.0 COMPONENTS regex REQUIRED) @@ -61,7 +83,7 @@ endif() # If Simple-Web-Server is not a sub-project: if(CMAKE_SOURCE_DIR STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") if(NOT MSVC) - add_compile_options(-Wall -Wextra) + add_compile_options(-Wall -Wextra -Wsign-conversion) if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wthread-safety) endif() diff --git a/asio_compatibility.hpp b/asio_compatibility.hpp index aa43451..e93c0cf 100644 --- a/asio_compatibility.hpp +++ b/asio_compatibility.hpp @@ -27,12 +27,18 @@ namespace SimpleWeb { #endif namespace SimpleWeb { -#if(defined(ASIO_STANDALONE) && ASIO_VERSION >= 101300) || BOOST_ASIO_VERSION >= 101300 +#if (defined(ASIO_STANDALONE) && ASIO_VERSION >= 103300) || BOOST_ASIO_VERSION >= 103300 + using const_buffer = asio::const_buffer; +#else + using const_buffer = asio::const_buffers_1; +#endif + +#if (defined(ASIO_STANDALONE) && ASIO_VERSION >= 101300) || BOOST_ASIO_VERSION >= 101300 using io_context = asio::io_context; using resolver_results = asio::ip::tcp::resolver::results_type; using async_connect_endpoint = asio::ip::tcp::endpoint; -#if(defined(ASIO_STANDALONE) && ASIO_VERSION >= 101800) || BOOST_ASIO_VERSION >= 101800 +#if (defined(ASIO_STANDALONE) && ASIO_VERSION >= 101800) || BOOST_ASIO_VERSION >= 101800 using strand = asio::strand; #else using strand = asio::strand; diff --git a/client_http.hpp b/client_http.hpp index e12fcd2..44f8ca1 100644 --- a/client_http.hpp +++ b/client_http.hpp @@ -17,7 +17,7 @@ namespace SimpleWeb { public: /// Match condition for asio::read_until to match both standard and non-standard HTTP header endings. - std::pair, bool> operator()(asio::buffers_iterator begin, asio::buffers_iterator end) { + std::pair, bool> operator()(asio::buffers_iterator begin, asio::buffers_iterator end) { auto it = begin; for(; it != end; ++it) { if(*it == '\n') { @@ -72,11 +72,11 @@ namespace SimpleWeb { friend class ClientBase; public: - std::size_t size() noexcept { + std::size_t size() const noexcept { return streambuf.size(); } /// Convenience function to return content as a string. - std::string string() noexcept { + std::string string() const noexcept { return std::string(asio::buffers_begin(streambuf.data()), asio::buffers_end(streambuf.data())); } diff --git a/client_https.hpp b/client_https.hpp index 86c042c..09d37ab 100644 --- a/client_https.hpp +++ b/client_https.hpp @@ -27,7 +27,7 @@ namespace SimpleWeb { Client(const std::string &server_port_path, bool verify_certificate = true, const std::string &certification_file = std::string(), const std::string &private_key_file = std::string(), const std::string &verify_file = std::string()) : ClientBase::ClientBase(server_port_path, 443), -#if(defined(ASIO_STANDALONE) && ASIO_VERSION >= 101300) || BOOST_ASIO_VERSION >= 101300 +#if (defined(ASIO_STANDALONE) && ASIO_VERSION >= 101300) || BOOST_ASIO_VERSION >= 101300 context(asio::ssl::context::tls_client) { // Disabling TLS 1.0 and 1.1 (see RFC 8996) context.set_options(asio::ssl::context::no_tlsv1); @@ -41,7 +41,11 @@ namespace SimpleWeb { } if(verify_certificate) +#if (defined(ASIO_STANDALONE) && ASIO_VERSION >= 103300) || BOOST_ASIO_VERSION >= 103300 + context.set_verify_callback(asio::ssl::host_name_verification(host)); +#else context.set_verify_callback(asio::ssl::rfc2818_verification(host)); +#endif if(verify_file.size() > 0) context.load_verify_file(verify_file); diff --git a/crypto.hpp b/crypto.hpp index 3629598..be9bfb9 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -70,7 +70,7 @@ namespace SimpleWeb { b64 = BIO_new(BIO_f_base64()); BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); // TODO: Remove in 2022 or later -#if(defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER < 0x1000214fL) || (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x2080000fL) +#if (defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER < 0x1000214fL) || (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x2080000fL) bio = BIO_new_mem_buf(const_cast(&base64[0]), static_cast(base64.size())); #else bio = BIO_new_mem_buf(&base64[0], static_cast(base64.size())); diff --git a/server_http.hpp b/server_http.hpp index 1db0368..50f7a86 100644 --- a/server_http.hpp +++ b/server_http.hpp @@ -135,7 +135,7 @@ namespace SimpleWeb { } public: - std::size_t size() noexcept { + std::size_t size() const noexcept { return streambuf->size(); } @@ -209,11 +209,11 @@ namespace SimpleWeb { friend class ServerBase; public: - std::size_t size() noexcept { + std::size_t size() const noexcept { return streambuf.size(); } /// Convenience function to return content as std::string. - std::string string() noexcept { + std::string string() const noexcept { return std::string(asio::buffers_begin(streambuf.data()), asio::buffers_end(streambuf.data())); } @@ -227,11 +227,16 @@ namespace SimpleWeb { friend class Server; friend class Session; - asio::streambuf streambuf; + std::unique_ptr streambuf; + asio::streambuf content_streambuf; std::weak_ptr connection; std::string optimization = std::to_string(0); // TODO: figure out what goes wrong in gcc optimization without this line - Request(std::size_t max_request_streambuf_size, const std::shared_ptr &connection_) noexcept : streambuf(max_request_streambuf_size), connection(connection_), content(streambuf) {} + Request(std::size_t max_request_streambuf_size, const std::shared_ptr &connection_) noexcept + : streambuf(new asio::streambuf(max_request_streambuf_size)), content_streambuf(max_request_streambuf_size), connection(connection_), content(content_streambuf) {} + Request(std::size_t max_request_streambuf_size, const std::shared_ptr &connection_, std::unique_ptr &&previous_streambuf) noexcept + : streambuf(std::move(previous_streambuf)), content_streambuf(max_request_streambuf_size), connection(connection_), content(content_streambuf) { + } public: std::string method, path, query_string, http_version; @@ -292,6 +297,10 @@ namespace SimpleWeb { CaseInsensitiveMultimap parse_query_string() const noexcept { return SimpleWeb::QueryString::parse(query_string); } + + std::shared_ptr connection_shared() const noexcept { + return connection.lock(); + } }; protected: @@ -347,7 +356,10 @@ namespace SimpleWeb { class Session { public: - Session(std::size_t max_request_streambuf_size, std::shared_ptr connection_) noexcept : connection(std::move(connection_)), request(new Request(max_request_streambuf_size, connection)) {} + Session(std::size_t max_request_streambuf_size, std::shared_ptr connection_) noexcept + : connection(std::move(connection_)), request(new Request(max_request_streambuf_size, connection)) {} + Session(std::size_t max_request_streambuf_size, std::shared_ptr connection_, std::unique_ptr &&previous_streambuf) noexcept + : connection(std::move(connection_)), request(new Request(max_request_streambuf_size, connection, std::move(previous_streambuf))) {} std::shared_ptr connection; std::shared_ptr request; @@ -558,7 +570,7 @@ namespace SimpleWeb { void read(const std::shared_ptr &session) { session->connection->set_timeout(config.timeout_request); - asio::async_read_until(*session->connection->socket, session->request->streambuf, "\r\n\r\n", [this, session](const error_code &ec, std::size_t bytes_transferred) { + asio::async_read_until(*session->connection->socket, *session->request->streambuf, "\r\n\r\n", [this, session](const error_code &ec, std::size_t bytes_transferred) { auto lock = session->connection->handler_runner->continue_lock(); if(!lock) return; @@ -566,13 +578,14 @@ namespace SimpleWeb { if(!ec) { session->connection->set_timeout(this->config.timeout_content); - // request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs: + // request->streambuf->size() is not necessarily the same as bytes_transferred, from Boost-docs: // "After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter" // The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the // streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content). - std::size_t num_additional_bytes = session->request->streambuf.size() - bytes_transferred; + std::size_t num_additional_bytes = session->request->streambuf->size() - bytes_transferred; - if(!RequestMessage::parse(session->request->content, session->request->method, session->request->path, + std::istream istream(session->request->streambuf.get()); + if(!RequestMessage::parse(istream, session->request->method, session->request->path, session->request->query_string, session->request->http_version, session->request->header)) { if(this->on_error) this->on_error(session->request, make_error_code::make_error_code(errc::protocol_error)); @@ -591,15 +604,24 @@ namespace SimpleWeb { this->on_error(session->request, make_error_code::make_error_code(errc::protocol_error)); return; } - if(content_length > session->request->streambuf.max_size()) { + if(content_length > session->request->streambuf->max_size()) { auto response = std::shared_ptr(new Response(session, this->config.timeout_content)); response->write(StatusCode::client_error_payload_too_large); if(this->on_error) this->on_error(session->request, make_error_code::make_error_code(errc::message_size)); return; } + + if(num_additional_bytes > 0) { + auto content_bytes = std::min(content_length, (unsigned long long)num_additional_bytes); + auto &source = *session->request->streambuf; + auto &target = session->request->content_streambuf; + target.commit(asio::buffer_copy(target.prepare(content_bytes), source.data(), content_bytes)); + source.consume(content_bytes); + } + if(content_length > num_additional_bytes) { - asio::async_read(*session->connection->socket, session->request->streambuf, asio::transfer_exactly(content_length - num_additional_bytes), [this, session](const error_code &ec, std::size_t /*bytes_transferred*/) { + asio::async_read(*session->connection->socket, session->request->content_streambuf, asio::transfer_exactly(content_length - num_additional_bytes), [this, session](const error_code &ec, std::size_t /*bytes_transferred*/) { auto lock = session->connection->handler_runner->continue_lock(); if(!lock) return; @@ -610,15 +632,16 @@ namespace SimpleWeb { this->on_error(session->request, ec); }); } - else + else { this->find_resource(session); + } } else if((header_it = session->request->header.find("Transfer-Encoding")) != session->request->header.end() && header_it->second == "chunked") { // Expect hex number to not exceed 16 bytes (64-bit number), but take into account previous additional read bytes - auto chunk_size_streambuf = std::make_shared(std::max(16 + 2, session->request->streambuf.size())); + auto chunk_size_streambuf = std::make_shared(std::max(16 + 2, session->request->streambuf->size())); // Move leftover bytes - auto &source = session->request->streambuf; + auto &source = *session->request->streambuf; auto &target = *chunk_size_streambuf; target.commit(asio::buffer_copy(target.prepare(source.size()), source.data())); source.consume(source.size()); @@ -654,12 +677,7 @@ namespace SimpleWeb { return; } - if(chunk_size == 0) { - this->find_resource(session); - return; - } - - if(chunk_size + session->request->streambuf.size() > session->request->streambuf.max_size()) { + if(chunk_size + session->request->content_streambuf.size() > session->request->content_streambuf.max_size()) { auto response = std::shared_ptr(new Response(session, this->config.timeout_content)); response->write(StatusCode::client_error_payload_too_large); if(this->on_error) @@ -667,19 +685,34 @@ namespace SimpleWeb { return; } + auto read_next = [this, chunk_size](const std::shared_ptr &session, const std::shared_ptr &chunk_size_streambuf) { + if(chunk_size == 0) { + if(chunk_size_streambuf->size() > 0) { + // Remaining bytes are the beginning of the next request + auto &source = *chunk_size_streambuf; + auto &target = *session->request->streambuf; + target.commit(asio::buffer_copy(target.prepare(chunk_size_streambuf->size()), source.data(), chunk_size_streambuf->size())); + source.consume(chunk_size_streambuf->size()); + } + this->find_resource(session); + } + else + this->read_chunked_transfer_encoded(session, chunk_size_streambuf); + }; + auto num_additional_bytes = chunk_size_streambuf->size() - bytes_transferred; auto bytes_to_move = std::min(chunk_size, num_additional_bytes); if(bytes_to_move > 0) { // Move leftover bytes auto &source = *chunk_size_streambuf; - auto &target = session->request->streambuf; + auto &target = session->request->content_streambuf; target.commit(asio::buffer_copy(target.prepare(bytes_to_move), source.data(), bytes_to_move)); source.consume(bytes_to_move); } if(chunk_size > num_additional_bytes) { - asio::async_read(*session->connection->socket, session->request->streambuf, asio::transfer_exactly(chunk_size - num_additional_bytes), [this, session, chunk_size_streambuf](const error_code &ec, size_t /*bytes_transferred*/) { + asio::async_read(*session->connection->socket, session->request->content_streambuf, asio::transfer_exactly(chunk_size - num_additional_bytes), [this, session, chunk_size_streambuf, read_next](const error_code &ec, size_t /*bytes_transferred*/) { auto lock = session->connection->handler_runner->continue_lock(); if(!lock) return; @@ -687,12 +720,12 @@ namespace SimpleWeb { if(!ec) { // Remove "\r\n" auto null_buffer = std::make_shared(2); - asio::async_read(*session->connection->socket, *null_buffer, asio::transfer_exactly(2), [this, session, chunk_size_streambuf, null_buffer](const error_code &ec, size_t /*bytes_transferred*/) { + asio::async_read(*session->connection->socket, *null_buffer, asio::transfer_exactly(2), [this, session, chunk_size_streambuf, null_buffer, read_next](const error_code &ec, size_t /*bytes_transferred*/) { auto lock = session->connection->handler_runner->continue_lock(); if(!lock) return; if(!ec) - read_chunked_transfer_encoded(session, chunk_size_streambuf); + read_next(session, chunk_size_streambuf); else this->on_error(session->request, ec); }); @@ -706,12 +739,12 @@ namespace SimpleWeb { if(2 + chunk_size - num_additional_bytes == 1) istream.get(); auto null_buffer = std::make_shared(2); - asio::async_read(*session->connection->socket, *null_buffer, asio::transfer_exactly(2 + chunk_size - num_additional_bytes), [this, session, chunk_size_streambuf, null_buffer](const error_code &ec, size_t /*bytes_transferred*/) { + asio::async_read(*session->connection->socket, *null_buffer, asio::transfer_exactly(2 + chunk_size - num_additional_bytes), [this, session, chunk_size_streambuf, null_buffer, read_next](const error_code &ec, size_t /*bytes_transferred*/) { auto lock = session->connection->handler_runner->continue_lock(); if(!lock) return; if(!ec) - read_chunked_transfer_encoded(session, chunk_size_streambuf); + read_next(session, chunk_size_streambuf); else this->on_error(session->request, ec); }); @@ -721,7 +754,7 @@ namespace SimpleWeb { istream.get(); istream.get(); - read_chunked_transfer_encoded(session, chunk_size_streambuf); + read_next(session, chunk_size_streambuf); } } else if(this->on_error) @@ -778,13 +811,13 @@ namespace SimpleWeb { if(case_insensitive_equal(it->second, "close")) return; else if(case_insensitive_equal(it->second, "keep-alive")) { - auto new_session = std::make_shared(this->config.max_request_streambuf_size, response->session->connection); + auto new_session = std::make_shared(this->config.max_request_streambuf_size, response->session->connection, std::move(response->session->request->streambuf)); this->read(new_session); return; } } if(response->session->request->http_version >= "1.1") { - auto new_session = std::make_shared(this->config.max_request_streambuf_size, response->session->connection); + auto new_session = std::make_shared(this->config.max_request_streambuf_size, response->session->connection, std::move(response->session->request->streambuf)); this->read(new_session); return; } diff --git a/server_https.hpp b/server_https.hpp index 9b77fdc..7a02b68 100644 --- a/server_https.hpp +++ b/server_https.hpp @@ -29,7 +29,7 @@ namespace SimpleWeb { */ Server(const std::string &certification_file, const std::string &private_key_file, const std::string &verify_file = std::string()) : ServerBase::ServerBase(443), -#if(defined(ASIO_STANDALONE) && ASIO_VERSION >= 101300) || BOOST_ASIO_VERSION >= 101300 +#if (defined(ASIO_STANDALONE) && ASIO_VERSION >= 101300) || BOOST_ASIO_VERSION >= 101300 context(asio::ssl::context::tls_server) { // Disabling TLS 1.0 and 1.1 (see RFC 8996) context.set_options(asio::ssl::context::no_tlsv1);