├── 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 [](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