├── web ├── test.html └── index.html ├── .clang-format ├── .gitignore ├── tests ├── CMakeLists.txt ├── crypto_test.cpp ├── parse_test.cpp └── io_test.cpp ├── .travis.yml ├── LICENSE ├── CMakeLists.txt ├── README.md ├── server_https.hpp ├── utility.hpp ├── client_https.hpp ├── status_code.hpp ├── crypto.hpp ├── http_examples.cpp ├── https_examples.cpp ├── server_http.hpp └── client_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 | 13 | # executables 14 | http_examples 15 | https_examples 16 | io_test 17 | parse_test 18 | crypto_test 19 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-access-control") 2 | 3 | add_executable(io_test io_test.cpp) 4 | target_link_libraries(io_test ${Boost_LIBRARIES}) 5 | target_link_libraries(io_test ${CMAKE_THREAD_LIBS_INIT}) 6 | 7 | add_executable(parse_test parse_test.cpp) 8 | target_link_libraries(parse_test ${Boost_LIBRARIES}) 9 | target_link_libraries(parse_test ${CMAKE_THREAD_LIBS_INIT}) 10 | 11 | if(MSYS) #TODO: Is MSYS true when MSVC is true? 12 | target_link_libraries(io_test ws2_32 wsock32) 13 | target_link_libraries(parse_test ws2_32 wsock32) 14 | endif() 15 | 16 | add_test(io_test io_test) 17 | add_test(parse_test parse_test) 18 | 19 | if(OPENSSL_FOUND) 20 | add_executable(crypto_test crypto_test.cpp) 21 | target_link_libraries(crypto_test ${OPENSSL_CRYPTO_LIBRARY}) 22 | add_test(crypto_test crypto_test) 23 | endif() 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | script: 7 | - sudo docker run -it -v "$PWD:/repository" debian:testing sh -c " 8 | apt-get update && 9 | apt-get install --yes cmake make g\+\+ clang perl libssl-dev libboost-thread-dev libboost-regex-dev libboost-date-time-dev libboost-filesystem-dev libasio-dev && 10 | cd /repository && mkdir build && cd build && 11 | scan-build cmake -DCMAKE_CXX_FLAGS=-Werror .. && 12 | scan-build --status-bugs make && 13 | rm -r * && 14 | CXX=clang++ cmake -DCMAKE_CXX_FLAGS=-Werror .. && 15 | make && 16 | rm -r * && 17 | CXX=g++ cmake -DCMAKE_CXX_FLAGS=-Werror .. && 18 | make && 19 | CTEST_OUTPUT_ON_FAILURE=1 make test && 20 | rm -r * && 21 | CXX=g++ cmake -DCMAKE_CXX_FLAGS=\"-Werror -O3 -DUSE_STANDALONE_ASIO\" .. && 22 | make && 23 | CTEST_OUTPUT_ON_FAILURE=1 make test 24 | " 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 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 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.8.8) 2 | project (Simple-Web-Server) 3 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Wextra") 4 | 5 | include_directories(.) 6 | 7 | find_package(Threads REQUIRED) 8 | 9 | set(BOOST_COMPONENTS system thread filesystem date_time) 10 | # Late 2017 TODO: remove the following checks and always use std::regex 11 | if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 12 | if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) 13 | set(BOOST_COMPONENTS ${BOOST_COMPONENTS} regex) 14 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUSE_BOOST_REGEX") 15 | endif() 16 | endif() 17 | find_package(Boost 1.53.0 COMPONENTS ${BOOST_COMPONENTS} REQUIRED) 18 | include_directories(SYSTEM ${Boost_INCLUDE_DIR}) 19 | 20 | if(APPLE) 21 | set(OPENSSL_ROOT_DIR "/usr/local/opt/openssl") 22 | endif() 23 | 24 | add_executable(http_examples http_examples.cpp) 25 | target_link_libraries(http_examples ${Boost_LIBRARIES}) 26 | target_link_libraries(http_examples ${CMAKE_THREAD_LIBS_INIT}) 27 | 28 | #TODO: add requirement for version 1.0.1g (can it be done in one line?) 29 | find_package(OpenSSL) 30 | 31 | if(OPENSSL_FOUND) 32 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_OPENSSL") 33 | target_link_libraries(http_examples ${OPENSSL_LIBRARIES}) 34 | include_directories(SYSTEM ${OPENSSL_INCLUDE_DIR}) 35 | 36 | add_executable(https_examples https_examples.cpp) 37 | target_link_libraries(https_examples ${Boost_LIBRARIES}) 38 | target_link_libraries(https_examples ${OPENSSL_LIBRARIES}) 39 | target_link_libraries(https_examples ${CMAKE_THREAD_LIBS_INIT}) 40 | endif() 41 | 42 | if(MSYS) #TODO: Is MSYS true when MSVC is true? 43 | target_link_libraries(http_examples ws2_32 wsock32) 44 | if(OPENSSL_FOUND) 45 | target_link_libraries(https_examples ws2_32 wsock32) 46 | endif() 47 | endif() 48 | 49 | enable_testing() 50 | add_subdirectory(tests) 51 | 52 | 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) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple-Web-Server [![Build Status](https://travis-ci.org/eidheim/Simple-Web-Server.svg?branch=master)](https://travis-ci.org/eidheim/Simple-Web-Server) 2 | ================= 3 | 4 | A very simple, fast, multithreaded, platform independent HTTP and HTTPS server and client library implemented using C++11 and Boost.Asio. Created to be an easy way to make REST resources available from C++ applications. 5 | 6 | See https://github.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://github.com/cppit/jucipp. 7 | 8 | ### Features 9 | 10 | * Asynchronous request handling 11 | * Thread pool if needed 12 | * Platform independent 13 | * HTTPS support 14 | * HTTP persistent connection (for HTTP/1.1) 15 | * Client supports chunked transfer encoding 16 | * 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) 17 | * Simple way to add REST resources using regex for path, and anonymous functions 18 | 19 | ### Usage 20 | 21 | See http_examples.cpp or https_examples.cpp for example usage. 22 | 23 | See particularly the JSON-POST (using Boost.PropertyTree) and the GET /match/[number] examples, which are most relevant. 24 | 25 | ### Dependencies 26 | 27 | * Boost C++ libraries 28 | * For HTTPS: OpenSSL libraries 29 | 30 | ### Compile and run 31 | 32 | Compile with a C++11 compliant compiler: 33 | ```sh 34 | mkdir build 35 | cd build 36 | cmake .. 37 | make 38 | cd .. 39 | ``` 40 | 41 | #### HTTP 42 | 43 | Run the server and client examples: `./build/http_examples` 44 | 45 | Direct your favorite browser to for instance http://localhost:8080/ 46 | 47 | #### HTTPS 48 | 49 | 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 50 | 51 | Run the server and client examples: `./build/https_examples` 52 | 53 | Direct your favorite browser to for instance https://localhost:8080/ 54 | 55 | -------------------------------------------------------------------------------- /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 | 18 | const vector> md5_string_tests = { 19 | {"", "d41d8cd98f00b204e9800998ecf8427e"}, 20 | {"The quick brown fox jumps over the lazy dog", "9e107d9d372bb6826bd81d3542a419d6"}}; 21 | 22 | const vector> sha1_string_tests = { 23 | {"", "da39a3ee5e6b4b0d3255bfef95601890afd80709"}, 24 | {"The quick brown fox jumps over the lazy dog", "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"}}; 25 | 26 | const vector> sha256_string_tests = { 27 | {"", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, 28 | {"The quick brown fox jumps over the lazy dog", "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"}}; 29 | 30 | const vector> sha512_string_tests = { 31 | {"", "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"}, 32 | {"The quick brown fox jumps over the lazy dog", "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6"}}; 33 | 34 | int main() { 35 | for(auto &string_test : base64_string_tests) { 36 | assert(Crypto::Base64::encode(string_test.first) == string_test.second); 37 | assert(Crypto::Base64::decode(string_test.second) == string_test.first); 38 | } 39 | 40 | for(auto &string_test : md5_string_tests) { 41 | assert(Crypto::to_hex_string(Crypto::md5(string_test.first)) == string_test.second); 42 | stringstream ss(string_test.first); 43 | assert(Crypto::to_hex_string(Crypto::md5(ss)) == string_test.second); 44 | } 45 | 46 | for(auto &string_test : sha1_string_tests) { 47 | assert(Crypto::to_hex_string(Crypto::sha1(string_test.first)) == string_test.second); 48 | stringstream ss(string_test.first); 49 | assert(Crypto::to_hex_string(Crypto::sha1(ss)) == string_test.second); 50 | } 51 | 52 | for(auto &string_test : sha256_string_tests) { 53 | assert(Crypto::to_hex_string(Crypto::sha256(string_test.first)) == string_test.second); 54 | stringstream ss(string_test.first); 55 | assert(Crypto::to_hex_string(Crypto::sha256(ss)) == string_test.second); 56 | } 57 | 58 | for(auto &string_test : sha512_string_tests) { 59 | assert(Crypto::to_hex_string(Crypto::sha512(string_test.first)) == string_test.second); 60 | stringstream ss(string_test.first); 61 | assert(Crypto::to_hex_string(Crypto::sha512(ss)) == string_test.second); 62 | } 63 | 64 | //Testing iterations 65 | assert(Crypto::to_hex_string(Crypto::sha1("Test", 1)) == "640ab2bae07bedc4c163f679a746f7ab7fb5d1fa"); 66 | assert(Crypto::to_hex_string(Crypto::sha1("Test", 2)) == "af31c6cbdecd88726d0a9b3798c71ef41f1624d5"); 67 | stringstream ss("Test"); 68 | assert(Crypto::to_hex_string(Crypto::sha1(ss, 2)) == "af31c6cbdecd88726d0a9b3798c71ef41f1624d5"); 69 | 70 | assert(Crypto::to_hex_string(Crypto::pbkdf2("Password", "Salt", 4096, 128 / 8)) == "f66df50f8aaa11e4d9721e1312ff2e66"); 71 | assert(Crypto::to_hex_string(Crypto::pbkdf2("Password", "Salt", 8192, 512 / 8)) == "a941ccbc34d1ee8ebbd1d34824a419c3dc4eac9cbc7c36ae6c7ca8725e2b618a6ad22241e787af937b0960cf85aa8ea3a258f243e05d3cc9b08af5dd93be046c"); 72 | } 73 | -------------------------------------------------------------------------------- /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 | typedef asio::ssl::stream HTTPS; 17 | 18 | template <> 19 | class Server : public ServerBase { 20 | std::string session_id_context; 21 | bool set_session_id_context = false; 22 | 23 | public: 24 | DEPRECATED Server(unsigned short port, size_t thread_pool_size, const std::string &cert_file, const std::string &private_key_file, 25 | long timeout_request = 5, long timeout_content = 300, const std::string &verify_file = std::string()) 26 | : Server(cert_file, private_key_file, verify_file) { 27 | config.port = port; 28 | config.thread_pool_size = thread_pool_size; 29 | config.timeout_request = timeout_request; 30 | config.timeout_content = timeout_content; 31 | } 32 | 33 | Server(const std::string &cert_file, const std::string &private_key_file, const std::string &verify_file = std::string()) 34 | : ServerBase::ServerBase(443), context(asio::ssl::context::tlsv12) { 35 | context.use_certificate_chain_file(cert_file); 36 | context.use_private_key_file(private_key_file, asio::ssl::context::pem); 37 | 38 | if(verify_file.size() > 0) { 39 | context.load_verify_file(verify_file); 40 | context.set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert | asio::ssl::verify_client_once); 41 | set_session_id_context = true; 42 | } 43 | } 44 | 45 | void start() { 46 | if(set_session_id_context) { 47 | // Creating session_id_context from address:port but reversed due to small SSL_MAX_SSL_SESSION_ID_LENGTH 48 | session_id_context = std::to_string(config.port) + ':'; 49 | session_id_context.append(config.address.rbegin(), config.address.rend()); 50 | SSL_CTX_set_session_id_context(context.native_handle(), reinterpret_cast(session_id_context.data()), 51 | std::min(session_id_context.size(), SSL_MAX_SSL_SESSION_ID_LENGTH)); 52 | } 53 | ServerBase::start(); 54 | } 55 | 56 | protected: 57 | asio::ssl::context context; 58 | 59 | void accept() { 60 | //Create new socket for this connection 61 | //Shared_ptr is used to pass temporary objects to the asynchronous functions 62 | auto socket = std::make_shared(*io_service, context); 63 | 64 | acceptor->async_accept((*socket).lowest_layer(), [this, socket](const error_code &ec) { 65 | //Immediately start accepting a new connection (if io_service hasn't been stopped) 66 | if(ec != asio::error::operation_aborted) 67 | accept(); 68 | 69 | 70 | if(!ec) { 71 | asio::ip::tcp::no_delay option(true); 72 | socket->lowest_layer().set_option(option); 73 | 74 | //Set timeout on the following asio::ssl::stream::async_handshake 75 | auto timer = get_timeout_timer(socket, config.timeout_request); 76 | socket->async_handshake(asio::ssl::stream_base::server, [this, socket, timer](const error_code &ec) { 77 | if(timer) 78 | timer->cancel(); 79 | if(!ec) 80 | read_request_and_content(socket); 81 | else if(on_error) 82 | on_error(std::shared_ptr(new Request(*socket)), ec); 83 | }); 84 | } 85 | else if(on_error) 86 | on_error(std::shared_ptr(new Request(*socket)), ec); 87 | }); 88 | } 89 | }; 90 | } // namespace SimpleWeb 91 | 92 | #endif /* SERVER_HTTPS_HPP */ 93 | -------------------------------------------------------------------------------- /utility.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIMPLE_WEB_SERVER_UTILITY_HPP 2 | #define SIMPLE_WEB_SERVER_UTILITY_HPP 3 | 4 | #include "status_code.hpp" 5 | #include 6 | #include 7 | #include 8 | 9 | // TODO when switching to c++14, use [[deprecated]] instead 10 | #ifndef DEPRECATED 11 | #ifdef __GNUC__ 12 | #define DEPRECATED __attribute__((deprecated)) 13 | #elif defined(_MSC_VER) 14 | #define DEPRECATED __declspec(deprecated) 15 | #else 16 | #define DEPRECATED 17 | #endif 18 | #endif 19 | 20 | namespace SimpleWeb { 21 | #ifndef CASE_INSENSITIVE_EQUAL_AND_HASH 22 | #define CASE_INSENSITIVE_EQUAL_AND_HASH 23 | inline bool case_insensitive_equal(const std::string &str1, const std::string &str2) { 24 | return str1.size() == str2.size() && 25 | std::equal(str1.begin(), str1.end(), str2.begin(), [](char a, char b) { 26 | return tolower(a) == tolower(b); 27 | }); 28 | } 29 | class CaseInsensitiveEqual { 30 | public: 31 | bool operator()(const std::string &str1, const std::string &str2) const { 32 | return case_insensitive_equal(str1, str2); 33 | } 34 | }; 35 | // Based on https://stackoverflow.com/questions/2590677/how-do-i-combine-hash-values-in-c0x/2595226#2595226 36 | class CaseInsensitiveHash { 37 | public: 38 | size_t operator()(const std::string &str) const { 39 | size_t h = 0; 40 | std::hash hash; 41 | for(auto c : str) 42 | h ^= hash(tolower(c)) + 0x9e3779b9 + (h << 6) + (h >> 2); 43 | return h; 44 | } 45 | }; 46 | #endif 47 | 48 | typedef std::unordered_multimap CaseInsensitiveMultimap; 49 | 50 | /// Percent encoding and decoding 51 | class Percent { 52 | public: 53 | /// Returns percent-encoded string 54 | static std::string encode(const std::string &value) { 55 | static auto hex_chars = "0123456789ABCDEF"; 56 | 57 | std::string result; 58 | result.reserve(value.size()); // minimum size of result 59 | 60 | for(auto &chr : value) { 61 | if(chr == ' ') 62 | result += '+'; 63 | else if(chr == '!' || chr == '#' || chr == '$' || (chr >= '&' && chr <= ',') || (chr >= '/' && chr <= ';') || chr == '=' || chr == '?' || chr == '@' || chr == '[' || chr == ']') 64 | result += std::string("%") + hex_chars[chr >> 4] + hex_chars[chr & 15]; 65 | else 66 | result += chr; 67 | } 68 | 69 | return result; 70 | } 71 | 72 | /// Returns percent-decoded string 73 | static std::string decode(const std::string &value) { 74 | std::string result; 75 | result.reserve(value.size() / 3 + (value.size() % 3)); // minimum size of result 76 | 77 | for(size_t i = 0; i < value.size(); ++i) { 78 | auto &chr = value[i]; 79 | if(chr == '%' && i + 2 < value.size()) { 80 | auto hex = value.substr(i + 1, 2); 81 | auto decoded_chr = static_cast(std::strtol(hex.c_str(), nullptr, 16)); 82 | result += decoded_chr; 83 | i += 2; 84 | } 85 | else if(chr == '+') 86 | result += ' '; 87 | else 88 | result += chr; 89 | } 90 | 91 | return result; 92 | } 93 | }; 94 | 95 | /// Query string creation and parsing 96 | class QueryString { 97 | public: 98 | /// Returns query string created from given field names and values 99 | static std::string create(const CaseInsensitiveMultimap &fields) { 100 | std::string result; 101 | 102 | bool first = true; 103 | for(auto &field : fields) { 104 | result += (!first ? "&" : "") + field.first + '=' + Percent::encode(field.second); 105 | first = false; 106 | } 107 | 108 | return result; 109 | } 110 | 111 | /// Returns query keys with percent-decoded values. 112 | static CaseInsensitiveMultimap parse(const std::string &query_string) { 113 | CaseInsensitiveMultimap result; 114 | 115 | if(query_string.empty()) 116 | return result; 117 | 118 | size_t name_pos = 0; 119 | size_t name_end_pos = -1; 120 | size_t value_pos = -1; 121 | for(size_t c = 0; c < query_string.size(); ++c) { 122 | if(query_string[c] == '&') { 123 | auto name = query_string.substr(name_pos, (name_end_pos == std::string::npos ? c : name_end_pos) - name_pos); 124 | if(!name.empty()) { 125 | auto value = value_pos == std::string::npos ? std::string() : query_string.substr(value_pos, c - value_pos); 126 | result.emplace(std::move(name), Percent::decode(value)); 127 | } 128 | name_pos = c + 1; 129 | name_end_pos = -1; 130 | value_pos = -1; 131 | } 132 | else if(query_string[c] == '=') { 133 | name_end_pos = c; 134 | value_pos = c + 1; 135 | } 136 | } 137 | if(name_pos < query_string.size()) { 138 | auto name = query_string.substr(name_pos, name_end_pos - name_pos); 139 | if(!name.empty()) { 140 | auto value = value_pos >= query_string.size() ? std::string() : query_string.substr(value_pos); 141 | result.emplace(std::move(name), Percent::decode(value)); 142 | } 143 | } 144 | 145 | return result; 146 | } 147 | }; 148 | 149 | /// Returns query keys with percent-decoded values. 150 | DEPRECATED inline CaseInsensitiveMultimap parse_query_string(const std::string &query_string) { 151 | return QueryString::parse(query_string); 152 | } 153 | } // namespace SimpleWeb 154 | 155 | #endif // SIMPLE_WEB_SERVER_UTILITY_HPP 156 | -------------------------------------------------------------------------------- /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 | typedef asio::ssl::stream HTTPS; 14 | 15 | template <> 16 | class Client : public ClientBase { 17 | public: 18 | friend ClientBase; 19 | 20 | Client(const std::string &server_port_path, bool verify_certificate = true, const std::string &cert_file = std::string(), 21 | const std::string &private_key_file = std::string(), const std::string &verify_file = std::string()) 22 | : ClientBase::ClientBase(server_port_path, 443), context(asio::ssl::context::tlsv12) { 23 | if(cert_file.size() > 0 && private_key_file.size() > 0) { 24 | context.use_certificate_chain_file(cert_file); 25 | context.use_private_key_file(private_key_file, asio::ssl::context::pem); 26 | } 27 | 28 | if(verify_certificate) 29 | context.set_verify_callback(asio::ssl::rfc2818_verification(host)); 30 | 31 | if(verify_file.size() > 0) 32 | context.load_verify_file(verify_file); 33 | else 34 | context.set_default_verify_paths(); 35 | 36 | if(verify_file.size() > 0 || verify_certificate) 37 | context.set_verify_mode(asio::ssl::verify_peer); 38 | else 39 | context.set_verify_mode(asio::ssl::verify_none); 40 | } 41 | 42 | protected: 43 | asio::ssl::context context; 44 | 45 | std::shared_ptr create_connection() override { 46 | return std::make_shared(host, port, config, std::unique_ptr(new HTTPS(*io_service, context))); 47 | } 48 | 49 | static void connect(const std::shared_ptr &session) { 50 | if(!session->connection->socket->lowest_layer().is_open()) { 51 | auto resolver = std::make_shared(*session->io_service); 52 | resolver->async_resolve(*session->connection->query, [session, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator it) { 53 | if(!ec) { 54 | auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); 55 | asio::async_connect(session->connection->socket->lowest_layer(), it, [session, resolver, timer](const error_code &ec, asio::ip::tcp::resolver::iterator /*it*/) { 56 | if(timer) 57 | timer->cancel(); 58 | if(!ec) { 59 | asio::ip::tcp::no_delay option(true); 60 | session->connection->socket->lowest_layer().set_option(option); 61 | 62 | if(!session->connection->config.proxy_server.empty()) { 63 | auto write_buffer = std::make_shared(); 64 | std::ostream write_stream(write_buffer.get()); 65 | auto host_port = session->connection->host + ':' + std::to_string(session->connection->port); 66 | write_stream << "CONNECT " + host_port + " HTTP/1.1\r\n" 67 | << "Host: " << host_port << "\r\n\r\n"; 68 | auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); 69 | asio::async_write(session->connection->socket->next_layer(), *write_buffer, [session, write_buffer, timer](const error_code &ec, size_t /*bytes_transferred*/) { 70 | if(timer) 71 | timer->cancel(); 72 | if(!ec) { 73 | std::shared_ptr response(new Response()); 74 | auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); 75 | asio::async_read_until(session->connection->socket->next_layer(), response->content_buffer, "\r\n\r\n", [session, response, timer](const error_code &ec, size_t /*bytes_transferred*/) { 76 | if(timer) 77 | timer->cancel(); 78 | if(!ec) { 79 | parse_response_header(response); 80 | if(response->status_code.empty() || response->status_code.compare(0, 3, "200") != 0) { 81 | close(session); 82 | session->callback(make_error_code::make_error_code(errc::permission_denied)); 83 | } 84 | else 85 | handshake(session); 86 | } 87 | else { 88 | close(session); 89 | session->callback(ec); 90 | } 91 | }); 92 | } 93 | else { 94 | close(session); 95 | session->callback(ec); 96 | } 97 | }); 98 | } 99 | else 100 | handshake(session); 101 | } 102 | else { 103 | close(session); 104 | session->callback(ec); 105 | } 106 | }); 107 | } 108 | else { 109 | close(session); 110 | session->callback(ec); 111 | } 112 | }); 113 | } 114 | else 115 | write(session); 116 | } 117 | 118 | static void handshake(const std::shared_ptr &session) { 119 | auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); 120 | session->connection->socket->async_handshake(asio::ssl::stream_base::client, [session, timer](const error_code &ec) { 121 | if(timer) 122 | timer->cancel(); 123 | if(!ec) 124 | write(session); 125 | else { 126 | close(session); 127 | session->callback(ec); 128 | } 129 | }); 130 | } 131 | }; 132 | } // namespace SimpleWeb 133 | 134 | #endif /* CLIENT_HTTPS_HPP */ 135 | -------------------------------------------------------------------------------- /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() {} 14 | 15 | void parse_request_test() { 16 | HTTP socket(*io_service); 17 | std::shared_ptr request(new Request(socket)); 18 | 19 | std::ostream stream(&request->content.streambuf); 20 | stream << "GET /test/ HTTP/1.1\r\n"; 21 | stream << "TestHeader: test\r\n"; 22 | stream << "TestHeader2:test2\r\n"; 23 | stream << "TestHeader3:test3a\r\n"; 24 | stream << "TestHeader3:test3b\r\n"; 25 | stream << "\r\n"; 26 | 27 | assert(parse_request(request)); 28 | 29 | assert(request->method == "GET"); 30 | assert(request->path == "/test/"); 31 | assert(request->http_version == "1.1"); 32 | 33 | assert(request->header.size() == 4); 34 | auto header_it = request->header.find("TestHeader"); 35 | assert(header_it != request->header.end() && header_it->second == "test"); 36 | header_it = request->header.find("TestHeader2"); 37 | assert(header_it != request->header.end() && header_it->second == "test2"); 38 | 39 | header_it = request->header.find("testheader"); 40 | assert(header_it != request->header.end() && header_it->second == "test"); 41 | header_it = request->header.find("testheader2"); 42 | assert(header_it != request->header.end() && header_it->second == "test2"); 43 | 44 | auto range = request->header.equal_range("testheader3"); 45 | auto first = range.first; 46 | auto second = first; 47 | ++second; 48 | assert(range.first != request->header.end() && range.second != 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() override { 59 | return nullptr; 60 | } 61 | 62 | void constructor_parse_test1() { 63 | assert(host == "test.org"); 64 | assert(port == 8080); 65 | } 66 | 67 | void constructor_parse_test2() { 68 | assert(host == "test.org"); 69 | assert(port == 80); 70 | } 71 | 72 | void parse_response_header_test() { 73 | std::shared_ptr response(new Response()); 74 | 75 | ostream stream(&response->content_buffer); 76 | stream << "HTTP/1.1 200 OK\r\n"; 77 | stream << "TestHeader: test\r\n"; 78 | stream << "TestHeader2:test2\r\n"; 79 | stream << "TestHeader3:test3a\r\n"; 80 | stream << "TestHeader3:test3b\r\n"; 81 | stream << "\r\n"; 82 | 83 | parse_response_header(response); 84 | 85 | assert(response->http_version == "1.1"); 86 | assert(response->status_code == "200 OK"); 87 | 88 | assert(response->header.size() == 4); 89 | auto header_it = response->header.find("TestHeader"); 90 | assert(header_it != response->header.end() && header_it->second == "test"); 91 | header_it = response->header.find("TestHeader2"); 92 | assert(header_it != response->header.end() && header_it->second == "test2"); 93 | 94 | 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 | auto range = response->header.equal_range("testheader3"); 100 | auto first = range.first; 101 | auto second = first; 102 | ++second; 103 | assert(range.first != response->header.end() && range.second != response->header.end() && 104 | ((first->second == "test3a" && second->second == "test3b") || 105 | (first->second == "test3b" && second->second == "test3a"))); 106 | } 107 | }; 108 | 109 | int main() { 110 | assert(case_insensitive_equal("Test", "tesT")); 111 | assert(case_insensitive_equal("tesT", "test")); 112 | assert(!case_insensitive_equal("test", "tseT")); 113 | CaseInsensitiveEqual equal; 114 | assert(equal("Test", "tesT")); 115 | assert(equal("tesT", "test")); 116 | assert(!equal("test", "tset")); 117 | CaseInsensitiveHash hash; 118 | assert(hash("Test") == hash("tesT")); 119 | assert(hash("tesT") == hash("test")); 120 | assert(hash("test") != hash("tset")); 121 | 122 | auto percent_decoded = "testing æøå !#$&'()*+,/:;=?@[]"; 123 | auto percent_encoded = "testing+æøå+%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"; 124 | assert(Percent::encode(percent_decoded) == percent_encoded); 125 | assert(Percent::decode(percent_encoded) == percent_decoded); 126 | assert(Percent::decode(Percent::encode(percent_decoded)) == percent_decoded); 127 | 128 | SimpleWeb::CaseInsensitiveMultimap fields = {{"test1", "æøå"}, {"test2", "!#$&'()*+,/:;=?@[]"}}; 129 | auto query_string1 = "test1=æøå&test2=%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"; 130 | auto query_string2 = "test2=%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D&test1=æøå"; 131 | auto query_string_result = QueryString::create(fields); 132 | assert(query_string_result == query_string1 || query_string_result == query_string2); 133 | auto fields_result1 = QueryString::parse(query_string1); 134 | auto fields_result2 = QueryString::parse(query_string2); 135 | assert(fields_result1 == fields_result2 && fields_result1 == fields); 136 | 137 | ServerTest serverTest; 138 | serverTest.io_service = std::make_shared(); 139 | 140 | serverTest.parse_request_test(); 141 | 142 | ClientTest clientTest("test.org:8080"); 143 | clientTest.constructor_parse_test1(); 144 | 145 | ClientTest clientTest2("test.org"); 146 | clientTest2.constructor_parse_test2(); 147 | 148 | clientTest2.parse_response_header_test(); 149 | 150 | 151 | asio::io_service io_service; 152 | asio::ip::tcp::socket socket(io_service); 153 | SimpleWeb::Server::Request request(socket); 154 | { 155 | request.path = "/?"; 156 | auto queries = request.parse_query_string(); 157 | assert(queries.empty()); 158 | } 159 | { 160 | request.path = "/"; 161 | auto queries = request.parse_query_string(); 162 | assert(queries.empty()); 163 | } 164 | { 165 | request.path = "/?="; 166 | auto queries = request.parse_query_string(); 167 | assert(queries.empty()); 168 | } 169 | { 170 | request.path = "/?=test"; 171 | auto queries = request.parse_query_string(); 172 | assert(queries.empty()); 173 | } 174 | { 175 | request.path = "/?a=1%202%20%203&b=3+4&c&d=æ%25ø%26å%3F"; 176 | auto queries = request.parse_query_string(); 177 | { 178 | auto range = queries.equal_range("a"); 179 | assert(range.first != range.second); 180 | assert(range.first->second == "1 2 3"); 181 | } 182 | { 183 | auto range = queries.equal_range("b"); 184 | assert(range.first != range.second); 185 | assert(range.first->second == "3 4"); 186 | } 187 | { 188 | auto range = queries.equal_range("c"); 189 | assert(range.first != range.second); 190 | assert(range.first->second == ""); 191 | } 192 | { 193 | auto range = queries.equal_range("d"); 194 | assert(range.first != range.second); 195 | assert(range.first->second == "æ%ø&å?"); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /status_code.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIMPLE_WEB_SERVER_STATUS_CODE_HPP 2 | #define SIMPLE_WEB_SERVER_STATUS_CODE_HPP 3 | 4 | #include 5 | #include 6 | 7 | namespace SimpleWeb { 8 | enum class StatusCode { 9 | unknown = 0, 10 | information_continue = 100, 11 | information_switching_protocols, 12 | information_processing, 13 | success_ok = 200, 14 | success_created, 15 | success_accepted, 16 | success_non_authoritative_information, 17 | success_no_content, 18 | success_reset_content, 19 | success_partial_content, 20 | success_multi_status, 21 | success_already_reported, 22 | success_im_used = 226, 23 | redirection_multiple_choices = 300, 24 | redirection_moved_permanently, 25 | redirection_found, 26 | redirection_see_other, 27 | redirection_not_modified, 28 | redirection_use_proxy, 29 | redirection_switch_proxy, 30 | redirection_temporary_redirect, 31 | redirection_permanent_redirect, 32 | client_error_bad_request = 400, 33 | client_error_unauthorized, 34 | client_error_payment_required, 35 | client_error_forbidden, 36 | client_error_not_found, 37 | client_error_method_not_allowed, 38 | client_error_not_acceptable, 39 | client_error_proxy_authentication_required, 40 | client_error_request_timeout, 41 | client_error_conflict, 42 | client_error_gone, 43 | client_error_length_required, 44 | client_error_precondition_failed, 45 | client_error_payload_too_large, 46 | client_error_uri_too_long, 47 | client_error_unsupported_media_type, 48 | client_error_range_not_satisfiable, 49 | client_error_expectation_failed, 50 | client_error_im_a_teapot, 51 | client_error_misdirection_required = 421, 52 | client_error_unprocessable_entity, 53 | client_error_locked, 54 | client_error_failed_dependency, 55 | client_error_upgrade_required = 426, 56 | client_error_precondition_required = 428, 57 | client_error_too_many_requests, 58 | client_error_request_header_fields_too_large = 431, 59 | client_error_unavailable_for_legal_reasons = 451, 60 | server_error_internal_server_error = 500, 61 | server_error_not_implemented, 62 | server_error_bad_gateway, 63 | server_error_service_unavailable, 64 | server_error_gateway_timeout, 65 | server_error_http_version_not_supported, 66 | server_error_variant_also_negotiates, 67 | server_error_insufficient_storage, 68 | server_error_loop_detected, 69 | server_error_not_extended = 510, 70 | server_error_network_authentication_required 71 | }; 72 | 73 | inline const static std::vector> &status_codes() { 74 | static std::vector> status_codes = { 75 | {StatusCode::unknown, ""}, 76 | {StatusCode::information_continue, "100 Continue"}, 77 | {StatusCode::information_switching_protocols, "101 Switching Protocols"}, 78 | {StatusCode::information_processing, "102 Processing"}, 79 | {StatusCode::success_ok, "200 OK"}, 80 | {StatusCode::success_created, "201 Created"}, 81 | {StatusCode::success_accepted, "202 Accepted"}, 82 | {StatusCode::success_non_authoritative_information, "203 Non-Authoritative Information"}, 83 | {StatusCode::success_no_content, "204 No Content"}, 84 | {StatusCode::success_reset_content, "205 Reset Content"}, 85 | {StatusCode::success_partial_content, "206 Partial Content"}, 86 | {StatusCode::success_multi_status, "207 Multi-Status"}, 87 | {StatusCode::success_already_reported, "208 Already Reported"}, 88 | {StatusCode::success_im_used, "226 IM Used"}, 89 | {StatusCode::redirection_multiple_choices, "300 Multiple Choices"}, 90 | {StatusCode::redirection_moved_permanently, "301 Moved Permanently"}, 91 | {StatusCode::redirection_found, "302 Found"}, 92 | {StatusCode::redirection_see_other, "303 See Other"}, 93 | {StatusCode::redirection_not_modified, "304 Not Modified"}, 94 | {StatusCode::redirection_use_proxy, "305 Use Proxy"}, 95 | {StatusCode::redirection_switch_proxy, "306 Switch Proxy"}, 96 | {StatusCode::redirection_temporary_redirect, "307 Temporary Redirect"}, 97 | {StatusCode::redirection_permanent_redirect, "308 Permanent Redirect"}, 98 | {StatusCode::client_error_bad_request, "400 Bad Request"}, 99 | {StatusCode::client_error_unauthorized, "401 Unauthorized"}, 100 | {StatusCode::client_error_payment_required, "402 Payment Required"}, 101 | {StatusCode::client_error_forbidden, "403 Forbidden"}, 102 | {StatusCode::client_error_not_found, "404 Not Found"}, 103 | {StatusCode::client_error_method_not_allowed, "405 Method Not Allowed"}, 104 | {StatusCode::client_error_not_acceptable, "406 Not Acceptable"}, 105 | {StatusCode::client_error_proxy_authentication_required, "407 Proxy Authentication Required"}, 106 | {StatusCode::client_error_request_timeout, "408 Request Timeout"}, 107 | {StatusCode::client_error_conflict, "409 Conflict"}, 108 | {StatusCode::client_error_gone, "410 Gone"}, 109 | {StatusCode::client_error_length_required, "411 Length Required"}, 110 | {StatusCode::client_error_precondition_failed, "412 Precondition Failed"}, 111 | {StatusCode::client_error_payload_too_large, "413 Payload Too Large"}, 112 | {StatusCode::client_error_uri_too_long, "414 URI Too Long"}, 113 | {StatusCode::client_error_unsupported_media_type, "415 Unsupported Media Type"}, 114 | {StatusCode::client_error_range_not_satisfiable, "416 Range Not Satisfiable"}, 115 | {StatusCode::client_error_expectation_failed, "417 Expectation Failed"}, 116 | {StatusCode::client_error_im_a_teapot, "418 I'm a teapot"}, 117 | {StatusCode::client_error_misdirection_required, "421 Misdirected Request"}, 118 | {StatusCode::client_error_unprocessable_entity, "422 Unprocessable Entity"}, 119 | {StatusCode::client_error_locked, "423 Locked"}, 120 | {StatusCode::client_error_failed_dependency, "424 Failed Dependency"}, 121 | {StatusCode::client_error_upgrade_required, "426 Upgrade Required"}, 122 | {StatusCode::client_error_precondition_required, "428 Precondition Required"}, 123 | {StatusCode::client_error_too_many_requests, "429 Too Many Requests"}, 124 | {StatusCode::client_error_request_header_fields_too_large, "431 Request Header Fields Too Large"}, 125 | {StatusCode::client_error_unavailable_for_legal_reasons, "451 Unavailable For Legal Reasons"}, 126 | {StatusCode::server_error_internal_server_error, "500 Internal Server Error"}, 127 | {StatusCode::server_error_not_implemented, "501 Not Implemented"}, 128 | {StatusCode::server_error_bad_gateway, "502 Bad Gateway"}, 129 | {StatusCode::server_error_service_unavailable, "503 Service Unavailable"}, 130 | {StatusCode::server_error_gateway_timeout, "504 Gateway Timeout"}, 131 | {StatusCode::server_error_http_version_not_supported, "505 HTTP Version Not Supported"}, 132 | {StatusCode::server_error_variant_also_negotiates, "506 Variant Also Negotiates"}, 133 | {StatusCode::server_error_insufficient_storage, "507 Insufficient Storage"}, 134 | {StatusCode::server_error_loop_detected, "508 Loop Detected"}, 135 | {StatusCode::server_error_not_extended, "510 Not Extended"}, 136 | {StatusCode::server_error_network_authentication_required, "511 Network Authentication Required"}}; 137 | return status_codes; 138 | } 139 | 140 | inline StatusCode status_code(const std::string &status_code_str) { 141 | for(auto &status_code : status_codes()) { 142 | if(status_code.second == status_code_str) 143 | return status_code.first; 144 | } 145 | return StatusCode::unknown; 146 | } 147 | 148 | inline const std::string &status_code(StatusCode status_code_enum) { 149 | for(auto &status_code : status_codes()) { 150 | if(status_code.first == status_code_enum) 151 | return status_code.second; 152 | } 153 | return status_codes()[0].second; 154 | } 155 | } // namespace SimpleWeb 156 | 157 | #endif // SIMPLE_WEB_SERVER_STATUS_CODE_HPP 158 | -------------------------------------------------------------------------------- /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 | //Moving these to a seperate namespace for minimal global namespace cluttering does not work with clang++ 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace SimpleWeb { 18 | //TODO 2017: remove workaround for MSVS 2012 19 | #if _MSC_VER == 1700 //MSVS 2012 has no definition for round() 20 | inline double round(double x) { //custom definition of round() for positive numbers 21 | return floor(x + 0.5); 22 | } 23 | #endif 24 | 25 | class Crypto { 26 | const static size_t buffer_size = 131072; 27 | 28 | public: 29 | class Base64 { 30 | public: 31 | static std::string encode(const std::string &ascii) { 32 | std::string base64; 33 | 34 | BIO *bio, *b64; 35 | BUF_MEM *bptr = BUF_MEM_new(); 36 | 37 | b64 = BIO_new(BIO_f_base64()); 38 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 39 | bio = BIO_new(BIO_s_mem()); 40 | BIO_push(b64, bio); 41 | BIO_set_mem_buf(b64, bptr, BIO_CLOSE); 42 | 43 | //Write directly to base64-buffer to avoid copy 44 | int base64_length = static_cast(round(4 * ceil((double)ascii.size() / 3.0))); 45 | base64.resize(base64_length); 46 | bptr->length = 0; 47 | bptr->max = base64_length + 1; 48 | bptr->data = (char *)&base64[0]; 49 | 50 | BIO_write(b64, &ascii[0], static_cast(ascii.size())); 51 | BIO_flush(b64); 52 | 53 | //To keep &base64[0] through BIO_free_all(b64) 54 | bptr->length = 0; 55 | bptr->max = 0; 56 | bptr->data = nullptr; 57 | 58 | BIO_free_all(b64); 59 | 60 | return base64; 61 | } 62 | 63 | static std::string decode(const std::string &base64) { 64 | std::string ascii; 65 | 66 | //Resize ascii, however, the size is a up to two bytes too large. 67 | ascii.resize((6 * base64.size()) / 8); 68 | BIO *b64, *bio; 69 | 70 | b64 = BIO_new(BIO_f_base64()); 71 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 72 | bio = BIO_new_mem_buf((char *)&base64[0], static_cast(base64.size())); 73 | bio = BIO_push(b64, bio); 74 | 75 | int decoded_length = BIO_read(bio, &ascii[0], static_cast(ascii.size())); 76 | ascii.resize(decoded_length); 77 | 78 | BIO_free_all(b64); 79 | 80 | return ascii; 81 | } 82 | }; 83 | 84 | /// Return hex string from bytes in input string. 85 | static std::string to_hex_string(const std::string &input) { 86 | std::stringstream hex_stream; 87 | hex_stream << std::hex << std::internal << std::setfill('0'); 88 | for(auto &byte : input) 89 | hex_stream << std::setw(2) << static_cast(static_cast(byte)); 90 | return hex_stream.str(); 91 | } 92 | 93 | static std::string md5(const std::string &input, size_t iterations = 1) { 94 | std::string hash; 95 | 96 | hash.resize(128 / 8); 97 | MD5(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); 98 | 99 | for(size_t c = 1; c < iterations; ++c) 100 | MD5(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 101 | 102 | return hash; 103 | } 104 | 105 | static std::string md5(std::istream &stream, size_t iterations = 1) { 106 | MD5_CTX context; 107 | MD5_Init(&context); 108 | std::streamsize read_length; 109 | std::vector buffer(buffer_size); 110 | while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) 111 | MD5_Update(&context, buffer.data(), read_length); 112 | std::string hash; 113 | hash.resize(128 / 8); 114 | MD5_Final(reinterpret_cast(&hash[0]), &context); 115 | 116 | for(size_t c = 1; c < iterations; ++c) 117 | MD5(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 118 | 119 | return hash; 120 | } 121 | 122 | static std::string sha1(const std::string &input, size_t iterations = 1) { 123 | std::string hash; 124 | 125 | hash.resize(160 / 8); 126 | SHA1(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); 127 | 128 | for(size_t c = 1; c < iterations; ++c) 129 | SHA1(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 130 | 131 | return hash; 132 | } 133 | 134 | static std::string sha1(std::istream &stream, size_t iterations = 1) { 135 | SHA_CTX context; 136 | SHA1_Init(&context); 137 | std::streamsize read_length; 138 | std::vector buffer(buffer_size); 139 | while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) 140 | SHA1_Update(&context, buffer.data(), read_length); 141 | std::string hash; 142 | hash.resize(160 / 8); 143 | SHA1_Final(reinterpret_cast(&hash[0]), &context); 144 | 145 | for(size_t c = 1; c < iterations; ++c) 146 | SHA1(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 147 | 148 | return hash; 149 | } 150 | 151 | static std::string sha256(const std::string &input, size_t iterations = 1) { 152 | std::string hash; 153 | 154 | hash.resize(256 / 8); 155 | SHA256(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); 156 | 157 | for(size_t c = 1; c < iterations; ++c) 158 | SHA256(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 159 | 160 | return hash; 161 | } 162 | 163 | static std::string sha256(std::istream &stream, size_t iterations = 1) { 164 | SHA256_CTX context; 165 | SHA256_Init(&context); 166 | std::streamsize read_length; 167 | std::vector buffer(buffer_size); 168 | while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) 169 | SHA256_Update(&context, buffer.data(), read_length); 170 | std::string hash; 171 | hash.resize(256 / 8); 172 | SHA256_Final(reinterpret_cast(&hash[0]), &context); 173 | 174 | for(size_t c = 1; c < iterations; ++c) 175 | SHA256(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 176 | 177 | return hash; 178 | } 179 | 180 | static std::string sha512(const std::string &input, size_t iterations = 1) { 181 | std::string hash; 182 | 183 | hash.resize(512 / 8); 184 | SHA512(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); 185 | 186 | for(size_t c = 1; c < iterations; ++c) 187 | SHA512(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 188 | 189 | return hash; 190 | } 191 | 192 | static std::string sha512(std::istream &stream, size_t iterations = 1) { 193 | SHA512_CTX context; 194 | SHA512_Init(&context); 195 | std::streamsize read_length; 196 | std::vector buffer(buffer_size); 197 | while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) 198 | SHA512_Update(&context, buffer.data(), read_length); 199 | std::string hash; 200 | hash.resize(512 / 8); 201 | SHA512_Final(reinterpret_cast(&hash[0]), &context); 202 | 203 | for(size_t c = 1; c < iterations; ++c) 204 | SHA512(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); 205 | 206 | return hash; 207 | } 208 | 209 | /// key_size is number of bytes of the returned key. 210 | static std::string pbkdf2(const std::string &password, const std::string &salt, int iterations, int key_size) { 211 | std::string key; 212 | key.resize(key_size); 213 | PKCS5_PBKDF2_HMAC_SHA1(password.c_str(), password.size(), 214 | reinterpret_cast(salt.c_str()), salt.size(), iterations, 215 | key_size, reinterpret_cast(&key[0])); 216 | return key; 217 | } 218 | }; 219 | } 220 | #endif /* SIMPLE_WEB_CRYPTO_HPP */ 221 | -------------------------------------------------------------------------------- /tests/io_test.cpp: -------------------------------------------------------------------------------- 1 | #include "client_http.hpp" 2 | #include "server_http.hpp" 3 | 4 | #include 5 | 6 | using namespace std; 7 | 8 | typedef SimpleWeb::Server HttpServer; 9 | typedef SimpleWeb::Client HttpClient; 10 | 11 | int main() { 12 | HttpServer server; 13 | server.config.port = 8080; 14 | 15 | server.resource["^/string$"]["POST"] = [](shared_ptr response, shared_ptr request) { 16 | auto content = request->content.string(); 17 | 18 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" 19 | << content; 20 | }; 21 | 22 | server.resource["^/string2$"]["POST"] = [](shared_ptr response, shared_ptr request) { 23 | response->write(request->content.string()); 24 | }; 25 | 26 | server.resource["^/string3$"]["POST"] = [](shared_ptr response, shared_ptr request) { 27 | std::stringstream stream; 28 | stream << request->content.rdbuf(); 29 | response->write(stream); 30 | }; 31 | 32 | server.resource["^/string4$"]["POST"] = [](shared_ptr response, shared_ptr /*request*/) { 33 | response->write(SimpleWeb::StatusCode::client_error_forbidden, {{"Test1", "test2"}, {"tesT3", "test4"}}); 34 | }; 35 | 36 | server.resource["^/info$"]["GET"] = [](shared_ptr response, shared_ptr request) { 37 | stringstream content_stream; 38 | content_stream << request->method << " " << request->path << " " << request->http_version << " "; 39 | content_stream << request->header.find("test parameter")->second; 40 | 41 | content_stream.seekp(0, ios::end); 42 | 43 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content_stream.tellp() << "\r\n\r\n" 44 | << content_stream.rdbuf(); 45 | }; 46 | 47 | server.resource["^/match/([0-9]+)$"]["GET"] = [&server](shared_ptr response, shared_ptr request) { 48 | string number = request->path_match[1]; 49 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" 50 | << number; 51 | }; 52 | 53 | server.resource["^/header$"]["GET"] = [](shared_ptr response, shared_ptr request) { 54 | auto content = request->header.find("test1")->second + request->header.find("test2")->second; 55 | 56 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" 57 | << content; 58 | }; 59 | 60 | thread server_thread([&server]() { 61 | //Start server 62 | server.start(); 63 | }); 64 | 65 | this_thread::sleep_for(chrono::seconds(1)); 66 | { 67 | HttpClient client("localhost:8080"); 68 | 69 | { 70 | stringstream output; 71 | auto r = client.request("POST", "/string", "A string"); 72 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 73 | output << r->content.rdbuf(); 74 | assert(output.str() == "A string"); 75 | } 76 | 77 | { 78 | stringstream output; 79 | auto r = client.request("POST", "/string", "A string"); 80 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 81 | assert(r->content.string() == "A string"); 82 | } 83 | 84 | { 85 | stringstream output; 86 | auto r = client.request("POST", "/string2", "A string"); 87 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 88 | output << r->content.rdbuf(); 89 | assert(output.str() == "A string"); 90 | } 91 | 92 | { 93 | stringstream output; 94 | auto r = client.request("POST", "/string3", "A string"); 95 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 96 | output << r->content.rdbuf(); 97 | assert(output.str() == "A string"); 98 | } 99 | 100 | { 101 | stringstream output; 102 | auto r = client.request("POST", "/string4", "A string"); 103 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::client_error_forbidden); 104 | assert(r->header.size() == 3); 105 | assert(r->header.find("test1")->second == "test2"); 106 | assert(r->header.find("tEst3")->second == "test4"); 107 | assert(r->header.find("content-length")->second == "0"); 108 | output << r->content.rdbuf(); 109 | assert(output.str() == ""); 110 | } 111 | 112 | { 113 | stringstream output; 114 | stringstream content("A string"); 115 | auto r = client.request("POST", "/string", content); 116 | output << r->content.rdbuf(); 117 | assert(output.str() == "A string"); 118 | } 119 | 120 | { 121 | stringstream output; 122 | auto r = client.request("GET", "/info", "", {{"Test Parameter", "test value"}}); 123 | output << r->content.rdbuf(); 124 | assert(output.str() == "GET /info 1.1 test value"); 125 | } 126 | 127 | { 128 | stringstream output; 129 | auto r = client.request("GET", "/match/123"); 130 | output << r->content.rdbuf(); 131 | assert(output.str() == "123"); 132 | } 133 | } 134 | { 135 | HttpClient client("localhost:8080"); 136 | 137 | HttpClient::Connection *connection; 138 | { 139 | // test performing the stream version of the request methods first 140 | stringstream output; 141 | stringstream content("A string"); 142 | auto r = client.request("POST", "/string", content); 143 | output << r->content.rdbuf(); 144 | assert(output.str() == "A string"); 145 | assert(client.connections->size() == 1); 146 | connection = client.connections->front().get(); 147 | } 148 | 149 | { 150 | stringstream output; 151 | auto r = client.request("POST", "/string", "A string"); 152 | output << r->content.rdbuf(); 153 | assert(output.str() == "A string"); 154 | assert(client.connections->size() == 1); 155 | assert(connection == client.connections->front().get()); 156 | } 157 | 158 | { 159 | stringstream output; 160 | auto r = client.request("GET", "/header", "", {{"test1", "test"}, {"test2", "ing"}}); 161 | output << r->content.rdbuf(); 162 | assert(output.str() == "testing"); 163 | assert(client.connections->size() == 1); 164 | assert(connection == client.connections->front().get()); 165 | } 166 | } 167 | 168 | { 169 | HttpClient client("localhost:8080"); 170 | bool call = false; 171 | client.request("GET", "/match/123", [&call](shared_ptr response, const SimpleWeb::error_code &ec) { 172 | assert(!ec); 173 | stringstream output; 174 | output << response->content.rdbuf(); 175 | assert(output.str() == "123"); 176 | call = true; 177 | }); 178 | client.io_service->run(); 179 | assert(call); 180 | 181 | { 182 | vector calls(100); 183 | vector threads; 184 | for(size_t c = 0; c < 100; ++c) { 185 | calls[c] = 0; 186 | threads.emplace_back([c, &client, &calls] { 187 | client.request("GET", "/match/123", [c, &calls](shared_ptr response, const SimpleWeb::error_code &ec) { 188 | assert(!ec); 189 | stringstream output; 190 | output << response->content.rdbuf(); 191 | assert(output.str() == "123"); 192 | calls[c] = 1; 193 | }); 194 | }); 195 | } 196 | for(auto &thread : threads) 197 | thread.join(); 198 | assert(client.connections->size() == 100); 199 | client.io_service->reset(); 200 | client.io_service->run(); 201 | assert(client.connections->size() == 1); 202 | for(auto call : calls) 203 | assert(call); 204 | } 205 | } 206 | 207 | { 208 | HttpClient client("localhost:8080"); 209 | assert(client.connections->size() == 0); 210 | for(size_t c = 0; c < 5000; ++c) { 211 | auto r1 = client.request("POST", "/string", "A string"); 212 | assert(SimpleWeb::status_code(r1->status_code) == SimpleWeb::StatusCode::success_ok); 213 | assert(r1->content.string() == "A string"); 214 | assert(client.connections->size() == 1); 215 | 216 | stringstream content("A string"); 217 | auto r2 = client.request("POST", "/string", content); 218 | assert(SimpleWeb::status_code(r2->status_code) == SimpleWeb::StatusCode::success_ok); 219 | assert(r2->content.string() == "A string"); 220 | assert(client.connections->size() == 1); 221 | } 222 | } 223 | 224 | for(size_t c = 0; c < 500; ++c) { 225 | { 226 | HttpClient client("localhost:8080"); 227 | auto r = client.request("POST", "/string", "A string"); 228 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 229 | assert(r->content.string() == "A string"); 230 | assert(client.connections->size() == 1); 231 | } 232 | 233 | { 234 | HttpClient client("localhost:8080"); 235 | stringstream content("A string"); 236 | auto r = client.request("POST", "/string", content); 237 | assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); 238 | assert(r->content.string() == "A string"); 239 | assert(client.connections->size() == 1); 240 | } 241 | } 242 | 243 | server.stop(); 244 | server_thread.join(); 245 | 246 | return 0; 247 | } 248 | -------------------------------------------------------------------------------- /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 | typedef SimpleWeb::Server HttpServer; 23 | typedef SimpleWeb::Client HttpClient; 24 | 25 | //Added for the default_resource example 26 | void default_resource_send(const HttpServer &server, const shared_ptr &response, const shared_ptr &ifs); 27 | 28 | int main() { 29 | //HTTP-server at port 8080 using 1 thread 30 | //Unless you do more heavy non-threaded processing in the resources, 31 | //1 thread is usually faster than several threads 32 | HttpServer server; 33 | server.config.port = 8080; 34 | 35 | //Add resources using path-regex and method-string, and an anonymous function 36 | //POST-example for the path /string, responds the posted string 37 | server.resource["^/string$"]["POST"] = [](shared_ptr response, shared_ptr request) { 38 | //Retrieve string: 39 | auto content = request->content.string(); 40 | //request->content.string() is a convenience function for: 41 | //stringstream ss; 42 | //ss << request->content.rdbuf(); 43 | //auto content=ss.str(); 44 | 45 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" 46 | << content; 47 | 48 | 49 | // Alternatively, use one of the convenience functions, for instance: 50 | // response->write(content); 51 | }; 52 | 53 | //POST-example for the path /json, responds firstName+" "+lastName from the posted json 54 | //Responds with an appropriate error message if the posted json is not valid, or if firstName or lastName is missing 55 | //Example posted json: 56 | //{ 57 | // "firstName": "John", 58 | // "lastName": "Smith", 59 | // "age": 25 60 | //} 61 | server.resource["^/json$"]["POST"] = [](shared_ptr response, shared_ptr request) { 62 | try { 63 | ptree pt; 64 | read_json(request->content, pt); 65 | 66 | auto name = pt.get("firstName") + " " + pt.get("lastName"); 67 | 68 | *response << "HTTP/1.1 200 OK\r\n" 69 | << "Content-Type: application/json\r\n" 70 | << "Content-Length: " << name.length() << "\r\n\r\n" 71 | << name; 72 | } 73 | catch(const exception &e) { 74 | *response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" 75 | << e.what(); 76 | } 77 | 78 | 79 | // Alternatively, using a convenience function: 80 | // try { 81 | // ptree pt; 82 | // read_json(request->content, pt); 83 | 84 | // auto name=pt.get("firstName")+" "+pt.get("lastName"); 85 | // response->write(name, {{"Content-Type", "application/json"}}); 86 | // } 87 | // catch(const exception &e) { 88 | // response->write(SimpleWeb::StatusCode::client_error_bad_request, e.what()); 89 | // } 90 | }; 91 | 92 | //GET-example for the path /info 93 | //Responds with request-information 94 | server.resource["^/info$"]["GET"] = [](shared_ptr response, shared_ptr request) { 95 | stringstream stream; 96 | stream << "

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

