├── web ├── test.html └── index.html ├── .clang-format ├── .gitignore ├── .travis.yml ├── tests ├── CMakeLists.txt ├── status_code_test.cpp ├── crypto_test.cpp ├── parse_test.cpp └── io_test.cpp ├── LICENSE ├── README.md ├── server_https.hpp ├── CMakeLists.txt ├── client_https.hpp ├── status_code.hpp ├── crypto.hpp ├── http_examples.cpp ├── https_examples.cpp ├── utility.hpp ├── client_http.hpp └── server_http.hpp /web/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple-Web-Server html-file 4 | 5 | 6 | This is the content of test.html 7 | 8 | 9 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple-Web-Server html-file 4 | 5 | 6 | This is the content of index.html 7 | 8 | 9 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | IndentWidth: 2 2 | AccessModifierOffset: -2 3 | UseTab: Never 4 | ColumnLimit: 0 5 | MaxEmptyLinesToKeep: 2 6 | SpaceBeforeParens: Never 7 | BreakBeforeBraces: Custom 8 | BraceWrapping: {BeforeElse: true, BeforeCatch: true} 9 | NamespaceIndentation: All 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://github.com/github/gitignore/blob/master/CMake.gitignore 2 | CMakeCache.txt 3 | CMakeFiles 4 | CMakeScripts 5 | Makefile 6 | cmake_install.cmake 7 | install_manifest.txt 8 | *.cmake 9 | #Additions to https://github.com/github/gitignore/blob/master/CMake.gitignore 10 | Testing 11 | compile_commands.json 12 | .usages_clang 13 | 14 | *.crt 15 | *.key 16 | 17 | # executables 18 | http_examples 19 | https_examples 20 | io_test 21 | parse_test 22 | crypto_test 23 | status_code_test 24 | 25 | # Visual Studio 2015/2017 cache/options directory 26 | .vs 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | script: 7 | - sudo docker run -it -v "$PWD:/repository" eidheim/testing sh -c " 8 | cd /repository && mkdir build && cd build && 9 | scan-build cmake -DCMAKE_CXX_FLAGS=-Werror .. && 10 | scan-build --status-bugs make && 11 | rm -r * && 12 | CXX=clang++ cmake -DCMAKE_CXX_FLAGS=-Werror .. && 13 | make && 14 | rm -r * && 15 | CXX=g++ cmake -DCMAKE_CXX_FLAGS=-Werror .. && 16 | make && 17 | CTEST_OUTPUT_ON_FAILURE=1 make test && 18 | rm -r * && 19 | CXX=g++ cmake -DUSE_STANDALONE_ASIO=ON -DCMAKE_CXX_FLAGS=\"-Werror -O3\" .. && 20 | make && 21 | CTEST_OUTPUT_ON_FAILURE=1 make test 22 | " 23 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(NOT MSVC) 2 | add_compile_options(-fno-access-control) 3 | 4 | add_executable(io_test io_test.cpp) 5 | target_link_libraries(io_test simple-web-server) 6 | add_test(io_test io_test) 7 | 8 | add_executable(parse_test parse_test.cpp) 9 | target_link_libraries(parse_test simple-web-server) 10 | add_test(parse_test parse_test) 11 | endif() 12 | 13 | if(OPENSSL_FOUND) 14 | add_executable(crypto_test crypto_test.cpp) 15 | target_link_libraries(crypto_test simple-web-server) 16 | add_test(crypto_test crypto_test) 17 | endif() 18 | 19 | add_executable(status_code_test status_code_test.cpp) 20 | target_link_libraries(status_code_test simple-web-server) 21 | add_test(status_code_test status_code_test) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2018 Ole Christian Eidheim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/status_code_test.cpp: -------------------------------------------------------------------------------- 1 | #include "status_code.hpp" 2 | #include 3 | 4 | using namespace SimpleWeb; 5 | 6 | 7 | int main() { 8 | assert(status_code("000 Error") == StatusCode::unknown); 9 | assert(status_code(StatusCode::unknown) == ""); 10 | assert(status_code("100 Continue") == StatusCode::information_continue); 11 | assert(status_code(StatusCode::information_continue) == "100 Continue"); 12 | assert(status_code("200 OK") == StatusCode::success_ok); 13 | assert(status_code(StatusCode::success_ok) == "200 OK"); 14 | assert(status_code("208 Already Reported") == StatusCode::success_already_reported); 15 | assert(status_code(StatusCode::success_already_reported) == "208 Already Reported"); 16 | assert(status_code("308 Permanent Redirect") == StatusCode::redirection_permanent_redirect); 17 | assert(status_code(StatusCode::redirection_permanent_redirect) == "308 Permanent Redirect"); 18 | assert(status_code("404 Not Found") == StatusCode::client_error_not_found); 19 | assert(status_code(StatusCode::client_error_not_found) == "404 Not Found"); 20 | assert(status_code("502 Bad Gateway") == StatusCode::server_error_bad_gateway); 21 | assert(status_code(StatusCode::server_error_bad_gateway) == "502 Bad Gateway"); 22 | assert(status_code("504 Gateway Timeout") == StatusCode::server_error_gateway_timeout); 23 | assert(status_code(StatusCode::server_error_gateway_timeout) == "504 Gateway Timeout"); 24 | assert(status_code("511 Network Authentication Required") == StatusCode::server_error_network_authentication_required); 25 | assert(status_code(StatusCode::server_error_network_authentication_required) == "511 Network Authentication Required"); 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **_This project has moved to https://gitlab.com/eidheim/Simple-Web-Server._** 2 | 3 | Simple-Web-Server 4 | ================= 5 | 6 | A very simple, fast, multithreaded, platform independent HTTP and HTTPS server and client library implemented using C++11 and Asio (both Boost.Asio and standalone Asio can be used). Created to be an easy way to make REST resources available from C++ applications. 7 | 8 | See https://gitlab.com/eidheim/Simple-WebSocket-Server for an easy way to make WebSocket/WebSocket Secure endpoints in C++. Also, feel free to check out the new C++ IDE supporting C++11/14/17: https://gitlab.com/cppit/jucipp. 9 | 10 | ### Features 11 | 12 | * Asynchronous request handling 13 | * Thread pool if needed 14 | * Platform independent 15 | * HTTPS support 16 | * HTTP persistent connection (for HTTP/1.1) 17 | * Client supports chunked transfer encoding 18 | * Timeouts, if any of Server::timeout_request and Server::timeout_content are >0 (default: Server::timeout_request=5 seconds, and Server::timeout_content=300 seconds) 19 | * Simple way to add REST resources using regex for path, and anonymous functions 20 | 21 | ### Usage 22 | 23 | See http_examples.cpp or https_examples.cpp for example usage. 24 | 25 | See particularly the JSON-POST (using Boost.PropertyTree) and the GET /match/[number] examples, which are most relevant. 26 | 27 | ### Dependencies 28 | 29 | * Boost.Asio or standalone Asio 30 | * Boost is required to compile the examples 31 | * For HTTPS: OpenSSL libraries 32 | 33 | ### Compile and run 34 | 35 | Compile with a C++11 compliant compiler: 36 | ```sh 37 | mkdir build 38 | cd build 39 | cmake .. 40 | make 41 | cd .. 42 | ``` 43 | 44 | #### HTTP 45 | 46 | Run the server and client examples: `./build/http_examples` 47 | 48 | Direct your favorite browser to for instance http://localhost:8080/ 49 | 50 | #### HTTPS 51 | 52 | Before running the server, an RSA private key (server.key) and an SSL certificate (server.crt) must be created. Follow, for instance, the instructions given here (for a self-signed certificate): http://www.akadia.com/services/ssh_test_certificate.html 53 | 54 | Run the server and client examples: `./build/https_examples` 55 | 56 | Direct your favorite browser to for instance https://localhost:8080/ 57 | 58 | -------------------------------------------------------------------------------- /server_https.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_HTTPS_HPP 2 | #define SERVER_HTTPS_HPP 3 | 4 | #include "server_http.hpp" 5 | 6 | #ifdef USE_STANDALONE_ASIO 7 | #include 8 | #else 9 | #include 10 | #endif 11 | 12 | #include 13 | #include 14 | 15 | namespace SimpleWeb { 16 | using HTTPS = asio::ssl::stream; 17 | 18 | template <> 19 | class Server : public ServerBase { 20 | bool set_session_id_context = false; 21 | 22 | public: 23 | Server(const std::string &cert_file, const std::string &private_key_file, const std::string &verify_file = std::string()) 24 | : ServerBase::ServerBase(443), context(asio::ssl::context::tlsv12) { 25 | context.use_certificate_chain_file(cert_file); 26 | context.use_private_key_file(private_key_file, asio::ssl::context::pem); 27 | 28 | if(verify_file.size() > 0) { 29 | context.load_verify_file(verify_file); 30 | context.set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert | asio::ssl::verify_client_once); 31 | set_session_id_context = true; 32 | } 33 | } 34 | 35 | protected: 36 | asio::ssl::context context; 37 | 38 | void after_bind() override { 39 | if(set_session_id_context) { 40 | // Creating session_id_context from address:port but reversed due to small SSL_MAX_SSL_SESSION_ID_LENGTH 41 | auto session_id_context = std::to_string(acceptor->local_endpoint().port()) + ':'; 42 | session_id_context.append(config.address.rbegin(), config.address.rend()); 43 | SSL_CTX_set_session_id_context(context.native_handle(), reinterpret_cast(session_id_context.data()), 44 | std::min(session_id_context.size(), SSL_MAX_SSL_SESSION_ID_LENGTH)); 45 | } 46 | } 47 | 48 | void accept() override { 49 | auto connection = create_connection(*io_service, context); 50 | 51 | acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const error_code &ec) { 52 | auto lock = connection->handler_runner->continue_lock(); 53 | if(!lock) 54 | return; 55 | 56 | if(ec != asio::error::operation_aborted) 57 | this->accept(); 58 | 59 | auto session = std::make_shared(config.max_request_streambuf_size, connection); 60 | 61 | if(!ec) { 62 | asio::ip::tcp::no_delay option(true); 63 | error_code ec; 64 | session->connection->socket->lowest_layer().set_option(option, ec); 65 | 66 | session->connection->set_timeout(config.timeout_request); 67 | session->connection->socket->async_handshake(asio::ssl::stream_base::server, [this, session](const error_code &ec) { 68 | session->connection->cancel_timeout(); 69 | auto lock = session->connection->handler_runner->continue_lock(); 70 | if(!lock) 71 | return; 72 | if(!ec) 73 | this->read(session); 74 | else if(this->on_error) 75 | this->on_error(session->request, ec); 76 | }); 77 | } 78 | else if(this->on_error) 79 | this->on_error(session->request, ec); 80 | }); 81 | } 82 | }; 83 | } // namespace SimpleWeb 84 | 85 | #endif /* SERVER_HTTPS_HPP */ 86 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.0) 2 | 3 | project (Simple-Web-Server) 4 | 5 | option(USE_STANDALONE_ASIO "set ON to use standalone Asio instead of Boost.Asio" OFF) 6 | option(BUILD_TESTING "set ON to build library tests" OFF) 7 | 8 | if(NOT MSVC) 9 | add_compile_options(-std=c++11 -Wall -Wextra -Wsign-conversion) 10 | else() 11 | add_compile_options(/W1) 12 | endif() 13 | 14 | add_library(simple-web-server INTERFACE) 15 | 16 | target_include_directories(simple-web-server INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) 17 | 18 | find_package(Threads REQUIRED) 19 | target_link_libraries(simple-web-server INTERFACE ${CMAKE_THREAD_LIBS_INIT}) 20 | 21 | # TODO 2020 when Debian Jessie LTS ends: 22 | # Remove Boost system, thread, regex components; use Boost:: aliases; remove Boost target_include_directories 23 | if(USE_STANDALONE_ASIO) 24 | target_compile_definitions(simple-web-server INTERFACE USE_STANDALONE_ASIO) 25 | include(CheckIncludeFileCXX) 26 | CHECK_INCLUDE_FILE_CXX(asio.hpp HAVE_ASIO) 27 | if(NOT HAVE_ASIO) 28 | message(FATAL_ERROR "Standalone Asio not found") 29 | endif() 30 | else() 31 | find_package(Boost 1.53.0 COMPONENTS system thread REQUIRED) 32 | target_link_libraries(simple-web-server INTERFACE ${Boost_LIBRARIES}) 33 | target_include_directories(simple-web-server INTERFACE ${Boost_INCLUDE_DIR}) 34 | if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) 35 | target_compile_definitions(simple-web-server INTERFACE USE_BOOST_REGEX) 36 | find_package(Boost 1.53.0 COMPONENTS regex REQUIRED) 37 | target_link_libraries(simple-web-server INTERFACE ${Boost_LIBRARIES}) 38 | target_include_directories(simple-web-server INTERFACE ${Boost_INCLUDE_DIR}) 39 | endif() 40 | endif() 41 | if(WIN32) 42 | target_link_libraries(simple-web-server INTERFACE ws2_32 wsock32) 43 | endif() 44 | 45 | if(APPLE) 46 | set(OPENSSL_ROOT_DIR "/usr/local/opt/openssl") 47 | endif() 48 | find_package(OpenSSL) 49 | if(OPENSSL_FOUND) 50 | target_compile_definitions(simple-web-server INTERFACE HAVE_OPENSSL) 51 | target_link_libraries(simple-web-server INTERFACE ${OPENSSL_LIBRARIES}) 52 | target_include_directories(simple-web-server INTERFACE ${OPENSSL_INCLUDE_DIR}) 53 | endif() 54 | 55 | # If Simple-Web-Server is not a sub-project: 56 | if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") 57 | add_executable(http_examples http_examples.cpp) 58 | target_link_libraries(http_examples simple-web-server) 59 | find_package(Boost 1.53.0 COMPONENTS system thread filesystem REQUIRED) 60 | target_link_libraries(http_examples ${Boost_LIBRARIES}) 61 | target_include_directories(http_examples PRIVATE ${Boost_INCLUDE_DIR}) 62 | if(OPENSSL_FOUND) 63 | add_executable(https_examples https_examples.cpp) 64 | target_link_libraries(https_examples simple-web-server) 65 | target_link_libraries(https_examples ${Boost_LIBRARIES}) 66 | target_include_directories(https_examples PRIVATE ${Boost_INCLUDE_DIR}) 67 | endif() 68 | 69 | set(BUILD_TESTING ON) 70 | 71 | 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) 72 | endif() 73 | 74 | if(BUILD_TESTING) 75 | enable_testing() 76 | add_subdirectory(tests) 77 | endif() 78 | -------------------------------------------------------------------------------- /tests/crypto_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "crypto.hpp" 5 | 6 | using namespace std; 7 | using namespace SimpleWeb; 8 | 9 | const vector> base64_string_tests = { 10 | {"", ""}, 11 | {"f", "Zg=="}, 12 | {"fo", "Zm8="}, 13 | {"foo", "Zm9v"}, 14 | {"foob", "Zm9vYg=="}, 15 | {"fooba", "Zm9vYmE="}, 16 | {"foobar", "Zm9vYmFy"}, 17 | {"The itsy bitsy spider climbed up the waterspout.\r\nDown came the rain\r\nand washed the spider out.\r\nOut came the sun\r\nand dried up all the rain\r\nand the itsy bitsy spider climbed up the spout again.", 18 | "VGhlIGl0c3kgYml0c3kgc3BpZGVyIGNsaW1iZWQgdXAgdGhlIHdhdGVyc3BvdXQuDQpEb3duIGNhbWUgdGhlIHJhaW4NCmFuZCB3YXNoZWQgdGhlIHNwaWRlciBvdXQuDQpPdXQgY2FtZSB0aGUgc3VuDQphbmQgZHJpZWQgdXAgYWxsIHRoZSByYWluDQphbmQgdGhlIGl0c3kgYml0c3kgc3BpZGVyIGNsaW1iZWQgdXAgdGhlIHNwb3V0IGFnYWluLg=="}}; 19 | 20 | const vector> md5_string_tests = { 21 | {"", "d41d8cd98f00b204e9800998ecf8427e"}, 22 | {"The quick brown fox jumps over the lazy dog", "9e107d9d372bb6826bd81d3542a419d6"}}; 23 | 24 | const vector> sha1_string_tests = { 25 | {"", "da39a3ee5e6b4b0d3255bfef95601890afd80709"}, 26 | {"The quick brown fox jumps over the lazy dog", "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"}}; 27 | 28 | const vector> sha256_string_tests = { 29 | {"", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, 30 | {"The quick brown fox jumps over the lazy dog", "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"}}; 31 | 32 | const vector> sha512_string_tests = { 33 | {"", "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"}, 34 | {"The quick brown fox jumps over the lazy dog", "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6"}}; 35 | 36 | int main() { 37 | for(auto &string_test : base64_string_tests) { 38 | assert(Crypto::Base64::encode(string_test.first) == string_test.second); 39 | assert(Crypto::Base64::decode(string_test.second) == string_test.first); 40 | } 41 | 42 | for(auto &string_test : md5_string_tests) { 43 | assert(Crypto::to_hex_string(Crypto::md5(string_test.first)) == string_test.second); 44 | stringstream ss(string_test.first); 45 | assert(Crypto::to_hex_string(Crypto::md5(ss)) == string_test.second); 46 | } 47 | 48 | for(auto &string_test : sha1_string_tests) { 49 | assert(Crypto::to_hex_string(Crypto::sha1(string_test.first)) == string_test.second); 50 | stringstream ss(string_test.first); 51 | assert(Crypto::to_hex_string(Crypto::sha1(ss)) == string_test.second); 52 | } 53 | 54 | for(auto &string_test : sha256_string_tests) { 55 | assert(Crypto::to_hex_string(Crypto::sha256(string_test.first)) == string_test.second); 56 | stringstream ss(string_test.first); 57 | assert(Crypto::to_hex_string(Crypto::sha256(ss)) == string_test.second); 58 | } 59 | 60 | for(auto &string_test : sha512_string_tests) { 61 | assert(Crypto::to_hex_string(Crypto::sha512(string_test.first)) == string_test.second); 62 | stringstream ss(string_test.first); 63 | assert(Crypto::to_hex_string(Crypto::sha512(ss)) == string_test.second); 64 | } 65 | 66 | // Testing iterations 67 | assert(Crypto::to_hex_string(Crypto::sha1("Test", 1)) == "640ab2bae07bedc4c163f679a746f7ab7fb5d1fa"); 68 | assert(Crypto::to_hex_string(Crypto::sha1("Test", 2)) == "af31c6cbdecd88726d0a9b3798c71ef41f1624d5"); 69 | stringstream ss("Test"); 70 | assert(Crypto::to_hex_string(Crypto::sha1(ss, 2)) == "af31c6cbdecd88726d0a9b3798c71ef41f1624d5"); 71 | 72 | assert(Crypto::to_hex_string(Crypto::pbkdf2("Password", "Salt", 4096, 128 / 8)) == "f66df50f8aaa11e4d9721e1312ff2e66"); 73 | assert(Crypto::to_hex_string(Crypto::pbkdf2("Password", "Salt", 8192, 512 / 8)) == "a941ccbc34d1ee8ebbd1d34824a419c3dc4eac9cbc7c36ae6c7ca8725e2b618a6ad22241e787af937b0960cf85aa8ea3a258f243e05d3cc9b08af5dd93be046c"); 74 | } 75 | -------------------------------------------------------------------------------- /client_https.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CLIENT_HTTPS_HPP 2 | #define CLIENT_HTTPS_HPP 3 | 4 | #include "client_http.hpp" 5 | 6 | #ifdef USE_STANDALONE_ASIO 7 | #include 8 | #else 9 | #include 10 | #endif 11 | 12 | namespace SimpleWeb { 13 | using HTTPS = asio::ssl::stream; 14 | 15 | template <> 16 | class Client : public ClientBase { 17 | public: 18 | Client(const std::string &server_port_path, bool verify_certificate = true, const std::string &cert_file = std::string(), 19 | const std::string &private_key_file = std::string(), const std::string &verify_file = std::string()) 20 | : ClientBase::ClientBase(server_port_path, 443), context(asio::ssl::context::tlsv12) { 21 | if(cert_file.size() > 0 && private_key_file.size() > 0) { 22 | context.use_certificate_chain_file(cert_file); 23 | context.use_private_key_file(private_key_file, asio::ssl::context::pem); 24 | } 25 | 26 | if(verify_certificate) 27 | context.set_verify_callback(asio::ssl::rfc2818_verification(host)); 28 | 29 | if(verify_file.size() > 0) 30 | context.load_verify_file(verify_file); 31 | else 32 | context.set_default_verify_paths(); 33 | 34 | if(verify_file.size() > 0 || verify_certificate) 35 | context.set_verify_mode(asio::ssl::verify_peer); 36 | else 37 | context.set_verify_mode(asio::ssl::verify_none); 38 | } 39 | 40 | protected: 41 | asio::ssl::context context; 42 | 43 | std::shared_ptr create_connection() noexcept override { 44 | return std::make_shared(handler_runner, config.timeout, *io_service, context); 45 | } 46 | 47 | void connect(const std::shared_ptr &session) override { 48 | if(!session->connection->socket->lowest_layer().is_open()) { 49 | auto resolver = std::make_shared(*io_service); 50 | resolver->async_resolve(*query, [this, session, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator it) { 51 | auto lock = session->connection->handler_runner->continue_lock(); 52 | if(!lock) 53 | return; 54 | if(!ec) { 55 | session->connection->set_timeout(this->config.timeout_connect); 56 | asio::async_connect(session->connection->socket->lowest_layer(), it, [this, session, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator /*it*/) { 57 | session->connection->cancel_timeout(); 58 | auto lock = session->connection->handler_runner->continue_lock(); 59 | if(!lock) 60 | return; 61 | if(!ec) { 62 | asio::ip::tcp::no_delay option(true); 63 | error_code ec; 64 | session->connection->socket->lowest_layer().set_option(option, ec); 65 | 66 | if(!this->config.proxy_server.empty()) { 67 | auto write_buffer = std::make_shared(); 68 | std::ostream write_stream(write_buffer.get()); 69 | auto host_port = this->host + ':' + std::to_string(this->port); 70 | write_stream << "CONNECT " + host_port + " HTTP/1.1\r\n" 71 | << "Host: " << host_port << "\r\n\r\n"; 72 | session->connection->set_timeout(this->config.timeout_connect); 73 | asio::async_write(session->connection->socket->next_layer(), *write_buffer, [this, session, write_buffer](const error_code &ec, std::size_t /*bytes_transferred*/) { 74 | session->connection->cancel_timeout(); 75 | auto lock = session->connection->handler_runner->continue_lock(); 76 | if(!lock) 77 | return; 78 | if(!ec) { 79 | std::shared_ptr response(new Response(this->config.max_response_streambuf_size)); 80 | session->connection->set_timeout(this->config.timeout_connect); 81 | asio::async_read_until(session->connection->socket->next_layer(), response->streambuf, "\r\n\r\n", [this, session, response](const error_code &ec, std::size_t /*bytes_transferred*/) { 82 | session->connection->cancel_timeout(); 83 | auto lock = session->connection->handler_runner->continue_lock(); 84 | if(!lock) 85 | return; 86 | if((!ec || ec == asio::error::not_found) && response->streambuf.size() == response->streambuf.max_size()) { 87 | session->callback(session->connection, make_error_code::make_error_code(errc::message_size)); 88 | return; 89 | } 90 | if(!ec) { 91 | if(!ResponseMessage::parse(response->content, response->http_version, response->status_code, response->header)) 92 | session->callback(session->connection, make_error_code::make_error_code(errc::protocol_error)); 93 | else { 94 | if(response->status_code.empty() || response->status_code.compare(0, 3, "200") != 0) 95 | session->callback(session->connection, make_error_code::make_error_code(errc::permission_denied)); 96 | else 97 | this->handshake(session); 98 | } 99 | } 100 | else 101 | session->callback(session->connection, ec); 102 | }); 103 | } 104 | else 105 | session->callback(session->connection, ec); 106 | }); 107 | } 108 | else 109 | this->handshake(session); 110 | } 111 | else 112 | session->callback(session->connection, ec); 113 | }); 114 | } 115 | else 116 | session->callback(session->connection, ec); 117 | }); 118 | } 119 | else 120 | write(session); 121 | } 122 | 123 | void handshake(const std::shared_ptr &session) { 124 | SSL_set_tlsext_host_name(session->connection->socket->native_handle(), this->host.c_str()); 125 | 126 | session->connection->set_timeout(this->config.timeout_connect); 127 | session->connection->socket->async_handshake(asio::ssl::stream_base::client, [this, session](const error_code &ec) { 128 | session->connection->cancel_timeout(); 129 | auto lock = session->connection->handler_runner->continue_lock(); 130 | if(!lock) 131 | return; 132 | if(!ec) 133 | this->write(session); 134 | else 135 | session->callback(session->connection, ec); 136 | }); 137 | } 138 | }; 139 | } // namespace SimpleWeb 140 | 141 | #endif /* CLIENT_HTTPS_HPP */ 142 | -------------------------------------------------------------------------------- /status_code.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIMPLE_WEB_STATUS_CODE_HPP 2 | #define SIMPLE_WEB_STATUS_CODE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace SimpleWeb { 10 | enum class StatusCode { 11 | unknown = 0, 12 | information_continue = 100, 13 | information_switching_protocols, 14 | information_processing, 15 | success_ok = 200, 16 | success_created, 17 | success_accepted, 18 | success_non_authoritative_information, 19 | success_no_content, 20 | success_reset_content, 21 | success_partial_content, 22 | success_multi_status, 23 | success_already_reported, 24 | success_im_used = 226, 25 | redirection_multiple_choices = 300, 26 | redirection_moved_permanently, 27 | redirection_found, 28 | redirection_see_other, 29 | redirection_not_modified, 30 | redirection_use_proxy, 31 | redirection_switch_proxy, 32 | redirection_temporary_redirect, 33 | redirection_permanent_redirect, 34 | client_error_bad_request = 400, 35 | client_error_unauthorized, 36 | client_error_payment_required, 37 | client_error_forbidden, 38 | client_error_not_found, 39 | client_error_method_not_allowed, 40 | client_error_not_acceptable, 41 | client_error_proxy_authentication_required, 42 | client_error_request_timeout, 43 | client_error_conflict, 44 | client_error_gone, 45 | client_error_length_required, 46 | client_error_precondition_failed, 47 | client_error_payload_too_large, 48 | client_error_uri_too_long, 49 | client_error_unsupported_media_type, 50 | client_error_range_not_satisfiable, 51 | client_error_expectation_failed, 52 | client_error_im_a_teapot, 53 | client_error_misdirection_required = 421, 54 | client_error_unprocessable_entity, 55 | client_error_locked, 56 | client_error_failed_dependency, 57 | client_error_upgrade_required = 426, 58 | client_error_precondition_required = 428, 59 | client_error_too_many_requests, 60 | client_error_request_header_fields_too_large = 431, 61 | client_error_unavailable_for_legal_reasons = 451, 62 | server_error_internal_server_error = 500, 63 | server_error_not_implemented, 64 | server_error_bad_gateway, 65 | server_error_service_unavailable, 66 | server_error_gateway_timeout, 67 | server_error_http_version_not_supported, 68 | server_error_variant_also_negotiates, 69 | server_error_insufficient_storage, 70 | server_error_loop_detected, 71 | server_error_not_extended = 510, 72 | server_error_network_authentication_required 73 | }; 74 | 75 | inline const std::map &status_code_strings() { 76 | static const std::map status_code_strings = { 77 | {StatusCode::unknown, ""}, 78 | {StatusCode::information_continue, "100 Continue"}, 79 | {StatusCode::information_switching_protocols, "101 Switching Protocols"}, 80 | {StatusCode::information_processing, "102 Processing"}, 81 | {StatusCode::success_ok, "200 OK"}, 82 | {StatusCode::success_created, "201 Created"}, 83 | {StatusCode::success_accepted, "202 Accepted"}, 84 | {StatusCode::success_non_authoritative_information, "203 Non-Authoritative Information"}, 85 | {StatusCode::success_no_content, "204 No Content"}, 86 | {StatusCode::success_reset_content, "205 Reset Content"}, 87 | {StatusCode::success_partial_content, "206 Partial Content"}, 88 | {StatusCode::success_multi_status, "207 Multi-Status"}, 89 | {StatusCode::success_already_reported, "208 Already Reported"}, 90 | {StatusCode::success_im_used, "226 IM Used"}, 91 | {StatusCode::redirection_multiple_choices, "300 Multiple Choices"}, 92 | {StatusCode::redirection_moved_permanently, "301 Moved Permanently"}, 93 | {StatusCode::redirection_found, "302 Found"}, 94 | {StatusCode::redirection_see_other, "303 See Other"}, 95 | {StatusCode::redirection_not_modified, "304 Not Modified"}, 96 | {StatusCode::redirection_use_proxy, "305 Use Proxy"}, 97 | {StatusCode::redirection_switch_proxy, "306 Switch Proxy"}, 98 | {StatusCode::redirection_temporary_redirect, "307 Temporary Redirect"}, 99 | {StatusCode::redirection_permanent_redirect, "308 Permanent Redirect"}, 100 | {StatusCode::client_error_bad_request, "400 Bad Request"}, 101 | {StatusCode::client_error_unauthorized, "401 Unauthorized"}, 102 | {StatusCode::client_error_payment_required, "402 Payment Required"}, 103 | {StatusCode::client_error_forbidden, "403 Forbidden"}, 104 | {StatusCode::client_error_not_found, "404 Not Found"}, 105 | {StatusCode::client_error_method_not_allowed, "405 Method Not Allowed"}, 106 | {StatusCode::client_error_not_acceptable, "406 Not Acceptable"}, 107 | {StatusCode::client_error_proxy_authentication_required, "407 Proxy Authentication Required"}, 108 | {StatusCode::client_error_request_timeout, "408 Request Timeout"}, 109 | {StatusCode::client_error_conflict, "409 Conflict"}, 110 | {StatusCode::client_error_gone, "410 Gone"}, 111 | {StatusCode::client_error_length_required, "411 Length Required"}, 112 | {StatusCode::client_error_precondition_failed, "412 Precondition Failed"}, 113 | {StatusCode::client_error_payload_too_large, "413 Payload Too Large"}, 114 | {StatusCode::client_error_uri_too_long, "414 URI Too Long"}, 115 | {StatusCode::client_error_unsupported_media_type, "415 Unsupported Media Type"}, 116 | {StatusCode::client_error_range_not_satisfiable, "416 Range Not Satisfiable"}, 117 | {StatusCode::client_error_expectation_failed, "417 Expectation Failed"}, 118 | {StatusCode::client_error_im_a_teapot, "418 I'm a teapot"}, 119 | {StatusCode::client_error_misdirection_required, "421 Misdirected Request"}, 120 | {StatusCode::client_error_unprocessable_entity, "422 Unprocessable Entity"}, 121 | {StatusCode::client_error_locked, "423 Locked"}, 122 | {StatusCode::client_error_failed_dependency, "424 Failed Dependency"}, 123 | {StatusCode::client_error_upgrade_required, "426 Upgrade Required"}, 124 | {StatusCode::client_error_precondition_required, "428 Precondition Required"}, 125 | {StatusCode::client_error_too_many_requests, "429 Too Many Requests"}, 126 | {StatusCode::client_error_request_header_fields_too_large, "431 Request Header Fields Too Large"}, 127 | {StatusCode::client_error_unavailable_for_legal_reasons, "451 Unavailable For Legal Reasons"}, 128 | {StatusCode::server_error_internal_server_error, "500 Internal Server Error"}, 129 | {StatusCode::server_error_not_implemented, "501 Not Implemented"}, 130 | {StatusCode::server_error_bad_gateway, "502 Bad Gateway"}, 131 | {StatusCode::server_error_service_unavailable, "503 Service Unavailable"}, 132 | {StatusCode::server_error_gateway_timeout, "504 Gateway Timeout"}, 133 | {StatusCode::server_error_http_version_not_supported, "505 HTTP Version Not Supported"}, 134 | {StatusCode::server_error_variant_also_negotiates, "506 Variant Also Negotiates"}, 135 | {StatusCode::server_error_insufficient_storage, "507 Insufficient Storage"}, 136 | {StatusCode::server_error_loop_detected, "508 Loop Detected"}, 137 | {StatusCode::server_error_not_extended, "510 Not Extended"}, 138 | {StatusCode::server_error_network_authentication_required, "511 Network Authentication Required"}}; 139 | return status_code_strings; 140 | } 141 | 142 | inline StatusCode status_code(const std::string &status_code_string) noexcept { 143 | class StringToStatusCode : public std::unordered_map { 144 | public: 145 | StringToStatusCode() { 146 | for(auto &status_code : status_code_strings()) 147 | emplace(status_code.second, status_code.first); 148 | } 149 | }; 150 | static StringToStatusCode string_to_status_code; 151 | auto pos = string_to_status_code.find(status_code_string); 152 | if(pos == string_to_status_code.end()) 153 | return StatusCode::unknown; 154 | return pos->second; 155 | } 156 | 157 | inline const std::string &status_code(StatusCode status_code_enum) noexcept { 158 | auto pos = status_code_strings().find(status_code_enum); 159 | if(pos == status_code_strings().end()) { 160 | static std::string empty_string; 161 | return empty_string; 162 | } 163 | return pos->second; 164 | } 165 | } // namespace SimpleWeb 166 | 167 | #endif // SIMPLE_WEB_STATUS_CODE_HPP 168 | -------------------------------------------------------------------------------- /crypto.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIMPLE_WEB_CRYPTO_HPP 2 | #define SIMPLE_WEB_CRYPTO_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace SimpleWeb { 17 | // TODO 2017: remove workaround for MSVS 2012 18 | #if _MSC_VER == 1700 // MSVS 2012 has no definition for round() 19 | inline double round(double x) noexcept { // Custom definition of round() for positive numbers 20 | return floor(x + 0.5); 21 | } 22 | #endif 23 | 24 | class Crypto { 25 | const static std::size_t buffer_size = 131072; 26 | 27 | public: 28 | class Base64 { 29 | public: 30 | static std::string encode(const std::string &ascii) noexcept { 31 | std::string base64; 32 | 33 | BIO *bio, *b64; 34 | BUF_MEM *bptr = BUF_MEM_new(); 35 | 36 | b64 = BIO_new(BIO_f_base64()); 37 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 38 | bio = BIO_new(BIO_s_mem()); 39 | BIO_push(b64, bio); 40 | BIO_set_mem_buf(b64, bptr, BIO_CLOSE); 41 | 42 | // Write directly to base64-buffer to avoid copy 43 | auto base64_length = static_cast(round(4 * ceil(static_cast(ascii.size()) / 3.0))); 44 | base64.resize(base64_length); 45 | bptr->length = 0; 46 | bptr->max = base64_length + 1; 47 | bptr->data = &base64[0]; 48 | 49 | if(BIO_write(b64, &ascii[0], static_cast(ascii.size())) <= 0 || BIO_flush(b64) <= 0) 50 | base64.clear(); 51 | 52 | // To keep &base64[0] through BIO_free_all(b64) 53 | bptr->length = 0; 54 | bptr->max = 0; 55 | bptr->data = nullptr; 56 | 57 | BIO_free_all(b64); 58 | 59 | return base64; 60 | } 61 | 62 | static std::string decode(const std::string &base64) noexcept { 63 | std::string ascii; 64 | 65 | // Resize ascii, however, the size is a up to two bytes too large. 66 | ascii.resize((6 * base64.size()) / 8); 67 | BIO *b64, *bio; 68 | 69 | b64 = BIO_new(BIO_f_base64()); 70 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 71 | // TODO: Remove in 2020 72 | #if OPENSSL_VERSION_NUMBER <= 0x1000115fL 73 | bio = BIO_new_mem_buf((char *)&base64[0], static_cast(base64.size())); 74 | #else 75 | bio = BIO_new_mem_buf(&base64[0], static_cast(base64.size())); 76 | #endif 77 | bio = BIO_push(b64, bio); 78 | 79 | auto decoded_length = BIO_read(bio, &ascii[0], static_cast(ascii.size())); 80 | if(decoded_length > 0) 81 | ascii.resize(static_cast(decoded_length)); 82 | else 83 | ascii.clear(); 84 | 85 | BIO_free_all(b64); 86 | 87 | return ascii; 88 | } 89 | }; 90 | 91 | /// Return hex string from bytes in input string. 92 | static std::string to_hex_string(const std::string &input) noexcept { 93 | std::stringstream hex_stream; 94 | hex_stream << std::hex << std::internal << std::setfill('0'); 95 | for(auto &byte : input) 96 | hex_stream << std::setw(2) << static_cast(static_cast(byte)); 97 | return hex_stream.str(); 98 | } 99 | 100 | static std::string md5(const std::string &input, std::size_t iterations = 1) noexcept { 101 | std::string hash; 102 | 103 | hash.resize(128 / 8); 104 | MD5(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); 105 | 106 | for(std::size_t c = 1; c < iterations; ++c) 107 | MD5(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 108 | 109 | return hash; 110 | } 111 | 112 | static std::string md5(std::istream &stream, std::size_t iterations = 1) noexcept { 113 | MD5_CTX context; 114 | MD5_Init(&context); 115 | std::streamsize read_length; 116 | std::vector buffer(buffer_size); 117 | while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) 118 | MD5_Update(&context, buffer.data(), static_cast(read_length)); 119 | std::string hash; 120 | hash.resize(128 / 8); 121 | MD5_Final(reinterpret_cast(&hash[0]), &context); 122 | 123 | for(std::size_t c = 1; c < iterations; ++c) 124 | MD5(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 125 | 126 | return hash; 127 | } 128 | 129 | static std::string sha1(const std::string &input, std::size_t iterations = 1) noexcept { 130 | std::string hash; 131 | 132 | hash.resize(160 / 8); 133 | SHA1(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); 134 | 135 | for(std::size_t c = 1; c < iterations; ++c) 136 | SHA1(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 137 | 138 | return hash; 139 | } 140 | 141 | static std::string sha1(std::istream &stream, std::size_t iterations = 1) noexcept { 142 | SHA_CTX context; 143 | SHA1_Init(&context); 144 | std::streamsize read_length; 145 | std::vector buffer(buffer_size); 146 | while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) 147 | SHA1_Update(&context, buffer.data(), static_cast(read_length)); 148 | std::string hash; 149 | hash.resize(160 / 8); 150 | SHA1_Final(reinterpret_cast(&hash[0]), &context); 151 | 152 | for(std::size_t c = 1; c < iterations; ++c) 153 | SHA1(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 154 | 155 | return hash; 156 | } 157 | 158 | static std::string sha256(const std::string &input, std::size_t iterations = 1) noexcept { 159 | std::string hash; 160 | 161 | hash.resize(256 / 8); 162 | SHA256(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); 163 | 164 | for(std::size_t c = 1; c < iterations; ++c) 165 | SHA256(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 166 | 167 | return hash; 168 | } 169 | 170 | static std::string sha256(std::istream &stream, std::size_t iterations = 1) noexcept { 171 | SHA256_CTX context; 172 | SHA256_Init(&context); 173 | std::streamsize read_length; 174 | std::vector buffer(buffer_size); 175 | while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) 176 | SHA256_Update(&context, buffer.data(), static_cast(read_length)); 177 | std::string hash; 178 | hash.resize(256 / 8); 179 | SHA256_Final(reinterpret_cast(&hash[0]), &context); 180 | 181 | for(std::size_t c = 1; c < iterations; ++c) 182 | SHA256(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 183 | 184 | return hash; 185 | } 186 | 187 | static std::string sha512(const std::string &input, std::size_t iterations = 1) noexcept { 188 | std::string hash; 189 | 190 | hash.resize(512 / 8); 191 | SHA512(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); 192 | 193 | for(std::size_t c = 1; c < iterations; ++c) 194 | SHA512(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 195 | 196 | return hash; 197 | } 198 | 199 | static std::string sha512(std::istream &stream, std::size_t iterations = 1) noexcept { 200 | SHA512_CTX context; 201 | SHA512_Init(&context); 202 | std::streamsize read_length; 203 | std::vector buffer(buffer_size); 204 | while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) 205 | SHA512_Update(&context, buffer.data(), static_cast(read_length)); 206 | std::string hash; 207 | hash.resize(512 / 8); 208 | SHA512_Final(reinterpret_cast(&hash[0]), &context); 209 | 210 | for(std::size_t c = 1; c < iterations; ++c) 211 | SHA512(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 212 | 213 | return hash; 214 | } 215 | 216 | /// key_size is number of bytes of the returned key. 217 | static std::string pbkdf2(const std::string &password, const std::string &salt, int iterations, int key_size) noexcept { 218 | std::string key; 219 | key.resize(static_cast(key_size)); 220 | PKCS5_PBKDF2_HMAC_SHA1(password.c_str(), password.size(), 221 | reinterpret_cast(salt.c_str()), salt.size(), iterations, 222 | key_size, reinterpret_cast(&key[0])); 223 | return key; 224 | } 225 | }; 226 | } 227 | #endif /* SIMPLE_WEB_CRYPTO_HPP */ 228 | -------------------------------------------------------------------------------- /http_examples.cpp: -------------------------------------------------------------------------------- 1 | #include "client_http.hpp" 2 | #include "server_http.hpp" 3 | 4 | // Added for the json-example 5 | #define BOOST_SPIRIT_THREADSAFE 6 | #include 7 | #include 8 | 9 | // Added for the default_resource example 10 | #include 11 | #include 12 | #include 13 | #include 14 | #ifdef HAVE_OPENSSL 15 | #include "crypto.hpp" 16 | #endif 17 | 18 | using namespace std; 19 | // Added for the json-example: 20 | using namespace boost::property_tree; 21 | 22 | using HttpServer = SimpleWeb::Server; 23 | using HttpClient = SimpleWeb::Client; 24 | 25 | int main() { 26 | // HTTP-server at port 8080 using 1 thread 27 | // Unless you do more heavy non-threaded processing in the resources, 28 | // 1 thread is usually faster than several threads 29 | HttpServer server; 30 | server.config.port = 8080; 31 | 32 | // Add resources using path-regex and method-string, and an anonymous function 33 | // POST-example for the path /string, responds the posted string 34 | server.resource["^/string$"]["POST"] = [](shared_ptr response, shared_ptr request) { 35 | // Retrieve string: 36 | auto content = request->content.string(); 37 | // request->content.string() is a convenience function for: 38 | // stringstream ss; 39 | // ss << request->content.rdbuf(); 40 | // auto content=ss.str(); 41 | 42 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" 43 | << content; 44 | 45 | 46 | // Alternatively, use one of the convenience functions, for instance: 47 | // response->write(content); 48 | }; 49 | 50 | // POST-example for the path /json, responds firstName+" "+lastName from the posted json 51 | // Responds with an appropriate error message if the posted json is not valid, or if firstName or lastName is missing 52 | // Example posted json: 53 | // { 54 | // "firstName": "John", 55 | // "lastName": "Smith", 56 | // "age": 25 57 | // } 58 | server.resource["^/json$"]["POST"] = [](shared_ptr response, shared_ptr request) { 59 | try { 60 | ptree pt; 61 | read_json(request->content, pt); 62 | 63 | auto name = pt.get("firstName") + " " + pt.get("lastName"); 64 | 65 | *response << "HTTP/1.1 200 OK\r\n" 66 | << "Content-Length: " << name.length() << "\r\n\r\n" 67 | << name; 68 | } 69 | catch(const exception &e) { 70 | *response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" 71 | << e.what(); 72 | } 73 | 74 | 75 | // Alternatively, using a convenience function: 76 | // try { 77 | // ptree pt; 78 | // read_json(request->content, pt); 79 | 80 | // auto name=pt.get("firstName")+" "+pt.get("lastName"); 81 | // response->write(name); 82 | // } 83 | // catch(const exception &e) { 84 | // response->write(SimpleWeb::StatusCode::client_error_bad_request, e.what()); 85 | // } 86 | }; 87 | 88 | // GET-example for the path /info 89 | // Responds with request-information 90 | server.resource["^/info$"]["GET"] = [](shared_ptr response, shared_ptr request) { 91 | stringstream stream; 92 | stream << "

