diff --git a/.travis.yml b/.travis.yml index 61370c3..2860748 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ script: make && CTEST_OUTPUT_ON_FAILURE=1 make test && rm -r * && - CXX=g++ cmake -DCMAKE_CXX_FLAGS=\"-Werror -O3 -DUSE_STANDALONE_ASIO\" .. && + CXX=g++ cmake -DUSE_STANDALONE_ASIO=ON -DCMAKE_CXX_FLAGS=\"-Werror -O3\" .. && make && CTEST_OUTPUT_ON_FAILURE=1 make test " diff --git a/CMakeLists.txt b/CMakeLists.txt index 950bd82..6289957 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,52 +1,77 @@ cmake_minimum_required (VERSION 2.8.8) -project (Simple-Web-Server) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Wextra -Wsign-conversion") -include_directories(.) +project (Simple-Web-Server) + +option(USE_STANDALONE_ASIO "set ON to use standalone Asio instead of Boost.Asio" OFF) +option(BUILD_TESTING "set ON to build library tests" OFF) + +if(NOT MSVC) + add_compile_options(-std=c++11 -Wall -Wextra -Wsign-conversion) +else() + add_compile_options(/W1) +endif() + +add_library(simple-web-server INTERFACE) + +target_include_directories(simple-web-server INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) find_package(Threads REQUIRED) +target_link_libraries(simple-web-server INTERFACE ${CMAKE_THREAD_LIBS_INIT}) -set(BOOST_COMPONENTS system filesystem thread) -# Late 2017 TODO: remove the following checks and always use std::regex -if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) - set(BOOST_COMPONENTS ${BOOST_COMPONENTS} regex) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUSE_BOOST_REGEX") +# TODO 2020 when Debian Jessie LTS ends: +# Remove Boost system, thread, regex components; use Boost:: aliases; remove Boost target_include_directories +if(USE_STANDALONE_ASIO) + target_compile_definitions(simple-web-server INTERFACE USE_STANDALONE_ASIO) + include(CheckIncludeFileCXX) + CHECK_INCLUDE_FILE_CXX(asio.hpp HAVE_ASIO) + if(NOT HAVE_ASIO) + message(FATAL_ERROR "Standalone Asio not found") + endif() +else() + find_package(Boost 1.53.0 COMPONENTS system thread REQUIRED) + target_link_libraries(simple-web-server INTERFACE ${Boost_LIBRARIES}) + target_include_directories(simple-web-server INTERFACE ${Boost_INCLUDE_DIR}) + 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) + target_link_libraries(simple-web-server INTERFACE ${Boost_LIBRARIES}) + target_include_directories(simple-web-server INTERFACE ${Boost_INCLUDE_DIR}) endif() endif() -find_package(Boost 1.53.0 COMPONENTS ${BOOST_COMPONENTS} REQUIRED) -include_directories(SYSTEM ${Boost_INCLUDE_DIR}) +if(WIN32) + target_link_libraries(simple-web-server INTERFACE ws2_32 wsock32) +endif() if(APPLE) set(OPENSSL_ROOT_DIR "/usr/local/opt/openssl") endif() - -add_executable(http_examples http_examples.cpp) -target_link_libraries(http_examples ${Boost_LIBRARIES}) -target_link_libraries(http_examples ${CMAKE_THREAD_LIBS_INIT}) - -#TODO: add requirement for version 1.0.1g (can it be done in one line?) find_package(OpenSSL) - if(OPENSSL_FOUND) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_OPENSSL") - target_link_libraries(http_examples ${OPENSSL_LIBRARIES}) - include_directories(SYSTEM ${OPENSSL_INCLUDE_DIR}) - - add_executable(https_examples https_examples.cpp) - target_link_libraries(https_examples ${Boost_LIBRARIES}) - target_link_libraries(https_examples ${OPENSSL_LIBRARIES}) - target_link_libraries(https_examples ${CMAKE_THREAD_LIBS_INIT}) + target_compile_definitions(simple-web-server INTERFACE HAVE_OPENSSL) + target_link_libraries(simple-web-server INTERFACE ${OPENSSL_LIBRARIES}) + target_include_directories(simple-web-server INTERFACE ${OPENSSL_INCLUDE_DIR}) endif() -if(MSYS) #TODO: Is MSYS true when MSVC is true? - target_link_libraries(http_examples ws2_32 wsock32) +# If Simple-Web-Server is not a sub-project: +if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") + add_executable(http_examples http_examples.cpp) + target_link_libraries(http_examples simple-web-server) + find_package(Boost 1.53.0 COMPONENTS system thread filesystem REQUIRED) + target_link_libraries(http_examples ${Boost_LIBRARIES}) + target_include_directories(http_examples PRIVATE ${Boost_INCLUDE_DIR}) if(OPENSSL_FOUND) - target_link_libraries(https_examples ws2_32 wsock32) + add_executable(https_examples https_examples.cpp) + target_link_libraries(https_examples simple-web-server) + target_link_libraries(https_examples ${Boost_LIBRARIES}) + target_include_directories(https_examples PRIVATE ${Boost_INCLUDE_DIR}) endif() + + set(BUILD_TESTING ON) + + install(FILES server_http.hpp client_http.hpp server_https.hpp client_https.hpp crypto.hpp utility.hpp status_code.hpp DESTINATION include/simple-web-server) endif() -enable_testing() -add_subdirectory(tests) - -install(FILES server_http.hpp client_http.hpp server_https.hpp client_https.hpp crypto.hpp utility.hpp status_code.hpp DESTINATION include/simple-web-server) +if(BUILD_TESTING) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/crypto.hpp b/crypto.hpp index 2c63133..8cf7cca 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -68,7 +68,12 @@ namespace SimpleWeb { b64 = BIO_new(BIO_f_base64()); BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); +// TODO: Cannot find the exact version this change happended. Remove in 2020 +#if OPENSSL_VERSION_NUMBER <= 0x1000114fL + bio = BIO_new_mem_buf((char *)&base64[0], static_cast(base64.size())); +#else bio = BIO_new_mem_buf(&base64[0], static_cast(base64.size())); +#endif bio = BIO_push(b64, bio); auto decoded_length = BIO_read(bio, &ascii[0], static_cast(ascii.size())); diff --git a/server_http.hpp b/server_http.hpp index 97ee7de..d0c60b0 100644 --- a/server_http.hpp +++ b/server_http.hpp @@ -183,10 +183,9 @@ namespace SimpleWeb { friend class Session; asio::streambuf streambuf; - std::shared_ptr remote_endpoint; Request(std::size_t max_request_streambuf_size, std::shared_ptr remote_endpoint) noexcept - : streambuf(max_request_streambuf_size), remote_endpoint(std::move(remote_endpoint)), content(streambuf) {} + : streambuf(max_request_streambuf_size), content(streambuf), remote_endpoint(std::move(remote_endpoint)) {} public: std::string method, path, query_string, http_version; @@ -197,6 +196,8 @@ namespace SimpleWeb { regex::smatch path_match; + std::shared_ptr remote_endpoint; + std::string remote_endpoint_address() noexcept { try { return remote_endpoint->address().to_string(); diff --git a/status_code.hpp b/status_code.hpp index 66022a5..51c8dde 100644 --- a/status_code.hpp +++ b/status_code.hpp @@ -1,7 +1,9 @@ #ifndef SIMPLE_WEB_STATUS_CODE_HPP #define SIMPLE_WEB_STATUS_CODE_HPP +#include #include +#include #include namespace SimpleWeb { @@ -138,19 +140,33 @@ namespace SimpleWeb { } inline StatusCode status_code(const std::string &status_code_str) noexcept { - for(auto &status_code : status_codes()) { - if(status_code.second == status_code_str) - return status_code.first; - } - return StatusCode::unknown; + class StringToStatusCode : public std::unordered_map { + public: + StringToStatusCode() { + for(auto &status_code : SimpleWeb::status_codes()) + emplace(status_code.second, status_code.first); + } + }; + static StringToStatusCode string_to_status_code; + auto pos = string_to_status_code.find(status_code_str); + if(pos == string_to_status_code.end()) + return StatusCode::unknown; + return pos->second; } const inline std::string &status_code(StatusCode status_code_enum) noexcept { - for(auto &status_code : status_codes()) { - if(status_code.first == status_code_enum) - return status_code.second; - } - return status_codes()[0].second; + class StatusCodeToString : public std::map { + public: + StatusCodeToString() { + for(auto &status_code : SimpleWeb::status_codes()) + emplace(status_code.first, status_code.second); + } + }; + static StatusCodeToString status_code_to_string; + auto pos = status_code_to_string.find(status_code_enum); + if(pos == status_code_to_string.end()) + return status_codes()[0].second; + return pos->second; } } // namespace SimpleWeb diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 08fc773..45fa95e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,26 +1,21 @@ -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-access-control") +if(NOT MSVC) + add_compile_options(-fno-access-control) + + add_executable(io_test io_test.cpp) + target_link_libraries(io_test simple-web-server) + add_test(io_test io_test) -add_executable(io_test io_test.cpp) -target_link_libraries(io_test ${Boost_LIBRARIES}) -target_link_libraries(io_test ${CMAKE_THREAD_LIBS_INIT}) - -add_executable(parse_test parse_test.cpp) -target_link_libraries(parse_test ${Boost_LIBRARIES}) -target_link_libraries(parse_test ${CMAKE_THREAD_LIBS_INIT}) - -if(MSYS) #TODO: Is MSYS true when MSVC is true? - target_link_libraries(io_test ws2_32 wsock32) - target_link_libraries(parse_test ws2_32 wsock32) + add_executable(parse_test parse_test.cpp) + target_link_libraries(parse_test simple-web-server) + add_test(parse_test parse_test) endif() -add_test(io_test io_test) -add_test(parse_test parse_test) - if(OPENSSL_FOUND) add_executable(crypto_test crypto_test.cpp) - target_link_libraries(crypto_test ${OPENSSL_CRYPTO_LIBRARY}) + target_link_libraries(crypto_test simple-web-server) add_test(crypto_test crypto_test) endif() add_executable(status_code_test status_code_test.cpp) +target_link_libraries(status_code_test simple-web-server) add_test(status_code_test status_code_test) diff --git a/tests/io_test.cpp b/tests/io_test.cpp index 84cb212..f1bc4e9 100644 --- a/tests/io_test.cpp +++ b/tests/io_test.cpp @@ -64,6 +64,9 @@ int main() { *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" << content; + + assert(!request->remote_endpoint_address().empty()); + assert(request->remote_endpoint_port() != 0); }; server.resource["^/string2$"]["POST"] = [](shared_ptr response, shared_ptr request) { diff --git a/tests/parse_test.cpp b/tests/parse_test.cpp index ce91224..f058a11 100644 --- a/tests/parse_test.cpp +++ b/tests/parse_test.cpp @@ -195,19 +195,97 @@ int main() { { { - SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}}; - auto parsed = SimpleWeb::ContentDisposition::parse("form-data"); + SimpleWeb::CaseInsensitiveMultimap solution; + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse(""); assert(parsed == solution); } + { + SimpleWeb::CaseInsensitiveMultimap solution = {{"a", ""}}; + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a"); + assert(parsed == solution); + } + { + SimpleWeb::CaseInsensitiveMultimap solution = {{"a", ""}, {"b", ""}}; + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a; b"); + assert(parsed == solution); + } + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a;b"); + assert(parsed == solution); + } + } + { + SimpleWeb::CaseInsensitiveMultimap solution = {{"a", ""}, {"b", "c"}}; + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a; b=c"); + assert(parsed == solution); + } + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a;b=c"); + assert(parsed == solution); + } + } + { + SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}}; + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data"); + assert(parsed == solution); + } + { + SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}, {"test", ""}}; + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; test"); + assert(parsed == solution); + } + } { SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}, {"name", "file"}}; - auto parsed = SimpleWeb::ContentDisposition::parse("form-data; name=\"file\""); - assert(parsed == solution); + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=\"file\""); + assert(parsed == solution); + } + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=file"); + assert(parsed == solution); + } } { SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}, {"name", "file"}, {"filename", "filename.png"}}; - auto parsed = SimpleWeb::ContentDisposition::parse("form-data; name=\"file\"; filename=\"filename.png\""); - assert(parsed == solution); + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=\"file\"; filename=\"filename.png\""); + assert(parsed == solution); + } + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data;name=\"file\";filename=\"filename.png\""); + assert(parsed == solution); + } + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=file; filename=filename.png"); + assert(parsed == solution); + } + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data;name=file;filename=filename.png"); + assert(parsed == solution); + } + } + { + SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}, {"name", "fi le"}, {"filename", "file name.png"}}; + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=\"fi le\"; filename=\"file name.png\""); + assert(parsed == solution); + } + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=\"fi%20le\"; filename=\"file%20name.png\""); + assert(parsed == solution); + } + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=fi le; filename=file name.png"); + assert(parsed == solution); + } + { + auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=fi%20le; filename=file%20name.png"); + assert(parsed == solution); + } } } } diff --git a/utility.hpp b/utility.hpp index 2af49e7..b1b33e2 100644 --- a/utility.hpp +++ b/utility.hpp @@ -155,7 +155,64 @@ namespace SimpleWeb { } return result; } - }; + + class FieldValue { + public: + class SemicolonSeparatedAttributes { + public: + /// Parse Set-Cookie or Content-Disposition header field value. Attribute values are percent-decoded. + static CaseInsensitiveMultimap parse(const std::string &str) { + CaseInsensitiveMultimap result; + + std::size_t name_start_pos = std::string::npos; + std::size_t name_end_pos = std::string::npos; + std::size_t value_start_pos = std::string::npos; + for(std::size_t c = 0; c < str.size(); ++c) { + if(name_start_pos == std::string::npos) { + if(str[c] != ' ' && str[c] != ';') + name_start_pos = c; + } + else { + if(name_end_pos == std::string::npos) { + if(str[c] == ';') { + result.emplace(str.substr(name_start_pos, c - name_start_pos), std::string()); + name_start_pos = std::string::npos; + } + else if(str[c] == '=') + name_end_pos = c; + } + else { + if(value_start_pos == std::string::npos) { + if(str[c] == '"' && c + 1 < str.size()) + value_start_pos = c + 1; + else + value_start_pos = c; + } + else if(str[c] == '"' || str[c] == ';') { + result.emplace(str.substr(name_start_pos, name_end_pos - name_start_pos), Percent::decode(str.substr(value_start_pos, c - value_start_pos))); + name_start_pos = std::string::npos; + name_end_pos = std::string::npos; + value_start_pos = std::string::npos; + } + } + } + } + if(name_start_pos != std::string::npos) { + if(name_end_pos == std::string::npos) + result.emplace(str.substr(name_start_pos), std::string()); + else if(value_start_pos != std::string::npos) { + if(str.back() == '"') + result.emplace(str.substr(name_start_pos, name_end_pos - name_start_pos), Percent::decode(str.substr(value_start_pos, str.size() - 1))); + else + result.emplace(str.substr(name_start_pos, name_end_pos - name_start_pos), Percent::decode(str.substr(value_start_pos))); + } + } + + return result; + } + }; + }; + }; // namespace SimpleWeb class RequestMessage { public: @@ -231,49 +288,6 @@ namespace SimpleWeb { return true; } }; - - class ContentDisposition { - public: - /// Can be used to parse the Content-Disposition header field value when - /// clients are posting requests with enctype="multipart/form-data" - static CaseInsensitiveMultimap parse(const std::string &line) { - CaseInsensitiveMultimap result; - - std::size_t para_start_pos = 0; - std::size_t para_end_pos = std::string::npos; - std::size_t value_start_pos = std::string::npos; - for(std::size_t c = 0; c < line.size(); ++c) { - if(para_start_pos != std::string::npos) { - if(para_end_pos == std::string::npos) { - if(line[c] == ';') { - result.emplace(line.substr(para_start_pos, c - para_start_pos), std::string()); - para_start_pos = std::string::npos; - } - else if(line[c] == '=') - para_end_pos = c; - } - else { - if(value_start_pos == std::string::npos) { - if(line[c] == '"' && c + 1 < line.size()) - value_start_pos = c + 1; - } - else if(line[c] == '"') { - result.emplace(line.substr(para_start_pos, para_end_pos - para_start_pos), line.substr(value_start_pos, c - value_start_pos)); - para_start_pos = std::string::npos; - para_end_pos = std::string::npos; - value_start_pos = std::string::npos; - } - } - } - else if(line[c] != ' ' && line[c] != ';') - para_start_pos = c; - } - if(para_start_pos != std::string::npos && para_end_pos == std::string::npos) - result.emplace(line.substr(para_start_pos), std::string()); - - return result; - } - }; } // namespace SimpleWeb #ifdef __SSE2__