"; 97 | stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; 98 | for(auto &header : request->header) 99 | stream << header.first << ": " << header.second << "
"; 100 | 101 | //find length of content_stream (length received using content_stream.tellp()) 102 | stream.seekp(0, ios::end); 103 | 104 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << stream.tellp() << "\r\n\r\n" 105 | << stream.rdbuf(); 106 | 107 | 108 | // Alternatively, using a convenience function: 109 | // stringstream stream; 110 | // stream << "

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

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

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

"; 95 | stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; 96 | for(auto &header : request->header) 97 | stream << header.first << ": " << header.second << "
"; 98 | 99 | //find length of content_stream (length received using content_stream.tellp()) 100 | stream.seekp(0, ios::end); 101 | 102 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << stream.tellp() << "\r\n\r\n" 103 | << stream.rdbuf(); 104 | 105 | 106 | // Alternatively, using a convenience function: 107 | // stringstream stream; 108 | // stream << "

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

"; 109 | // stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; 110 | // for(auto &header: request->header) 111 | // stream << header.first << ": " << header.second << "
"; 112 | // response->write(stream); 113 | }; 114 | 115 | //GET-example for the path /match/[number], responds with the matched string in path (number) 116 | //For instance a request GET /match/123 will receive: 123 117 | server.resource["^/match/([0-9]+)$"]["GET"] = [](shared_ptr response, shared_ptr request) { 118 | string number = request->path_match[1]; 119 | *response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" 120 | << number; 121 | 122 | 123 | // Alternatively, using a convenience function: 124 | // response->write(request->path_match[1]); 125 | }; 126 | 127 | //Get example simulating heavy work in a separate thread 128 | server.resource["^/work$"]["GET"] = [](shared_ptr response, shared_ptr /*request*/) { 129 | thread work_thread([response] { 130 | this_thread::sleep_for(chrono::seconds(5)); 131 | response->write("Work done"); 132 | }); 133 | work_thread.detach(); 134 | }; 135 | 136 | //Default GET-example. If no other matches, this anonymous function will be called. 137 | //Will respond with content in the web/-directory, and its subdirectories. 138 | //Default file: index.html 139 | //Can for instance be used to retrieve an HTML 5 client that uses REST-resources on this server 140 | server.default_resource["GET"] = [&server](shared_ptr response, shared_ptr request) { 141 | try { 142 | auto web_root_path = boost::filesystem::canonical("web"); 143 | auto path = boost::filesystem::canonical(web_root_path / request->path); 144 | //Check if path is within web_root_path 145 | if(distance(web_root_path.begin(), web_root_path.end()) > distance(path.begin(), path.end()) || 146 | !equal(web_root_path.begin(), web_root_path.end(), path.begin())) 147 | throw invalid_argument("path must be within root path"); 148 | if(boost::filesystem::is_directory(path)) 149 | path /= "index.html"; 150 | 151 | SimpleWeb::CaseInsensitiveMultimap header; 152 | 153 | // Uncomment the following line to enable Cache-Control 154 | // header.emplace("Cache-Control", "max-age=86400"); 155 | 156 | #ifdef HAVE_OPENSSL 157 | // Uncomment the following lines to enable ETag 158 | // { 159 | // ifstream ifs(path.string(), ifstream::in | ios::binary); 160 | // if(ifs) { 161 | // auto hash = SimpleWeb::Crypto::to_hex_string(SimpleWeb::Crypto::md5(ifs)); 162 | // header.emplace("ETag", "\"" + hash + "\""); 163 | // auto it = request->header.find("If-None-Match"); 164 | // if(it != request->header.end()) { 165 | // if(!it->second.empty() && it->second.compare(1, hash.size(), hash) == 0) { 166 | // response->write(SimpleWeb::StatusCode::redirection_not_modified, header); 167 | // return; 168 | // } 169 | // } 170 | // } 171 | // else 172 | // throw invalid_argument("could not read file"); 173 | // } 174 | #endif 175 | 176 | auto ifs = make_shared(); 177 | ifs->open(path.string(), ifstream::in | ios::binary | ios::ate); 178 | 179 | if(*ifs) { 180 | auto length = ifs->tellg(); 181 | ifs->seekg(0, ios::beg); 182 | 183 | header.emplace("Content-Length", to_string(length)); 184 | response->write(header); 185 | default_resource_send(server, response, ifs); 186 | } 187 | else 188 | throw invalid_argument("could not read file"); 189 | } 190 | catch(const exception &e) { 191 | response->write(SimpleWeb::StatusCode::client_error_bad_request, "Could not open path " + request->path + ": " + e.what()); 192 | } 193 | }; 194 | 195 | server.on_error = [](std::shared_ptr /*request*/, const SimpleWeb::error_code & /*ec*/) { 196 | // handle errors here 197 | }; 198 | 199 | thread server_thread([&server]() { 200 | //Start server 201 | server.start(); 202 | }); 203 | 204 | //Wait for server to start so that the client can connect 205 | this_thread::sleep_for(chrono::seconds(1)); 206 | 207 | //Client examples 208 | //Second Client() parameter set to false: no certificate verification 209 | HttpsClient client("localhost:8080", false); 210 | 211 | // synchronous request examples 212 | auto r1 = client.request("GET", "/match/123"); 213 | cout << r1->content.rdbuf() << endl; // Alternatively, use the convenience function r1->content.string() 214 | 215 | string json_string = "{\"firstName\": \"John\",\"lastName\": \"Smith\",\"age\": 25}"; 216 | auto r2 = client.request("POST", "/string", json_string); 217 | cout << r2->content.rdbuf() << endl; 218 | 219 | // asynchronous request example 220 | client.request("POST", "/json", json_string, [](std::shared_ptr response, const SimpleWeb::error_code &ec) { 221 | if(!ec) 222 | cout << response->content.rdbuf() << endl; 223 | }); 224 | client.io_service->reset(); // needed because the io_service has been run already in the synchronous examples 225 | client.io_service->run(); 226 | 227 | server_thread.join(); 228 | } 229 | 230 | void default_resource_send(const HttpsServer &server, const shared_ptr &response, const shared_ptr &ifs) { 231 | //read and send 128 KB at a time 232 | static vector buffer(131072); // Safe when server is running on one thread 233 | streamsize read_length; 234 | if((read_length = ifs->read(&buffer[0], buffer.size()).gcount()) > 0) { 235 | response->write(&buffer[0], read_length); 236 | if(read_length == static_cast(buffer.size())) { 237 | server.send(response, [&server, response, ifs](const SimpleWeb::error_code &ec) { 238 | if(!ec) 239 | default_resource_send(server, response, ifs); 240 | else 241 | cerr << "Connection interrupted" << endl; 242 | }); 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /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 | 11 | #ifdef USE_STANDALONE_ASIO 12 | #include 13 | namespace SimpleWeb { 14 | using error_code = std::error_code; 15 | using errc = std::errc; 16 | namespace make_error_code = std; 17 | } // namespace SimpleWeb 18 | #else 19 | #include 20 | namespace SimpleWeb { 21 | namespace asio = boost::asio; 22 | using error_code = boost::system::error_code; 23 | namespace errc = boost::system::errc; 24 | namespace make_error_code = boost::system::errc; 25 | } // namespace SimpleWeb 26 | #endif 27 | 28 | // Late 2017 TODO: remove the following checks and always use std::regex 29 | #ifdef USE_BOOST_REGEX 30 | #include 31 | namespace SimpleWeb { 32 | namespace regex = boost; 33 | } 34 | #else 35 | #include 36 | namespace SimpleWeb { 37 | namespace regex = std; 38 | } 39 | #endif 40 | 41 | namespace SimpleWeb { 42 | template 43 | class Server; 44 | 45 | template 46 | class ServerBase { 47 | public: 48 | virtual ~ServerBase() {} 49 | 50 | class Response : public std::ostream { 51 | friend class ServerBase; 52 | 53 | asio::streambuf streambuf; 54 | 55 | std::shared_ptr socket; 56 | 57 | Response(const std::shared_ptr &socket) : std::ostream(&streambuf), socket(socket) {} 58 | 59 | template 60 | void write_header(const CaseInsensitiveMultimap &header, size_type size) { 61 | bool content_length_written = false; 62 | bool chunked_transfer_encoding = false; 63 | for(auto &field : header) { 64 | if(!content_length_written && case_insensitive_equal(field.first, "content-length")) 65 | content_length_written = true; 66 | else if(!chunked_transfer_encoding && case_insensitive_equal(field.first, "transfer-encoding") && case_insensitive_equal(field.second, "chunked")) 67 | chunked_transfer_encoding = true; 68 | 69 | *this << field.first << ": " << field.second << "\r\n"; 70 | } 71 | if(!content_length_written && !chunked_transfer_encoding && !close_connection_after_response) 72 | *this << "Content-Length: " << size << "\r\n\r\n"; 73 | else 74 | *this << "\r\n"; 75 | } 76 | 77 | public: 78 | size_t size() { 79 | return streambuf.size(); 80 | } 81 | 82 | /// Write directly to stream buffer using std::ostream::write 83 | void write(const char_type *ptr, std::streamsize n) { 84 | std::ostream::write(ptr, n); 85 | } 86 | 87 | /// Convenience function for writing status line, potential header fields, and empty content 88 | void write(StatusCode status_code = StatusCode::success_ok, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 89 | *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; 90 | write_header(header, 0); 91 | } 92 | 93 | /// Convenience function for writing status line, header fields, and content 94 | void write(StatusCode status_code, const std::string &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 95 | *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; 96 | write_header(header, content.size()); 97 | if(!content.empty()) 98 | *this << content; 99 | } 100 | 101 | /// Convenience function for writing status line, header fields, and content 102 | void write(StatusCode status_code, std::istream &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 103 | *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; 104 | content.seekg(0, std::ios::end); 105 | auto size = content.tellg(); 106 | content.seekg(0, std::ios::beg); 107 | write_header(header, size); 108 | if(size) 109 | *this << content.rdbuf(); 110 | } 111 | 112 | /// Convenience function for writing success status line, header fields, and content 113 | void write(const std::string &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 114 | write(StatusCode::success_ok, content, header); 115 | } 116 | 117 | /// Convenience function for writing success status line, header fields, and content 118 | void write(std::istream &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 119 | write(StatusCode::success_ok, content, header); 120 | } 121 | 122 | /// Convenience function for writing success status line, and header fields 123 | void write(const CaseInsensitiveMultimap &header) { 124 | write(StatusCode::success_ok, std::string(), header); 125 | } 126 | 127 | /// If true, force server to close the connection after the response have been sent. 128 | /// 129 | /// This is useful when implementing a HTTP/1.0-server sending content 130 | /// without specifying the content length. 131 | bool close_connection_after_response = false; 132 | }; 133 | 134 | class Content : public std::istream { 135 | friend class ServerBase; 136 | 137 | public: 138 | size_t size() { 139 | return streambuf.size(); 140 | } 141 | /// Convenience function to return std::string. Note that the stream buffer is emptied when this functions is used. 142 | std::string string() { 143 | std::stringstream ss; 144 | ss << rdbuf(); 145 | return ss.str(); 146 | } 147 | 148 | private: 149 | asio::streambuf &streambuf; 150 | Content(asio::streambuf &streambuf) : std::istream(&streambuf), streambuf(streambuf) {} 151 | }; 152 | 153 | class Request { 154 | friend class ServerBase; 155 | friend class Server; 156 | 157 | public: 158 | std::string method, path, http_version; 159 | 160 | Content content; 161 | 162 | CaseInsensitiveMultimap header; 163 | 164 | regex::smatch path_match; 165 | 166 | std::string remote_endpoint_address; 167 | unsigned short remote_endpoint_port; 168 | 169 | /// Returns query keys with percent-decoded values. 170 | CaseInsensitiveMultimap parse_query_string() { 171 | auto pos = path.find('?'); 172 | if(pos != std::string::npos && pos + 1 < path.size()) 173 | return SimpleWeb::QueryString::parse(path.substr(pos + 1)); 174 | else 175 | return CaseInsensitiveMultimap(); 176 | } 177 | 178 | private: 179 | Request(const socket_type &socket) : content(streambuf) { 180 | try { 181 | remote_endpoint_address = socket.lowest_layer().remote_endpoint().address().to_string(); 182 | remote_endpoint_port = socket.lowest_layer().remote_endpoint().port(); 183 | } 184 | catch(...) { 185 | } 186 | } 187 | 188 | asio::streambuf streambuf; 189 | }; 190 | 191 | class Config { 192 | friend class ServerBase; 193 | 194 | Config(unsigned short port) : port(port) {} 195 | 196 | public: 197 | /// Port number to use. Defaults to 80 for HTTP and 443 for HTTPS. 198 | unsigned short port; 199 | /// Number of threads that the server will use when start() is called. Defaults to 1 thread. 200 | size_t thread_pool_size = 1; 201 | /// Timeout on request handling. Defaults to 5 seconds. 202 | size_t timeout_request = 5; 203 | /// Timeout on content handling. Defaults to 300 seconds. 204 | size_t timeout_content = 300; 205 | /// IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation. 206 | /// If empty, the address will be any address. 207 | std::string address; 208 | /// Set to false to avoid binding the socket to an address that is already in use. Defaults to true. 209 | bool reuse_address = true; 210 | }; 211 | ///Set before calling start(). 212 | Config config; 213 | 214 | private: 215 | class regex_orderable : public regex::regex { 216 | std::string str; 217 | 218 | public: 219 | regex_orderable(const char *regex_cstr) : regex::regex(regex_cstr), str(regex_cstr) {} 220 | regex_orderable(const std::string ®ex_str) : regex::regex(regex_str), str(regex_str) {} 221 | bool operator<(const regex_orderable &rhs) const { 222 | return str < rhs.str; 223 | } 224 | }; 225 | 226 | public: 227 | /// Warning: do not add or remove resources after start() is called 228 | std::map::Response>, std::shared_ptr::Request>)>>> resource; 229 | 230 | std::map::Response>, std::shared_ptr::Request>)>> default_resource; 231 | 232 | std::function::Request>, const error_code &)> on_error; 233 | 234 | std::function socket, std::shared_ptr::Request>)> on_upgrade; 235 | 236 | virtual void start() { 237 | if(!io_service) 238 | io_service = std::make_shared(); 239 | 240 | if(io_service->stopped()) 241 | io_service->reset(); 242 | 243 | asio::ip::tcp::endpoint endpoint; 244 | if(config.address.size() > 0) 245 | endpoint = asio::ip::tcp::endpoint(asio::ip::address::from_string(config.address), config.port); 246 | else 247 | endpoint = asio::ip::tcp::endpoint(asio::ip::tcp::v4(), config.port); 248 | 249 | if(!acceptor) 250 | acceptor = std::unique_ptr(new asio::ip::tcp::acceptor(*io_service)); 251 | acceptor->open(endpoint.protocol()); 252 | acceptor->set_option(asio::socket_base::reuse_address(config.reuse_address)); 253 | acceptor->bind(endpoint); 254 | acceptor->listen(); 255 | 256 | accept(); 257 | 258 | //If thread_pool_size>1, start m_io_service.run() in (thread_pool_size-1) threads for thread-pooling 259 | threads.clear(); 260 | for(size_t c = 1; c < config.thread_pool_size; c++) { 261 | threads.emplace_back([this]() { 262 | io_service->run(); 263 | }); 264 | } 265 | 266 | //Main thread 267 | if(config.thread_pool_size > 0) 268 | io_service->run(); 269 | 270 | //Wait for the rest of the threads, if any, to finish as well 271 | for(auto &t : threads) { 272 | t.join(); 273 | } 274 | } 275 | 276 | void stop() { 277 | acceptor->close(); 278 | if(config.thread_pool_size > 0) 279 | io_service->stop(); 280 | } 281 | 282 | ///Use this function if you need to recursively send parts of a longer message 283 | void send(const std::shared_ptr &response, const std::function &callback = nullptr) const { 284 | asio::async_write(*response->socket, response->streambuf, [this, response, callback](const error_code &ec, size_t /*bytes_transferred*/) { 285 | if(callback) 286 | callback(ec); 287 | }); 288 | } 289 | 290 | /// If you have your own asio::io_service, store its pointer here before running start(). 291 | /// You might also want to set config.thread_pool_size to 0. 292 | std::shared_ptr io_service; 293 | 294 | protected: 295 | std::unique_ptr acceptor; 296 | std::vector threads; 297 | 298 | ServerBase(unsigned short port) : config(port) {} 299 | 300 | virtual void accept() = 0; 301 | 302 | std::shared_ptr get_timeout_timer(const std::shared_ptr &socket, long seconds) { 303 | if(seconds == 0) 304 | return nullptr; 305 | 306 | auto timer = std::make_shared(*io_service); 307 | timer->expires_from_now(boost::posix_time::seconds(seconds)); 308 | timer->async_wait([socket](const error_code &ec) { 309 | if(!ec) { 310 | error_code ec; 311 | socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ec); 312 | socket->lowest_layer().close(); 313 | } 314 | }); 315 | return timer; 316 | } 317 | 318 | void read_request_and_content(const std::shared_ptr &socket) { 319 | //Create new streambuf (Request::streambuf) for async_read_until() 320 | //shared_ptr is used to pass temporary objects to the asynchronous functions 321 | std::shared_ptr request(new Request(*socket)); 322 | 323 | //Set timeout on the following asio::async-read or write function 324 | auto timer = this->get_timeout_timer(socket, config.timeout_request); 325 | 326 | asio::async_read_until(*socket, request->streambuf, "\r\n\r\n", [this, socket, request, timer](const error_code &ec, size_t bytes_transferred) { 327 | if(timer) 328 | timer->cancel(); 329 | if(!ec) { 330 | //request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs: 331 | //"After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter" 332 | //The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the 333 | //streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content). 334 | size_t num_additional_bytes = request->streambuf.size() - bytes_transferred; 335 | 336 | if(!this->parse_request(request)) 337 | return; 338 | 339 | //If content, read that as well 340 | auto it = request->header.find("Content-Length"); 341 | if(it != request->header.end()) { 342 | unsigned long long content_length; 343 | try { 344 | content_length = stoull(it->second); 345 | } 346 | catch(const std::exception &e) { 347 | if(on_error) 348 | on_error(request, make_error_code::make_error_code(errc::protocol_error)); 349 | return; 350 | } 351 | if(content_length > num_additional_bytes) { 352 | //Set timeout on the following asio::async-read or write function 353 | auto timer = this->get_timeout_timer(socket, config.timeout_content); 354 | asio::async_read(*socket, request->streambuf, asio::transfer_exactly(content_length - num_additional_bytes), [this, socket, request, timer](const error_code &ec, size_t /*bytes_transferred*/) { 355 | if(timer) 356 | timer->cancel(); 357 | if(!ec) 358 | this->find_resource(socket, request); 359 | else if(on_error) 360 | on_error(request, ec); 361 | }); 362 | } 363 | else 364 | this->find_resource(socket, request); 365 | } 366 | else 367 | this->find_resource(socket, request); 368 | } 369 | else if(on_error) 370 | on_error(request, ec); 371 | }); 372 | } 373 | 374 | bool parse_request(const std::shared_ptr &request) const { 375 | std::string line; 376 | getline(request->content, line); 377 | size_t method_end; 378 | if((method_end = line.find(' ')) != std::string::npos) { 379 | size_t path_end; 380 | if((path_end = line.find(' ', method_end + 1)) != std::string::npos) { 381 | request->method = line.substr(0, method_end); 382 | request->path = line.substr(method_end + 1, path_end - method_end - 1); 383 | 384 | size_t protocol_end; 385 | if((protocol_end = line.find('/', path_end + 1)) != std::string::npos) { 386 | if(line.compare(path_end + 1, protocol_end - path_end - 1, "HTTP") != 0) 387 | return false; 388 | request->http_version = line.substr(protocol_end + 1, line.size() - protocol_end - 2); 389 | } 390 | else 391 | return false; 392 | 393 | getline(request->content, line); 394 | size_t param_end; 395 | while((param_end = line.find(':')) != std::string::npos) { 396 | size_t value_start = param_end + 1; 397 | if((value_start) < line.size()) { 398 | if(line[value_start] == ' ') 399 | value_start++; 400 | if(value_start < line.size()) 401 | request->header.emplace(line.substr(0, param_end), line.substr(value_start, line.size() - value_start - 1)); 402 | } 403 | 404 | getline(request->content, line); 405 | } 406 | } 407 | else 408 | return false; 409 | } 410 | else 411 | return false; 412 | return true; 413 | } 414 | 415 | void find_resource(const std::shared_ptr &socket, const std::shared_ptr &request) { 416 | //Upgrade connection 417 | if(on_upgrade) { 418 | auto it = request->header.find("Upgrade"); 419 | if(it != request->header.end()) { 420 | on_upgrade(socket, request); 421 | return; 422 | } 423 | } 424 | //Find path- and method-match, and call write_response 425 | for(auto ®ex_method : resource) { 426 | auto it = regex_method.second.find(request->method); 427 | if(it != regex_method.second.end()) { 428 | regex::smatch sm_res; 429 | if(regex::regex_match(request->path, sm_res, regex_method.first)) { 430 | request->path_match = std::move(sm_res); 431 | write_response(socket, request, it->second); 432 | return; 433 | } 434 | } 435 | } 436 | auto it = default_resource.find(request->method); 437 | if(it != default_resource.end()) { 438 | write_response(socket, request, it->second); 439 | } 440 | } 441 | 442 | void write_response(const std::shared_ptr &socket, const std::shared_ptr &request, 443 | std::function::Response>, std::shared_ptr::Request>)> &resource_function) { 444 | //Set timeout on the following asio::async-read or write function 445 | auto timer = this->get_timeout_timer(socket, config.timeout_content); 446 | 447 | auto response = std::shared_ptr(new Response(socket), [this, request, timer](Response *response_ptr) { 448 | auto response = std::shared_ptr(response_ptr); 449 | this->send(response, [this, response, request, timer](const error_code &ec) { 450 | if(timer) 451 | timer->cancel(); 452 | if(!ec) { 453 | if(response->close_connection_after_response) 454 | return; 455 | 456 | auto range = request->header.equal_range("Connection"); 457 | for(auto it = range.first; it != range.second; it++) { 458 | if(case_insensitive_equal(it->second, "close")) { 459 | return; 460 | } 461 | else if(case_insensitive_equal(it->second, "keep-alive")) { 462 | this->read_request_and_content(response->socket); 463 | return; 464 | } 465 | } 466 | if(request->http_version >= "1.1") 467 | this->read_request_and_content(response->socket); 468 | } 469 | else if(on_error) 470 | on_error(request, ec); 471 | }); 472 | }); 473 | 474 | try { 475 | resource_function(response, request); 476 | } 477 | catch(const std::exception &e) { 478 | if(on_error) 479 | on_error(request, make_error_code::make_error_code(errc::operation_canceled)); 480 | return; 481 | } 482 | } 483 | }; 484 | 485 | template 486 | class Server : public ServerBase {}; 487 | 488 | typedef asio::ip::tcp::socket HTTP; 489 | 490 | template <> 491 | class Server : public ServerBase { 492 | public: 493 | DEPRECATED Server(unsigned short port, size_t thread_pool_size = 1, long timeout_request = 5, long timeout_content = 300) : Server() { 494 | config.port = port; 495 | config.thread_pool_size = thread_pool_size; 496 | config.timeout_request = timeout_request; 497 | config.timeout_content = timeout_content; 498 | } 499 | 500 | Server() : ServerBase::ServerBase(80) {} 501 | 502 | protected: 503 | void accept() { 504 | //Create new socket for this connection 505 | //Shared_ptr is used to pass temporary objects to the asynchronous functions 506 | auto socket = std::make_shared(*io_service); 507 | 508 | acceptor->async_accept(*socket, [this, socket](const error_code &ec) { 509 | //Immediately start accepting a new connection (if io_service hasn't been stopped) 510 | if(ec != asio::error::operation_aborted) 511 | accept(); 512 | 513 | if(!ec) { 514 | asio::ip::tcp::no_delay option(true); 515 | socket->set_option(option); 516 | 517 | this->read_request_and_content(socket); 518 | } 519 | else if(on_error) 520 | on_error(std::shared_ptr(new Request(*socket)), ec); 521 | }); 522 | } 523 | }; 524 | } // namespace SimpleWeb 525 | 526 | #endif /* SERVER_HTTP_HPP */ 527 | -------------------------------------------------------------------------------- /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 | 9 | #ifdef USE_STANDALONE_ASIO 10 | #include 11 | namespace SimpleWeb { 12 | using error_code = std::error_code; 13 | using errc = std::errc; 14 | using system_error = std::system_error; 15 | namespace make_error_code = std; 16 | using string_view = const std::string &; // TODO c++17: use std::string_view 17 | } // namespace SimpleWeb 18 | #else 19 | #include 20 | #include 21 | namespace SimpleWeb { 22 | namespace asio = boost::asio; 23 | using error_code = boost::system::error_code; 24 | namespace errc = boost::system::errc; 25 | using system_error = boost::system::system_error; 26 | namespace make_error_code = boost::system::errc; 27 | using string_view = boost::string_ref; 28 | } // namespace SimpleWeb 29 | #endif 30 | 31 | namespace SimpleWeb { 32 | template 33 | class Client; 34 | 35 | template 36 | class ClientBase { 37 | public: 38 | class Content : public std::istream { 39 | friend class ClientBase; 40 | 41 | public: 42 | size_t size() { 43 | return streambuf.size(); 44 | } 45 | /// Convenience function to return std::string. Note that the stream buffer is emptied when this functions is used. 46 | std::string string() { 47 | std::stringstream ss; 48 | ss << rdbuf(); 49 | return ss.str(); 50 | } 51 | 52 | private: 53 | asio::streambuf &streambuf; 54 | Content(asio::streambuf &streambuf) : std::istream(&streambuf), streambuf(streambuf) {} 55 | }; 56 | 57 | class Response { 58 | friend class ClientBase; 59 | friend class Client; 60 | 61 | public: 62 | std::string http_version, status_code; 63 | 64 | Content content; 65 | 66 | CaseInsensitiveMultimap header; 67 | 68 | private: 69 | asio::streambuf content_buffer; 70 | 71 | Response() : content(content_buffer) {} 72 | }; 73 | 74 | class Config { 75 | friend class ClientBase; 76 | 77 | private: 78 | Config() {} 79 | 80 | public: 81 | /// Set timeout on requests in seconds. Default value: 0 (no timeout). 82 | size_t timeout = 0; 83 | /// Set connect timeout in seconds. Default value: 0 (Config::timeout is then used instead). 84 | size_t timeout_connect = 0; 85 | /// Set proxy server (server:port) 86 | std::string proxy_server; 87 | }; 88 | 89 | protected: 90 | class Connection { 91 | public: 92 | Connection(const std::string &host, unsigned short port, const Config &config, std::unique_ptr &&socket) 93 | : host(host), port(port), config(config), socket(std::move(socket)) { 94 | if(config.proxy_server.empty()) 95 | query = std::unique_ptr(new asio::ip::tcp::resolver::query(host, std::to_string(port))); 96 | else { 97 | auto proxy_host_port = parse_host_port(config.proxy_server, 8080); 98 | query = std::unique_ptr(new asio::ip::tcp::resolver::query(proxy_host_port.first, std::to_string(proxy_host_port.second))); 99 | } 100 | } 101 | 102 | std::string host; 103 | unsigned short port; 104 | Config config; 105 | 106 | std::unique_ptr socket; 107 | bool in_use = false; 108 | bool reconnecting = false; 109 | 110 | std::unique_ptr query; 111 | }; 112 | 113 | class Session { 114 | public: 115 | Session(const std::shared_ptr &io_service, const std::shared_ptr &connection, std::unique_ptr &&request_buffer) 116 | : io_service(io_service), connection(connection), request_buffer(std::move(request_buffer)), response(new Response()) {} 117 | std::shared_ptr io_service; 118 | std::shared_ptr connection; 119 | std::unique_ptr request_buffer; 120 | std::shared_ptr response; 121 | std::function callback; 122 | }; 123 | 124 | public: 125 | /// Set before calling request 126 | Config config; 127 | 128 | /// If you have your own asio::io_service, store its pointer here before calling request(). 129 | /// When using asynchronous requests, running the io_service is up to the programmer. 130 | std::shared_ptr io_service; 131 | 132 | virtual ~ClientBase() {} 133 | 134 | /// Convenience function to perform synchronous request. The io_service is run within this function. 135 | /// If reusing the io_service for other tasks, please use the asynchronous request functions instead. 136 | std::shared_ptr request(const std::string &method, const std::string &path = std::string("/"), 137 | string_view content = "", const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 138 | std::shared_ptr response; 139 | request(method, path, content, header, [&response](std::shared_ptr response_, const error_code &ec) { 140 | response = response_; 141 | if(ec) 142 | throw system_error(ec); 143 | }); 144 | 145 | io_service->reset(); 146 | io_service->run(); 147 | 148 | return response; 149 | } 150 | 151 | /// Convenience function to perform synchronous request. The io_service is run within this function. 152 | /// If reusing the io_service for other tasks, please use the asynchronous request functions instead. 153 | std::shared_ptr request(const std::string &method, const std::string &path, std::istream &content, 154 | const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { 155 | std::shared_ptr response; 156 | request(method, path, content, header, [&response](std::shared_ptr response_, const error_code &ec) { 157 | response = response_; 158 | if(ec) 159 | throw system_error(ec); 160 | }); 161 | 162 | io_service->reset(); 163 | io_service->run(); 164 | 165 | return response; 166 | } 167 | 168 | /// Asynchronous request where setting and/or running Client's io_service is required. 169 | void request(const std::string &method, const std::string &path, string_view content, const CaseInsensitiveMultimap &header, 170 | std::function, const error_code &)> &&request_callback_) { 171 | auto session = std::make_shared(io_service, get_connection(), create_request_header(method, path, header)); 172 | auto connection = session->connection; 173 | auto response = session->response; 174 | auto request_callback = std::make_shared, const error_code &)>>(std::move(request_callback_)); 175 | auto connections = this->connections; 176 | auto connections_mutex = this->connections_mutex; 177 | session->callback = [connection, response, request_callback, connections, connections_mutex](const error_code &ec) { 178 | { 179 | std::lock_guard lock(*connections_mutex); 180 | connection->in_use = false; 181 | 182 | // Remove unused connections, but keep one open for HTTP persistent connection: 183 | size_t unused_connections = 0; 184 | for(auto it = connections->begin(); it != connections->end();) { 185 | if((*it)->in_use) 186 | ++it; 187 | else { 188 | ++unused_connections; 189 | if(unused_connections > 1) 190 | it = connections->erase(it); 191 | else 192 | ++it; 193 | } 194 | } 195 | } 196 | 197 | if(*request_callback) 198 | (*request_callback)(response, ec); 199 | }; 200 | 201 | std::ostream write_stream(session->request_buffer.get()); 202 | if(content.size() > 0) 203 | write_stream << "Content-Length: " << content.size() << "\r\n"; 204 | write_stream << "\r\n" 205 | << content; 206 | 207 | Client::connect(session); 208 | } 209 | 210 | /// Asynchronous request where setting and/or running Client's io_service is required. 211 | void request(const std::string &method, const std::string &path, string_view content, 212 | std::function, const error_code &)> &&request_callback) { 213 | request(method, path, content, CaseInsensitiveMultimap(), std::move(request_callback)); 214 | } 215 | 216 | /// Asynchronous request where setting and/or running Client's io_service is required. 217 | void request(const std::string &method, const std::string &path, 218 | std::function, const error_code &)> &&request_callback) { 219 | request(method, path, std::string(), CaseInsensitiveMultimap(), std::move(request_callback)); 220 | } 221 | 222 | /// Asynchronous request where setting and/or running Client's io_service is required. 223 | void request(const std::string &method, std::function, const error_code &)> &&request_callback) { 224 | request(method, std::string("/"), std::string(), CaseInsensitiveMultimap(), std::move(request_callback)); 225 | } 226 | 227 | /// Asynchronous request where setting and/or running Client's io_service is required. 228 | void request(const std::string &method, const std::string &path, std::istream &content, const CaseInsensitiveMultimap &header, 229 | std::function, const error_code &)> &&request_callback_) { 230 | auto session = std::make_shared(io_service, get_connection(), create_request_header(method, path, header)); 231 | auto connection = session->connection; 232 | auto response = session->response; 233 | auto request_callback = std::make_shared, const error_code &)>>(std::move(request_callback_)); 234 | auto connections = this->connections; 235 | auto connections_mutex = this->connections_mutex; 236 | session->callback = [connection, response, request_callback, connections, connections_mutex](const error_code &ec) { 237 | { 238 | std::lock_guard lock(*connections_mutex); 239 | connection->in_use = false; 240 | 241 | // Remove unused connections, but keep one open for HTTP persistent connection: 242 | size_t unused_connections = 0; 243 | for(auto it = connections->begin(); it != connections->end();) { 244 | if((*it)->in_use) 245 | ++it; 246 | else { 247 | ++unused_connections; 248 | if(unused_connections > 1) 249 | it = connections->erase(it); 250 | else 251 | ++it; 252 | } 253 | } 254 | } 255 | 256 | if(*request_callback) 257 | (*request_callback)(response, ec); 258 | }; 259 | 260 | content.seekg(0, std::ios::end); 261 | auto content_length = content.tellg(); 262 | content.seekg(0, std::ios::beg); 263 | std::ostream write_stream(session->request_buffer.get()); 264 | if(content_length > 0) 265 | write_stream << "Content-Length: " << content_length << "\r\n"; 266 | write_stream << "\r\n"; 267 | if(content_length > 0) 268 | write_stream << content.rdbuf(); 269 | 270 | Client::connect(session); 271 | } 272 | 273 | /// Asynchronous request where setting and/or running Client's io_service is required. 274 | void request(const std::string &method, const std::string &path, std::istream &content, 275 | std::function, const error_code &)> &&request_callback) { 276 | request(method, path, content, CaseInsensitiveMultimap(), std::move(request_callback)); 277 | } 278 | 279 | protected: 280 | std::string host; 281 | unsigned short port; 282 | 283 | std::shared_ptr>> connections; 284 | std::shared_ptr connections_mutex; 285 | 286 | ClientBase(const std::string &host_port, unsigned short default_port) 287 | : io_service(new asio::io_service()), connections(new std::vector>()), connections_mutex(new std::mutex()) { 288 | auto parsed_host_port = parse_host_port(host_port, default_port); 289 | host = parsed_host_port.first; 290 | port = parsed_host_port.second; 291 | } 292 | 293 | std::shared_ptr get_connection() { 294 | std::shared_ptr connection; 295 | std::lock_guard lock(*connections_mutex); 296 | for(auto it = connections->begin(); it != connections->end(); ++it) { 297 | if(!(*it)->in_use && !connection) { 298 | connection = *it; 299 | break; 300 | } 301 | } 302 | if(!connection) { 303 | connection = create_connection(); 304 | connections->emplace_back(connection); 305 | } 306 | connection->reconnecting = false; 307 | connection->in_use = true; 308 | return connection; 309 | } 310 | 311 | virtual std::shared_ptr create_connection() = 0; 312 | 313 | std::unique_ptr create_request_header(const std::string &method, const std::string &path, const CaseInsensitiveMultimap &header) const { 314 | auto corrected_path = path; 315 | if(corrected_path == "") 316 | corrected_path = "/"; 317 | if(!config.proxy_server.empty() && std::is_same::value) 318 | corrected_path = "http://" + host + ':' + std::to_string(port) + corrected_path; 319 | 320 | std::unique_ptr request_buffer(new asio::streambuf()); 321 | std::ostream write_stream(request_buffer.get()); 322 | write_stream << method << " " << corrected_path << " HTTP/1.1\r\n"; 323 | write_stream << "Host: " << host << "\r\n"; 324 | for(auto &h : header) 325 | write_stream << h.first << ": " << h.second << "\r\n"; 326 | return request_buffer; 327 | } 328 | 329 | static std::pair parse_host_port(const std::string &host_port, unsigned short default_port) { 330 | std::pair parsed_host_port; 331 | size_t host_end = host_port.find(':'); 332 | if(host_end == std::string::npos) { 333 | parsed_host_port.first = host_port; 334 | parsed_host_port.second = default_port; 335 | } 336 | else { 337 | parsed_host_port.first = host_port.substr(0, host_end); 338 | parsed_host_port.second = static_cast(stoul(host_port.substr(host_end + 1))); 339 | } 340 | return parsed_host_port; 341 | } 342 | 343 | static std::shared_ptr get_timeout_timer(const std::shared_ptr &session, size_t timeout = 0) { 344 | if(timeout == 0) 345 | timeout = session->connection->config.timeout; 346 | if(timeout == 0) 347 | return nullptr; 348 | 349 | auto timer = std::make_shared(*session->io_service); 350 | timer->expires_from_now(boost::posix_time::seconds(timeout)); 351 | timer->async_wait([session](const error_code &ec) { 352 | if(!ec) 353 | close(session); 354 | }); 355 | return timer; 356 | } 357 | 358 | static void parse_response_header(const std::shared_ptr &response) { 359 | std::string line; 360 | getline(response->content, line); 361 | size_t version_end = line.find(' '); 362 | if(version_end != std::string::npos) { 363 | if(5 < line.size()) 364 | response->http_version = line.substr(5, version_end - 5); 365 | if((version_end + 1) < line.size()) 366 | response->status_code = line.substr(version_end + 1, line.size() - (version_end + 1) - 1); 367 | 368 | getline(response->content, line); 369 | size_t param_end; 370 | while((param_end = line.find(':')) != std::string::npos) { 371 | size_t value_start = param_end + 1; 372 | if((value_start) < line.size()) { 373 | if(line[value_start] == ' ') 374 | value_start++; 375 | if(value_start < line.size()) 376 | response->header.insert(std::make_pair(line.substr(0, param_end), line.substr(value_start, line.size() - value_start - 1))); 377 | } 378 | 379 | getline(response->content, line); 380 | } 381 | } 382 | } 383 | 384 | static void write(const std::shared_ptr &session) { 385 | auto timer = get_timeout_timer(session); 386 | asio::async_write(*session->connection->socket, session->request_buffer->data(), [session, timer](const error_code &ec, size_t /*bytes_transferred*/) { 387 | if(timer) 388 | timer->cancel(); 389 | if(!ec) 390 | read(session); 391 | else { 392 | close(session); 393 | session->callback(ec); 394 | } 395 | }); 396 | } 397 | 398 | static void read(const std::shared_ptr &session) { 399 | auto timer = get_timeout_timer(session); 400 | asio::async_read_until(*session->connection->socket, session->response->content_buffer, "\r\n\r\n", [session, timer](const error_code &ec, size_t bytes_transferred) { 401 | if(timer) 402 | timer->cancel(); 403 | if(!ec) { 404 | session->connection->reconnecting = false; 405 | 406 | size_t num_additional_bytes = session->response->content_buffer.size() - bytes_transferred; 407 | 408 | parse_response_header(session->response); 409 | 410 | auto header_it = session->response->header.find("Content-Length"); 411 | if(header_it != session->response->header.end()) { 412 | auto content_length = stoull(header_it->second); 413 | if(content_length > num_additional_bytes) { 414 | auto timer = get_timeout_timer(session); 415 | asio::async_read(*session->connection->socket, session->response->content_buffer, asio::transfer_exactly(content_length - num_additional_bytes), [session, timer](const error_code &ec, size_t /*bytes_transferred*/) { 416 | if(timer) 417 | timer->cancel(); 418 | if(!ec) 419 | session->callback(ec); 420 | else { 421 | close(session); 422 | session->callback(ec); 423 | } 424 | }); 425 | } 426 | else 427 | session->callback(ec); 428 | } 429 | else if((header_it = session->response->header.find("Transfer-Encoding")) != session->response->header.end() && header_it->second == "chunked") { 430 | auto tmp_streambuf = std::make_shared(); 431 | read_chunked(session, tmp_streambuf); 432 | } 433 | else if(session->response->http_version < "1.1" || ((header_it = session->response->header.find("Session")) != session->response->header.end() && header_it->second == "close")) { 434 | auto timer = get_timeout_timer(session); 435 | asio::async_read(*session->connection->socket, session->response->content_buffer, [session, timer](const error_code &ec, size_t /*bytes_transferred*/) { 436 | if(timer) 437 | timer->cancel(); 438 | if(!ec) 439 | session->callback(ec); 440 | else { 441 | close(session); 442 | if(ec == asio::error::eof) { 443 | error_code ec; 444 | session->callback(ec); 445 | } 446 | else 447 | session->callback(ec); 448 | } 449 | }); 450 | } 451 | else 452 | session->callback(ec); 453 | } 454 | else { 455 | if(!session->connection->reconnecting) { 456 | session->connection->reconnecting = true; 457 | close(session); 458 | Client::connect(session); 459 | } 460 | else { 461 | close(session); 462 | session->callback(ec); 463 | } 464 | } 465 | }); 466 | } 467 | 468 | static void read_chunked(const std::shared_ptr &session, const std::shared_ptr &tmp_streambuf) { 469 | auto timer = get_timeout_timer(session); 470 | asio::async_read_until(*session->connection->socket, session->response->content_buffer, "\r\n", [session, tmp_streambuf, timer](const error_code &ec, size_t bytes_transferred) { 471 | if(timer) 472 | timer->cancel(); 473 | if(!ec) { 474 | std::string line; 475 | getline(session->response->content, line); 476 | bytes_transferred -= line.size() + 1; 477 | line.pop_back(); 478 | std::streamsize length = stol(line, 0, 16); 479 | 480 | auto num_additional_bytes = static_cast(session->response->content_buffer.size() - bytes_transferred); 481 | 482 | auto post_process = [session, tmp_streambuf, length] { 483 | std::ostream tmp_stream(tmp_streambuf.get()); 484 | if(length > 0) { 485 | std::vector buffer(static_cast(length)); 486 | session->response->content.read(&buffer[0], length); 487 | tmp_stream.write(&buffer[0], length); 488 | } 489 | 490 | //Remove "\r\n" 491 | session->response->content.get(); 492 | session->response->content.get(); 493 | 494 | if(length > 0) 495 | read_chunked(session, tmp_streambuf); 496 | else { 497 | std::ostream response_stream(&session->response->content_buffer); 498 | response_stream << tmp_stream.rdbuf(); 499 | error_code ec; 500 | session->callback(ec); 501 | } 502 | }; 503 | 504 | if((2 + length) > num_additional_bytes) { 505 | auto timer = get_timeout_timer(session); 506 | asio::async_read(*session->connection->socket, session->response->content_buffer, asio::transfer_exactly(2 + length - num_additional_bytes), [session, post_process, timer](const error_code &ec, size_t /*bytes_transferred*/) { 507 | if(timer) 508 | timer->cancel(); 509 | if(!ec) 510 | post_process(); 511 | else { 512 | close(session); 513 | session->callback(ec); 514 | } 515 | }); 516 | } 517 | else 518 | post_process(); 519 | } 520 | else { 521 | close(session); 522 | session->callback(ec); 523 | } 524 | }); 525 | } 526 | 527 | static void close(const std::shared_ptr &session) { 528 | error_code ec; 529 | session->connection->socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ec); 530 | session->connection->socket->lowest_layer().close(ec); 531 | } 532 | }; 533 | 534 | template 535 | class Client : public ClientBase {}; 536 | 537 | typedef asio::ip::tcp::socket HTTP; 538 | 539 | template <> 540 | class Client : public ClientBase { 541 | public: 542 | friend ClientBase; 543 | 544 | Client(const std::string &server_port_path) : ClientBase::ClientBase(server_port_path, 80) {} 545 | 546 | protected: 547 | std::shared_ptr create_connection() override { 548 | return std::make_shared(host, port, config, std::unique_ptr(new HTTP(*io_service))); 549 | } 550 | 551 | static void connect(const std::shared_ptr &session) { 552 | if(!session->connection->socket->lowest_layer().is_open()) { 553 | auto resolver = std::make_shared(*session->io_service); 554 | auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); 555 | resolver->async_resolve(*session->connection->query, [session, timer, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator it) { 556 | if(timer) 557 | timer->cancel(); 558 | if(!ec) { 559 | auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); 560 | asio::async_connect(*session->connection->socket, it, [session, timer, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator /*it*/) { 561 | if(timer) 562 | timer->cancel(); 563 | if(!ec) { 564 | asio::ip::tcp::no_delay option(true); 565 | session->connection->socket->set_option(option); 566 | write(session); 567 | } 568 | else { 569 | close(session); 570 | session->callback(ec); 571 | } 572 | }); 573 | } 574 | else { 575 | close(session); 576 | session->callback(ec); 577 | } 578 | }); 579 | } 580 | else 581 | write(session); 582 | } 583 | }; 584 | } // namespace SimpleWeb 585 | 586 | #endif /* CLIENT_HTTP_HPP */ 587 | --------------------------------------------------------------------------------