Request from " << request->remote_endpoint_address() << ":" << request->remote_endpoint_port() << "

"; 93 | 94 | stream << request->method << " " << request->path << " HTTP/" << request->http_version; 95 | 96 | stream << "

Query Fields

"; 97 | auto query_fields = request->parse_query_string(); 98 | for(auto &field : query_fields) 99 | stream << field.first << ": " << field.second << "
"; 100 | 101 | stream << "

Header Fields

"; 102 | for(auto &field : request->header) 103 | stream << field.first << ": " << field.second << "
"; 104 | 105 | response->write(stream); 106 | }; 107 | 108 | // GET-example for the path /match/[number], responds with the matched string in path (number) 109 | // For instance a request GET /match/123 will receive: 123 110 | server.resource["^/match/([0-9]+)$"]["GET"] = [](shared_ptr response, shared_ptr request) { 111 | response->write(request->path_match[1]); 112 | }; 113 | 114 | // GET-example simulating heavy work in a separate thread 115 | server.resource["^/work$"]["GET"] = [](shared_ptr response, shared_ptr /*request*/) { 116 | thread work_thread([response] { 117 | this_thread::sleep_for(chrono::seconds(5)); 118 | response->write("Work done"); 119 | }); 120 | work_thread.detach(); 121 | }; 122 | 123 | // Default GET-example. If no other matches, this anonymous function will be called. 124 | // Will respond with content in the web/-directory, and its subdirectories. 125 | // Default file: index.html 126 | // Can for instance be used to retrieve an HTML 5 client that uses REST-resources on this server 127 | server.default_resource["GET"] = [](shared_ptr response, shared_ptr request) { 128 | try { 129 | auto web_root_path = boost::filesystem::canonical("web"); 130 | auto path = boost::filesystem::canonical(web_root_path / request->path); 131 | // Check if path is within web_root_path 132 | if(distance(web_root_path.begin(), web_root_path.end()) > distance(path.begin(), path.end()) || 133 | !equal(web_root_path.begin(), web_root_path.end(), path.begin())) 134 | throw invalid_argument("path must be within root path"); 135 | if(boost::filesystem::is_directory(path)) 136 | path /= "index.html"; 137 | 138 | SimpleWeb::CaseInsensitiveMultimap header; 139 | 140 | // Uncomment the following line to enable Cache-Control 141 | // header.emplace("Cache-Control", "max-age=86400"); 142 | 143 | #ifdef HAVE_OPENSSL 144 | // Uncomment the following lines to enable ETag 145 | // { 146 | // ifstream ifs(path.string(), ifstream::in | ios::binary); 147 | // if(ifs) { 148 | // auto hash = SimpleWeb::Crypto::to_hex_string(SimpleWeb::Crypto::md5(ifs)); 149 | // header.emplace("ETag", "\"" + hash + "\""); 150 | // auto it = request->header.find("If-None-Match"); 151 | // if(it != request->header.end()) { 152 | // if(!it->second.empty() && it->second.compare(1, hash.size(), hash) == 0) { 153 | // response->write(SimpleWeb::StatusCode::redirection_not_modified, header); 154 | // return; 155 | // } 156 | // } 157 | // } 158 | // else 159 | // throw invalid_argument("could not read file"); 160 | // } 161 | #endif 162 | 163 | auto ifs = make_shared(); 164 | ifs->open(path.string(), ifstream::in | ios::binary | ios::ate); 165 | 166 | if(*ifs) { 167 | auto length = ifs->tellg(); 168 | ifs->seekg(0, ios::beg); 169 | 170 | header.emplace("Content-Length", to_string(length)); 171 | response->write(header); 172 | 173 | // Trick to define a recursive function within this scope (for example purposes) 174 | class FileServer { 175 | public: 176 | static void read_and_send(const shared_ptr &response, const shared_ptr &ifs) { 177 | // Read and send 128 KB at a time 178 | static vector buffer(131072); // Safe when server is running on one thread 179 | streamsize read_length; 180 | if((read_length = ifs->read(&buffer[0], static_cast(buffer.size())).gcount()) > 0) { 181 | response->write(&buffer[0], read_length); 182 | if(read_length == static_cast(buffer.size())) { 183 | response->send([response, ifs](const SimpleWeb::error_code &ec) { 184 | if(!ec) 185 | read_and_send(response, ifs); 186 | else 187 | cerr << "Connection interrupted" << endl; 188 | }); 189 | } 190 | } 191 | } 192 | }; 193 | FileServer::read_and_send(response, ifs); 194 | } 195 | else 196 | throw invalid_argument("could not read file"); 197 | } 198 | catch(const exception &e) { 199 | response->write(SimpleWeb::StatusCode::client_error_bad_request, "Could not open path " + request->path + ": " + e.what()); 200 | } 201 | }; 202 | 203 | server.on_error = [](shared_ptr /*request*/, const SimpleWeb::error_code & /*ec*/) { 204 | // Handle errors here 205 | // Note that connection timeouts will also call this handle with ec set to SimpleWeb::errc::operation_canceled 206 | }; 207 | 208 | thread server_thread([&server]() { 209 | // Start server 210 | server.start(); 211 | }); 212 | 213 | // Wait for server to start so that the client can connect 214 | this_thread::sleep_for(chrono::seconds(1)); 215 | 216 | // Client examples 217 | HttpClient client("localhost:8080"); 218 | 219 | string json_string = "{\"firstName\": \"John\",\"lastName\": \"Smith\",\"age\": 25}"; 220 | 221 | // Synchronous request examples 222 | try { 223 | auto r1 = client.request("GET", "/match/123"); 224 | cout << r1->content.rdbuf() << endl; // Alternatively, use the convenience function r1->content.string() 225 | 226 | auto r2 = client.request("POST", "/string", json_string); 227 | cout << r2->content.rdbuf() << endl; 228 | } 229 | catch(const SimpleWeb::system_error &e) { 230 | cerr << "Client request error: " << e.what() << endl; 231 | } 232 | 233 | // Asynchronous request example 234 | client.request("POST", "/json", json_string, [](shared_ptr response, const SimpleWeb::error_code &ec) { 235 | if(!ec) 236 | cout << response->content.rdbuf() << endl; 237 | }); 238 | client.io_service->run(); 239 | 240 | server_thread.join(); 241 | } 242 | -------------------------------------------------------------------------------- /https_examples.cpp: -------------------------------------------------------------------------------- 1 | #include "client_https.hpp" 2 | #include "server_https.hpp" 3 | 4 | // Added for the json-example 5 | #define BOOST_SPIRIT_THREADSAFE 6 | #include 7 | #include 8 | 9 | // Added for the default_resource example 10 | #include "crypto.hpp" 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace std; 17 | // Added for the json-example: 18 | using namespace boost::property_tree; 19 | 20 | using HttpsServer = SimpleWeb::Server; 21 | using HttpsClient = SimpleWeb::Client; 22 | 23 | int main() { 24 | // HTTPS-server at port 8080 using 1 thread 25 | // Unless you do more heavy non-threaded processing in the resources, 26 | // 1 thread is usually faster than several threads 27 | HttpsServer server("server.crt", "server.key"); 28 | server.config.port = 8080; 29 | 30 | // Add resources using path-regex and method-string, and an anonymous function 31 | // POST-example for the path /string, responds the posted string 32 | server.resource["^/string$"]["POST"] = [](shared_ptr response, shared_ptr request) { 33 | // Retrieve string: 34 | auto content = request->content.string(); 35 | // request->content.string() is a convenience function for: 36 | // stringstream ss; 37 | // ss << request->content.rdbuf(); 38 | // auto content=ss.str(); 39 | 40 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" 41 | << content; 42 | 43 | 44 | // Alternatively, use one of the convenience functions, for instance: 45 | // response->write(content); 46 | }; 47 | 48 | // POST-example for the path /json, responds firstName+" "+lastName from the posted json 49 | // Responds with an appropriate error message if the posted json is not valid, or if firstName or lastName is missing 50 | // Example posted json: 51 | // { 52 | // "firstName": "John", 53 | // "lastName": "Smith", 54 | // "age": 25 55 | // } 56 | server.resource["^/json$"]["POST"] = [](shared_ptr response, shared_ptr request) { 57 | try { 58 | ptree pt; 59 | read_json(request->content, pt); 60 | 61 | auto name = pt.get("firstName") + " " + pt.get("lastName"); 62 | 63 | *response << "HTTP/1.1 200 OK\r\n" 64 | << "Content-Length: " << name.length() << "\r\n\r\n" 65 | << name; 66 | } 67 | catch(const exception &e) { 68 | *response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" 69 | << e.what(); 70 | } 71 | 72 | 73 | // Alternatively, using a convenience function: 74 | // try { 75 | // ptree pt; 76 | // read_json(request->content, pt); 77 | 78 | // auto name=pt.get("firstName")+" "+pt.get("lastName"); 79 | // response->write(name); 80 | // } 81 | // catch(const exception &e) { 82 | // response->write(SimpleWeb::StatusCode::client_error_bad_request, e.what()); 83 | // } 84 | }; 85 | 86 | // GET-example for the path /info 87 | // Responds with request-information 88 | server.resource["^/info$"]["GET"] = [](shared_ptr response, shared_ptr request) { 89 | stringstream stream; 90 | stream << "

Request from " << request->remote_endpoint_address() << ":" << request->remote_endpoint_port() << "

"; 91 | 92 | stream << request->method << " " << request->path << " HTTP/" << request->http_version; 93 | 94 | stream << "

Query Fields

"; 95 | auto query_fields = request->parse_query_string(); 96 | for(auto &field : query_fields) 97 | stream << field.first << ": " << field.second << "
"; 98 | 99 | stream << "

Header Fields

"; 100 | for(auto &field : request->header) 101 | stream << field.first << ": " << field.second << "
"; 102 | 103 | response->write(stream); 104 | }; 105 | 106 | // GET-example for the path /match/[number], responds with the matched string in path (number) 107 | // For instance a request GET /match/123 will receive: 123 108 | server.resource["^/match/([0-9]+)$"]["GET"] = [](shared_ptr response, shared_ptr request) { 109 | response->write(request->path_match[1]); 110 | }; 111 | 112 | // GET-example simulating heavy work in a separate thread 113 | server.resource["^/work$"]["GET"] = [](shared_ptr response, shared_ptr /*request*/) { 114 | thread work_thread([response] { 115 | this_thread::sleep_for(chrono::seconds(5)); 116 | response->write("Work done"); 117 | }); 118 | work_thread.detach(); 119 | }; 120 | 121 | // Default GET-example. If no other matches, this anonymous function will be called. 122 | // Will respond with content in the web/-directory, and its subdirectories. 123 | // Default file: index.html 124 | // Can for instance be used to retrieve an HTML 5 client that uses REST-resources on this server 125 | server.default_resource["GET"] = [](shared_ptr response, shared_ptr request) { 126 | try { 127 | auto web_root_path = boost::filesystem::canonical("web"); 128 | auto path = boost::filesystem::canonical(web_root_path / request->path); 129 | // Check if path is within web_root_path 130 | if(distance(web_root_path.begin(), web_root_path.end()) > distance(path.begin(), path.end()) || 131 | !equal(web_root_path.begin(), web_root_path.end(), path.begin())) 132 | throw invalid_argument("path must be within root path"); 133 | if(boost::filesystem::is_directory(path)) 134 | path /= "index.html"; 135 | 136 | SimpleWeb::CaseInsensitiveMultimap header; 137 | 138 | // Uncomment the following line to enable Cache-Control 139 | // header.emplace("Cache-Control", "max-age=86400"); 140 | 141 | #ifdef HAVE_OPENSSL 142 | // Uncomment the following lines to enable ETag 143 | // { 144 | // ifstream ifs(path.string(), ifstream::in | ios::binary); 145 | // if(ifs) { 146 | // auto hash = SimpleWeb::Crypto::to_hex_string(SimpleWeb::Crypto::md5(ifs)); 147 | // header.emplace("ETag", "\"" + hash + "\""); 148 | // auto it = request->header.find("If-None-Match"); 149 | // if(it != request->header.end()) { 150 | // if(!it->second.empty() && it->second.compare(1, hash.size(), hash) == 0) { 151 | // response->write(SimpleWeb::StatusCode::redirection_not_modified, header); 152 | // return; 153 | // } 154 | // } 155 | // } 156 | // else 157 | // throw invalid_argument("could not read file"); 158 | // } 159 | #endif 160 | 161 | auto ifs = make_shared(); 162 | ifs->open(path.string(), ifstream::in | ios::binary | ios::ate); 163 | 164 | if(*ifs) { 165 | auto length = ifs->tellg(); 166 | ifs->seekg(0, ios::beg); 167 | 168 | header.emplace("Content-Length", to_string(length)); 169 | response->write(header); 170 | 171 | // Trick to define a recursive function within this scope (for example purposes) 172 | class FileServer { 173 | public: 174 | static void read_and_send(const shared_ptr &response, const shared_ptr &ifs) { 175 | // Read and send 128 KB at a time 176 | static vector buffer(131072); // Safe when server is running on one thread 177 | streamsize read_length; 178 | if((read_length = ifs->read(&buffer[0], static_cast(buffer.size())).gcount()) > 0) { 179 | response->write(&buffer[0], read_length); 180 | if(read_length == static_cast(buffer.size())) { 181 | response->send([response, ifs](const SimpleWeb::error_code &ec) { 182 | if(!ec) 183 | read_and_send(response, ifs); 184 | else 185 | cerr << "Connection interrupted" << endl; 186 | }); 187 | } 188 | } 189 | } 190 | }; 191 | FileServer::read_and_send(response, ifs); 192 | } 193 | else 194 | throw invalid_argument("could not read file"); 195 | } 196 | catch(const exception &e) { 197 | response->write(SimpleWeb::StatusCode::client_error_bad_request, "Could not open path " + request->path + ": " + e.what()); 198 | } 199 | }; 200 | 201 | server.on_error = [](shared_ptr /*request*/, const SimpleWeb::error_code & /*ec*/) { 202 | // Handle errors here 203 | // Note that connection timeouts will also call this handle with ec set to SimpleWeb::errc::operation_canceled 204 | }; 205 | 206 | thread server_thread([&server]() { 207 | // Start server 208 | server.start(); 209 | }); 210 | 211 | // Wait for server to start so that the client can connect 212 | this_thread::sleep_for(chrono::seconds(1)); 213 | 214 | // Client examples 215 | // Second create() parameter set to false: no certificate verification 216 | HttpsClient client("localhost:8080", false); 217 | 218 | string json_string = "{\"firstName\": \"John\",\"lastName\": \"Smith\",\"age\": 25}"; 219 | 220 | // Synchronous request examples 221 | try { 222 | auto r1 = client.request("GET", "/match/123"); 223 | cout << r1->content.rdbuf() << endl; // Alternatively, use the convenience function r1->content.string() 224 | 225 | auto r2 = client.request("POST", "/string", json_string); 226 | cout << r2->content.rdbuf() << endl; 227 | } 228 | catch(const SimpleWeb::system_error &e) { 229 | cerr << "Client request error: " << e.what() << endl; 230 | } 231 | 232 | // Asynchronous request example 233 | client.request("POST", "/json", json_string, [](shared_ptr response, const SimpleWeb::error_code &ec) { 234 | if(!ec) 235 | cout << response->content.rdbuf() << endl; 236 | }); 237 | client.io_service->run(); 238 | 239 | server_thread.join(); 240 | } 241 | -------------------------------------------------------------------------------- /tests/parse_test.cpp: -------------------------------------------------------------------------------- 1 | #include "client_http.hpp" 2 | #include "server_http.hpp" 3 | #include 4 | #include 5 | 6 | using namespace std; 7 | using namespace SimpleWeb; 8 | 9 | class ServerTest : public ServerBase { 10 | public: 11 | ServerTest() : ServerBase::ServerBase(8080) {} 12 | 13 | void accept() noexcept override {} 14 | 15 | void parse_request_test() { 16 | auto session = std::make_shared(static_cast(-1), create_connection(*io_service)); 17 | 18 | std::ostream stream(&session->request->content.streambuf); 19 | stream << "GET /test/ HTTP/1.1\r\n"; 20 | stream << "TestHeader: test\r\n"; 21 | stream << "TestHeader2:test2\r\n"; 22 | stream << "TestHeader3:test3a\r\n"; 23 | stream << "TestHeader3:test3b\r\n"; 24 | stream << "\r\n"; 25 | 26 | assert(RequestMessage::parse(session->request->content, session->request->method, session->request->path, 27 | session->request->query_string, session->request->http_version, session->request->header)); 28 | 29 | assert(session->request->method == "GET"); 30 | assert(session->request->path == "/test/"); 31 | assert(session->request->http_version == "1.1"); 32 | 33 | assert(session->request->header.size() == 4); 34 | auto header_it = session->request->header.find("TestHeader"); 35 | assert(header_it != session->request->header.end() && header_it->second == "test"); 36 | header_it = session->request->header.find("TestHeader2"); 37 | assert(header_it != session->request->header.end() && header_it->second == "test2"); 38 | 39 | header_it = session->request->header.find("testheader"); 40 | assert(header_it != session->request->header.end() && header_it->second == "test"); 41 | header_it = session->request->header.find("testheader2"); 42 | assert(header_it != session->request->header.end() && header_it->second == "test2"); 43 | 44 | auto range = session->request->header.equal_range("testheader3"); 45 | auto first = range.first; 46 | auto second = first; 47 | ++second; 48 | assert(range.first != session->request->header.end() && range.second != session->request->header.end() && 49 | ((first->second == "test3a" && second->second == "test3b") || 50 | (first->second == "test3b" && second->second == "test3a"))); 51 | } 52 | }; 53 | 54 | class ClientTest : public ClientBase { 55 | public: 56 | ClientTest(const std::string &server_port_path) : ClientBase::ClientBase(server_port_path, 80) {} 57 | 58 | std::shared_ptr create_connection() noexcept override { 59 | return nullptr; 60 | } 61 | 62 | void connect(const std::shared_ptr &) noexcept override {} 63 | 64 | void constructor_parse_test1() { 65 | assert(host == "test.org"); 66 | assert(port == 8080); 67 | } 68 | 69 | void constructor_parse_test2() { 70 | assert(host == "test.org"); 71 | assert(port == 80); 72 | } 73 | 74 | void parse_response_header_test() { 75 | std::shared_ptr response(new Response(static_cast(-1))); 76 | 77 | ostream stream(&response->streambuf); 78 | stream << "HTTP/1.1 200 OK\r\n"; 79 | stream << "TestHeader: test\r\n"; 80 | stream << "TestHeader2: test2\r\n"; 81 | stream << "TestHeader3:test3a\r\n"; 82 | stream << "TestHeader3:test3b\r\n"; 83 | stream << "TestHeader4:\r\n"; 84 | stream << "TestHeader5: \r\n"; 85 | stream << "TestHeader6: \r\n"; 86 | stream << "\r\n"; 87 | 88 | assert(ResponseMessage::parse(response->content, response->http_version, response->status_code, response->header)); 89 | 90 | assert(response->http_version == "1.1"); 91 | assert(response->status_code == "200 OK"); 92 | 93 | assert(response->header.size() == 7); 94 | auto header_it = response->header.find("TestHeader"); 95 | assert(header_it != response->header.end() && header_it->second == "test"); 96 | header_it = response->header.find("TestHeader2"); 97 | assert(header_it != response->header.end() && header_it->second == "test2"); 98 | 99 | header_it = response->header.find("testheader"); 100 | assert(header_it != response->header.end() && header_it->second == "test"); 101 | header_it = response->header.find("testheader2"); 102 | assert(header_it != response->header.end() && header_it->second == "test2"); 103 | 104 | auto range = response->header.equal_range("testheader3"); 105 | auto first = range.first; 106 | auto second = first; 107 | ++second; 108 | assert(range.first != response->header.end() && range.second != response->header.end() && 109 | ((first->second == "test3a" && second->second == "test3b") || 110 | (first->second == "test3b" && second->second == "test3a"))); 111 | 112 | header_it = response->header.find("TestHeader4"); 113 | assert(header_it != response->header.end() && header_it->second == ""); 114 | header_it = response->header.find("TestHeader5"); 115 | assert(header_it != response->header.end() && header_it->second == ""); 116 | header_it = response->header.find("TestHeader6"); 117 | assert(header_it != response->header.end() && header_it->second == ""); 118 | } 119 | }; 120 | 121 | int main() { 122 | assert(case_insensitive_equal("Test", "tesT")); 123 | assert(case_insensitive_equal("tesT", "test")); 124 | assert(!case_insensitive_equal("test", "tseT")); 125 | CaseInsensitiveEqual equal; 126 | assert(equal("Test", "tesT")); 127 | assert(equal("tesT", "test")); 128 | assert(!equal("test", "tset")); 129 | CaseInsensitiveHash hash; 130 | assert(hash("Test") == hash("tesT")); 131 | assert(hash("tesT") == hash("test")); 132 | assert(hash("test") != hash("tset")); 133 | 134 | auto percent_decoded = "testing æøå !#$&'()*+,/:;=?@[]123-._~\r\n"; 135 | auto percent_encoded = "testing%20%C3%A6%C3%B8%C3%A5%20%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D123-._~%0D%0A"; 136 | assert(Percent::encode(percent_decoded) == percent_encoded); 137 | assert(Percent::decode(percent_encoded) == percent_decoded); 138 | assert(Percent::decode(Percent::encode(percent_decoded)) == percent_decoded); 139 | 140 | SimpleWeb::CaseInsensitiveMultimap fields = {{"test1", "æøå"}, {"test2", "!#$&'()*+,/:;=?@[]"}}; 141 | auto query_string1 = "test1=%C3%A6%C3%B8%C3%A5&test2=%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"; 142 | auto query_string2 = "test2=%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D&test1=%C3%A6%C3%B8%C3%A5"; 143 | auto query_string_result = QueryString::create(fields); 144 | assert(query_string_result == query_string1 || query_string_result == query_string2); 145 | auto fields_result1 = QueryString::parse(query_string1); 146 | auto fields_result2 = QueryString::parse(query_string2); 147 | assert(fields_result1 == fields_result2 && fields_result1 == fields); 148 | 149 | auto serverTest = make_shared(); 150 | serverTest->io_service = std::make_shared(); 151 | 152 | serverTest->parse_request_test(); 153 | 154 | auto clientTest = make_shared("test.org:8080"); 155 | clientTest->constructor_parse_test1(); 156 | 157 | auto clientTest2 = make_shared("test.org"); 158 | clientTest2->constructor_parse_test2(); 159 | 160 | clientTest2->parse_response_header_test(); 161 | 162 | 163 | asio::io_service io_service; 164 | asio::ip::tcp::socket socket(io_service); 165 | SimpleWeb::Server::Request request(static_cast(-1), nullptr); 166 | { 167 | request.query_string = ""; 168 | auto queries = request.parse_query_string(); 169 | assert(queries.empty()); 170 | } 171 | { 172 | request.query_string = "="; 173 | auto queries = request.parse_query_string(); 174 | assert(queries.empty()); 175 | } 176 | { 177 | request.query_string = "=test"; 178 | auto queries = request.parse_query_string(); 179 | assert(queries.empty()); 180 | } 181 | { 182 | request.query_string = "a=1%202%20%203&b=3+4&c&d=æ%25ø%26å%3F"; 183 | auto queries = request.parse_query_string(); 184 | { 185 | auto range = queries.equal_range("a"); 186 | assert(range.first != range.second); 187 | assert(range.first->second == "1 2 3"); 188 | } 189 | { 190 | auto range = queries.equal_range("b"); 191 | assert(range.first != range.second); 192 | assert(range.first->second == "3 4"); 193 | } 194 | { 195 | auto range = queries.equal_range("c"); 196 | assert(range.first != range.second); 197 | assert(range.first->second == ""); 198 | } 199 | { 200 | auto range = queries.equal_range("d"); 201 | assert(range.first != range.second); 202 | assert(range.first->second == "æ%ø&å?"); 203 | } 204 | } 205 | 206 | { 207 | { 208 | SimpleWeb::CaseInsensitiveMultimap solution; 209 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse(""); 210 | assert(parsed == solution); 211 | } 212 | { 213 | SimpleWeb::CaseInsensitiveMultimap solution = {{"a", ""}}; 214 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a"); 215 | assert(parsed == solution); 216 | } 217 | { 218 | SimpleWeb::CaseInsensitiveMultimap solution = {{"a", ""}, {"b", ""}}; 219 | { 220 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a; b"); 221 | assert(parsed == solution); 222 | } 223 | { 224 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a;b"); 225 | assert(parsed == solution); 226 | } 227 | } 228 | { 229 | SimpleWeb::CaseInsensitiveMultimap solution = {{"a", ""}, {"b", "c"}}; 230 | { 231 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a; b=c"); 232 | assert(parsed == solution); 233 | } 234 | { 235 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("a;b=c"); 236 | assert(parsed == solution); 237 | } 238 | } 239 | { 240 | SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}}; 241 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data"); 242 | assert(parsed == solution); 243 | } 244 | { 245 | SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}, {"test", ""}}; 246 | { 247 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; test"); 248 | assert(parsed == solution); 249 | } 250 | } 251 | { 252 | SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}, {"name", "file"}}; 253 | { 254 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=\"file\""); 255 | assert(parsed == solution); 256 | } 257 | { 258 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=file"); 259 | assert(parsed == solution); 260 | } 261 | } 262 | { 263 | SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}, {"name", "file"}, {"filename", "filename.png"}}; 264 | { 265 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=\"file\"; filename=\"filename.png\""); 266 | assert(parsed == solution); 267 | } 268 | { 269 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data;name=\"file\";filename=\"filename.png\""); 270 | assert(parsed == solution); 271 | } 272 | { 273 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=file; filename=filename.png"); 274 | assert(parsed == solution); 275 | } 276 | { 277 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data;name=file;filename=filename.png"); 278 | assert(parsed == solution); 279 | } 280 | } 281 | { 282 | SimpleWeb::CaseInsensitiveMultimap solution = {{"form-data", ""}, {"name", "fi le"}, {"filename", "file name.png"}}; 283 | { 284 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=\"fi le\"; filename=\"file name.png\""); 285 | assert(parsed == solution); 286 | } 287 | { 288 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=\"fi%20le\"; filename=\"file%20name.png\""); 289 | assert(parsed == solution); 290 | } 291 | { 292 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=fi le; filename=file name.png"); 293 | assert(parsed == solution); 294 | } 295 | { 296 | auto parsed = SimpleWeb::HttpHeader::FieldValue::SemicolonSeparatedAttributes::parse("form-data; name=fi%20le; filename=file%20name.png"); 297 | assert(parsed == solution); 298 | } 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /utility.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIMPLE_WEB_UTILITY_HPP 2 | #define SIMPLE_WEB_UTILITY_HPP 3 | 4 | #include "status_code.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace SimpleWeb { 12 | inline bool case_insensitive_equal(const std::string &str1, const std::string &str2) noexcept { 13 | return str1.size() == str2.size() && 14 | std::equal(str1.begin(), str1.end(), str2.begin(), [](char a, char b) { 15 | return tolower(a) == tolower(b); 16 | }); 17 | } 18 | class CaseInsensitiveEqual { 19 | public: 20 | bool operator()(const std::string &str1, const std::string &str2) const noexcept { 21 | return case_insensitive_equal(str1, str2); 22 | } 23 | }; 24 | // Based on https://stackoverflow.com/questions/2590677/how-do-i-combine-hash-values-in-c0x/2595226#2595226 25 | class CaseInsensitiveHash { 26 | public: 27 | std::size_t operator()(const std::string &str) const noexcept { 28 | std::size_t h = 0; 29 | std::hash hash; 30 | for(auto c : str) 31 | h ^= hash(tolower(c)) + 0x9e3779b9 + (h << 6) + (h >> 2); 32 | return h; 33 | } 34 | }; 35 | 36 | using CaseInsensitiveMultimap = std::unordered_multimap; 37 | 38 | /// Percent encoding and decoding 39 | class Percent { 40 | public: 41 | /// Returns percent-encoded string 42 | static std::string encode(const std::string &value) noexcept { 43 | static auto hex_chars = "0123456789ABCDEF"; 44 | 45 | std::string result; 46 | result.reserve(value.size()); // Minimum size of result 47 | 48 | for(auto &chr : value) { 49 | if(!((chr >= '0' && chr <= '9') || (chr >= 'A' && chr <= 'Z') || (chr >= 'a' && chr <= 'z') || chr == '-' || chr == '.' || chr == '_' || chr == '~')) 50 | result += std::string("%") + hex_chars[static_cast(chr) >> 4] + hex_chars[static_cast(chr) & 15]; 51 | else 52 | result += chr; 53 | } 54 | 55 | return result; 56 | } 57 | 58 | /// Returns percent-decoded string 59 | static std::string decode(const std::string &value) noexcept { 60 | std::string result; 61 | result.reserve(value.size() / 3 + (value.size() % 3)); // Minimum size of result 62 | 63 | for(std::size_t i = 0; i < value.size(); ++i) { 64 | auto &chr = value[i]; 65 | if(chr == '%' && i + 2 < value.size()) { 66 | auto hex = value.substr(i + 1, 2); 67 | auto decoded_chr = static_cast(std::strtol(hex.c_str(), nullptr, 16)); 68 | result += decoded_chr; 69 | i += 2; 70 | } 71 | else if(chr == '+') 72 | result += ' '; 73 | else 74 | result += chr; 75 | } 76 | 77 | return result; 78 | } 79 | }; 80 | 81 | /// Query string creation and parsing 82 | class QueryString { 83 | public: 84 | /// Returns query string created from given field names and values 85 | static std::string create(const CaseInsensitiveMultimap &fields) noexcept { 86 | std::string result; 87 | 88 | bool first = true; 89 | for(auto &field : fields) { 90 | result += (!first ? "&" : "") + field.first + '=' + Percent::encode(field.second); 91 | first = false; 92 | } 93 | 94 | return result; 95 | } 96 | 97 | /// Returns query keys with percent-decoded values. 98 | static CaseInsensitiveMultimap parse(const std::string &query_string) noexcept { 99 | CaseInsensitiveMultimap result; 100 | 101 | if(query_string.empty()) 102 | return result; 103 | 104 | std::size_t name_pos = 0; 105 | auto name_end_pos = std::string::npos; 106 | auto value_pos = std::string::npos; 107 | for(std::size_t c = 0; c < query_string.size(); ++c) { 108 | if(query_string[c] == '&') { 109 | auto name = query_string.substr(name_pos, (name_end_pos == std::string::npos ? c : name_end_pos) - name_pos); 110 | if(!name.empty()) { 111 | auto value = value_pos == std::string::npos ? std::string() : query_string.substr(value_pos, c - value_pos); 112 | result.emplace(std::move(name), Percent::decode(value)); 113 | } 114 | name_pos = c + 1; 115 | name_end_pos = std::string::npos; 116 | value_pos = std::string::npos; 117 | } 118 | else if(query_string[c] == '=') { 119 | name_end_pos = c; 120 | value_pos = c + 1; 121 | } 122 | } 123 | if(name_pos < query_string.size()) { 124 | auto name = query_string.substr(name_pos, name_end_pos - name_pos); 125 | if(!name.empty()) { 126 | auto value = value_pos >= query_string.size() ? std::string() : query_string.substr(value_pos); 127 | result.emplace(std::move(name), Percent::decode(value)); 128 | } 129 | } 130 | 131 | return result; 132 | } 133 | }; 134 | 135 | class HttpHeader { 136 | public: 137 | /// Parse header fields 138 | static CaseInsensitiveMultimap parse(std::istream &stream) noexcept { 139 | CaseInsensitiveMultimap result; 140 | std::string line; 141 | getline(stream, line); 142 | std::size_t param_end; 143 | while((param_end = line.find(':')) != std::string::npos) { 144 | std::size_t value_start = param_end + 1; 145 | while(value_start + 1 < line.size() && line[value_start] == ' ') 146 | ++value_start; 147 | if(value_start < line.size()) 148 | result.emplace(line.substr(0, param_end), line.substr(value_start, line.size() - value_start - 1)); 149 | 150 | getline(stream, line); 151 | } 152 | return result; 153 | } 154 | 155 | class FieldValue { 156 | public: 157 | class SemicolonSeparatedAttributes { 158 | public: 159 | /// Parse Set-Cookie or Content-Disposition header field value. Attribute values are percent-decoded. 160 | static CaseInsensitiveMultimap parse(const std::string &str) { 161 | CaseInsensitiveMultimap result; 162 | 163 | std::size_t name_start_pos = std::string::npos; 164 | std::size_t name_end_pos = std::string::npos; 165 | std::size_t value_start_pos = std::string::npos; 166 | for(std::size_t c = 0; c < str.size(); ++c) { 167 | if(name_start_pos == std::string::npos) { 168 | if(str[c] != ' ' && str[c] != ';') 169 | name_start_pos = c; 170 | } 171 | else { 172 | if(name_end_pos == std::string::npos) { 173 | if(str[c] == ';') { 174 | result.emplace(str.substr(name_start_pos, c - name_start_pos), std::string()); 175 | name_start_pos = std::string::npos; 176 | } 177 | else if(str[c] == '=') 178 | name_end_pos = c; 179 | } 180 | else { 181 | if(value_start_pos == std::string::npos) { 182 | if(str[c] == '"' && c + 1 < str.size()) 183 | value_start_pos = c + 1; 184 | else 185 | value_start_pos = c; 186 | } 187 | else if(str[c] == '"' || str[c] == ';') { 188 | result.emplace(str.substr(name_start_pos, name_end_pos - name_start_pos), Percent::decode(str.substr(value_start_pos, c - value_start_pos))); 189 | name_start_pos = std::string::npos; 190 | name_end_pos = std::string::npos; 191 | value_start_pos = std::string::npos; 192 | } 193 | } 194 | } 195 | } 196 | if(name_start_pos != std::string::npos) { 197 | if(name_end_pos == std::string::npos) 198 | result.emplace(str.substr(name_start_pos), std::string()); 199 | else if(value_start_pos != std::string::npos) { 200 | if(str.back() == '"') 201 | result.emplace(str.substr(name_start_pos, name_end_pos - name_start_pos), Percent::decode(str.substr(value_start_pos, str.size() - 1))); 202 | else 203 | result.emplace(str.substr(name_start_pos, name_end_pos - name_start_pos), Percent::decode(str.substr(value_start_pos))); 204 | } 205 | } 206 | 207 | return result; 208 | } 209 | }; 210 | }; 211 | }; // namespace SimpleWeb 212 | 213 | class RequestMessage { 214 | public: 215 | /// Parse request line and header fields 216 | static bool parse(std::istream &stream, std::string &method, std::string &path, std::string &query_string, std::string &version, CaseInsensitiveMultimap &header) noexcept { 217 | header.clear(); 218 | std::string line; 219 | getline(stream, line); 220 | std::size_t method_end; 221 | if((method_end = line.find(' ')) != std::string::npos) { 222 | method = line.substr(0, method_end); 223 | 224 | std::size_t query_start = std::string::npos; 225 | std::size_t path_and_query_string_end = std::string::npos; 226 | for(std::size_t i = method_end + 1; i < line.size(); ++i) { 227 | if(line[i] == '?' && (i + 1) < line.size()) 228 | query_start = i + 1; 229 | else if(line[i] == ' ') { 230 | path_and_query_string_end = i; 231 | break; 232 | } 233 | } 234 | if(path_and_query_string_end != std::string::npos) { 235 | if(query_start != std::string::npos) { 236 | path = line.substr(method_end + 1, query_start - method_end - 2); 237 | query_string = line.substr(query_start, path_and_query_string_end - query_start); 238 | } 239 | else 240 | path = line.substr(method_end + 1, path_and_query_string_end - method_end - 1); 241 | 242 | std::size_t protocol_end; 243 | if((protocol_end = line.find('/', path_and_query_string_end + 1)) != std::string::npos) { 244 | if(line.compare(path_and_query_string_end + 1, protocol_end - path_and_query_string_end - 1, "HTTP") != 0) 245 | return false; 246 | version = line.substr(protocol_end + 1, line.size() - protocol_end - 2); 247 | } 248 | else 249 | return false; 250 | 251 | header = HttpHeader::parse(stream); 252 | } 253 | else 254 | return false; 255 | } 256 | else 257 | return false; 258 | return true; 259 | } 260 | }; 261 | 262 | class ResponseMessage { 263 | public: 264 | /// Parse status line and header fields 265 | static bool parse(std::istream &stream, std::string &version, std::string &status_code, CaseInsensitiveMultimap &header) noexcept { 266 | header.clear(); 267 | std::string line; 268 | getline(stream, line); 269 | std::size_t version_end = line.find(' '); 270 | if(version_end != std::string::npos) { 271 | if(5 < line.size()) 272 | version = line.substr(5, version_end - 5); 273 | else 274 | return false; 275 | if((version_end + 1) < line.size()) 276 | status_code = line.substr(version_end + 1, line.size() - (version_end + 1) - 1); 277 | else 278 | return false; 279 | 280 | header = HttpHeader::parse(stream); 281 | } 282 | else 283 | return false; 284 | return true; 285 | } 286 | }; 287 | } // namespace SimpleWeb 288 | 289 | #ifdef __SSE2__ 290 | #include 291 | namespace SimpleWeb { 292 | inline void spin_loop_pause() noexcept { _mm_pause(); } 293 | } // namespace SimpleWeb 294 | // TODO: need verification that the following checks are correct: 295 | #elif defined(_MSC_VER) && _MSC_VER >= 1800 && (defined(_M_X64) || defined(_M_IX86)) 296 | #include 297 | namespace SimpleWeb { 298 | inline void spin_loop_pause() noexcept { _mm_pause(); } 299 | } // namespace SimpleWeb 300 | #else 301 | namespace SimpleWeb { 302 | inline void spin_loop_pause() noexcept {} 303 | } // namespace SimpleWeb 304 | #endif 305 | 306 | namespace SimpleWeb { 307 | /// Makes it possible to for instance cancel Asio handlers without stopping asio::io_service 308 | class ScopeRunner { 309 | /// Scope count that is set to -1 if scopes are to be canceled 310 | std::atomic count; 311 | 312 | public: 313 | class SharedLock { 314 | friend class ScopeRunner; 315 | std::atomic &count; 316 | SharedLock(std::atomic &count) noexcept : count(count) {} 317 | SharedLock &operator=(const SharedLock &) = delete; 318 | SharedLock(const SharedLock &) = delete; 319 | 320 | public: 321 | ~SharedLock() noexcept { 322 | count.fetch_sub(1); 323 | } 324 | }; 325 | 326 | ScopeRunner() noexcept : count(0) {} 327 | 328 | /// Returns nullptr if scope should be exited, or a shared lock otherwise 329 | std::unique_ptr continue_lock() noexcept { 330 | long expected = count; 331 | while(expected >= 0 && !count.compare_exchange_weak(expected, expected + 1)) 332 | spin_loop_pause(); 333 | 334 | if(expected < 0) 335 | return nullptr; 336 | else 337 | return std::unique_ptr(new SharedLock(count)); 338 | } 339 | 340 | /// Blocks until all shared locks are released, then prevents future shared locks 341 | void stop() noexcept { 342 | long expected = 0; 343 | while(!count.compare_exchange_weak(expected, -1)) { 344 | if(expected < 0) 345 | return; 346 | expected = 0; 347 | spin_loop_pause(); 348 | } 349 | } 350 | }; 351 | } // namespace SimpleWeb 352 | 353 | #endif // SIMPLE_WEB_UTILITY_HPP 354 | -------------------------------------------------------------------------------- /tests/io_test.cpp: -------------------------------------------------------------------------------- 1 | #include "client_http.hpp" 2 | #include "server_http.hpp" 3 | 4 | #include 5 | 6 | using namespace std; 7 | 8 | #ifndef USE_STANDALONE_ASIO 9 | namespace asio = boost::asio; 10 | #endif 11 | 12 | using HttpServer = SimpleWeb::Server; 13 | using HttpClient = SimpleWeb::Client; 14 | 15 | int main() { 16 | // Test ScopeRunner 17 | { 18 | SimpleWeb::ScopeRunner scope_runner; 19 | std::thread cancel_thread; 20 | { 21 | assert(scope_runner.count == 0); 22 | auto lock = scope_runner.continue_lock(); 23 | assert(lock); 24 | assert(scope_runner.count == 1); 25 | { 26 | auto lock = scope_runner.continue_lock(); 27 | assert(lock); 28 | assert(scope_runner.count == 2); 29 | } 30 | assert(scope_runner.count == 1); 31 | cancel_thread = thread([&scope_runner] { 32 | scope_runner.stop(); 33 | assert(scope_runner.count == -1); 34 | }); 35 | this_thread::sleep_for(chrono::milliseconds(500)); 36 | assert(scope_runner.count == 1); 37 | } 38 | cancel_thread.join(); 39 | assert(scope_runner.count == -1); 40 | auto lock = scope_runner.continue_lock(); 41 | assert(!lock); 42 | scope_runner.stop(); 43 | assert(scope_runner.count == -1); 44 | 45 | scope_runner.count = 0; 46 | 47 | vector threads; 48 | for(size_t c = 0; c < 100; ++c) { 49 | threads.emplace_back([&scope_runner] { 50 | auto lock = scope_runner.continue_lock(); 51 | assert(scope_runner.count > 0); 52 | }); 53 | } 54 | for(auto &thread : threads) 55 | thread.join(); 56 | assert(scope_runner.count == 0); 57 | } 58 | 59 | HttpServer server; 60 | server.config.port = 8080; 61 | 62 | server.resource["^/string$"]["POST"] = [](shared_ptr response, shared_ptr request) { 63 | auto content = request->content.string(); 64 | 65 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" 66 | << content; 67 | 68 | assert(!request->remote_endpoint_address().empty()); 69 | assert(request->remote_endpoint_port() != 0); 70 | }; 71 | 72 | server.resource["^/string2$"]["POST"] = [](shared_ptr response, shared_ptr request) { 73 | response->write(request->content.string()); 74 | }; 75 | 76 | server.resource["^/string3$"]["POST"] = [](shared_ptr response, shared_ptr request) { 77 | stringstream stream; 78 | stream << request->content.rdbuf(); 79 | response->write(stream); 80 | }; 81 | 82 | server.resource["^/string4$"]["POST"] = [](shared_ptr response, shared_ptr /*request*/) { 83 | response->write(SimpleWeb::StatusCode::client_error_forbidden, {{"Test1", "test2"}, {"tesT3", "test4"}}); 84 | }; 85 | 86 | server.resource["^/info$"]["GET"] = [](shared_ptr response, shared_ptr request) { 87 | stringstream content_stream; 88 | content_stream << request->method << " " << request->path << " " << request->http_version << " "; 89 | content_stream << request->header.find("test parameter")->second; 90 | 91 | content_stream.seekp(0, ios::end); 92 | 93 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content_stream.tellp() << "\r\n\r\n" 94 | << content_stream.rdbuf(); 95 | }; 96 | 97 | server.resource["^/work$"]["GET"] = [](shared_ptr response, shared_ptr /*request*/) { 98 | thread work_thread([response] { 99 | this_thread::sleep_for(chrono::seconds(5)); 100 | response->write("Work done"); 101 | }); 102 | work_thread.detach(); 103 | }; 104 | 105 | server.resource["^/match/([0-9]+)$"]["GET"] = [](shared_ptr response, shared_ptr request) { 106 | string number = request->path_match[1]; 107 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" 108 | << number; 109 | }; 110 | 111 | server.resource["^/header$"]["GET"] = [](shared_ptr response, shared_ptr request) { 112 | auto content = request->header.find("test1")->second + request->header.find("test2")->second; 113 | 114 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" 115 | << content; 116 | }; 117 | 118 | server.resource["^/query_string$"]["GET"] = [](shared_ptr response, shared_ptr request) { 119 | assert(request->path == "/query_string"); 120 | assert(request->query_string == "testing"); 121 | auto queries = request->parse_query_string(); 122 | auto it = queries.find("Testing"); 123 | assert(it != queries.end() && it->first == "testing" && it->second == ""); 124 | response->write(request->query_string); 125 | }; 126 | 127 | server.resource["^/chunked$"]["POST"] = [](shared_ptr response, shared_ptr request) { 128 | assert(request->path == "/chunked"); 129 | 130 | assert(request->content.string() == "SimpleWeb in\r\n\r\nchunks."); 131 | 132 | response->write("6\r\nSimple\r\n3\r\nWeb\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n", {{"Transfer-Encoding", "chunked"}}); 133 | }; 134 | 135 | thread server_thread([&server]() { 136 | // Start server 137 | server.start(); 138 | }); 139 | 140 | this_thread::sleep_for(chrono::seconds(1)); 141 | 142 | server.stop(); 143 | server_thread.join(); 144 | 145 | server_thread = thread([&server]() { 146 | // Start server 147 | server.start(); 148 | }); 149 | 150 | this_thread::sleep_for(chrono::seconds(1)); 151 | 152 | // Test various request types 153 | { 154 | HttpClient client("localhost:8080"); 155 | { 156 | stringstream output; 157 | auto r = client.request("POST", "/string", "A string"); 158 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 159 | output << r->content.rdbuf(); 160 | assert(output.str() == "A string"); 161 | } 162 | 163 | { 164 | auto r = client.request("POST", "/string", "A string"); 165 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 166 | assert(r->content.string() == "A string"); 167 | } 168 | 169 | { 170 | stringstream output; 171 | auto r = client.request("POST", "/string2", "A string"); 172 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 173 | output << r->content.rdbuf(); 174 | assert(output.str() == "A string"); 175 | } 176 | 177 | { 178 | stringstream output; 179 | auto r = client.request("POST", "/string3", "A string"); 180 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 181 | output << r->content.rdbuf(); 182 | assert(output.str() == "A string"); 183 | } 184 | 185 | { 186 | stringstream output; 187 | auto r = client.request("POST", "/string4", "A string"); 188 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::client_error_forbidden); 189 | assert(r->header.size() == 3); 190 | assert(r->header.find("test1")->second == "test2"); 191 | assert(r->header.find("tEst3")->second == "test4"); 192 | assert(r->header.find("content-length")->second == "0"); 193 | output << r->content.rdbuf(); 194 | assert(output.str() == ""); 195 | } 196 | 197 | { 198 | stringstream output; 199 | stringstream content("A string"); 200 | auto r = client.request("POST", "/string", content); 201 | output << r->content.rdbuf(); 202 | assert(output.str() == "A string"); 203 | } 204 | 205 | { 206 | stringstream output; 207 | auto r = client.request("GET", "/info", "", {{"Test Parameter", "test value"}}); 208 | output << r->content.rdbuf(); 209 | assert(output.str() == "GET /info 1.1 test value"); 210 | } 211 | 212 | { 213 | stringstream output; 214 | auto r = client.request("GET", "/match/123"); 215 | output << r->content.rdbuf(); 216 | assert(output.str() == "123"); 217 | } 218 | { 219 | auto r = client.request("POST", "/chunked", "6\r\nSimple\r\n3\r\nWeb\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n", {{"Transfer-Encoding", "chunked"}}); 220 | assert(r->content.string() == "SimpleWeb in\r\n\r\nchunks."); 221 | } 222 | } 223 | { 224 | HttpClient client("localhost:8080"); 225 | 226 | HttpClient::Connection *connection; 227 | { 228 | // test performing the stream version of the request methods first 229 | stringstream output; 230 | stringstream content("A string"); 231 | auto r = client.request("POST", "/string", content); 232 | output << r->content.rdbuf(); 233 | assert(output.str() == "A string"); 234 | assert(client.connections.size() == 1); 235 | connection = client.connections.begin()->get(); 236 | } 237 | 238 | { 239 | stringstream output; 240 | auto r = client.request("POST", "/string", "A string"); 241 | output << r->content.rdbuf(); 242 | assert(output.str() == "A string"); 243 | assert(client.connections.size() == 1); 244 | assert(connection == client.connections.begin()->get()); 245 | } 246 | 247 | { 248 | stringstream output; 249 | auto r = client.request("GET", "/header", "", {{"test1", "test"}, {"test2", "ing"}}); 250 | output << r->content.rdbuf(); 251 | assert(output.str() == "testing"); 252 | assert(client.connections.size() == 1); 253 | assert(connection == client.connections.begin()->get()); 254 | } 255 | 256 | { 257 | stringstream output; 258 | auto r = client.request("GET", "/query_string?testing"); 259 | assert(r->content.string() == "testing"); 260 | assert(client.connections.size() == 1); 261 | assert(connection == client.connections.begin()->get()); 262 | } 263 | } 264 | 265 | // Test asynchronous requests 266 | { 267 | HttpClient client("localhost:8080"); 268 | bool call = false; 269 | client.request("GET", "/match/123", [&call](shared_ptr response, const SimpleWeb::error_code &ec) { 270 | assert(!ec); 271 | stringstream output; 272 | output << response->content.rdbuf(); 273 | assert(output.str() == "123"); 274 | call = true; 275 | }); 276 | client.io_service->run(); 277 | assert(call); 278 | 279 | { 280 | vector calls(100, 0); 281 | vector threads; 282 | for(size_t c = 0; c < 100; ++c) { 283 | threads.emplace_back([c, &client, &calls] { 284 | client.request("GET", "/match/123", [c, &calls](shared_ptr response, const SimpleWeb::error_code &ec) { 285 | assert(!ec); 286 | stringstream output; 287 | output << response->content.rdbuf(); 288 | assert(output.str() == "123"); 289 | calls[c] = 1; 290 | }); 291 | }); 292 | } 293 | for(auto &thread : threads) 294 | thread.join(); 295 | assert(client.connections.size() == 100); 296 | client.io_service->reset(); 297 | client.io_service->run(); 298 | assert(client.connections.size() == 1); 299 | for(auto call : calls) 300 | assert(call); 301 | } 302 | } 303 | 304 | // Test concurrent synchronous request calls 305 | { 306 | HttpClient client("localhost:8080"); 307 | { 308 | vector calls(2, 0); 309 | vector threads; 310 | for(size_t c = 0; c < 2; ++c) { 311 | threads.emplace_back([c, &client, &calls] { 312 | try { 313 | auto r = client.request("GET", "/match/123"); 314 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 315 | assert(r->content.string() == "123"); 316 | calls[c] = 1; 317 | } 318 | catch(...) { 319 | assert(false); 320 | } 321 | }); 322 | } 323 | for(auto &thread : threads) 324 | thread.join(); 325 | assert(client.connections.size() == 1); 326 | for(auto call : calls) 327 | assert(call); 328 | } 329 | } 330 | 331 | // Test multiple requests through a persistent connection 332 | { 333 | HttpClient client("localhost:8080"); 334 | assert(client.connections.size() == 0); 335 | for(size_t c = 0; c < 5000; ++c) { 336 | auto r1 = client.request("POST", "/string", "A string"); 337 | assert(SimpleWeb::status_code(r1->status_code) == SimpleWeb::StatusCode::success_ok); 338 | assert(r1->content.string() == "A string"); 339 | assert(client.connections.size() == 1); 340 | 341 | stringstream content("A string"); 342 | auto r2 = client.request("POST", "/string", content); 343 | assert(SimpleWeb::status_code(r2->status_code) == SimpleWeb::StatusCode::success_ok); 344 | assert(r2->content.string() == "A string"); 345 | assert(client.connections.size() == 1); 346 | } 347 | } 348 | 349 | // Test multiple requests through new several client objects 350 | for(size_t c = 0; c < 100; ++c) { 351 | { 352 | HttpClient client("localhost:8080"); 353 | auto r = client.request("POST", "/string", "A string"); 354 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 355 | assert(r->content.string() == "A string"); 356 | assert(client.connections.size() == 1); 357 | } 358 | 359 | { 360 | HttpClient client("localhost:8080"); 361 | stringstream content("A string"); 362 | auto r = client.request("POST", "/string", content); 363 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 364 | assert(r->content.string() == "A string"); 365 | assert(client.connections.size() == 1); 366 | } 367 | } 368 | 369 | // Test Client client's stop() 370 | for(size_t c = 0; c < 40; ++c) { 371 | auto io_service = make_shared(); 372 | bool call = false; 373 | HttpClient client("localhost:8080"); 374 | client.io_service = io_service; 375 | client.request("GET", "/work", [&call](shared_ptr /*response*/, const SimpleWeb::error_code &ec) { 376 | call = true; 377 | assert(ec); 378 | }); 379 | thread thread([io_service] { 380 | io_service->run(); 381 | }); 382 | this_thread::sleep_for(chrono::milliseconds(100)); 383 | client.stop(); 384 | this_thread::sleep_for(chrono::milliseconds(100)); 385 | thread.join(); 386 | assert(call); 387 | } 388 | 389 | // Test Client destructor that should cancel the client's request 390 | for(size_t c = 0; c < 40; ++c) { 391 | auto io_service = make_shared(); 392 | { 393 | HttpClient client("localhost:8080"); 394 | client.io_service = io_service; 395 | client.request("GET", "/work", [](shared_ptr /*response*/, const SimpleWeb::error_code & /*ec*/) { 396 | assert(false); 397 | }); 398 | thread thread([io_service] { 399 | io_service->run(); 400 | }); 401 | thread.detach(); 402 | this_thread::sleep_for(chrono::milliseconds(100)); 403 | } 404 | this_thread::sleep_for(chrono::milliseconds(100)); 405 | } 406 | 407 | server.stop(); 408 | server_thread.join(); 409 | 410 | // Test server destructor 411 | { 412 | auto io_service = make_shared(); 413 | bool call = false; 414 | bool client_catch = false; 415 | { 416 | HttpServer server; 417 | server.config.port = 8081; 418 | server.io_service = io_service; 419 | server.resource["^/test$"]["GET"] = [&call](shared_ptr response, shared_ptr /*request*/) { 420 | call = true; 421 | thread sleep_thread([response] { 422 | this_thread::sleep_for(chrono::seconds(5)); 423 | response->write(SimpleWeb::StatusCode::success_ok, "test"); 424 | response->send([](const SimpleWeb::error_code & /*ec*/) { 425 | assert(false); 426 | }); 427 | }); 428 | sleep_thread.detach(); 429 | }; 430 | server.start(); 431 | thread server_thread([io_service] { 432 | io_service->run(); 433 | }); 434 | server_thread.detach(); 435 | this_thread::sleep_for(chrono::seconds(1)); 436 | thread client_thread([&client_catch] { 437 | HttpClient client("localhost:8081"); 438 | try { 439 | auto r = client.request("GET", "/test"); 440 | assert(false); 441 | } 442 | catch(...) { 443 | client_catch = true; 444 | } 445 | }); 446 | client_thread.detach(); 447 | this_thread::sleep_for(chrono::seconds(1)); 448 | } 449 | this_thread::sleep_for(chrono::seconds(5)); 450 | assert(call); 451 | assert(client_catch); 452 | io_service->stop(); 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /client_http.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CLIENT_HTTP_HPP 2 | #define CLIENT_HTTP_HPP 3 | 4 | #include "utility.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #ifdef USE_STANDALONE_ASIO 12 | #include 13 | #include 14 | namespace SimpleWeb { 15 | using error_code = std::error_code; 16 | using errc = std::errc; 17 | using system_error = std::system_error; 18 | namespace make_error_code = std; 19 | } // namespace SimpleWeb 20 | #else 21 | #include 22 | #include 23 | namespace SimpleWeb { 24 | namespace asio = boost::asio; 25 | using error_code = boost::system::error_code; 26 | namespace errc = boost::system::errc; 27 | using system_error = boost::system::system_error; 28 | namespace make_error_code = boost::system::errc; 29 | } // namespace SimpleWeb 30 | #endif 31 | 32 | #if __cplusplus > 201402L || (defined(_MSC_VER) && _MSC_VER >= 1910) 33 | #include 34 | namespace SimpleWeb { 35 | using string_view = std::string_view; 36 | } 37 | #elif !defined(USE_STANDALONE_ASIO) 38 | #include 39 | namespace SimpleWeb { 40 | using string_view = boost::string_ref; 41 | } 42 | #else 43 | namespace SimpleWeb { 44 | using string_view = const std::string &; 45 | } 46 | #endif 47 | 48 | namespace SimpleWeb { 49 | template 50 | class Client; 51 | 52 | template 53 | class ClientBase { 54 | public: 55 | class Content : public std::istream { 56 | friend class ClientBase; 57 | 58 | public: 59 | std::size_t size() noexcept { 60 | return streambuf.size(); 61 | } 62 | /// Convenience function to return std::string. The stream buffer is consumed. 63 | std::string string() noexcept { 64 | try { 65 | std::string str; 66 | auto size = streambuf.size(); 67 | str.resize(size); 68 | read(&str[0], static_cast(size)); 69 | return str; 70 | } 71 | catch(...) { 72 | return std::string(); 73 | } 74 | } 75 | 76 | private: 77 | asio::streambuf &streambuf; 78 | Content(asio::streambuf &streambuf) noexcept : std::istream(&streambuf), streambuf(streambuf) {} 79 | }; 80 | 81 | class Response { 82 | friend class ClientBase; 83 | friend class Client; 84 | 85 | asio::streambuf streambuf; 86 | 87 | Response(std::size_t max_response_streambuf_size) noexcept : streambuf(max_response_streambuf_size), content(streambuf) {} 88 | 89 | public: 90 | std::string http_version, status_code; 91 | 92 | Content content; 93 | 94 | CaseInsensitiveMultimap header; 95 | }; 96 | 97 | class Config { 98 | friend class ClientBase; 99 | 100 | private: 101 | Config() noexcept {} 102 | 103 | public: 104 | /// Set timeout on requests in seconds. Default value: 0 (no timeout). 105 | long timeout = 0; 106 | /// Set connect timeout in seconds. Default value: 0 (Config::timeout is then used instead). 107 | long timeout_connect = 0; 108 | /// Maximum size of response stream buffer. Defaults to architecture maximum. 109 | /// Reaching this limit will result in a message_size error code. 110 | std::size_t max_response_streambuf_size = std::numeric_limits::max(); 111 | /// Set proxy server (server:port) 112 | std::string proxy_server; 113 | }; 114 | 115 | protected: 116 | class Connection : public std::enable_shared_from_this { 117 | public: 118 | template 119 | Connection(std::shared_ptr handler_runner, long timeout, Args &&... args) noexcept 120 | : handler_runner(std::move(handler_runner)), timeout(timeout), socket(new socket_type(std::forward(args)...)) {} 121 | 122 | std::shared_ptr handler_runner; 123 | long timeout; 124 | 125 | std::unique_ptr socket; // Socket must be unique_ptr since asio::ssl::stream is not movable 126 | bool in_use = false; 127 | bool attempt_reconnect = true; 128 | 129 | std::unique_ptr timer; 130 | 131 | void set_timeout(long seconds = 0) noexcept { 132 | if(seconds == 0) 133 | seconds = timeout; 134 | if(seconds == 0) { 135 | timer = nullptr; 136 | return; 137 | } 138 | timer = std::unique_ptr(new asio::steady_timer(socket->get_io_service())); 139 | timer->expires_from_now(std::chrono::seconds(seconds)); 140 | auto self = this->shared_from_this(); 141 | timer->async_wait([self](const error_code &ec) { 142 | if(!ec) { 143 | error_code ec; 144 | self->socket->lowest_layer().cancel(ec); 145 | } 146 | }); 147 | } 148 | 149 | void cancel_timeout() noexcept { 150 | if(timer) { 151 | error_code ec; 152 | timer->cancel(ec); 153 | } 154 | } 155 | }; 156 | 157 | class Session { 158 | public: 159 | Session(std::size_t max_response_streambuf_size, std::shared_ptr connection, std::unique_ptr request_streambuf) noexcept 160 | : connection(std::move(connection)), request_streambuf(std::move(request_streambuf)), response(new Response(max_response_streambuf_size)) {} 161 | 162 | std::shared_ptr connection; 163 | std::unique_ptr request_streambuf; 164 | std::shared_ptr response; 165 | std::function &, const error_code &)> callback; 166 | }; 167 | 168 | public: 169 | /// Set before calling request 170 | Config config; 171 | 172 | /// If you have your own asio::io_service, store its pointer here before calling request(). 173 | /// When using asynchronous requests, running the io_service is up to the programmer. 174 | std::shared_ptr io_service; 175 | 176 | /// Convenience function to perform synchronous request. The io_service is run within this function. 177 | /// If reusing the io_service for other tasks, use the asynchronous request functions instead. 178 | /// Do not use concurrently with the asynchronous request functions. 179 | std::shared_ptr request(const std::string &method, const std::string &path = std::string("/"), 180 | string_view content = "", const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 181 | std::shared_ptr response; 182 | error_code ec; 183 | request(method, path, content, header, [&response, &ec](std::shared_ptr response_, const error_code &ec_) { 184 | response = response_; 185 | ec = ec_; 186 | }); 187 | 188 | { 189 | std::unique_lock lock(concurrent_synchronous_requests_mutex); 190 | ++concurrent_synchronous_requests; 191 | } 192 | io_service->run(); 193 | { 194 | std::unique_lock lock(concurrent_synchronous_requests_mutex); 195 | --concurrent_synchronous_requests; 196 | if(!concurrent_synchronous_requests) 197 | io_service->reset(); 198 | } 199 | 200 | if(ec) 201 | throw system_error(ec); 202 | 203 | return response; 204 | } 205 | 206 | /// Convenience function to perform synchronous request. The io_service is run within this function. 207 | /// If reusing the io_service for other tasks, use the asynchronous request functions instead. 208 | /// Do not use concurrently with the asynchronous request functions. 209 | std::shared_ptr request(const std::string &method, const std::string &path, std::istream &content, 210 | const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 211 | std::shared_ptr response; 212 | error_code ec; 213 | request(method, path, content, header, [&response, &ec](std::shared_ptr response_, const error_code &ec_) { 214 | response = response_; 215 | ec = ec_; 216 | }); 217 | 218 | { 219 | std::unique_lock lock(concurrent_synchronous_requests_mutex); 220 | ++concurrent_synchronous_requests; 221 | } 222 | io_service->run(); 223 | { 224 | std::unique_lock lock(concurrent_synchronous_requests_mutex); 225 | --concurrent_synchronous_requests; 226 | if(!concurrent_synchronous_requests) 227 | io_service->reset(); 228 | } 229 | 230 | if(ec) 231 | throw system_error(ec); 232 | 233 | return response; 234 | } 235 | 236 | /// Asynchronous request where setting and/or running Client's io_service is required. 237 | /// Do not use concurrently with the synchronous request functions. 238 | void request(const std::string &method, const std::string &path, string_view content, const CaseInsensitiveMultimap &header, 239 | std::function, const error_code &)> &&request_callback_) { 240 | auto session = std::make_shared(config.max_response_streambuf_size, get_connection(), create_request_header(method, path, header)); 241 | auto response = session->response; 242 | auto request_callback = std::make_shared, const error_code &)>>(std::move(request_callback_)); 243 | session->callback = [this, response, request_callback](const std::shared_ptr &connection, const error_code &ec) { 244 | { 245 | std::unique_lock lock(this->connections_mutex); 246 | connection->in_use = false; 247 | 248 | // Remove unused connections, but keep one open for HTTP persistent connection: 249 | std::size_t unused_connections = 0; 250 | for(auto it = this->connections.begin(); it != this->connections.end();) { 251 | if(ec && connection == *it) 252 | it = this->connections.erase(it); 253 | else if((*it)->in_use) 254 | ++it; 255 | else { 256 | ++unused_connections; 257 | if(unused_connections > 1) 258 | it = this->connections.erase(it); 259 | else 260 | ++it; 261 | } 262 | } 263 | } 264 | 265 | if(*request_callback) 266 | (*request_callback)(response, ec); 267 | }; 268 | 269 | std::ostream write_stream(session->request_streambuf.get()); 270 | if(content.size() > 0) { 271 | auto header_it = header.find("Content-Length"); 272 | if(header_it == header.end()) { 273 | header_it = header.find("Transfer-Encoding"); 274 | if(header_it == header.end() || header_it->second != "chunked") 275 | write_stream << "Content-Length: " << content.size() << "\r\n"; 276 | } 277 | } 278 | write_stream << "\r\n" 279 | << content; 280 | 281 | connect(session); 282 | } 283 | 284 | /// Asynchronous request where setting and/or running Client's io_service is required. 285 | /// Do not use concurrently with the synchronous request functions. 286 | void request(const std::string &method, const std::string &path, string_view content, 287 | std::function, const error_code &)> &&request_callback) { 288 | request(method, path, content, CaseInsensitiveMultimap(), std::move(request_callback)); 289 | } 290 | 291 | /// Asynchronous request where setting and/or running Client's io_service is required. 292 | void request(const std::string &method, const std::string &path, 293 | std::function, const error_code &)> &&request_callback) { 294 | request(method, path, std::string(), CaseInsensitiveMultimap(), std::move(request_callback)); 295 | } 296 | 297 | /// Asynchronous request where setting and/or running Client's io_service is required. 298 | void request(const std::string &method, std::function, const error_code &)> &&request_callback) { 299 | request(method, std::string("/"), std::string(), CaseInsensitiveMultimap(), std::move(request_callback)); 300 | } 301 | 302 | /// Asynchronous request where setting and/or running Client's io_service is required. 303 | void request(const std::string &method, const std::string &path, std::istream &content, const CaseInsensitiveMultimap &header, 304 | std::function, const error_code &)> &&request_callback_) { 305 | auto session = std::make_shared(config.max_response_streambuf_size, get_connection(), create_request_header(method, path, header)); 306 | auto response = session->response; 307 | auto request_callback = std::make_shared, const error_code &)>>(std::move(request_callback_)); 308 | session->callback = [this, response, request_callback](const std::shared_ptr &connection, const error_code &ec) { 309 | { 310 | std::unique_lock lock(this->connections_mutex); 311 | connection->in_use = false; 312 | 313 | // Remove unused connections, but keep one open for HTTP persistent connection: 314 | std::size_t unused_connections = 0; 315 | for(auto it = this->connections.begin(); it != this->connections.end();) { 316 | if(ec && connection == *it) 317 | it = this->connections.erase(it); 318 | else if((*it)->in_use) 319 | ++it; 320 | else { 321 | ++unused_connections; 322 | if(unused_connections > 1) 323 | it = this->connections.erase(it); 324 | else 325 | ++it; 326 | } 327 | } 328 | } 329 | 330 | if(*request_callback) 331 | (*request_callback)(response, ec); 332 | }; 333 | 334 | content.seekg(0, std::ios::end); 335 | auto content_length = content.tellg(); 336 | content.seekg(0, std::ios::beg); 337 | std::ostream write_stream(session->request_streambuf.get()); 338 | if(content_length > 0) { 339 | auto header_it = header.find("Content-Length"); 340 | if(header_it == header.end()) { 341 | header_it = header.find("Transfer-Encoding"); 342 | if(header_it == header.end() || header_it->second != "chunked") 343 | write_stream << "Content-Length: " << content_length << "\r\n"; 344 | } 345 | } 346 | write_stream << "\r\n"; 347 | if(content_length > 0) 348 | write_stream << content.rdbuf(); 349 | 350 | connect(session); 351 | } 352 | 353 | /// Asynchronous request where setting and/or running Client's io_service is required. 354 | void request(const std::string &method, const std::string &path, std::istream &content, 355 | std::function, const error_code &)> &&request_callback) { 356 | request(method, path, content, CaseInsensitiveMultimap(), std::move(request_callback)); 357 | } 358 | 359 | /// Close connections 360 | void stop() noexcept { 361 | std::unique_lock lock(connections_mutex); 362 | for(auto it = connections.begin(); it != connections.end();) { 363 | error_code ec; 364 | (*it)->socket->lowest_layer().cancel(ec); 365 | it = connections.erase(it); 366 | } 367 | } 368 | 369 | virtual ~ClientBase() noexcept { 370 | handler_runner->stop(); 371 | stop(); 372 | } 373 | 374 | protected: 375 | bool internal_io_service = false; 376 | 377 | std::string host; 378 | unsigned short port; 379 | unsigned short default_port; 380 | 381 | std::unique_ptr query; 382 | 383 | std::unordered_set> connections; 384 | std::mutex connections_mutex; 385 | 386 | std::shared_ptr handler_runner; 387 | 388 | std::size_t concurrent_synchronous_requests = 0; 389 | std::mutex concurrent_synchronous_requests_mutex; 390 | 391 | ClientBase(const std::string &host_port, unsigned short default_port) noexcept : default_port(default_port), handler_runner(new ScopeRunner()) { 392 | auto parsed_host_port = parse_host_port(host_port, default_port); 393 | host = parsed_host_port.first; 394 | port = parsed_host_port.second; 395 | } 396 | 397 | std::shared_ptr get_connection() noexcept { 398 | std::shared_ptr connection; 399 | std::unique_lock lock(connections_mutex); 400 | 401 | if(!io_service) { 402 | io_service = std::make_shared(); 403 | internal_io_service = true; 404 | } 405 | 406 | for(auto it = connections.begin(); it != connections.end(); ++it) { 407 | if(!(*it)->in_use && !connection) { 408 | connection = *it; 409 | break; 410 | } 411 | } 412 | if(!connection) { 413 | connection = create_connection(); 414 | connections.emplace(connection); 415 | } 416 | connection->attempt_reconnect = true; 417 | connection->in_use = true; 418 | 419 | if(!query) { 420 | if(config.proxy_server.empty()) 421 | query = std::unique_ptr(new asio::ip::tcp::resolver::query(host, std::to_string(port))); 422 | else { 423 | auto proxy_host_port = parse_host_port(config.proxy_server, 8080); 424 | query = std::unique_ptr(new asio::ip::tcp::resolver::query(proxy_host_port.first, std::to_string(proxy_host_port.second))); 425 | } 426 | } 427 | 428 | return connection; 429 | } 430 | 431 | virtual std::shared_ptr create_connection() noexcept = 0; 432 | virtual void connect(const std::shared_ptr &) = 0; 433 | 434 | std::unique_ptr create_request_header(const std::string &method, const std::string &path, const CaseInsensitiveMultimap &header) const { 435 | auto corrected_path = path; 436 | if(corrected_path == "") 437 | corrected_path = "/"; 438 | if(!config.proxy_server.empty() && std::is_same::value) 439 | corrected_path = "http://" + host + ':' + std::to_string(port) + corrected_path; 440 | 441 | std::unique_ptr streambuf(new asio::streambuf()); 442 | std::ostream write_stream(streambuf.get()); 443 | write_stream << method << " " << corrected_path << " HTTP/1.1\r\n"; 444 | write_stream << "Host: " << host; 445 | if(port != default_port) 446 | write_stream << ':' << std::to_string(port); 447 | write_stream << "\r\n"; 448 | for(auto &h : header) 449 | write_stream << h.first << ": " << h.second << "\r\n"; 450 | return streambuf; 451 | } 452 | 453 | std::pair parse_host_port(const std::string &host_port, unsigned short default_port) const noexcept { 454 | std::pair parsed_host_port; 455 | std::size_t host_end = host_port.find(':'); 456 | if(host_end == std::string::npos) { 457 | parsed_host_port.first = host_port; 458 | parsed_host_port.second = default_port; 459 | } 460 | else { 461 | parsed_host_port.first = host_port.substr(0, host_end); 462 | parsed_host_port.second = static_cast(stoul(host_port.substr(host_end + 1))); 463 | } 464 | return parsed_host_port; 465 | } 466 | 467 | void write(const std::shared_ptr &session) { 468 | session->connection->set_timeout(); 469 | asio::async_write(*session->connection->socket, session->request_streambuf->data(), [this, session](const error_code &ec, std::size_t /*bytes_transferred*/) { 470 | session->connection->cancel_timeout(); 471 | auto lock = session->connection->handler_runner->continue_lock(); 472 | if(!lock) 473 | return; 474 | if(!ec) 475 | this->read(session); 476 | else 477 | session->callback(session->connection, ec); 478 | }); 479 | } 480 | 481 | void read(const std::shared_ptr &session) { 482 | session->connection->set_timeout(); 483 | asio::async_read_until(*session->connection->socket, session->response->streambuf, "\r\n\r\n", [this, session](const error_code &ec, std::size_t bytes_transferred) { 484 | session->connection->cancel_timeout(); 485 | auto lock = session->connection->handler_runner->continue_lock(); 486 | if(!lock) 487 | return; 488 | if((!ec || ec == asio::error::not_found) && session->response->streambuf.size() == session->response->streambuf.max_size()) { 489 | session->callback(session->connection, make_error_code::make_error_code(errc::message_size)); 490 | return; 491 | } 492 | if(!ec) { 493 | session->connection->attempt_reconnect = true; 494 | std::size_t num_additional_bytes = session->response->streambuf.size() - bytes_transferred; 495 | 496 | if(!ResponseMessage::parse(session->response->content, session->response->http_version, session->response->status_code, session->response->header)) { 497 | session->callback(session->connection, make_error_code::make_error_code(errc::protocol_error)); 498 | return; 499 | } 500 | 501 | auto header_it = session->response->header.find("Content-Length"); 502 | if(header_it != session->response->header.end()) { 503 | auto content_length = stoull(header_it->second); 504 | if(content_length > num_additional_bytes) { 505 | session->connection->set_timeout(); 506 | asio::async_read(*session->connection->socket, session->response->streambuf, asio::transfer_exactly(content_length - num_additional_bytes), [session](const error_code &ec, std::size_t /*bytes_transferred*/) { 507 | session->connection->cancel_timeout(); 508 | auto lock = session->connection->handler_runner->continue_lock(); 509 | if(!lock) 510 | return; 511 | if(!ec) { 512 | if(session->response->streambuf.size() == session->response->streambuf.max_size()) { 513 | session->callback(session->connection, make_error_code::make_error_code(errc::message_size)); 514 | return; 515 | } 516 | session->callback(session->connection, ec); 517 | } 518 | else 519 | session->callback(session->connection, ec); 520 | }); 521 | } 522 | else 523 | session->callback(session->connection, ec); 524 | } 525 | else if((header_it = session->response->header.find("Transfer-Encoding")) != session->response->header.end() && header_it->second == "chunked") { 526 | auto chunks_streambuf = std::make_shared(this->config.max_response_streambuf_size); 527 | this->read_chunked_transfer_encoded(session, chunks_streambuf); 528 | } 529 | else if(session->response->http_version < "1.1" || ((header_it = session->response->header.find("Session")) != session->response->header.end() && header_it->second == "close")) { 530 | session->connection->set_timeout(); 531 | asio::async_read(*session->connection->socket, session->response->streambuf, [session](const error_code &ec, std::size_t /*bytes_transferred*/) { 532 | session->connection->cancel_timeout(); 533 | auto lock = session->connection->handler_runner->continue_lock(); 534 | if(!lock) 535 | return; 536 | if(!ec) { 537 | if(session->response->streambuf.size() == session->response->streambuf.max_size()) { 538 | session->callback(session->connection, make_error_code::make_error_code(errc::message_size)); 539 | return; 540 | } 541 | session->callback(session->connection, ec); 542 | } 543 | else 544 | session->callback(session->connection, ec == asio::error::eof ? error_code() : ec); 545 | }); 546 | } 547 | else 548 | session->callback(session->connection, ec); 549 | } 550 | else { 551 | if(session->connection->attempt_reconnect && ec != asio::error::operation_aborted) { 552 | std::unique_lock lock(connections_mutex); 553 | auto it = connections.find(session->connection); 554 | if(it != connections.end()) { 555 | connections.erase(it); 556 | session->connection = create_connection(); 557 | session->connection->attempt_reconnect = false; 558 | session->connection->in_use = true; 559 | connections.emplace(session->connection); 560 | lock.unlock(); 561 | this->connect(session); 562 | } 563 | else { 564 | lock.unlock(); 565 | session->callback(session->connection, ec); 566 | } 567 | } 568 | else 569 | session->callback(session->connection, ec); 570 | } 571 | }); 572 | } 573 | 574 | void read_chunked_transfer_encoded(const std::shared_ptr &session, const std::shared_ptr &chunks_streambuf) { 575 | session->connection->set_timeout(); 576 | asio::async_read_until(*session->connection->socket, session->response->streambuf, "\r\n", [this, session, chunks_streambuf](const error_code &ec, size_t bytes_transferred) { 577 | session->connection->cancel_timeout(); 578 | auto lock = session->connection->handler_runner->continue_lock(); 579 | if(!lock) 580 | return; 581 | if((!ec || ec == asio::error::not_found) && session->response->streambuf.size() == session->response->streambuf.max_size()) { 582 | session->callback(session->connection, make_error_code::make_error_code(errc::message_size)); 583 | return; 584 | } 585 | if(!ec) { 586 | std::string line; 587 | getline(session->response->content, line); 588 | bytes_transferred -= line.size() + 1; 589 | line.pop_back(); 590 | unsigned long length = 0; 591 | try { 592 | length = stoul(line, 0, 16); 593 | } 594 | catch(...) { 595 | session->callback(session->connection, make_error_code::make_error_code(errc::protocol_error)); 596 | return; 597 | } 598 | 599 | auto num_additional_bytes = session->response->streambuf.size() - bytes_transferred; 600 | 601 | if((2 + length) > num_additional_bytes) { 602 | session->connection->set_timeout(); 603 | asio::async_read(*session->connection->socket, session->response->streambuf, asio::transfer_exactly(2 + length - num_additional_bytes), [this, session, chunks_streambuf, length](const error_code &ec, size_t /*bytes_transferred*/) { 604 | session->connection->cancel_timeout(); 605 | auto lock = session->connection->handler_runner->continue_lock(); 606 | if(!lock) 607 | return; 608 | if(!ec) { 609 | if(session->response->streambuf.size() == session->response->streambuf.max_size()) { 610 | session->callback(session->connection, make_error_code::make_error_code(errc::message_size)); 611 | return; 612 | } 613 | this->read_chunked_transfer_encoded_chunk(session, chunks_streambuf, length); 614 | } 615 | else 616 | session->callback(session->connection, ec); 617 | }); 618 | } 619 | else 620 | this->read_chunked_transfer_encoded_chunk(session, chunks_streambuf, length); 621 | } 622 | else 623 | session->callback(session->connection, ec); 624 | }); 625 | } 626 | 627 | void read_chunked_transfer_encoded_chunk(const std::shared_ptr &session, const std::shared_ptr &chunks_streambuf, unsigned long length) { 628 | std::ostream tmp_stream(chunks_streambuf.get()); 629 | if(length > 0) { 630 | std::unique_ptr buffer(new char[length]); 631 | session->response->content.read(buffer.get(), static_cast(length)); 632 | tmp_stream.write(buffer.get(), static_cast(length)); 633 | if(chunks_streambuf->size() == chunks_streambuf->max_size()) { 634 | session->callback(session->connection, make_error_code::make_error_code(errc::message_size)); 635 | return; 636 | } 637 | } 638 | 639 | // Remove "\r\n" 640 | session->response->content.get(); 641 | session->response->content.get(); 642 | 643 | if(length > 0) 644 | read_chunked_transfer_encoded(session, chunks_streambuf); 645 | else { 646 | if(chunks_streambuf->size() > 0) { 647 | std::ostream ostream(&session->response->streambuf); 648 | ostream << chunks_streambuf.get(); 649 | } 650 | error_code ec; 651 | session->callback(session->connection, ec); 652 | } 653 | } 654 | }; 655 | 656 | template 657 | class Client : public ClientBase {}; 658 | 659 | using HTTP = asio::ip::tcp::socket; 660 | 661 | template <> 662 | class Client : public ClientBase { 663 | public: 664 | Client(const std::string &server_port_path) noexcept : ClientBase::ClientBase(server_port_path, 80) {} 665 | 666 | protected: 667 | std::shared_ptr create_connection() noexcept override { 668 | return std::make_shared(handler_runner, config.timeout, *io_service); 669 | } 670 | 671 | void connect(const std::shared_ptr &session) override { 672 | if(!session->connection->socket->lowest_layer().is_open()) { 673 | auto resolver = std::make_shared(*io_service); 674 | session->connection->set_timeout(config.timeout_connect); 675 | resolver->async_resolve(*query, [this, session, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator it) { 676 | session->connection->cancel_timeout(); 677 | auto lock = session->connection->handler_runner->continue_lock(); 678 | if(!lock) 679 | return; 680 | if(!ec) { 681 | session->connection->set_timeout(config.timeout_connect); 682 | asio::async_connect(*session->connection->socket, it, [this, session, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator /*it*/) { 683 | session->connection->cancel_timeout(); 684 | auto lock = session->connection->handler_runner->continue_lock(); 685 | if(!lock) 686 | return; 687 | if(!ec) { 688 | asio::ip::tcp::no_delay option(true); 689 | error_code ec; 690 | session->connection->socket->set_option(option, ec); 691 | this->write(session); 692 | } 693 | else 694 | session->callback(session->connection, ec); 695 | }); 696 | } 697 | else 698 | session->callback(session->connection, ec); 699 | }); 700 | } 701 | else 702 | write(session); 703 | } 704 | }; 705 | } // namespace SimpleWeb 706 | 707 | #endif /* CLIENT_HTTP_HPP */ 708 | -------------------------------------------------------------------------------- /server_http.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_HTTP_HPP 2 | #define SERVER_HTTP_HPP 3 | 4 | #include "utility.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #ifdef USE_STANDALONE_ASIO 15 | #include 16 | #include 17 | namespace SimpleWeb { 18 | using error_code = std::error_code; 19 | using errc = std::errc; 20 | namespace make_error_code = std; 21 | } // namespace SimpleWeb 22 | #else 23 | #include 24 | #include 25 | namespace SimpleWeb { 26 | namespace asio = boost::asio; 27 | using error_code = boost::system::error_code; 28 | namespace errc = boost::system::errc; 29 | namespace make_error_code = boost::system::errc; 30 | } // namespace SimpleWeb 31 | #endif 32 | 33 | // Late 2017 TODO: remove the following checks and always use std::regex 34 | #ifdef USE_BOOST_REGEX 35 | #include 36 | namespace SimpleWeb { 37 | namespace regex = boost; 38 | } 39 | #else 40 | #include 41 | namespace SimpleWeb { 42 | namespace regex = std; 43 | } 44 | #endif 45 | 46 | namespace SimpleWeb { 47 | template 48 | class Server; 49 | 50 | template 51 | class ServerBase { 52 | protected: 53 | class Session; 54 | 55 | public: 56 | class Response : public std::enable_shared_from_this, public std::ostream { 57 | friend class ServerBase; 58 | friend class Server; 59 | 60 | asio::streambuf streambuf; 61 | 62 | std::shared_ptr session; 63 | long timeout_content; 64 | 65 | Response(std::shared_ptr session, long timeout_content) noexcept : std::ostream(&streambuf), session(std::move(session)), timeout_content(timeout_content) {} 66 | 67 | template 68 | void write_header(const CaseInsensitiveMultimap &header, size_type size) { 69 | bool content_length_written = false; 70 | bool chunked_transfer_encoding = false; 71 | for(auto &field : header) { 72 | if(!content_length_written && case_insensitive_equal(field.first, "content-length")) 73 | content_length_written = true; 74 | else if(!chunked_transfer_encoding && case_insensitive_equal(field.first, "transfer-encoding") && case_insensitive_equal(field.second, "chunked")) 75 | chunked_transfer_encoding = true; 76 | 77 | *this << field.first << ": " << field.second << "\r\n"; 78 | } 79 | if(!content_length_written && !chunked_transfer_encoding && !close_connection_after_response) 80 | *this << "Content-Length: " << size << "\r\n\r\n"; 81 | else 82 | *this << "\r\n"; 83 | } 84 | 85 | public: 86 | std::size_t size() noexcept { 87 | return streambuf.size(); 88 | } 89 | 90 | /// Use this function if you need to recursively send parts of a longer message 91 | void send(const std::function &callback = nullptr) noexcept { 92 | session->connection->set_timeout(timeout_content); 93 | auto self = this->shared_from_this(); // Keep Response instance alive through the following async_write 94 | asio::async_write(*session->connection->socket, streambuf, [self, callback](const error_code &ec, std::size_t /*bytes_transferred*/) { 95 | self->session->connection->cancel_timeout(); 96 | auto lock = self->session->connection->handler_runner->continue_lock(); 97 | if(!lock) 98 | return; 99 | if(callback) 100 | callback(ec); 101 | }); 102 | } 103 | 104 | /// Write directly to stream buffer using std::ostream::write 105 | void write(const char_type *ptr, std::streamsize n) { 106 | std::ostream::write(ptr, n); 107 | } 108 | 109 | /// Convenience function for writing status line, potential header fields, and empty content 110 | void write(StatusCode status_code = StatusCode::success_ok, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 111 | *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; 112 | write_header(header, 0); 113 | } 114 | 115 | /// Convenience function for writing status line, header fields, and content 116 | void write(StatusCode status_code, const std::string &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 117 | *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; 118 | write_header(header, content.size()); 119 | if(!content.empty()) 120 | *this << content; 121 | } 122 | 123 | /// Convenience function for writing status line, header fields, and content 124 | void write(StatusCode status_code, std::istream &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 125 | *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; 126 | content.seekg(0, std::ios::end); 127 | auto size = content.tellg(); 128 | content.seekg(0, std::ios::beg); 129 | write_header(header, size); 130 | if(size) 131 | *this << content.rdbuf(); 132 | } 133 | 134 | /// Convenience function for writing success status line, header fields, and content 135 | void write(const std::string &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 136 | write(StatusCode::success_ok, content, header); 137 | } 138 | 139 | /// Convenience function for writing success status line, header fields, and content 140 | void write(std::istream &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 141 | write(StatusCode::success_ok, content, header); 142 | } 143 | 144 | /// Convenience function for writing success status line, and header fields 145 | void write(const CaseInsensitiveMultimap &header) { 146 | write(StatusCode::success_ok, std::string(), header); 147 | } 148 | 149 | /// If true, force server to close the connection after the response have been sent. 150 | /// 151 | /// This is useful when implementing a HTTP/1.0-server sending content 152 | /// without specifying the content length. 153 | bool close_connection_after_response = false; 154 | }; 155 | 156 | class Content : public std::istream { 157 | friend class ServerBase; 158 | 159 | public: 160 | std::size_t size() noexcept { 161 | return streambuf.size(); 162 | } 163 | /// Convenience function to return std::string. The stream buffer is consumed. 164 | std::string string() noexcept { 165 | try { 166 | std::string str; 167 | auto size = streambuf.size(); 168 | str.resize(size); 169 | read(&str[0], static_cast(size)); 170 | return str; 171 | } 172 | catch(...) { 173 | return std::string(); 174 | } 175 | } 176 | 177 | private: 178 | asio::streambuf &streambuf; 179 | Content(asio::streambuf &streambuf) noexcept : std::istream(&streambuf), streambuf(streambuf) {} 180 | }; 181 | 182 | class Request { 183 | friend class ServerBase; 184 | friend class Server; 185 | friend class Session; 186 | 187 | asio::streambuf streambuf; 188 | 189 | Request(std::size_t max_request_streambuf_size, std::shared_ptr remote_endpoint) noexcept 190 | : streambuf(max_request_streambuf_size), content(streambuf), remote_endpoint(std::move(remote_endpoint)) {} 191 | 192 | public: 193 | std::string method, path, query_string, http_version; 194 | 195 | Content content; 196 | 197 | CaseInsensitiveMultimap header; 198 | 199 | regex::smatch path_match; 200 | 201 | std::shared_ptr remote_endpoint; 202 | 203 | /// The time point when the request header was fully read. 204 | std::chrono::system_clock::time_point header_read_time; 205 | 206 | std::string remote_endpoint_address() noexcept { 207 | try { 208 | return remote_endpoint->address().to_string(); 209 | } 210 | catch(...) { 211 | return std::string(); 212 | } 213 | } 214 | 215 | unsigned short remote_endpoint_port() noexcept { 216 | return remote_endpoint->port(); 217 | } 218 | 219 | /// Returns query keys with percent-decoded values. 220 | CaseInsensitiveMultimap parse_query_string() noexcept { 221 | return SimpleWeb::QueryString::parse(query_string); 222 | } 223 | }; 224 | 225 | protected: 226 | class Connection : public std::enable_shared_from_this { 227 | public: 228 | template 229 | Connection(std::shared_ptr handler_runner, Args &&... args) noexcept : handler_runner(std::move(handler_runner)), socket(new socket_type(std::forward(args)...)) {} 230 | 231 | std::shared_ptr handler_runner; 232 | 233 | std::unique_ptr socket; // Socket must be unique_ptr since asio::ssl::stream is not movable 234 | std::mutex socket_close_mutex; 235 | 236 | std::unique_ptr timer; 237 | 238 | std::shared_ptr remote_endpoint; 239 | 240 | void close() noexcept { 241 | error_code ec; 242 | std::unique_lock lock(socket_close_mutex); // The following operations seems to be needed to run sequentially 243 | socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ec); 244 | socket->lowest_layer().close(ec); 245 | } 246 | 247 | void set_timeout(long seconds) noexcept { 248 | if(seconds == 0) { 249 | timer = nullptr; 250 | return; 251 | } 252 | 253 | timer = std::unique_ptr(new asio::steady_timer(socket->get_io_service())); 254 | timer->expires_from_now(std::chrono::seconds(seconds)); 255 | auto self = this->shared_from_this(); 256 | timer->async_wait([self](const error_code &ec) { 257 | if(!ec) 258 | self->close(); 259 | }); 260 | } 261 | 262 | void cancel_timeout() noexcept { 263 | if(timer) { 264 | error_code ec; 265 | timer->cancel(ec); 266 | } 267 | } 268 | }; 269 | 270 | class Session { 271 | public: 272 | Session(std::size_t max_request_streambuf_size, std::shared_ptr connection) noexcept : connection(std::move(connection)) { 273 | if(!this->connection->remote_endpoint) { 274 | error_code ec; 275 | this->connection->remote_endpoint = std::make_shared(this->connection->socket->lowest_layer().remote_endpoint(ec)); 276 | } 277 | request = std::shared_ptr(new Request(max_request_streambuf_size, this->connection->remote_endpoint)); 278 | } 279 | 280 | std::shared_ptr connection; 281 | std::shared_ptr request; 282 | }; 283 | 284 | public: 285 | class Config { 286 | friend class ServerBase; 287 | 288 | Config(unsigned short port) noexcept : port(port) {} 289 | 290 | public: 291 | /// Port number to use. Defaults to 80 for HTTP and 443 for HTTPS. Set to 0 get an assigned port. 292 | unsigned short port; 293 | /// If io_service is not set, number of threads that the server will use when start() is called. 294 | /// Defaults to 1 thread. 295 | std::size_t thread_pool_size = 1; 296 | /// Timeout on request handling. Defaults to 5 seconds. 297 | long timeout_request = 5; 298 | /// Timeout on content handling. Defaults to 300 seconds. 299 | long timeout_content = 300; 300 | /// Maximum size of request stream buffer. Defaults to architecture maximum. 301 | /// Reaching this limit will result in a message_size error code. 302 | std::size_t max_request_streambuf_size = std::numeric_limits::max(); 303 | /// IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation. 304 | /// If empty, the address will be any address. 305 | std::string address; 306 | /// Set to false to avoid binding the socket to an address that is already in use. Defaults to true. 307 | bool reuse_address = true; 308 | }; 309 | /// Set before calling start(). 310 | Config config; 311 | 312 | private: 313 | class regex_orderable : public regex::regex { 314 | std::string str; 315 | 316 | public: 317 | regex_orderable(const char *regex_cstr) : regex::regex(regex_cstr), str(regex_cstr) {} 318 | regex_orderable(std::string regex_str) : regex::regex(regex_str), str(std::move(regex_str)) {} 319 | bool operator<(const regex_orderable &rhs) const noexcept { 320 | return str < rhs.str; 321 | } 322 | }; 323 | 324 | public: 325 | /// Warning: do not add or remove resources after start() is called 326 | std::map::Response>, std::shared_ptr::Request>)>>> resource; 327 | 328 | std::map::Response>, std::shared_ptr::Request>)>> default_resource; 329 | 330 | std::function::Request>, const error_code &)> on_error; 331 | 332 | std::function &, std::shared_ptr::Request>)> on_upgrade; 333 | 334 | /// If you have your own asio::io_service, store its pointer here before running start(). 335 | std::shared_ptr io_service; 336 | 337 | /// If you know the server port in advance, use start() instead. 338 | /// Returns assigned port. If io_service is not set, an internal io_service is created instead. 339 | /// Call before accept_and_run(). 340 | unsigned short bind() { 341 | asio::ip::tcp::endpoint endpoint; 342 | if(config.address.size() > 0) 343 | endpoint = asio::ip::tcp::endpoint(asio::ip::address::from_string(config.address), config.port); 344 | else 345 | endpoint = asio::ip::tcp::endpoint(asio::ip::tcp::v4(), config.port); 346 | 347 | if(!io_service) { 348 | io_service = std::make_shared(); 349 | internal_io_service = true; 350 | } 351 | 352 | if(!acceptor) 353 | acceptor = std::unique_ptr(new asio::ip::tcp::acceptor(*io_service)); 354 | acceptor->open(endpoint.protocol()); 355 | acceptor->set_option(asio::socket_base::reuse_address(config.reuse_address)); 356 | acceptor->bind(endpoint); 357 | 358 | after_bind(); 359 | 360 | return acceptor->local_endpoint().port(); 361 | } 362 | 363 | /// If you know the server port in advance, use start() instead. 364 | /// Accept requests, and if io_service was not set before calling bind(), run the internal io_service instead. 365 | /// Call after bind(). 366 | void accept_and_run() { 367 | acceptor->listen(); 368 | accept(); 369 | 370 | if(internal_io_service) { 371 | if(io_service->stopped()) 372 | io_service->reset(); 373 | 374 | // If thread_pool_size>1, start m_io_service.run() in (thread_pool_size-1) threads for thread-pooling 375 | threads.clear(); 376 | for(std::size_t c = 1; c < config.thread_pool_size; c++) { 377 | threads.emplace_back([this]() { 378 | this->io_service->run(); 379 | }); 380 | } 381 | 382 | // Main thread 383 | if(config.thread_pool_size > 0) 384 | io_service->run(); 385 | 386 | // Wait for the rest of the threads, if any, to finish as well 387 | for(auto &t : threads) 388 | t.join(); 389 | } 390 | } 391 | 392 | /// Start the server by calling bind() and accept_and_run() 393 | void start() { 394 | bind(); 395 | accept_and_run(); 396 | } 397 | 398 | /// Stop accepting new requests, and close current connections. 399 | void stop() noexcept { 400 | if(acceptor) { 401 | error_code ec; 402 | acceptor->close(ec); 403 | 404 | { 405 | std::unique_lock lock(*connections_mutex); 406 | for(auto &connection : *connections) 407 | connection->close(); 408 | connections->clear(); 409 | } 410 | 411 | if(internal_io_service) 412 | io_service->stop(); 413 | } 414 | } 415 | 416 | virtual ~ServerBase() noexcept { 417 | handler_runner->stop(); 418 | stop(); 419 | } 420 | 421 | protected: 422 | bool internal_io_service = false; 423 | 424 | std::unique_ptr acceptor; 425 | std::vector threads; 426 | 427 | std::shared_ptr> connections; 428 | std::shared_ptr connections_mutex; 429 | 430 | std::shared_ptr handler_runner; 431 | 432 | ServerBase(unsigned short port) noexcept : config(port), connections(new std::unordered_set()), connections_mutex(new std::mutex()), handler_runner(new ScopeRunner()) {} 433 | 434 | virtual void after_bind() {} 435 | virtual void accept() = 0; 436 | 437 | template 438 | std::shared_ptr create_connection(Args &&... args) noexcept { 439 | auto connections = this->connections; 440 | auto connections_mutex = this->connections_mutex; 441 | auto connection = std::shared_ptr(new Connection(handler_runner, std::forward(args)...), [connections, connections_mutex](Connection *connection) { 442 | { 443 | std::unique_lock lock(*connections_mutex); 444 | auto it = connections->find(connection); 445 | if(it != connections->end()) 446 | connections->erase(it); 447 | } 448 | delete connection; 449 | }); 450 | { 451 | std::unique_lock lock(*connections_mutex); 452 | connections->emplace(connection.get()); 453 | } 454 | return connection; 455 | } 456 | 457 | void read(const std::shared_ptr &session) { 458 | session->connection->set_timeout(config.timeout_request); 459 | 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) { 460 | session->connection->cancel_timeout(); 461 | auto lock = session->connection->handler_runner->continue_lock(); 462 | if(!lock) 463 | return; 464 | session->request->header_read_time = std::chrono::system_clock::now(); 465 | if((!ec || ec == asio::error::not_found) && session->request->streambuf.size() == session->request->streambuf.max_size()) { 466 | auto response = std::shared_ptr(new Response(session, this->config.timeout_content)); 467 | response->write(StatusCode::client_error_payload_too_large); 468 | response->send(); 469 | if(this->on_error) 470 | this->on_error(session->request, make_error_code::make_error_code(errc::message_size)); 471 | return; 472 | } 473 | if(!ec) { 474 | // request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs: 475 | // "After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter" 476 | // The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the 477 | // streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content). 478 | std::size_t num_additional_bytes = session->request->streambuf.size() - bytes_transferred; 479 | 480 | if(!RequestMessage::parse(session->request->content, session->request->method, session->request->path, 481 | session->request->query_string, session->request->http_version, session->request->header)) { 482 | if(this->on_error) 483 | this->on_error(session->request, make_error_code::make_error_code(errc::protocol_error)); 484 | return; 485 | } 486 | 487 | // If content, read that as well 488 | auto header_it = session->request->header.find("Content-Length"); 489 | if(header_it != session->request->header.end()) { 490 | unsigned long long content_length = 0; 491 | try { 492 | content_length = stoull(header_it->second); 493 | } 494 | catch(const std::exception &) { 495 | if(this->on_error) 496 | this->on_error(session->request, make_error_code::make_error_code(errc::protocol_error)); 497 | return; 498 | } 499 | if(content_length > num_additional_bytes) { 500 | session->connection->set_timeout(config.timeout_content); 501 | 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*/) { 502 | session->connection->cancel_timeout(); 503 | auto lock = session->connection->handler_runner->continue_lock(); 504 | if(!lock) 505 | return; 506 | if(!ec) { 507 | if(session->request->streambuf.size() == session->request->streambuf.max_size()) { 508 | auto response = std::shared_ptr(new Response(session, this->config.timeout_content)); 509 | response->write(StatusCode::client_error_payload_too_large); 510 | response->send(); 511 | if(this->on_error) 512 | this->on_error(session->request, make_error_code::make_error_code(errc::message_size)); 513 | return; 514 | } 515 | this->find_resource(session); 516 | } 517 | else if(this->on_error) 518 | this->on_error(session->request, ec); 519 | }); 520 | } 521 | else 522 | this->find_resource(session); 523 | } 524 | else if((header_it = session->request->header.find("Transfer-Encoding")) != session->request->header.end() && header_it->second == "chunked") { 525 | auto chunks_streambuf = std::make_shared(this->config.max_request_streambuf_size); 526 | this->read_chunked_transfer_encoded(session, chunks_streambuf); 527 | } 528 | else 529 | this->find_resource(session); 530 | } 531 | else if(this->on_error) 532 | this->on_error(session->request, ec); 533 | }); 534 | } 535 | 536 | void read_chunked_transfer_encoded(const std::shared_ptr &session, const std::shared_ptr &chunks_streambuf) { 537 | session->connection->set_timeout(config.timeout_content); 538 | asio::async_read_until(*session->connection->socket, session->request->streambuf, "\r\n", [this, session, chunks_streambuf](const error_code &ec, size_t bytes_transferred) { 539 | session->connection->cancel_timeout(); 540 | auto lock = session->connection->handler_runner->continue_lock(); 541 | if(!lock) 542 | return; 543 | if((!ec || ec == asio::error::not_found) && session->request->streambuf.size() == session->request->streambuf.max_size()) { 544 | auto response = std::shared_ptr(new Response(session, this->config.timeout_content)); 545 | response->write(StatusCode::client_error_payload_too_large); 546 | response->send(); 547 | if(this->on_error) 548 | this->on_error(session->request, make_error_code::make_error_code(errc::message_size)); 549 | return; 550 | } 551 | if(!ec) { 552 | std::string line; 553 | getline(session->request->content, line); 554 | bytes_transferred -= line.size() + 1; 555 | line.pop_back(); 556 | unsigned long length = 0; 557 | try { 558 | length = stoul(line, 0, 16); 559 | } 560 | catch(...) { 561 | if(this->on_error) 562 | this->on_error(session->request, make_error_code::make_error_code(errc::protocol_error)); 563 | return; 564 | } 565 | 566 | auto num_additional_bytes = session->request->streambuf.size() - bytes_transferred; 567 | 568 | if((2 + length) > num_additional_bytes) { 569 | session->connection->set_timeout(config.timeout_content); 570 | asio::async_read(*session->connection->socket, session->request->streambuf, asio::transfer_exactly(2 + length - num_additional_bytes), [this, session, chunks_streambuf, length](const error_code &ec, size_t /*bytes_transferred*/) { 571 | session->connection->cancel_timeout(); 572 | auto lock = session->connection->handler_runner->continue_lock(); 573 | if(!lock) 574 | return; 575 | if(!ec) { 576 | if(session->request->streambuf.size() == session->request->streambuf.max_size()) { 577 | auto response = std::shared_ptr(new Response(session, this->config.timeout_content)); 578 | response->write(StatusCode::client_error_payload_too_large); 579 | response->send(); 580 | if(this->on_error) 581 | this->on_error(session->request, make_error_code::make_error_code(errc::message_size)); 582 | return; 583 | } 584 | this->read_chunked_transfer_encoded_chunk(session, chunks_streambuf, length); 585 | } 586 | else if(this->on_error) 587 | this->on_error(session->request, ec); 588 | }); 589 | } 590 | else 591 | this->read_chunked_transfer_encoded_chunk(session, chunks_streambuf, length); 592 | } 593 | else if(this->on_error) 594 | this->on_error(session->request, ec); 595 | }); 596 | } 597 | 598 | void read_chunked_transfer_encoded_chunk(const std::shared_ptr &session, const std::shared_ptr &chunks_streambuf, unsigned long length) { 599 | std::ostream tmp_stream(chunks_streambuf.get()); 600 | if(length > 0) { 601 | std::unique_ptr buffer(new char[length]); 602 | session->request->content.read(buffer.get(), static_cast(length)); 603 | tmp_stream.write(buffer.get(), static_cast(length)); 604 | if(chunks_streambuf->size() == chunks_streambuf->max_size()) { 605 | auto response = std::shared_ptr(new Response(session, this->config.timeout_content)); 606 | response->write(StatusCode::client_error_payload_too_large); 607 | response->send(); 608 | if(this->on_error) 609 | this->on_error(session->request, make_error_code::make_error_code(errc::message_size)); 610 | return; 611 | } 612 | } 613 | 614 | // Remove "\r\n" 615 | session->request->content.get(); 616 | session->request->content.get(); 617 | 618 | if(length > 0) 619 | read_chunked_transfer_encoded(session, chunks_streambuf); 620 | else { 621 | if(chunks_streambuf->size() > 0) { 622 | std::ostream ostream(&session->request->streambuf); 623 | ostream << chunks_streambuf.get(); 624 | } 625 | this->find_resource(session); 626 | } 627 | } 628 | 629 | void find_resource(const std::shared_ptr &session) { 630 | // Upgrade connection 631 | if(on_upgrade) { 632 | auto it = session->request->header.find("Upgrade"); 633 | if(it != session->request->header.end()) { 634 | // remove connection from connections 635 | { 636 | std::unique_lock lock(*connections_mutex); 637 | auto it = connections->find(session->connection.get()); 638 | if(it != connections->end()) 639 | connections->erase(it); 640 | } 641 | 642 | on_upgrade(session->connection->socket, session->request); 643 | return; 644 | } 645 | } 646 | // Find path- and method-match, and call write 647 | for(auto ®ex_method : resource) { 648 | auto it = regex_method.second.find(session->request->method); 649 | if(it != regex_method.second.end()) { 650 | regex::smatch sm_res; 651 | if(regex::regex_match(session->request->path, sm_res, regex_method.first)) { 652 | session->request->path_match = std::move(sm_res); 653 | write(session, it->second); 654 | return; 655 | } 656 | } 657 | } 658 | auto it = default_resource.find(session->request->method); 659 | if(it != default_resource.end()) 660 | write(session, it->second); 661 | } 662 | 663 | void write(const std::shared_ptr &session, 664 | std::function::Response>, std::shared_ptr::Request>)> &resource_function) { 665 | session->connection->set_timeout(config.timeout_content); 666 | auto response = std::shared_ptr(new Response(session, config.timeout_content), [this](Response *response_ptr) { 667 | auto response = std::shared_ptr(response_ptr); 668 | response->send([this, response](const error_code &ec) { 669 | if(!ec) { 670 | if(response->close_connection_after_response) 671 | return; 672 | 673 | auto range = response->session->request->header.equal_range("Connection"); 674 | for(auto it = range.first; it != range.second; it++) { 675 | if(case_insensitive_equal(it->second, "close")) 676 | return; 677 | else if(case_insensitive_equal(it->second, "keep-alive")) { 678 | auto new_session = std::make_shared(this->config.max_request_streambuf_size, response->session->connection); 679 | this->read(new_session); 680 | return; 681 | } 682 | } 683 | if(response->session->request->http_version >= "1.1") { 684 | auto new_session = std::make_shared(this->config.max_request_streambuf_size, response->session->connection); 685 | this->read(new_session); 686 | return; 687 | } 688 | } 689 | else if(this->on_error) 690 | this->on_error(response->session->request, ec); 691 | }); 692 | }); 693 | 694 | try { 695 | resource_function(response, session->request); 696 | } 697 | catch(const std::exception &) { 698 | if(on_error) 699 | on_error(session->request, make_error_code::make_error_code(errc::operation_canceled)); 700 | return; 701 | } 702 | } 703 | }; 704 | 705 | template 706 | class Server : public ServerBase {}; 707 | 708 | using HTTP = asio::ip::tcp::socket; 709 | 710 | template <> 711 | class Server : public ServerBase { 712 | public: 713 | Server() noexcept : ServerBase::ServerBase(80) {} 714 | 715 | protected: 716 | void accept() override { 717 | auto connection = create_connection(*io_service); 718 | 719 | acceptor->async_accept(*connection->socket, [this, connection](const error_code &ec) { 720 | auto lock = connection->handler_runner->continue_lock(); 721 | if(!lock) 722 | return; 723 | 724 | // Immediately start accepting a new connection (unless io_service has been stopped) 725 | if(ec != asio::error::operation_aborted) 726 | this->accept(); 727 | 728 | auto session = std::make_shared(config.max_request_streambuf_size, connection); 729 | 730 | if(!ec) { 731 | asio::ip::tcp::no_delay option(true); 732 | error_code ec; 733 | session->connection->socket->set_option(option, ec); 734 | 735 | this->read(session); 736 | } 737 | else if(this->on_error) 738 | this->on_error(session->request, ec); 739 | }); 740 | } 741 | }; 742 | } // namespace SimpleWeb 743 | 744 | #endif /* SERVER_HTTP_HPP */ 745 | --------------------------------------------------------------------------------