summaryrefslogtreecommitdiffstatshomepage
path: root/src/lib/util
diff options
context:
space:
mode:
author Miodrag Milanovic <mmicko@gmail.com>2017-01-04 16:15:57 +0100
committer Miodrag Milanovic <mmicko@gmail.com>2017-01-04 16:15:57 +0100
commit63e3f487751eb096152e6e6bd90d8b47643a496b (patch)
tree3e42b162973ac8153a261c1a1351fbae4b09e748 /src/lib/util
parent47c4f47bd40def91566dda49cc378d4ae0d63e37 (diff)
Added initial HTTP/HTTPS webserver/websocket server support (nw)
Diffstat (limited to 'src/lib/util')
-rw-r--r--src/lib/util/base64.hpp167
-rw-r--r--src/lib/util/client_http.hpp438
-rw-r--r--src/lib/util/client_https.hpp152
-rw-r--r--src/lib/util/client_ws.hpp512
-rw-r--r--src/lib/util/client_wss.hpp75
-rw-r--r--src/lib/util/crypto.hpp18
-rw-r--r--src/lib/util/path_to_regex.cpp217
-rw-r--r--src/lib/util/path_to_regex.hpp110
-rw-r--r--src/lib/util/server_http.hpp497
-rw-r--r--src/lib/util/server_https.hpp99
-rw-r--r--src/lib/util/server_ws.hpp708
-rw-r--r--src/lib/util/server_wss.hpp77
-rw-r--r--src/lib/util/sha1.hpp181
13 files changed, 3251 insertions, 0 deletions
diff --git a/src/lib/util/base64.hpp b/src/lib/util/base64.hpp
new file mode 100644
index 00000000000..dc8f69335dc
--- /dev/null
+++ b/src/lib/util/base64.hpp
@@ -0,0 +1,167 @@
+// license:BSD-3-Clause
+// copyright-holders:René Nyffenegger
+/*
+ base64.cpp and base64.h
+
+ Copyright (C) 2004-2008 René Nyffenegger
+
+ This source code is provided 'as-is', without any express or implied
+ warranty. In no event will the author be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this source code must not be misrepresented; you must not
+ claim that you wrote the original source code. If you use this source code
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original source code.
+
+ 3. This notice may not be removed or altered from any source distribution.
+
+ René Nyffenegger rene.nyffenegger@adp-gmbh.ch
+
+*/
+
+#ifndef _BASE64_HPP_
+#define _BASE64_HPP_
+
+#include <string>
+
+static std::string const base64_chars =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz"
+ "0123456789+/";
+
+/// Test whether a character is a valid base64 character
+/**
+ * @param c The character to test
+ * @return true if c is a valid base64 character
+ */
+static inline bool is_base64(unsigned char c) {
+ return (c == 43 || // +
+ (c >= 47 && c <= 57) || // /-9
+ (c >= 65 && c <= 90) || // A-Z
+ (c >= 97 && c <= 122)); // a-z
+}
+
+/// Encode a char buffer into a base64 string
+/**
+ * @param input The input data
+ * @param len The length of input in bytes
+ * @return A base64 encoded string representing input
+ */
+inline std::string base64_encode(unsigned char const * input, size_t len) {
+ std::string ret;
+ int i = 0;
+ int j;
+ unsigned char char_array_3[3];
+ unsigned char char_array_4[4];
+
+ while (len--) {
+ char_array_3[i++] = *(input++);
+ if (i == 3) {
+ char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+ char_array_4[1] = ((char_array_3[0] & 0x03) << 4) +
+ ((char_array_3[1] & 0xf0) >> 4);
+ char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) +
+ ((char_array_3[2] & 0xc0) >> 6);
+ char_array_4[3] = char_array_3[2] & 0x3f;
+
+ for(i = 0; (i <4) ; i++) {
+ ret += base64_chars[char_array_4[i]];
+ }
+ i = 0;
+ }
+ }
+
+ if (i) {
+ for(j = i; j < 3; j++) {
+ char_array_3[j] = '\0';
+ }
+
+ char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+ char_array_4[1] = ((char_array_3[0] & 0x03) << 4) +
+ ((char_array_3[1] & 0xf0) >> 4);
+ char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) +
+ ((char_array_3[2] & 0xc0) >> 6);
+ char_array_4[3] = char_array_3[2] & 0x3f;
+
+ for (j = 0; (j < i + 1); j++) {
+ ret += base64_chars[char_array_4[j]];
+ }
+
+ while((i++ < 3)) {
+ ret += '=';
+ }
+ }
+
+ return ret;
+}
+
+/// Encode a string into a base64 string
+/**
+ * @param input The input data
+ * @return A base64 encoded string representing input
+ */
+inline std::string base64_encode(std::string const & input) {
+ return base64_encode(
+ reinterpret_cast<const unsigned char *>(input.data()),
+ input.size()
+ );
+}
+
+/// Decode a base64 encoded string into a string of raw bytes
+/**
+ * @param input The base64 encoded input data
+ * @return A string representing the decoded raw bytes
+ */
+inline std::string base64_decode(std::string const & input) {
+ size_t in_len = input.size();
+ int i = 0;
+ int j;
+ int in_ = 0;
+ unsigned char char_array_4[4], char_array_3[3];
+ std::string ret;
+
+ while (in_len-- && ( input[in_] != '=') && is_base64(input[in_])) {
+ char_array_4[i++] = input[in_]; in_++;
+ if (i ==4) {
+ for (i = 0; i <4; i++) {
+ char_array_4[i] = static_cast<unsigned char>(base64_chars.find(char_array_4[i]));
+ }
+
+ char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
+ char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
+ char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
+
+ for (i = 0; (i < 3); i++) {
+ ret += char_array_3[i];
+ }
+ i = 0;
+ }
+ }
+
+ if (i) {
+ for (j = i; j <4; j++)
+ char_array_4[j] = 0;
+
+ for (j = 0; j <4; j++)
+ char_array_4[j] = static_cast<unsigned char>(base64_chars.find(char_array_4[j]));
+
+ char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
+ char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
+ char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
+
+ for (j = 0; (j < i - 1); j++) {
+ ret += static_cast<std::string::value_type>(char_array_3[j]);
+ }
+ }
+
+ return ret;
+}
+#endif // _BASE64_HPP_
diff --git a/src/lib/util/client_http.hpp b/src/lib/util/client_http.hpp
new file mode 100644
index 00000000000..df188090f87
--- /dev/null
+++ b/src/lib/util/client_http.hpp
@@ -0,0 +1,438 @@
+// license:MIT
+// copyright-holders:Ole Christian Eidheim, Miodrag Milanovic
+#ifndef CLIENT_HTTP_HPP
+#define CLIENT_HTTP_HPP
+
+#if defined(_MSC_VER)
+#pragma warning(disable:4503)
+#endif
+
+#include "asio.h"
+
+#include <unordered_map>
+#include <map>
+#include <random>
+#include <mutex>
+
+#ifndef CASE_INSENSITIVE_EQUALS_AND_HASH
+#define CASE_INSENSITIVE_EQUALS_AND_HASH
+class case_insensitive_equals {
+public:
+ bool operator()(const std::string &key1, const std::string &key2) const {
+ return key1.size() == key2.size()
+ && equal(key1.cbegin(), key1.cend(), key2.cbegin(),
+ [](std::string::value_type key1v, std::string::value_type key2v)
+ { return tolower(key1v) == tolower(key2v); });
+ }
+};
+class case_insensitive_hash {
+public:
+ size_t operator()(const std::string &key) const {
+ size_t seed = 0;
+ for (auto &c : key) {
+ std::hash<char> hasher;
+ seed ^= hasher(std::tolower(c)) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+ }
+ return seed;
+ }
+};
+#endif
+
+namespace webpp {
+ template <class socket_type>
+ class Client;
+
+ template <class socket_type>
+ class ClientBase {
+ public:
+ virtual ~ClientBase() {}
+
+ class Response {
+ friend class ClientBase<socket_type>;
+ friend class Client<socket_type>;
+ public:
+ std::string http_version, status_code;
+
+ std::istream content;
+
+ std::unordered_multimap<std::string, std::string, case_insensitive_hash, case_insensitive_equals> header;
+
+ private:
+ asio::streambuf content_buffer;
+
+ Response(): content(&content_buffer) {}
+ };
+
+ class Config {
+ friend class ClientBase<socket_type>;
+ private:
+ Config() {}
+ public:
+ /// Set timeout on requests in seconds. Default value: 0 (no timeout).
+ size_t timeout = 0;
+ /// Set proxy server (server:port)
+ std::string proxy_server;
+ };
+
+ /// Set before calling request
+ Config config;
+
+ std::shared_ptr<Response> request(const std::string& request_type, const std::string& path="/", const std::string content="",
+ const std::map<std::string, std::string>& header=std::map<std::string, std::string>()) {
+ auto corrected_path=path;
+ if(corrected_path=="")
+ corrected_path="/";
+ if (!config.proxy_server.empty() && std::is_same<socket_type, asio::ip::tcp::socket>::value)
+ corrected_path = "http://" + host + ':' + std::to_string(port) + corrected_path;
+
+ asio::streambuf write_buffer;
+ std::ostream write_stream(&write_buffer);
+ write_stream << request_type << " " << corrected_path << " HTTP/1.1\r\n";
+ write_stream << "Host: " << host << "\r\n";
+ for(auto& h: header) {
+ write_stream << h.first << ": " << h.second << "\r\n";
+ }
+ if(content.size()>0)
+ write_stream << "Content-Length: " << content.size() << "\r\n";
+ write_stream << "\r\n";
+
+ connect();
+
+ auto timer = get_timeout_timer();
+ asio::async_write(*socket, write_buffer,
+ [this, &content, timer](const std::error_code &ec, size_t /*bytes_transferred*/) {
+ if (timer)
+ timer->cancel();
+ if (!ec) {
+ if (!content.empty()) {
+ auto timer2 = get_timeout_timer();
+ asio::async_write(*socket, asio::buffer(content.data(), content.size()),
+ [this,timer2](const std::error_code &ec, size_t /*bytes_transferred*/) {
+ if (timer2)
+ timer2->cancel();
+ if (ec) {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ }
+ }
+ else {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ io_context.reset();
+ io_context.run();
+ return request_read();
+ }
+
+ std::shared_ptr<Response> request(const std::string& request_type, const std::string& path, std::iostream& content,
+ const std::map<std::string, std::string>& header=std::map<std::string, std::string>()) {
+ auto corrected_path=path;
+ if(corrected_path=="")
+ corrected_path="/";
+ if (!config.proxy_server.empty() && std::is_same<socket_type, asio::ip::tcp::socket>::value)
+ corrected_path = "http://" + host + ':' + std::to_string(port) + corrected_path;
+
+ content.seekp(0, std::ios::end);
+ auto content_length=content.tellp();
+ content.seekp(0, std::ios::beg);
+
+ asio::streambuf write_buffer;
+ std::ostream write_stream(&write_buffer);
+ write_stream << request_type << " " << corrected_path << " HTTP/1.1\r\n";
+ write_stream << "Host: " << host << "\r\n";
+ for(auto& h: header) {
+ write_stream << h.first << ": " << h.second << "\r\n";
+ }
+ if(content_length>0)
+ write_stream << "Content-Length: " << content_length << "\r\n";
+ write_stream << "\r\n";
+ if(content_length>0)
+ write_stream << content.rdbuf();
+
+ connect();
+
+ auto timer = get_timeout_timer();
+ asio::async_write(*socket, write_buffer,
+ [this,timer](const std::error_code &ec, size_t /*bytes_transferred*/) {
+ if (timer)
+ timer->cancel();
+ if (ec) {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ io_context.reset();
+ io_context.run();
+
+ return request_read();
+ }
+ void close() {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ if (socket) {
+ std::error_code ec;
+ socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
+ socket->lowest_layer().close();
+ }
+ }
+ protected:
+ asio::io_context io_context;
+ asio::ip::tcp::resolver resolver;
+
+ std::unique_ptr<socket_type> socket;
+ std::mutex socket_mutex;
+
+ std::string host;
+ unsigned short port;
+
+ ClientBase(const std::string& host_port, unsigned short default_port) : resolver(io_context) {
+ auto parsed_host_port = parse_host_port(host_port, default_port);
+ host = parsed_host_port.first;
+ port = parsed_host_port.second;
+ }
+
+ std::pair<std::string, unsigned short> parse_host_port(const std::string &host_port, unsigned short default_port) const
+ {
+ std::pair<std::string, unsigned short> parsed_host_port;
+ size_t host_end=host_port.find(':');
+ if(host_end==std::string::npos) {
+ parsed_host_port.first =host_port;
+ parsed_host_port.second =default_port;
+ }
+ else {
+ parsed_host_port.first =host_port.substr(0, host_end);
+ parsed_host_port.second =static_cast<unsigned short>(stoul(host_port.substr(host_end+1)));
+ }
+ return parsed_host_port;
+ }
+
+ virtual void connect()=0;
+
+ std::shared_ptr<asio::system_timer> get_timeout_timer() {
+ if (config.timeout == 0)
+ return nullptr;
+
+ auto timer = std::make_shared<asio::system_timer>(io_context);
+ timer->expires_from_now(std::chrono::seconds(config.timeout));
+ timer->async_wait([this](const std::error_code& ec) {
+ if (!ec) {
+ close();
+ }
+ });
+ return timer;
+ }
+
+ void parse_response_header(const std::shared_ptr<Response> &response) const {
+ std::string line;
+ getline(response->content, line);
+ size_t version_end=line.find(' ');
+ if(version_end!=std::string::npos) {
+ if(5<line.size())
+ response->http_version=line.substr(5, version_end-5);
+ if((version_end+1)<line.size())
+ response->status_code=line.substr(version_end+1, line.size()-(version_end+1)-1);
+
+ getline(response->content, line);
+ size_t param_end;
+ while((param_end=line.find(':'))!=std::string::npos) {
+ size_t value_start=param_end+1;
+ if((value_start)<line.size()) {
+ if(line[value_start]==' ')
+ value_start++;
+ if(value_start<line.size())
+ response->header.insert(std::make_pair(line.substr(0, param_end), line.substr(value_start, line.size()-value_start-1)));
+ }
+
+ getline(response->content, line);
+ }
+ }
+ }
+
+ std::shared_ptr<Response> request_read() {
+ std::shared_ptr<Response> response(new Response());
+
+ asio::streambuf chunked_streambuf;
+
+ auto timer = get_timeout_timer();
+ asio::async_read_until(*socket, response->content_buffer, "\r\n\r\n",
+ [this, &response, &chunked_streambuf,timer](const std::error_code& ec, size_t bytes_transferred) {
+ if (timer)
+ timer->cancel();
+ if (!ec) {
+ size_t num_additional_bytes = response->content_buffer.size() - bytes_transferred;
+
+ parse_response_header(response);
+
+ auto header_it = response->header.find("Content-Length");
+ if (header_it != response->header.end()) {
+ auto content_length = stoull(header_it->second);
+ if (content_length>num_additional_bytes) {
+ auto timer2 = get_timeout_timer();
+ asio::async_read(*socket, response->content_buffer,
+ asio::transfer_exactly(size_t(content_length) - num_additional_bytes),
+ [this,timer2](const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if (timer2)
+ timer2->cancel();
+ if (ec) {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ }
+ }
+ else if ((header_it = response->header.find("Transfer-Encoding")) != response->header.end() && header_it->second == "chunked") {
+ request_read_chunked(response, chunked_streambuf);
+ }
+ }
+ else {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ io_context.reset();
+ io_context.run();
+
+ return response;
+ }
+
+ void request_read_chunked(const std::shared_ptr<Response> &response, asio::streambuf &streambuf) {
+ auto timer = get_timeout_timer();
+ asio::async_read_until(*socket, response->content_buffer, "\r\n",
+ [this, &response, &streambuf,timer](const std::error_code& ec, size_t bytes_transferred) {
+ if (timer)
+ timer->cancel();
+ if (!ec) {
+ std::string line;
+ getline(response->content, line);
+ bytes_transferred -= line.size() + 1;
+ line.pop_back();
+ std::streamsize length = stol(line, nullptr, 16);
+
+ auto num_additional_bytes = static_cast<std::streamsize>(response->content_buffer.size() - bytes_transferred);
+
+ auto post_process = [this, &response, &streambuf, length] {
+ std::ostream stream(&streambuf);
+ if (length > 0) {
+ std::vector<char> buffer(static_cast<size_t>(length));
+ response->content.read(&buffer[0], length);
+ stream.write(&buffer[0], length);
+ }
+
+ //Remove "\r\n"
+ response->content.get();
+ response->content.get();
+
+ if (length>0)
+ request_read_chunked(response, streambuf);
+ else {
+ std::ostream response_stream(&response->content_buffer);
+ response_stream << stream.rdbuf();
+ }
+ };
+
+ if ((2 + length)>num_additional_bytes) {
+ auto timer2 = get_timeout_timer();
+ asio::async_read(*socket, response->content_buffer,
+ asio::transfer_exactly(size_t(2 + length) - size_t(num_additional_bytes)),
+ [this, post_process, timer2](const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if (timer2)
+ timer2->cancel();
+ if (!ec) {
+ post_process();
+ }
+ else {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ }
+ else
+ post_process();
+ }
+ else {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ }
+ };
+
+ template<class socket_type>
+ class Client : public ClientBase<socket_type> {
+ public:
+ Client(const std::string& host_port, unsigned short default_port)
+ : ClientBase<socket_type>(host_port, default_port)
+ {
+ }
+ };
+
+ using HTTP = asio::ip::tcp::socket;
+
+ template<>
+ class Client<HTTP> : public ClientBase<HTTP> {
+ public:
+ explicit Client(const std::string& server_port_path) : ClientBase(server_port_path, 80) { }
+
+ protected:
+ void connect() override {
+ if(!socket || !socket->is_open()) {
+ std::unique_ptr<asio::ip::tcp::resolver::query> query;
+ if (config.proxy_server.empty())
+ query = std::make_unique<asio::ip::tcp::resolver::query>(host, std::to_string(port));
+ else {
+ auto proxy_host_port = parse_host_port(config.proxy_server, 8080);
+ query = std::make_unique<asio::ip::tcp::resolver::query>(proxy_host_port.first, std::to_string(proxy_host_port.second));
+
+ }
+ resolver.async_resolve(*query, [this](const std::error_code &ec,
+ asio::ip::tcp::resolver::iterator it){
+ if(!ec) {
+ {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = std::make_unique<HTTP>(io_context);
+ }
+
+ auto timer = get_timeout_timer();
+ asio::async_connect(*socket, it, [this,timer]
+ (const std::error_code &ec, asio::ip::tcp::resolver::iterator /*it*/){
+ if (timer)
+ timer->cancel();
+ if(!ec) {
+ asio::ip::tcp::no_delay option(true);
+ socket->set_option(option);
+ }
+ else {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket=nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ }
+ else {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket=nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ io_context.reset();
+ io_context.run();
+ }
+ }
+ };
+
+ class http_client : public Client<HTTP> {
+ public:
+ explicit http_client(const std::string& server_port_path) : Client<HTTP>::Client(server_port_path) {}
+ };
+}
+
+#endif /* CLIENT_HTTP_HPP */
diff --git a/src/lib/util/client_https.hpp b/src/lib/util/client_https.hpp
new file mode 100644
index 00000000000..fc1f12bb075
--- /dev/null
+++ b/src/lib/util/client_https.hpp
@@ -0,0 +1,152 @@
+// license:MIT
+// copyright-holders:Ole Christian Eidheim, Miodrag Milanovic
+#ifndef CLIENT_HTTPS_HPP
+#define CLIENT_HTTPS_HPP
+
+#include "client_http.hpp"
+#include "asio/ssl.hpp"
+
+namespace webpp {
+ using HTTPS = asio::ssl::stream<asio::ip::tcp::socket>;
+
+ template<>
+ class Client<HTTPS> : public ClientBase<HTTPS> {
+ public:
+ explicit Client(const std::string& server_port_path, bool verify_certificate=true,
+ const std::string& cert_file=std::string(), const std::string& private_key_file=std::string(),
+ const std::string& verify_file=std::string()) :
+ ClientBase(server_port_path, 443), m_context(asio::ssl::context::tlsv12) {
+ if(cert_file.size()>0 && private_key_file.size()>0) {
+ m_context.use_certificate_chain_file(cert_file);
+ m_context.use_private_key_file(private_key_file, asio::ssl::context::pem);
+ }
+
+ if(verify_certificate)
+ m_context.set_verify_callback(asio::ssl::rfc2818_verification(host));
+
+ if(verify_file.size()>0)
+ m_context.load_verify_file(verify_file);
+ else
+ m_context.set_default_verify_paths();
+
+ if(verify_file.size()>0 || verify_certificate)
+ m_context.set_verify_mode(asio::ssl::verify_peer);
+ else
+ m_context.set_verify_mode(asio::ssl::verify_none);
+ }
+
+ protected:
+ asio::ssl::context m_context;
+
+ void connect() override {
+ if(!socket || !socket->lowest_layer().is_open()) {
+ std::unique_ptr<asio::ip::tcp::resolver::query> query;
+ if (config.proxy_server.empty()) {
+ query = std::make_unique<asio::ip::tcp::resolver::query>(host, std::to_string(port));
+ }
+ else {
+ auto proxy_host_port = parse_host_port(config.proxy_server, 8080);
+ query = std::make_unique<asio::ip::tcp::resolver::query>(proxy_host_port.first, std::to_string(proxy_host_port.second));
+ }
+ resolver.async_resolve(*query, [this]
+ (const std::error_code &ec, asio::ip::tcp::resolver::iterator it) {
+ if (!ec) {
+ {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = std::make_unique<HTTPS>(io_context, m_context);
+ }
+
+ auto timer = get_timeout_timer();
+ asio::async_connect(socket->lowest_layer(), it, [this, timer]
+ (const std::error_code &ec, asio::ip::tcp::resolver::iterator /*it*/) {
+ if (timer)
+ timer->cancel();
+ if (!ec) {
+ asio::ip::tcp::no_delay option(true);
+ socket->lowest_layer().set_option(option);
+ }
+ else {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ }
+ else {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ io_context.reset();
+ io_context.run();
+
+ if (!config.proxy_server.empty()) {
+ asio::streambuf write_buffer;
+ std::ostream write_stream(&write_buffer);
+ auto host_port = host + ':' + std::to_string(port);
+ write_stream << "CONNECT " + host_port + " HTTP/1.1\r\n" << "Host: " << host_port << "\r\n\r\n";
+ auto timer = get_timeout_timer();
+ asio::async_write(socket->next_layer(), write_buffer,
+ [this, timer](const std::error_code &ec, size_t /*bytes_transferred*/) {
+ if (timer)
+ timer->cancel();
+ if (ec) {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ io_context.reset();
+ io_context.run();
+
+ std::shared_ptr<Response> response(new Response());
+ timer=get_timeout_timer();
+ asio::async_read_until(socket->next_layer(), response->content_buffer, "\r\n\r\n",
+ [this, timer](const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(timer)
+ timer->cancel();
+ if(ec) {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket=nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ io_context.reset();
+ io_context.run();
+ parse_response_header(response);
+
+ if (response->status_code.size()>0 && response->status_code.compare(0, 3, "200") != 0) {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(std::error_code(int(std::errc::permission_denied), std::generic_category()));
+ }
+ }
+
+ auto timer = get_timeout_timer();
+ this->socket->async_handshake(asio::ssl::stream_base::client,
+ [this, timer](const std::error_code& ec) {
+ if (timer)
+ timer->cancel();
+ if (ec) {
+ std::lock_guard<std::mutex> lock(socket_mutex);
+ socket = nullptr;
+ throw std::system_error(ec);
+ }
+ });
+ io_context.reset();
+ io_context.run();
+ }
+ }
+ };
+
+ class https_client : public Client<HTTPS> {
+ public:
+ explicit https_client(const std::string& server_port_path, bool verify_certificate = true,
+ const std::string& cert_file = std::string(), const std::string& private_key_file = std::string(),
+ const std::string& verify_file = std::string()) : Client<HTTPS>::Client(server_port_path, verify_certificate, cert_file, private_key_file, verify_file) {}
+ };
+
+}
+
+#endif /* CLIENT_HTTPS_HPP */
diff --git a/src/lib/util/client_ws.hpp b/src/lib/util/client_ws.hpp
new file mode 100644
index 00000000000..52f294bc983
--- /dev/null
+++ b/src/lib/util/client_ws.hpp
@@ -0,0 +1,512 @@
+// license:MIT
+// copyright-holders:Ole Christian Eidheim, Miodrag Milanovic
+#ifndef CLIENT_WS_HPP
+#define CLIENT_WS_HPP
+
+#include "asio.h"
+
+#include <unordered_map>
+#include <iostream>
+#include <random>
+#include <atomic>
+#include <list>
+#include "crypto.hpp"
+
+#ifndef CASE_INSENSITIVE_EQUALS_AND_HASH
+#define CASE_INSENSITIVE_EQUALS_AND_HASH
+class case_insensitive_equals {
+public:
+ bool operator()(const std::string &key1, const std::string &key2) const {
+ return key1.size() == key2.size()
+ && equal(key1.cbegin(), key1.cend(), key2.cbegin(),
+ [](std::string::value_type key1v, std::string::value_type key2v)
+ { return tolower(key1v) == tolower(key2v); });
+ }
+};
+class case_insensitive_hash {
+public:
+ size_t operator()(const std::string &key) const {
+ size_t seed = 0;
+ for (auto &c : key) {
+ std::hash<char> hasher;
+ seed ^= hasher(std::tolower(c)) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+ }
+ return seed;
+ }
+};
+#endif
+
+namespace webpp {
+ template <class socket_type>
+ class SocketClient;
+
+ template <class socket_type>
+ class SocketClientBase {
+ public:
+ virtual ~SocketClientBase() { connection.reset(); }
+
+ class SendStream : public std::iostream {
+ friend class SocketClientBase<socket_type>;
+ asio::streambuf streambuf;
+ public:
+ SendStream(): std::iostream(&streambuf) {}
+ size_t size() const {
+ return streambuf.size();
+ }
+ };
+
+ class Connection {
+ friend class SocketClientBase<socket_type>;
+ friend class SocketClient<socket_type>;
+
+ public:
+ std::unordered_multimap<std::string, std::string, case_insensitive_hash, case_insensitive_equals> header;
+ std::string remote_endpoint_address;
+ unsigned short remote_endpoint_port;
+
+ private:
+ explicit Connection(socket_type* socket): remote_endpoint_port(0), socket(socket), strand(socket->get_io_context()), closed(false) { }
+
+ class SendData {
+ public:
+ SendData(const std::shared_ptr<SendStream> &send_stream, const std::function<void(const std::error_code)> &callback) :
+ send_stream(send_stream), callback(callback) {}
+ std::shared_ptr<SendStream> send_stream;
+ std::function<void(const std::error_code)> callback;
+ };
+
+ std::unique_ptr<socket_type> socket;
+
+ asio::io_context::strand strand;
+
+ std::list<SendData> send_queue;
+
+ void send_from_queue() {
+ strand.post([this]() {
+ asio::async_write(*socket, send_queue.begin()->send_stream->streambuf,
+ strand.wrap([this](const std::error_code& ec, size_t /*bytes_transferred*/) {
+ auto send_queued=send_queue.begin();
+ if(send_queued->callback)
+ send_queued->callback(ec);
+ if(!ec) {
+ send_queue.erase(send_queued);
+ if(send_queue.size()>0)
+ send_from_queue();
+ }
+ else
+ send_queue.clear();
+ }));
+ });
+ }
+
+ std::atomic<bool> closed;
+
+ void read_remote_endpoint_data() {
+ try {
+ remote_endpoint_address=socket->lowest_layer().remote_endpoint().address().to_string();
+ remote_endpoint_port=socket->lowest_layer().remote_endpoint().port();
+ }
+ catch(const std::exception& e) {
+ std::cerr << e.what() << std::endl;
+ }
+ }
+ };
+
+ std::shared_ptr<Connection> connection;
+
+ class Message : public std::istream {
+ friend class SocketClientBase<socket_type>;
+
+ public:
+ unsigned char fin_rsv_opcode;
+ size_t size() const {
+ return length;
+ }
+ std::string string() const {
+ std::stringstream ss;
+ ss << rdbuf();
+ return ss.str();
+ }
+ private:
+ Message(): std::istream(&streambuf), fin_rsv_opcode(0), length(0) { }
+
+ size_t length;
+ asio::streambuf streambuf;
+ };
+
+ std::function<void()> on_open;
+ std::function<void(std::shared_ptr<Message>)> on_message;
+ std::function<void(int, const std::string&)> on_close;
+ std::function<void(const std::error_code&)> on_error;
+
+ void start() {
+ if(!io_context) {
+ io_context=std::make_shared<asio::io_context>();
+ internal_io_context=true;
+ }
+
+ if(io_context->stopped())
+ io_context->reset();
+
+ if(!resolver)
+ resolver= std::make_unique<asio::ip::tcp::resolver>(*io_context);
+
+ connect();
+
+ if(internal_io_context)
+ io_context->run();
+ }
+
+ void stop() const {
+ resolver->cancel();
+ if(internal_io_context)
+ io_context->stop();
+ }
+
+ ///fin_rsv_opcode: 129=one fragment, text, 130=one fragment, binary, 136=close connection.
+ ///See http://tools.ietf.org/html/rfc6455#section-5.2 for more information
+ void send(const std::shared_ptr<SendStream> &message_stream, const std::function<void(const std::error_code&)>& callback=nullptr,
+ unsigned char fin_rsv_opcode=129) {
+ //Create mask
+ std::vector<unsigned char> mask;
+ mask.resize(4);
+ std::uniform_int_distribution<unsigned short> dist(0,255);
+ std::random_device rd;
+ for(int c=0;c<4;c++) {
+ mask[c]=static_cast<unsigned char>(dist(rd));
+ }
+
+ auto send_stream = std::make_shared<SendStream>();
+
+ size_t length=message_stream->size();
+
+ send_stream->put(fin_rsv_opcode);
+ //masked (first length byte>=128)
+ if(length>=126) {
+ int num_bytes;
+ if(length>0xffff) {
+ num_bytes=8;
+ send_stream->put(static_cast<unsigned char>(127+128));
+ }
+ else {
+ num_bytes=2;
+ send_stream->put(static_cast<unsigned char>(126+128));
+ }
+
+ for(int c=num_bytes-1;c>=0;c--) {
+ send_stream->put((static_cast<unsigned long long>(length) >> (8 * c)) % 256);
+ }
+ }
+ else
+ send_stream->put(static_cast<unsigned char>(length+128));
+
+ for(int c=0;c<4;c++) {
+ send_stream->put(mask[c]);
+ }
+
+ for(size_t c=0;c<length;c++) {
+ send_stream->put(message_stream->get()^mask[c%4]);
+ }
+
+ connection->strand.post([this, send_stream, callback]() {
+ connection->send_queue.emplace_back(send_stream, callback);
+ if(connection->send_queue.size()==1)
+ connection->send_from_queue();
+ });
+ }
+
+ void send_close(int status, const std::string& reason="", const std::function<void(const std::error_code&)>& callback=nullptr) {
+ //Send close only once (in case close is initiated by client)
+ if(connection->closed)
+ return;
+ connection->closed=true;
+
+ auto send_stream=std::make_shared<SendStream>();
+
+ send_stream->put(status>>8);
+ send_stream->put(status%256);
+
+ *send_stream << reason;
+
+ //fin_rsv_opcode=136: message close
+ send(send_stream, callback, 136);
+ }
+
+ /// If you have your own asio::io_context, store its pointer here before running start().
+ std::shared_ptr<asio::io_context> io_context;
+ protected:
+ const std::string ws_magic_string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+ bool internal_io_context=false;
+ std::unique_ptr<asio::ip::tcp::resolver> resolver;
+
+ std::string host;
+ unsigned short port;
+ std::string path;
+
+ SocketClientBase(const std::string& host_port_path, unsigned short default_port) {
+ size_t host_end=host_port_path.find(':');
+ size_t host_port_end=host_port_path.find('/');
+ if(host_end==std::string::npos) {
+ host_end=host_port_end;
+ port=default_port;
+ }
+ else {
+ if(host_port_end==std::string::npos)
+ port=static_cast<unsigned short>(stoul(host_port_path.substr(host_end + 1)));
+ else
+ port=static_cast<unsigned short>(stoul(host_port_path.substr(host_end + 1, host_port_end - (host_end + 1))));
+ }
+ if(host_port_end==std::string::npos)
+ path="/";
+ else
+ path=host_port_path.substr(host_port_end);
+ if(host_end==std::string::npos)
+ host=host_port_path;
+ else
+ host=host_port_path.substr(0, host_end);
+ }
+
+ virtual void connect()=0;
+
+ void handshake() {
+ connection->read_remote_endpoint_data();
+
+ auto write_buffer = std::make_shared<asio::streambuf>();
+
+ std::ostream request(write_buffer.get());
+
+ request << "GET " << path << " HTTP/1.1" << "\r\n";
+ request << "Host: " << host << "\r\n";
+ request << "Upgrade: websocket\r\n";
+ request << "Connection: Upgrade\r\n";
+
+ //Make random 16-byte nonce
+ std::string nonce;
+ nonce.resize(16);
+ std::uniform_int_distribution<unsigned short> dist(0,255);
+ std::random_device rd;
+ for(int c=0;c<16;c++)
+ nonce[c]=static_cast<unsigned char>(dist(rd));
+
+ auto nonce_base64 = std::make_shared<std::string>(base64_encode(nonce));
+ request << "Sec-WebSocket-Key: " << *nonce_base64 << "\r\n";
+ request << "Sec-WebSocket-Version: 13\r\n";
+ request << "\r\n";
+
+ asio::async_write(*connection->socket, *write_buffer,
+ [this, write_buffer, nonce_base64]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ std::shared_ptr<Message> message(new Message());
+
+ asio::async_read_until(*connection->socket, message->streambuf, "\r\n\r\n",
+ [this, message, nonce_base64]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ parse_handshake(*message);
+ auto header_it = connection->header.find("Sec-WebSocket-Accept");
+ if (header_it != connection->header.end() &&
+ base64_decode(header_it->second) == sha1_encode(*nonce_base64 + ws_magic_string)) {
+ if(on_open)
+ on_open();
+ read_message(message);
+ }
+ else if(on_error)
+ on_error(std::error_code(int(std::errc::protocol_error), std::generic_category()));
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+
+ void parse_handshake(std::istream& stream) const {
+ std::string line;
+ getline(stream, line);
+ //Not parsing the first line
+
+ getline(stream, line);
+ size_t param_end;
+ while((param_end=line.find(':'))!=std::string::npos) {
+ size_t value_start=param_end+1;
+ if((value_start)<line.size()) {
+ if(line[value_start]==' ')
+ value_start++;
+ if(value_start<line.size())
+ connection->header.emplace(line.substr(0, param_end), line.substr(value_start, line.size()-value_start-1));
+ }
+
+ getline(stream, line);
+ }
+ }
+
+ void read_message(const std::shared_ptr<Message> &message) {
+ asio::async_read(*connection->socket, message->streambuf, asio::transfer_exactly(2),
+ [this, message](const std::error_code& ec, size_t bytes_transferred) {
+ if(!ec) {
+ if(bytes_transferred==0) { //TODO: This might happen on server at least, might also happen here
+ read_message(message);
+ return;
+ }
+ std::vector<unsigned char> first_bytes;
+ first_bytes.resize(2);
+ message->read(reinterpret_cast<char*>(&first_bytes[0]), 2);
+
+ message->fin_rsv_opcode=first_bytes[0];
+
+ //Close connection if masked message from server (protocol error)
+ if(first_bytes[1]>=128) {
+ const std::string reason("message from server masked");
+ auto kept_connection=connection;
+ send_close(1002, reason, [this, kept_connection](const std::error_code& /*ec*/) {});
+ if(on_close)
+ on_close(1002, reason);
+ return;
+ }
+
+ size_t length=(first_bytes[1]&127);
+
+ if(length==126) {
+ //2 next bytes is the size of content
+ asio::async_read(*connection->socket, message->streambuf, asio::transfer_exactly(2),
+ [this, message]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ std::vector<unsigned char> length_bytes;
+ length_bytes.resize(2);
+ message->read(reinterpret_cast<char*>(&length_bytes[0]), 2);
+
+ size_t length=0;
+ int num_bytes=2;
+ for(int c=0;c<num_bytes;c++)
+ length+=length_bytes[c]<<(8*(num_bytes-1-c));
+
+ message->length=length;
+ read_message_content(message);
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+ else if(length==127) {
+ //8 next bytes is the size of content
+ asio::async_read(*connection->socket, message->streambuf, asio::transfer_exactly(8),
+ [this, message]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ std::vector<unsigned char> length_bytes;
+ length_bytes.resize(8);
+ message->read(reinterpret_cast<char*>(&length_bytes[0]), 8);
+
+ size_t length=0;
+ int num_bytes=8;
+ for(int c=0;c<num_bytes;c++)
+ length+=length_bytes[c]<<(8*(num_bytes-1-c));
+
+ message->length=length;
+ read_message_content(message);
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+ else {
+ message->length=length;
+ read_message_content(message);
+ }
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+
+ void read_message_content(const std::shared_ptr<Message> &message) {
+ asio::async_read(*connection->socket, message->streambuf, asio::transfer_exactly(message->length),
+ [this, message]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ //If connection close
+ if((message->fin_rsv_opcode&0x0f)==8) {
+ int status=0;
+ if(message->length>=2) {
+ unsigned char byte1=message->get();
+ unsigned char byte2=message->get();
+ status=(byte1<<8)+byte2;
+ }
+
+ auto reason=message->string();
+ auto kept_connection=connection;
+ send_close(status, reason, [this, kept_connection](const std::error_code& /*ec*/) {});
+ if(on_close)
+ on_close(status, reason);
+ return;
+ }
+ //If ping
+ else if((message->fin_rsv_opcode&0x0f)==9) {
+ //send pong
+ auto empty_send_stream=std::make_shared<SendStream>();
+ send(empty_send_stream, nullptr, message->fin_rsv_opcode+1);
+ }
+ else if(on_message) {
+ on_message(message);
+ }
+
+ //Next message
+ std::shared_ptr<Message> next_message(new Message());
+ read_message(next_message);
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+ };
+
+ template<class socket_type>
+ class SocketClient : public SocketClientBase<socket_type> {
+ public:
+ SocketClient(const std::string& host_port_path, unsigned short default_port)
+ : SocketClientBase<socket_type>(host_port_path, default_port)
+ {
+ }
+ };
+
+ using WS = asio::ip::tcp::socket;
+
+ template<>
+ class SocketClient<WS> : public SocketClientBase<WS> {
+ public:
+ explicit SocketClient(const std::string& server_port_path) : SocketClientBase(server_port_path, 80) {};
+
+ protected:
+ void connect() override {
+ asio::ip::tcp::resolver::query query(host, std::to_string(port));
+
+ resolver->async_resolve(query, [this]
+ (const std::error_code &ec, asio::ip::tcp::resolver::iterator it){
+ if(!ec) {
+ connection=std::shared_ptr<Connection>(new Connection(new WS(*io_context)));
+
+ asio::async_connect(*connection->socket, it, [this]
+ (const std::error_code &ec, asio::ip::tcp::resolver::iterator /*it*/){
+ if(!ec) {
+ asio::ip::tcp::no_delay option(true);
+ connection->socket->set_option(option);
+
+ handshake();
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+ };
+}
+
+#endif /* CLIENT_WS_HPP */
diff --git a/src/lib/util/client_wss.hpp b/src/lib/util/client_wss.hpp
new file mode 100644
index 00000000000..ae00c0346cb
--- /dev/null
+++ b/src/lib/util/client_wss.hpp
@@ -0,0 +1,75 @@
+// license:MIT
+// copyright-holders:Ole Christian Eidheim, Miodrag Milanovic
+#ifndef CLIENT_WSS_HPP
+#define CLIENT_WSS_HPP
+
+#include "client_ws.hpp"
+#include "asio/ssl.hpp"
+
+namespace webpp {
+ using WSS = asio::ssl::stream<asio::ip::tcp::socket>;
+
+ template<>
+ class SocketClient<WSS> : public SocketClientBase<WSS> {
+ public:
+ explicit SocketClient(const std::string& server_port_path, bool verify_certificate=true,
+ const std::string& cert_file=std::string(), const std::string& private_key_file=std::string(),
+ const std::string& verify_file=std::string()) :
+ SocketClientBase(server_port_path, 443),
+ context(asio::ssl::context::tlsv12) {
+ if(cert_file.size()>0 && private_key_file.size()>0) {
+ context.use_certificate_chain_file(cert_file);
+ context.use_private_key_file(private_key_file, asio::ssl::context::pem);
+ }
+
+ if (verify_certificate)
+ context.set_verify_callback(asio::ssl::rfc2818_verification(host));
+
+ if (verify_file.size() > 0)
+ context.load_verify_file(verify_file);
+ else
+ context.set_default_verify_paths();
+
+ if (verify_file.size()>0 || verify_certificate)
+ context.set_verify_mode(asio::ssl::verify_peer);
+ else
+ context.set_verify_mode(asio::ssl::verify_none);
+ };
+
+ protected:
+ asio::ssl::context context;
+
+ void connect() override {
+ asio::ip::tcp::resolver::query query(host, std::to_string(port));
+
+ resolver->async_resolve(query, [this]
+ (const std::error_code &ec, asio::ip::tcp::resolver::iterator it){
+ if(!ec) {
+ connection=std::shared_ptr<Connection>(new Connection(new WSS(*io_context, context)));
+
+ asio::async_connect(connection->socket->lowest_layer(), it, [this]
+ (const std::error_code &ec, asio::ip::tcp::resolver::iterator /*it*/){
+ if(!ec) {
+ asio::ip::tcp::no_delay option(true);
+ connection->socket->lowest_layer().set_option(option);
+
+ connection->socket->async_handshake(asio::ssl::stream_base::client,
+ [this](const std::error_code& ec) {
+ if(!ec)
+ handshake();
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+ else if(on_error)
+ on_error(ec);
+ });
+ }
+ };
+}
+
+#endif /* CLIENT_WSS_HPP */
diff --git a/src/lib/util/crypto.hpp b/src/lib/util/crypto.hpp
new file mode 100644
index 00000000000..705aad6a375
--- /dev/null
+++ b/src/lib/util/crypto.hpp
@@ -0,0 +1,18 @@
+// license:MIT
+// copyright-holders:Miodrag Milanovic
+#ifndef CRYPTO_HPP
+#define CRYPTO_HPP
+
+#include "base64.hpp"
+#include "sha1.hpp"
+
+inline std::string sha1_encode(const std::string& input)
+{
+ char message_digest[20];
+ sha1::calc(input.c_str(),input.length(),reinterpret_cast<unsigned char*>(message_digest));
+
+ return std::string(message_digest, sizeof(message_digest));
+
+}
+#endif /* CRYPTO_HPP */
+
diff --git a/src/lib/util/path_to_regex.cpp b/src/lib/util/path_to_regex.cpp
new file mode 100644
index 00000000000..927ababbb8a
--- /dev/null
+++ b/src/lib/util/path_to_regex.cpp
@@ -0,0 +1,217 @@
+// license:MIT
+// copyright-holders:Alfred Bratterud
+// NOTE: Author allowed MAME project to distribute this file under MIT
+// license. Other projects need to do it under Apache 2 license
+//
+// This file is a part of the IncludeOS unikernel - www.includeos.org
+//
+// Copyright 2015-2016 Oslo and Akershus University College of Applied Sciences
+// and Alfred Bratterud
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// https://github.com/pillarjs/path-to-regexp/blob/master/index.js
+
+#include "path_to_regex.hpp"
+
+namespace path2regex {
+
+const std::regex PATH_REGEXP =
+ std::regex{"((\\\\.)|(([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))))"};
+
+std::regex path_to_regex(const std::string& path, Keys& keys, const Options& options) {
+ Tokens all_tokens = parse(path);
+ tokens_to_keys(all_tokens, keys); // fill keys with relevant tokens
+ return tokens_to_regex(all_tokens, options);
+}
+
+std::regex path_to_regex(const std::string& path, const Options& options) {
+ return tokens_to_regex(parse(path), options);
+}
+
+// Parse a string for the raw tokens
+std::vector<Token> parse(const std::string& str) {
+ if (str.empty())
+ return {};
+
+ Tokens tokens;
+ int key = 0;
+ int index = 0;
+ std::string path = "";
+ std::smatch res;
+
+ for (std::sregex_iterator i = std::sregex_iterator{str.begin(), str.end(), PATH_REGEXP};
+ i != std::sregex_iterator{}; ++i) {
+
+ res = *i;
+
+ std::string m = res[0]; // the parameter, f.ex. /:test
+ std::string escaped = res[2];
+ int offset = res.position();
+
+ // JS: path += str.slice(index, offset); from and included index to and included offset-1
+ path += str.substr(index, (offset - index)); // from index, number of chars: offset - index
+
+ index = offset + m.size();
+
+ if (!escaped.empty()) {
+ path += escaped[1]; // if escaped == \a, escaped[1] == a (if str is "/\\a" f.ex.)
+ continue;
+ }
+
+ std::string next = (size_t(index) < str.size()) ? std::string{str.at(index)} : "";
+
+ std::string prefix = res[4]; // f.ex. /
+ std::string name = res[5]; // f.ex. test
+ std::string capture = res[6]; // f.ex. \d+
+ std::string group = res[7]; // f.ex. (users|admins)
+ std::string modifier = res[8]; // f.ex. ?
+ std::string asterisk = res[9]; // * if path is /*
+
+ // Push the current path onto the tokens
+ if (!path.empty()) {
+ Token stringToken;
+ stringToken.set_string_token(path);
+ tokens.push_back(stringToken);
+ path = "";
+ }
+
+ bool partial = (!prefix.empty()) && (!next.empty()) && (next != prefix);
+ bool repeat = (modifier == "+") || (modifier == "*");
+ bool optional = (modifier == "?") || (modifier == "*");
+ std::string delimiter = (!prefix.empty()) ? prefix : "/";
+ std::string pattern;
+
+ if (!capture.empty())
+ pattern = capture;
+ else if (!group.empty())
+ pattern = group;
+ else
+ pattern = (!asterisk.empty()) ? ".*" : ("[^" + delimiter + "]+?");
+
+ Token t;
+ t.name = (!name.empty()) ? name : std::to_string(key++);
+ t.prefix = prefix;
+ t.delimiter = delimiter;
+ t.optional = optional;
+ t.repeat = repeat;
+ t.partial = partial;
+ t.asterisk = (asterisk == "*");
+ t.pattern = pattern;
+ t.is_string = false;
+ tokens.push_back(t);
+ }
+
+ // Match any characters still remaining
+ if (size_t(index) < str.size())
+ path += str.substr(index);
+
+ // If the path exists, push it onto the end
+ if (!path.empty()) {
+ Token stringToken;
+ stringToken.set_string_token(path);
+ tokens.push_back(stringToken);
+ }
+
+ return tokens;
+}
+
+// Creates a regex based on the given tokens and options (optional)
+std::regex tokens_to_regex(const Tokens& tokens, const Options& options) {
+ if (tokens.empty())
+ return std::regex{""};
+
+ // Set default values for options:
+ bool strict = false;
+ bool sensitive = false;
+ bool end = true;
+
+ if (!options.empty()) {
+ auto it = options.find("strict");
+ strict = (it != options.end()) ? options.find("strict")->second : false;
+
+ it = options.find("sensitive");
+ sensitive = (it != options.end()) ? options.find("sensitive")->second : false;
+
+ it = options.find("end");
+ end = (it != options.end()) ? options.find("end")->second : true;
+ }
+
+ std::string route = "";
+ Token lastToken = tokens[tokens.size() - 1];
+ std::regex re{"(.*\\/$)"};
+ bool endsWithSlash = lastToken.is_string && std::regex_match(lastToken.name, re);
+ // endsWithSlash if the last char in lastToken's name is a slash
+
+ // Iterate over the tokens and create our regexp string
+ for (size_t i = 0; i < tokens.size(); i++) {
+ Token token = tokens[i];
+
+ if (token.is_string) {
+ route += token.name;
+ } else {
+ std::string prefix = token.prefix;
+ std::string capture = "(?:" + token.pattern + ")";
+
+ if (token.repeat)
+ capture += "(?:" + prefix + capture + ")*";
+
+ if (token.optional) {
+
+ if (!token.partial)
+ capture = "(?:" + prefix + "(" + capture + "))?";
+ else
+ capture = prefix + "(" + capture + ")?";
+
+ } else {
+ capture = prefix + "(" + capture + ")";
+ }
+
+ route += capture;
+ }
+ }
+
+ // In non-strict mode we allow a slash at the end of match. If the path to
+ // match already ends with a slash, we remove it for consistency. The slash
+ // is valid at the end of a path match, not in the middle. This is important
+ // in non-ending mode, where "/test/" shouldn't match "/test//route".
+
+ if (!strict) {
+ if (endsWithSlash)
+ route = route.substr(0, (route.size() - 1));
+
+ route += "(?:\\/(?=$))?";
+ }
+
+ if (end) {
+ route += "$";
+ } else {
+ // In non-ending mode, we need the capturing groups to match as much as
+ // possible by using a positive lookahead to the end or next path segment
+ if (!(strict && endsWithSlash))
+ route += "(?=\\/|$)";
+ }
+
+ if (sensitive)
+ return std::regex{"^" + route};
+
+ return std::regex{"^" + route, std::regex_constants::ECMAScript | std::regex_constants::icase};
+}
+
+void tokens_to_keys(const Tokens& tokens, Keys& keys) {
+ for (const auto& token : tokens)
+ if (!token.is_string)
+ keys.push_back(token);
+}
+
+} //< namespace route
diff --git a/src/lib/util/path_to_regex.hpp b/src/lib/util/path_to_regex.hpp
new file mode 100644
index 00000000000..7608d77bb5d
--- /dev/null
+++ b/src/lib/util/path_to_regex.hpp
@@ -0,0 +1,110 @@
+// license:MIT
+// copyright-holders:Alfred Bratterud
+// NOTE: Author allowed MAME project to distribute this file under MIT
+// license. Other projects need to do it under Apache 2 license
+//
+// This file is a part of the IncludeOS unikernel - www.includeos.org
+//
+// Copyright 2015-2016 Oslo and Akershus University College of Applied Sciences
+// and Alfred Bratterud
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// https://github.com/pillarjs/path-to-regexp/blob/master/index.js
+
+#ifndef PATH_TO_REGEX_HPP
+#define PATH_TO_REGEX_HPP
+
+#include <map>
+#include <regex>
+#include <vector>
+
+namespace path2regex {
+
+struct Token {
+ std::string name {}; // can be a string or an int (index)
+ std::string prefix {};
+ std::string delimiter {};
+ std::string pattern {};
+ bool optional {false};
+ bool repeat {false};
+ bool partial {false};
+ bool asterisk {false};
+ bool is_string {false}; // If it is a string we only put/have a string in the name-attribute (path in parse-method)
+ // So if this is true, we can ignore all attributes except name
+
+ void set_string_token(const std::string& name_) {
+ name = name_;
+ is_string = true;
+ }
+}; //< struct Token
+
+using Keys = std::vector<Token>;
+using Tokens = std::vector<Token>;
+using Options = std::map<std::string, bool>;
+
+ /**
+ * Creates a path-regex from string input (path)
+ * Updates keys-vector (empty input parameter)
+ *
+ * Calls parse-method and then tokens_to_regex-method based on the tokens returned from parse-method
+ * Puts the tokens that are keys (not string-tokens) into keys-vector
+ *
+ * std::vector<Token> keys (empty)
+ * Is created outside the class and sent in as parameter
+ * One Token-object in the keys-vector per parameter found in the path
+ *
+ * std::map<std::string, bool> options (optional)
+ * Can contain bool-values for the keys "sensitive", "strict" and/or "end"
+ * Default:
+ * strict = false
+ * sensitive = false
+ * end = true
+ */
+ std::regex path_to_regex(const std::string& path, Keys& keys, const Options& options = Options{});
+
+ /**
+ * Creates a path-regex from string input (path)
+ *
+ * Calls parse-method and then tokens_to_regex-method based on the tokens returned from parse-method
+ *
+ * std::map<std::string, bool> options (optional)
+ * Can contain bool-values for the keys "sensitive", "strict" and/or "end"
+ * Default:
+ * strict = false
+ * sensitive = false
+ * end = true
+ */
+ std::regex path_to_regex(const std::string& path, const Options& options = Options{});
+
+ /**
+ * Creates vector of tokens based on the given string (this vector of tokens can be sent as
+ * input to tokens_to_regex-method and includes tokens that are strings, not only tokens
+ * that are parameters in str)
+ */
+ Tokens parse(const std::string& str);
+
+ /**
+ * Creates a regex based on the tokens and options (optional) given
+ */
+ std::regex tokens_to_regex(const Tokens& tokens, const Options& options = Options{});
+
+ /**
+ * Goes through the tokens-vector and push all tokens that are not string-tokens
+ * onto keys-vector
+ */
+ void tokens_to_keys(const Tokens& tokens, Keys& keys);
+
+} //< namespace path2regex
+
+#endif //< PATH_TO_REGEX_HPP
diff --git a/src/lib/util/server_http.hpp b/src/lib/util/server_http.hpp
new file mode 100644
index 00000000000..57fd57e6993
--- /dev/null
+++ b/src/lib/util/server_http.hpp
@@ -0,0 +1,497 @@
+// license:MIT
+// copyright-holders:Ole Christian Eidheim, Miodrag Milanovic
+#ifndef SERVER_HTTP_HPP
+#define SERVER_HTTP_HPP
+
+#if defined(_MSC_VER)
+#pragma warning(disable:4503)
+#endif
+
+#include "asio.h"
+#include "asio/system_timer.hpp"
+#include "path_to_regex.hpp"
+
+#include <map>
+#include <unordered_map>
+#include <thread>
+#include <functional>
+#include <iostream>
+#include <sstream>
+#include <regex>
+
+#ifndef CASE_INSENSITIVE_EQUALS_AND_HASH
+#define CASE_INSENSITIVE_EQUALS_AND_HASH
+class case_insensitive_equals {
+public:
+ bool operator()(const std::string &key1, const std::string &key2) const {
+ return key1.size() == key2.size()
+ && equal(key1.cbegin(), key1.cend(), key2.cbegin(),
+ [](std::string::value_type key1v, std::string::value_type key2v)
+ { return tolower(key1v) == tolower(key2v); });
+ }
+};
+class case_insensitive_hash {
+public:
+ size_t operator()(const std::string &key) const {
+ size_t seed = 0;
+ for (auto &c : key) {
+ std::hash<char> hasher;
+ seed ^= hasher(std::tolower(c)) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+ }
+ return seed;
+ }
+};
+#endif
+namespace webpp {
+ template <class socket_type>
+ class Server;
+
+ template <class socket_type>
+ class ServerBase {
+ public:
+ virtual ~ServerBase() {}
+
+ class Response {
+ friend class ServerBase<socket_type>;
+
+ asio::streambuf m_streambuf;
+
+ std::shared_ptr<socket_type> m_socket;
+ std::ostream m_ostream;
+ std::stringstream m_header;
+ explicit Response(const std::shared_ptr<socket_type> &socket) : m_socket(socket), m_ostream(&m_streambuf) {}
+
+ static std::string statusToString(int status)
+ {
+ switch (status) {
+ default:
+ case 200: return "HTTP/1.0 200 OK\r\n";
+ case 201: return "HTTP/1.0 201 Created\r\n";
+ case 202: return "HTTP/1.0 202 Accepted\r\n";
+ case 204: return "HTTP/1.0 204 No Content\r\n";
+ case 300: return "HTTP/1.0 300 Multiple Choices\r\n";
+ case 301: return "HTTP/1.0 301 Moved Permanently\r\n";
+ case 302: return "HTTP/1.0 302 Moved Temporarily\r\n";
+ case 304: return "HTTP/1.0 304 Not Modified\r\n";
+ case 400: return "HTTP/1.0 400 Bad Request\r\n";
+ case 401: return "HTTP/1.0 401 Unauthorized\r\n";
+ case 403: return "HTTP/1.0 403 Forbidden\r\n";
+ case 404: return "HTTP/1.0 404 Not Found\r\n";
+ case 500: return "HTTP/1.0 500 Internal Server Error\r\n";
+ case 501: return "HTTP/1.0 501 Not Implemented\r\n";
+ case 502: return "HTTP/1.0 502 Bad Gateway\r\n";
+ case 504: return "HTTP/1.0 503 Service Unavailable\r\n";
+ }
+ }
+ public:
+ Response& status(int number) { m_ostream << statusToString(number); return *this; }
+ void type(std::string str) { m_header << "Content-Type: "<< str << "\r\n"; }
+ void send(std::string str) { m_ostream << m_header.str() << "Content-Length: " << str.length() << "\r\n\r\n" << str; }
+ size_t size() const { return m_streambuf.size(); }
+ std::shared_ptr<socket_type> socket() { return m_socket; }
+ };
+
+ class Content : public std::istream {
+ friend class ServerBase<socket_type>;
+ public:
+ size_t size() const {
+ return streambuf.size();
+ }
+ std::string string() const {
+ std::stringstream ss;
+ ss << rdbuf();
+ return ss.str();
+ }
+ private:
+ asio::streambuf &streambuf;
+ explicit Content(asio::streambuf &streambuf): std::istream(&streambuf), streambuf(streambuf) {}
+ };
+
+ class Request {
+ friend class ServerBase<socket_type>;
+ friend class Server<socket_type>;
+ public:
+ std::string method, path, http_version;
+
+ Content content;
+
+ std::unordered_multimap<std::string, std::string, case_insensitive_hash, case_insensitive_equals> header;
+
+ path2regex::Keys keys;
+ std::map<std::string, std::string> params;
+
+ std::string remote_endpoint_address;
+ unsigned short remote_endpoint_port;
+
+ private:
+ Request(const socket_type &socket): content(streambuf) {
+ try {
+ remote_endpoint_address=socket.lowest_layer().remote_endpoint().address().to_string();
+ remote_endpoint_port=socket.lowest_layer().remote_endpoint().port();
+ }
+ catch(...) {}
+ }
+ asio::streambuf streambuf;
+ };
+
+ class Config {
+ friend class ServerBase<socket_type>;
+
+ Config(unsigned short port) : port(port) {}
+ public:
+ /// Port number to use. Defaults to 80 for HTTP and 443 for HTTPS.
+ unsigned short port;
+ /// Number of threads that the server will use when start() is called. Defaults to 1 thread.
+ size_t thread_pool_size=1;
+ /// Timeout on request handling. Defaults to 5 seconds.
+ size_t timeout_request=5;
+ /// Timeout on content handling. Defaults to 300 seconds.
+ size_t timeout_content=300;
+ /// IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation.
+ /// If empty, the address will be any address.
+ std::string address;
+ /// Set to false to avoid binding the socket to an address that is already in use. Defaults to true.
+ bool reuse_address=true;
+ };
+ ///Set before calling start().
+ Config m_config;
+ private:
+ class regex_orderable : public std::regex {
+ std::string str;
+ public:
+ regex_orderable(std::regex reg, const std::string &regex_str) : std::regex(reg), str(regex_str) {}
+ bool operator<(const regex_orderable &rhs) const {
+ return str<rhs.str;
+ }
+ std::string getstr() const { return str; }
+ };
+ using http_handler = std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)>;
+
+ public:
+ template<class T> void on_get(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg,regex)]["GET"] = std::make_tuple(std::move(keys), func); }
+ template<class T> void on_get(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["GET"] = func; }
+ template<class T> void on_post(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg, regex)]["POST"] = std::make_tuple(std::move(keys), func); }
+ template<class T> void on_post(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["POST"] = func; }
+ template<class T> void on_put(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg, regex)]["PUT"] = std::make_tuple(std::move(keys), func); }
+ template<class T> void on_put(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["PUT"] = func; }
+ template<class T> void on_patch(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg, regex)]["PATCH"] = std::make_tuple(std::move(keys), func); }
+ template<class T> void on_patch(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["PATCH"] = func; }
+ template<class T> void on_delete(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg, regex)]["DELETE"] = std::make_tuple(std::move(keys), func); }
+ template<class T> void on_delete(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["DELETE"] = func; }
+
+ void remove_handler(std::string regex)
+ {
+ std::lock_guard<std::mutex> lock(m_resource_mutex);
+ for (auto &it = m_resource.begin(); it != m_resource.end(); ++it)
+ {
+ if (it->first.getstr() == regex)
+ {
+ m_resource.erase(it);
+ break;
+ }
+ }
+ }
+
+ std::function<void(std::shared_ptr<typename ServerBase<socket_type>::Request>, const std::error_code&)> on_error;
+
+ std::function<void(std::shared_ptr<socket_type> socket, std::shared_ptr<typename ServerBase<socket_type>::Request>)> on_upgrade;
+ private:
+ /// Warning: do not add or remove resources after start() is called
+ std::map<regex_orderable, std::map<std::string, std::tuple<path2regex::Keys, http_handler>>> m_resource;
+
+ std::map<std::string, http_handler> m_default_resource;
+
+ std::mutex m_resource_mutex;
+ public:
+ virtual void start() {
+ if(!m_io_context)
+ m_io_context=std::make_shared<asio::io_context>();
+
+ if(m_io_context->stopped())
+ m_io_context.reset();
+
+ asio::ip::tcp::endpoint endpoint;
+ if(m_config.address.size()>0)
+ endpoint=asio::ip::tcp::endpoint(asio::ip::make_address(m_config.address), m_config.port);
+ else
+ endpoint=asio::ip::tcp::endpoint(asio::ip::tcp::v4(), m_config.port);
+
+ if(!acceptor)
+ acceptor= std::make_unique<asio::ip::tcp::acceptor>(*m_io_context);
+ acceptor->open(endpoint.protocol());
+ acceptor->set_option(asio::socket_base::reuse_address(m_config.reuse_address));
+ acceptor->bind(endpoint);
+ acceptor->listen();
+
+ accept();
+
+ if (!m_external_context)
+ m_io_context->run();
+ }
+
+ void stop() const
+ {
+ acceptor->close();
+ if (!m_external_context)
+ m_io_context->stop();
+ }
+
+ ///Use this function if you need to recursively send parts of a longer message
+ void send(const std::shared_ptr<Response> &response, const std::function<void(const std::error_code&)>& callback=nullptr) const {
+ asio::async_write(*response->socket(), response->m_streambuf, [this, response, callback](const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(callback)
+ callback(ec);
+ });
+ }
+
+ void set_io_context(std::shared_ptr<asio::io_context> new_io_context)
+ {
+ m_io_context = new_io_context;
+ m_external_context = true;
+ }
+ protected:
+ std::shared_ptr<asio::io_context> m_io_context;
+ bool m_external_context;
+ std::unique_ptr<asio::ip::tcp::acceptor> acceptor;
+ std::vector<std::thread> threads;
+
+ ServerBase(unsigned short port) : m_config(port), m_external_context(false) {}
+
+ virtual void accept()=0;
+
+ std::shared_ptr<asio::system_timer> get_timeout_timer(const std::shared_ptr<socket_type> &socket, long seconds) {
+ if(seconds==0)
+ return nullptr;
+ auto timer = std::make_shared<asio::system_timer>(*m_io_context);
+ timer->expires_at(std::chrono::system_clock::now() + std::chrono::seconds(seconds));
+ timer->async_wait([socket](const std::error_code& ec){
+ if(!ec) {
+ std::error_code newec = ec;
+ socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, newec);
+ socket->lowest_layer().close();
+ }
+ });
+ return timer;
+ }
+
+ void read_request_and_content(const std::shared_ptr<socket_type> &socket) {
+ //Create new streambuf (Request::streambuf) for async_read_until()
+ //shared_ptr is used to pass temporary objects to the asynchronous functions
+ std::shared_ptr<Request> request(new Request(*socket));
+
+ //Set timeout on the following asio::async-read or write function
+ auto timer = get_timeout_timer(socket, m_config.timeout_request);
+
+ asio::async_read_until(*socket, request->streambuf, "\r\n\r\n",
+ [this, socket, request, timer](const std::error_code& ec, size_t bytes_transferred) {
+ if(timer)
+ timer->cancel();
+ if(!ec) {
+ //request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs:
+ //"After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter"
+ //The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the
+ //streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content).
+ size_t num_additional_bytes=request->streambuf.size()-bytes_transferred;
+
+ if (!parse_request(request))
+ return;
+
+ //If content, read that as well
+ auto it = request->header.find("Content-Length");
+ if (it != request->header.end()) {
+ unsigned long long content_length;
+ try {
+ content_length = stoull(it->second);
+ }
+ catch (const std::exception &) {
+ if (on_error)
+ on_error(request, std::error_code(EPROTO, std::generic_category()));
+ return;
+ }
+ if (content_length > num_additional_bytes) {
+ //Set timeout on the following asio::async-read or write function
+ auto timer2 = get_timeout_timer(socket, m_config.timeout_content);
+ asio::async_read(*socket, request->streambuf,
+ asio::transfer_exactly(size_t(content_length) - num_additional_bytes),
+ [this, socket, request, timer2]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if (timer2)
+ timer2->cancel();
+ if (!ec)
+ find_resource(socket, request);
+ else if (on_error)
+ on_error(request, ec);
+ });
+ }
+ else {
+ find_resource(socket, request);
+ }
+ }
+ else {
+ find_resource(socket, request);
+ }
+ }
+ else if (on_error)
+ on_error(request, ec);
+ });
+ }
+
+ bool parse_request(const std::shared_ptr<Request> &request) const {
+ std::string line;
+ getline(request->content, line);
+ size_t method_end;
+ if((method_end=line.find(' '))!=std::string::npos) {
+ size_t path_end;
+ if((path_end=line.find(' ', method_end+1))!=std::string::npos) {
+ request->method=line.substr(0, method_end);
+ request->path=line.substr(method_end+1, path_end-method_end-1);
+
+ size_t protocol_end;
+ if((protocol_end=line.find('/', path_end+1))!=std::string::npos) {
+ if(line.compare(path_end+1, protocol_end-path_end-1, "HTTP")!=0)
+ return false;
+ request->http_version=line.substr(protocol_end+1, line.size()-protocol_end-2);
+ }
+ else
+ return false;
+
+ getline(request->content, line);
+ size_t param_end;
+ while((param_end=line.find(':'))!=std::string::npos) {
+ size_t value_start=param_end+1;
+ if((value_start)<line.size()) {
+ if(line[value_start]==' ')
+ value_start++;
+ if(value_start<line.size())
+ request->header.emplace(line.substr(0, param_end), line.substr(value_start, line.size() - value_start - 1));
+ }
+
+ getline(request->content, line);
+ }
+ }
+ else
+ return false;
+ }
+ else
+ return false;
+ return true;
+ }
+
+ void find_resource(const std::shared_ptr<socket_type> &socket, const std::shared_ptr<Request> &request) {
+ std::lock_guard<std::mutex> lock(m_resource_mutex);
+ //Upgrade connection
+ if(on_upgrade) {
+ auto it=request->header.find("Upgrade");
+ if(it!=request->header.end()) {
+ on_upgrade(socket, request);
+ return;
+ }
+ }
+ //Find path- and method-match, and call write_response
+ for(auto& regex_method : m_resource) {
+ auto it = regex_method.second.find(request->method);
+ if (it != regex_method.second.end()) {
+ std::smatch sm_res;
+ if (std::regex_match(request->path, sm_res, regex_method.first)) {
+ request->keys = std::get<0>(it->second);
+ for (size_t i = 0; i < request->keys.size(); i++) {
+ request->params.insert(std::pair<std::string, std::string>(request->keys[i].name, sm_res[i + 1]));
+ }
+ write_response(socket, request, std::get<1>(it->second));
+ return;
+ }
+ }
+ }
+ auto it=m_default_resource.find(request->method);
+ if(it!=m_default_resource.end()) {
+ write_response(socket, request, it->second);
+ }
+ }
+
+ void write_response(const std::shared_ptr<socket_type> &socket, const std::shared_ptr<Request> &request, http_handler& resource_function) {
+ //Set timeout on the following asio::async-read or write function
+ auto timer = get_timeout_timer(socket, m_config.timeout_content);
+
+ auto response=std::shared_ptr<Response>(new Response(socket), [this, request, timer](Response *response_ptr) {
+ auto response=std::shared_ptr<Response>(response_ptr);
+ send(response, [this, response, request, timer](const std::error_code& ec) {
+ if (timer)
+ timer->cancel();
+ if (!ec) {
+ float http_version;
+ try {
+ http_version = stof(request->http_version);
+ }
+ catch (const std::exception &) {
+ if (on_error)
+ on_error(request, std::error_code(EPROTO, std::generic_category()));
+ return;
+ }
+
+ auto range = request->header.equal_range("Connection");
+ case_insensitive_equals check;
+ for (auto it = range.first; it != range.second; ++it) {
+ if (check(it->second, "close"))
+ return;
+ }
+ if (http_version > 1.05)
+ read_request_and_content(response->socket());
+ }
+ else if (on_error)
+ on_error(request, ec);
+ });
+ });
+
+ try {
+ resource_function(response, request);
+ }
+ catch(const std::exception &) {
+ if (on_error)
+ on_error(request, std::error_code(EPROTO, std::generic_category()));
+ }
+ }
+ };
+
+ template<class socket_type>
+ class Server : public ServerBase<socket_type> {
+ public:
+ Server(unsigned short port, size_t num_threads, long timeout_request, long timeout_send_or_receive)
+ : ServerBase<socket_type>(port, num_threads, timeout_request, timeout_send_or_receive)
+ {
+ }
+ };
+
+ using HTTP = asio::ip::tcp::socket;
+
+ template<>
+ class Server<HTTP> : public ServerBase<HTTP> {
+ public:
+ Server() : ServerBase<HTTP>::ServerBase(80) {}
+ protected:
+ void accept() override {
+ //Create new socket for this connection
+ //Shared_ptr is used to pass temporary objects to the asynchronous functions
+ auto socket = std::make_shared<HTTP>(*m_io_context);
+
+ acceptor->async_accept(*socket, [this, socket](const std::error_code& ec){
+ //Immediately start accepting a new connection (if io_context hasn't been stopped)
+ if (ec != asio::error::operation_aborted)
+ accept();
+
+ if(!ec) {
+ asio::ip::tcp::no_delay option(true);
+ socket->set_option(option);
+
+ read_request_and_content(socket);
+ } else if (on_error)
+ on_error(std::shared_ptr<Request>(new Request(*socket)), ec);
+ });
+ }
+ };
+
+ class http_server : public Server<HTTP> {
+ public:
+ http_server() : Server<HTTP>::Server() {}
+ };
+}
+#endif /* SERVER_HTTP_HPP */
diff --git a/src/lib/util/server_https.hpp b/src/lib/util/server_https.hpp
new file mode 100644
index 00000000000..e721cbb3e25
--- /dev/null
+++ b/src/lib/util/server_https.hpp
@@ -0,0 +1,99 @@
+// license:MIT
+// copyright-holders:Ole Christian Eidheim, Miodrag Milanovic
+#ifndef SERVER_HTTPS_HPP
+#define SERVER_HTTPS_HPP
+
+#include "server_http.hpp"
+#include "asio/ssl.hpp"
+#include <openssl/ssl.h>
+#include <algorithm>
+
+namespace webpp {
+ using HTTPS = asio::ssl::stream<asio::ip::tcp::socket>;
+
+ template<>
+ class Server<HTTPS> : public ServerBase<HTTPS> {
+ std::string session_id_context;
+ bool set_session_id_context = false;
+ public:
+ Server(unsigned short port, size_t thread_pool_size, const std::string& cert_file, const std::string& private_key_file,
+ long timeout_request = 5, long timeout_content = 300,
+ const std::string& verify_file = std::string()) :
+ Server(cert_file, private_key_file, verify_file) {
+ m_config.port=port;
+ m_config.thread_pool_size= thread_pool_size;
+ m_config.timeout_request=timeout_request;
+ m_config.timeout_content=timeout_content;
+ }
+
+ Server(const std::string& cert_file, const std::string& private_key_file, const std::string& verify_file = std::string()) :
+ ServerBase<HTTPS>::ServerBase(443), context(asio::ssl::context::tlsv12) {
+
+ context.use_certificate_chain_file(cert_file);
+ context.use_private_key_file(private_key_file, asio::ssl::context::pem);
+
+ if (verify_file.size() > 0) {
+ context.load_verify_file(verify_file);
+ context.set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert |
+ asio::ssl::verify_client_once);
+ set_session_id_context = true;
+ }
+ }
+ void start() override {
+ if (set_session_id_context) {
+ // Creating session_id_context from address:port but reversed due to small SSL_MAX_SSL_SESSION_ID_LENGTH
+ session_id_context = std::to_string(m_config.port) + ':';
+ session_id_context.append(m_config.address.rbegin(), m_config.address.rend());
+ SSL_CTX_set_session_id_context(context.native_handle(), reinterpret_cast<const unsigned char*>(session_id_context.data()),
+ std::min<size_t>(session_id_context.size(), SSL_MAX_SSL_SESSION_ID_LENGTH));
+
+ }
+ ServerBase::start();
+ }
+
+ protected:
+ asio::ssl::context context;
+
+ void accept() override {
+ //Create new socket for this connection
+ //Shared_ptr is used to pass temporary objects to the asynchronous functions
+ auto socket = std::make_shared<HTTPS>(*m_io_context, context);
+
+ acceptor->async_accept((*socket).lowest_layer(), [this, socket](const std::error_code& ec) {
+ //Immediately start accepting a new connection (if io_context hasn't been stopped)
+ if (ec != asio::error::operation_aborted)
+ accept();
+
+
+ if(!ec) {
+ asio::ip::tcp::no_delay option(true);
+ socket->lowest_layer().set_option(option);
+
+ //Set timeout on the following asio::ssl::stream::async_handshake
+ auto timer = get_timeout_timer(socket, m_config.timeout_request);
+ socket->async_handshake(asio::ssl::stream_base::server, [this, socket, timer]
+ (const std::error_code& ec) {
+ if(timer)
+ timer->cancel();
+ if(!ec)
+ read_request_and_content(socket);
+ else if(on_error)
+ on_error(std::shared_ptr<Request>(new Request(*socket)), ec);
+ });
+ }
+ else if(on_error)
+ on_error(std::shared_ptr<Request>(new Request(*socket)), ec);
+ });
+ }
+ };
+
+ class https_server : public Server<HTTPS> {
+ public:
+ explicit https_server(const std::string& cert_file, const std::string& private_key_file, const std::string& verify_file = std::string()) : Server<HTTPS>::Server(cert_file, private_key_file, verify_file) {}
+ };
+
+}
+
+
+#endif /* SERVER_HTTPS_HPP */
+
diff --git a/src/lib/util/server_ws.hpp b/src/lib/util/server_ws.hpp
new file mode 100644
index 00000000000..94437026196
--- /dev/null
+++ b/src/lib/util/server_ws.hpp
@@ -0,0 +1,708 @@
+// license:MIT
+// copyright-holders:Ole Christian Eidheim, Miodrag Milanovic
+#ifndef SERVER_WS_HPP
+#define SERVER_WS_HPP
+#include "path_to_regex.hpp"
+#include "crypto.hpp"
+
+#include "asio.h"
+#include "asio/system_timer.hpp"
+
+#include <unordered_map>
+#include <thread>
+#include <mutex>
+#include <unordered_set>
+#include <list>
+#include <memory>
+#include <atomic>
+#include <iostream>
+#include <map>
+#include <chrono>
+#include <regex>
+
+#ifndef CASE_INSENSITIVE_EQUALS_AND_HASH
+#define CASE_INSENSITIVE_EQUALS_AND_HASH
+class case_insensitive_equals {
+public:
+ bool operator()(const std::string &key1, const std::string &key2) const {
+ return key1.size() == key2.size()
+ && equal(key1.cbegin(), key1.cend(), key2.cbegin(),
+ [](std::string::value_type key1v, std::string::value_type key2v)
+ { return tolower(key1v) == tolower(key2v); });
+ }
+};
+class case_insensitive_hash {
+public:
+ size_t operator()(const std::string &key) const {
+ size_t seed = 0;
+ for (auto &c : key) {
+ std::hash<char> hasher;
+ seed ^= hasher(std::tolower(c)) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+ }
+ return seed;
+ }
+};
+#endif
+
+namespace webpp {
+ template <class socket_type>
+ class SocketServer;
+
+ template <class socket_type>
+ class SocketServerBase {
+ public:
+ virtual ~SocketServerBase() {}
+
+ class SendStream : public std::ostream {
+ friend class SocketServerBase<socket_type>;
+
+ asio::streambuf streambuf;
+ public:
+ SendStream(): std::ostream(&streambuf) {}
+ size_t size() const {
+ return streambuf.size();
+ }
+ };
+
+ class Connection {
+ friend class SocketServerBase<socket_type>;
+ friend class SocketServer<socket_type>;
+
+ public:
+ explicit Connection(const std::shared_ptr<socket_type> &socket) : remote_endpoint_port(0), socket(socket), strand(socket->get_io_service()), closed(false) { }
+
+ std::string method, path, http_version;
+
+ std::unordered_multimap<std::string, std::string, case_insensitive_hash, case_insensitive_equals> header;
+
+ std::smatch path_match;
+
+ std::string remote_endpoint_address;
+ unsigned short remote_endpoint_port;
+
+ private:
+ explicit Connection(socket_type *socket): remote_endpoint_port(0), socket(socket), strand(socket->get_io_service()), closed(false) { }
+
+ class SendData {
+ public:
+ SendData(const std::shared_ptr<SendStream> &header_stream, const std::shared_ptr<SendStream> &message_stream,
+ const std::function<void(const std::error_code)> &callback) :
+ header_stream(header_stream), message_stream(message_stream), callback(callback) {}
+ std::shared_ptr<SendStream> header_stream;
+ std::shared_ptr<SendStream> message_stream;
+ std::function<void(const std::error_code)> callback;
+ };
+
+ std::shared_ptr<socket_type> socket;
+
+ asio::io_context::strand strand;
+
+ std::list<SendData> send_queue;
+
+ void send_from_queue(const std::shared_ptr<Connection> &connection) {
+ strand.post([this, connection]() {
+ asio::async_write(*socket, send_queue.begin()->header_stream->streambuf,
+ strand.wrap([this, connection](const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ asio::async_write(*socket, send_queue.begin()->message_stream->streambuf,
+ strand.wrap([this, connection]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ auto send_queued=send_queue.begin();
+ if(send_queued->callback)
+ send_queued->callback(ec);
+ if(!ec) {
+ send_queue.erase(send_queued);
+ if(send_queue.size()>0)
+ send_from_queue(connection);
+ }
+ else
+ send_queue.clear();
+ }));
+ }
+ else {
+ auto send_queued=send_queue.begin();
+ if(send_queued->callback)
+ send_queued->callback(ec);
+ send_queue.clear();
+ }
+ }));
+ });
+ }
+
+ std::atomic<bool> closed;
+
+ std::unique_ptr<asio::system_timer> timer_idle;
+
+ void read_remote_endpoint_data() {
+ try {
+ remote_endpoint_address=socket->lowest_layer().remote_endpoint().address().to_string();
+ remote_endpoint_port=socket->lowest_layer().remote_endpoint().port();
+ }
+ catch (...) {}
+ }
+ };
+
+ class Message : public std::istream {
+ friend class SocketServerBase<socket_type>;
+
+ public:
+ unsigned char fin_rsv_opcode;
+ size_t size() const {
+ return length;
+ }
+ std::string string() const {
+ std::stringstream ss;
+ ss << rdbuf();
+ return ss.str();
+ }
+ private:
+ Message(): std::istream(&streambuf), fin_rsv_opcode(0), length(0) {}
+
+ size_t length;
+ asio::streambuf streambuf;
+ };
+
+ class Endpoint {
+ friend class SocketServerBase<socket_type>;
+ std::unordered_set<std::shared_ptr<Connection> > connections;
+ std::mutex connections_mutex;
+
+ public:
+ std::function<void(std::shared_ptr<Connection>)> on_open;
+ std::function<void(std::shared_ptr<Connection>, std::shared_ptr<Message>)> on_message;
+ std::function<void(std::shared_ptr<Connection>, int, const std::string&)> on_close;
+ std::function<void(std::shared_ptr<Connection>, const std::error_code&)> on_error;
+
+ std::unordered_set<std::shared_ptr<Connection> > get_connections() {
+ std::lock_guard<std::mutex> lock(connections_mutex);
+ auto copy=connections;
+ return copy;
+ }
+ };
+
+ class Config {
+ friend class SocketServerBase<socket_type>;
+
+ Config(unsigned short port) : port(port) {}
+ public:
+ /// Port number to use. Defaults to 80 for HTTP and 443 for HTTPS.
+ unsigned short port;
+ /// Number of threads that the server will use when start() is called. Defaults to 1 thread.
+ size_t thread_pool_size=1;
+ /// Timeout on request handling. Defaults to 5 seconds.
+ size_t timeout_request=5;
+ /// Idle timeout. Defaults to no timeout.
+ size_t timeout_idle=0;
+ /// IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation.
+ /// If empty, the address will be any address.
+ std::string address;
+ /// Set to false to avoid binding the socket to an address that is already in use. Defaults to true.
+ bool reuse_address=true;
+ };
+ ///Set before calling start().
+ Config config;
+
+ private:
+ class regex_orderable : public std::regex {
+ std::string str;
+ path2regex::Keys keys;
+ public:
+ regex_orderable(const char *regex_cstr) : std::regex(path2regex::path_to_regex(regex_cstr, keys)), str(regex_cstr) {}
+ regex_orderable(const std::string &regex_cstr) : std::regex(path2regex::path_to_regex(regex_cstr, keys)), str(regex_cstr) {}
+ bool operator<(const regex_orderable &rhs) const {
+ return str<rhs.str;
+ }
+ };
+ public:
+ /// Warning: do not add or remove endpoints after start() is called
+ std::map<regex_orderable, Endpoint> endpoint;
+
+ virtual void start() {
+ if(!io_context)
+ io_context=std::make_shared<asio::io_context>();
+
+ if(io_context->stopped())
+ io_context->reset();
+
+ asio::ip::tcp::endpoint endpoint;
+ if(config.address.size()>0)
+ endpoint=asio::ip::tcp::endpoint(asio::ip::address::from_string(config.address), config.port);
+ else
+ endpoint=asio::ip::tcp::endpoint(asio::ip::tcp::v4(), config.port);
+
+ if(!acceptor)
+ acceptor= std::make_unique<asio::ip::tcp::acceptor>(*io_context);
+ acceptor->open(endpoint.protocol());
+ acceptor->set_option(asio::socket_base::reuse_address(config.reuse_address));
+ acceptor->bind(endpoint);
+ acceptor->listen();
+
+ accept();
+
+ io_context->run();
+ }
+
+ void stop() {
+ acceptor->close();
+ io_context->stop();
+
+ for(auto& p: endpoint)
+ p.second.connections.clear();
+ }
+
+ ///fin_rsv_opcode: 129=one fragment, text, 130=one fragment, binary, 136=close connection.
+ ///See http://tools.ietf.org/html/rfc6455#section-5.2 for more information
+ void send(const std::shared_ptr<Connection> &connection, const std::shared_ptr<SendStream> &message_stream,
+ const std::function<void(const std::error_code&)>& callback=nullptr,
+ unsigned char fin_rsv_opcode=129) const {
+ if(fin_rsv_opcode!=136)
+ timer_idle_reset(connection);
+
+ auto header_stream = std::make_shared<SendStream>();
+
+ size_t length=message_stream->size();
+
+ header_stream->put(fin_rsv_opcode);
+ //unmasked (first length byte<128)
+ if(length>=126) {
+ int num_bytes;
+ if(length>0xffff) {
+ num_bytes=8;
+ header_stream->put(127);
+ }
+ else {
+ num_bytes=2;
+ header_stream->put(126);
+ }
+
+ for(int c=num_bytes-1;c>=0;c--) {
+ header_stream->put((static_cast<unsigned long long>(length) >> (8 * c)) % 256);
+ }
+ }
+ else
+ header_stream->put(static_cast<unsigned char>(length));
+
+ connection->strand.post([this, connection, header_stream, message_stream, callback]() {
+ connection->send_queue.emplace_back(header_stream, message_stream, callback);
+ if(connection->send_queue.size()==1)
+ connection->send_from_queue(connection);
+ });
+ }
+
+ void send_close(const std::shared_ptr<Connection> &connection, int status, const std::string& reason="",
+ const std::function<void(const std::error_code&)>& callback=nullptr) const {
+ //Send close only once (in case close is initiated by server)
+ if(connection->closed)
+ return;
+ connection->closed=true;
+
+ auto send_stream=std::make_shared<SendStream>();
+
+ send_stream->put(status>>8);
+ send_stream->put(status%256);
+
+ *send_stream << reason;
+
+ //fin_rsv_opcode=136: message close
+ send(connection, send_stream, callback, 136);
+ }
+
+ std::unordered_set<std::shared_ptr<Connection> > get_connections() {
+ std::unordered_set<std::shared_ptr<Connection> > all_connections;
+ for(auto& e: endpoint) {
+ std::lock_guard<std::mutex> lock(e.second.connections_mutex);
+ all_connections.insert(e.second.connections.begin(), e.second.connections.end());
+ }
+ return all_connections;
+ }
+
+ /**
+ * Upgrades a request, from for instance Simple-Web-Server, to a WebSocket connection.
+ * The parameters are moved to the Connection object.
+ * See also Server::on_upgrade in the Simple-Web-Server project.
+ * The socket's io_service is used, thus running start() is not needed.
+ *
+ * Example use:
+ * server.on_upgrade=[&socket_server] (auto socket, auto request) {
+ * auto connection=std::make_shared<SimpleWeb::SocketServer<SimpleWeb::WS>::Connection>(socket);
+ * connection->method=std::move(request->method);
+ * connection->path=std::move(request->path);
+ * connection->http_version=std::move(request->http_version);
+ * connection->header=std::move(request->header);
+ * connection->remote_endpoint_address=std::move(request->remote_endpoint_address);
+ * connection->remote_endpoint_port=request->remote_endpoint_port;
+ * socket_server.upgrade(connection);
+ * }
+ */
+ void upgrade(const std::shared_ptr<Connection> &connection) {
+ auto read_buffer=std::make_shared<asio::streambuf>();
+ write_handshake(connection, read_buffer);
+ }
+
+ /// If you have your own asio::io_context, store its pointer here before running start().
+ /// You might also want to set config.num_threads to 0.
+ std::shared_ptr<asio::io_context> io_context;
+ protected:
+ const std::string ws_magic_string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+ std::unique_ptr<asio::ip::tcp::acceptor> acceptor;
+
+ std::vector<std::thread> threads;
+
+ SocketServerBase(unsigned short port) :
+ config(port) {}
+
+ virtual void accept()=0;
+
+ std::shared_ptr<asio::system_timer> get_timeout_timer(const std::shared_ptr<Connection> &connection, size_t seconds) {
+ if (seconds == 0)
+ return nullptr;
+ auto timer = std::make_shared<asio::system_timer>(connection->socket->get_io_service());
+ timer->expires_at(std::chrono::system_clock::now() + std::chrono::seconds(static_cast<long>(seconds)));
+ timer->async_wait([connection](const std::error_code& ec){
+ if(!ec) {
+ connection->socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both);
+ connection->socket->lowest_layer().close();
+ }
+ });
+ return timer;
+ }
+
+ void read_handshake(const std::shared_ptr<Connection> &connection) {
+ connection->read_remote_endpoint_data();
+
+ //Create new read_buffer for async_read_until()
+ //Shared_ptr is used to pass temporary objects to the asynchronous functions
+ auto read_buffer = std::make_shared<asio::streambuf>();
+
+ //Set timeout on the following asio::async-read or write function
+ auto timer = get_timeout_timer(connection, config.timeout_request);
+
+ asio::async_read_until(*connection->socket, *read_buffer, "\r\n\r\n",
+ [this, connection, read_buffer, timer]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(timer)
+ timer->cancel();
+ if(!ec) {
+ //Convert to istream to extract string-lines
+ std::istream stream(read_buffer.get());
+
+ parse_handshake(connection, stream);
+
+ write_handshake(connection, read_buffer);
+ }
+ });
+ }
+
+ void parse_handshake(const std::shared_ptr<Connection> &connection, std::istream& stream) const {
+ std::string line;
+ getline(stream, line);
+ size_t method_end;
+ if((method_end=line.find(' '))!=std::string::npos) {
+ size_t path_end;
+ if((path_end=line.find(' ', method_end+1))!=std::string::npos) {
+ connection->method=line.substr(0, method_end);
+ connection->path=line.substr(method_end+1, path_end-method_end-1);
+ if((path_end+6)<line.size())
+ connection->http_version=line.substr(path_end+6, line.size()-(path_end+6)-1);
+ else
+ connection->http_version="1.1";
+
+ getline(stream, line);
+ size_t param_end;
+ while((param_end=line.find(':'))!=std::string::npos) {
+ size_t value_start=param_end+1;
+ if((value_start)<line.size()) {
+ if(line[value_start]==' ')
+ value_start++;
+ if(value_start<line.size())
+ connection->header.emplace(line.substr(0, param_end), line.substr(value_start, line.size()-value_start-1));
+ }
+
+ getline(stream, line);
+ }
+ }
+ }
+ }
+
+ void write_handshake(const std::shared_ptr<Connection> &connection, const std::shared_ptr<asio::streambuf> &read_buffer) {
+ //Find path- and method-match, and generate response
+ for (auto &regex_endpoint : endpoint) {
+ std::smatch path_match;
+ if(std::regex_match(connection->path, path_match, regex_endpoint.first)) {
+ auto write_buffer = std::make_shared<asio::streambuf>();
+ std::ostream handshake(write_buffer.get());
+
+ if(generate_handshake(connection, handshake)) {
+ connection->path_match=std::move(path_match);
+ //Capture write_buffer in lambda so it is not destroyed before async_write is finished
+ asio::async_write(*connection->socket, *write_buffer,
+ [this, connection, write_buffer, read_buffer, &regex_endpoint]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ connection_open(connection, regex_endpoint.second);
+ read_message(connection, read_buffer, regex_endpoint.second);
+ }
+ else
+ connection_error(connection, regex_endpoint.second, ec);
+ });
+ }
+ return;
+ }
+ }
+ }
+
+ bool generate_handshake(const std::shared_ptr<Connection> &connection, std::ostream& handshake) const {
+ auto header_it = connection->header.find("Sec-WebSocket-Key");
+ if (header_it == connection->header.end())
+ return false;
+
+ auto sha1=sha1_encode(header_it->second + ws_magic_string);
+
+ handshake << "HTTP/1.1 101 Web Socket Protocol Handshake\r\n";
+ handshake << "Upgrade: websocket\r\n";
+ handshake << "Connection: Upgrade\r\n";
+ handshake << "Sec-WebSocket-Accept: " << base64_encode(sha1) << "\r\n";
+ handshake << "\r\n";
+
+ return true;
+ }
+
+ void read_message(const std::shared_ptr<Connection> &connection,
+ const std::shared_ptr<asio::streambuf> &read_buffer, Endpoint& endpoint) const {
+ asio::async_read(*connection->socket, *read_buffer, asio::transfer_exactly(2),
+ [this, connection, read_buffer, &endpoint]
+ (const std::error_code& ec, size_t bytes_transferred) {
+ if(!ec) {
+ if(bytes_transferred==0) { //TODO: why does this happen sometimes?
+ read_message(connection, read_buffer, endpoint);
+ return;
+ }
+ std::istream stream(read_buffer.get());
+
+ std::vector<unsigned char> first_bytes;
+ first_bytes.resize(2);
+ stream.read(reinterpret_cast<char*>(&first_bytes[0]), 2);
+
+ unsigned char fin_rsv_opcode=first_bytes[0];
+
+ //Close connection if unmasked message from client (protocol error)
+ if(first_bytes[1]<128) {
+ const std::string reason("message from client not masked");
+ send_close(connection, 1002, reason, [this, connection](const std::error_code& /*ec*/) {});
+ connection_close(connection, endpoint, 1002, reason);
+ return;
+ }
+
+ size_t length=(first_bytes[1]&127);
+
+ if(length==126) {
+ //2 next bytes is the size of content
+ asio::async_read(*connection->socket, *read_buffer, asio::transfer_exactly(2),
+ [this, connection, read_buffer, &endpoint, fin_rsv_opcode]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ std::istream stream(read_buffer.get());
+
+ std::vector<unsigned char> length_bytes;
+ length_bytes.resize(2);
+ stream.read(reinterpret_cast<char*>(&length_bytes[0]), 2);
+
+ size_t length=0;
+ int num_bytes=2;
+ for(int c=0;c<num_bytes;c++)
+ length+=length_bytes[c]<<(8*(num_bytes-1-c));
+
+ read_message_content(connection, read_buffer, length, endpoint, fin_rsv_opcode);
+ }
+ else
+ connection_error(connection, endpoint, ec);
+ });
+ }
+ else if(length==127) {
+ //8 next bytes is the size of content
+ asio::async_read(*connection->socket, *read_buffer, asio::transfer_exactly(8),
+ [this, connection, read_buffer, &endpoint, fin_rsv_opcode]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ std::istream stream(read_buffer.get());
+
+ std::vector<unsigned char> length_bytes;
+ length_bytes.resize(8);
+ stream.read(reinterpret_cast<char*>(&length_bytes[0]), 8);
+
+ size_t length=0;
+ int num_bytes=8;
+ for(int c=0;c<num_bytes;c++)
+ length+=length_bytes[c]<<(8*(num_bytes-1-c));
+
+ read_message_content(connection, read_buffer, length, endpoint, fin_rsv_opcode);
+ }
+ else
+ connection_error(connection, endpoint, ec);
+ });
+ }
+ else
+ read_message_content(connection, read_buffer, length, endpoint, fin_rsv_opcode);
+ }
+ else
+ connection_error(connection, endpoint, ec);
+ });
+ }
+
+ void read_message_content(const std::shared_ptr<Connection> &connection, const std::shared_ptr<asio::streambuf> &read_buffer,
+ size_t length, Endpoint& endpoint, unsigned char fin_rsv_opcode) const {
+ asio::async_read(*connection->socket, *read_buffer, asio::transfer_exactly(4+length),
+ [this, connection, read_buffer, length, &endpoint, fin_rsv_opcode]
+ (const std::error_code& ec, size_t /*bytes_transferred*/) {
+ if(!ec) {
+ std::istream raw_message_data(read_buffer.get());
+
+ //Read mask
+ std::vector<unsigned char> mask;
+ mask.resize(4);
+ raw_message_data.read(reinterpret_cast<char*>(&mask[0]), 4);
+
+ std::shared_ptr<Message> message(new Message());
+ message->length=length;
+ message->fin_rsv_opcode=fin_rsv_opcode;
+
+ std::ostream message_data_out_stream(&message->streambuf);
+ for(size_t c=0;c<length;c++) {
+ message_data_out_stream.put(raw_message_data.get()^mask[c%4]);
+ }
+
+ //If connection close
+ if((fin_rsv_opcode&0x0f)==8) {
+ int status=0;
+ if(length>=2) {
+ unsigned char byte1=message->get();
+ unsigned char byte2=message->get();
+ status=(byte1<<8)+byte2;
+ }
+
+ auto reason=message->string();
+ send_close(connection, status, reason, [this, connection](const std::error_code& /*ec*/) {});
+ connection_close(connection, endpoint, status, reason);
+ return;
+ }
+ else {
+ //If ping
+ if((fin_rsv_opcode&0x0f)==9) {
+ //send pong
+ auto empty_send_stream=std::make_shared<SendStream>();
+ send(connection, empty_send_stream, nullptr, fin_rsv_opcode+1);
+ }
+ else if(endpoint.on_message) {
+ timer_idle_reset(connection);
+ endpoint.on_message(connection, message);
+ }
+
+ //Next message
+ read_message(connection, read_buffer, endpoint);
+ }
+ }
+ else
+ connection_error(connection, endpoint, ec);
+ });
+ }
+
+ void connection_open(const std::shared_ptr<Connection> &connection, Endpoint& endpoint) {
+ timer_idle_init(connection);
+
+ {
+ std::lock_guard<std::mutex> lock(endpoint.connections_mutex);
+ endpoint.connections.insert(connection);
+ }
+
+ if(endpoint.on_open)
+ endpoint.on_open(connection);
+ }
+
+ void connection_close(const std::shared_ptr<Connection> &connection, Endpoint& endpoint, int status, const std::string& reason) const {
+ timer_idle_cancel(connection);
+
+ {
+ std::lock_guard<std::mutex> lock(endpoint.connections_mutex);
+ endpoint.connections.erase(connection);
+ }
+
+ if(endpoint.on_close)
+ endpoint.on_close(connection, status, reason);
+ }
+
+ void connection_error(const std::shared_ptr<Connection> &connection, Endpoint& endpoint, const std::error_code& ec) const {
+ timer_idle_cancel(connection);
+
+ {
+ std::lock_guard<std::mutex> lock(endpoint.connections_mutex);
+ endpoint.connections.erase(connection);
+ }
+
+ if(endpoint.on_error) {
+ std::error_code ec_tmp=ec;
+ endpoint.on_error(connection, ec_tmp);
+ }
+ }
+
+ void timer_idle_init(const std::shared_ptr<Connection> &connection) {
+ if(config.timeout_idle>0) {
+ connection->timer_idle= std::make_unique<asio::system_timer>(connection->socket->get_io_service());
+ connection->timer_idle->expires_from_now(std::chrono::seconds(static_cast<unsigned long>(config.timeout_idle)));
+ timer_idle_expired_function(connection);
+ }
+ }
+ void timer_idle_reset(const std::shared_ptr<Connection> &connection) const {
+ if(config.timeout_idle>0 && connection->timer_idle->expires_from_now(std::chrono::seconds(static_cast<unsigned long>(config.timeout_idle)))>0)
+ timer_idle_expired_function(connection);
+ }
+ void timer_idle_cancel(const std::shared_ptr<Connection> &connection) const {
+ if(config.timeout_idle>0)
+ connection->timer_idle->cancel();
+ }
+
+ void timer_idle_expired_function(const std::shared_ptr<Connection> &connection) const {
+ connection->timer_idle->async_wait([this, connection](const std::error_code& ec){
+ if(!ec)
+ send_close(connection, 1000, "idle timeout"); //1000=normal closure
+ });
+ }
+ };
+
+ template<class socket_type>
+ class SocketServer : public SocketServerBase<socket_type> {
+ public:
+ SocketServer(unsigned short port, size_t timeout_request, size_t timeout_idle)
+ : SocketServerBase<socket_type>(port, timeout_request, timeout_idle)
+ {
+ }
+ };
+
+ using WS = asio::ip::tcp::socket;
+
+ template<>
+ class SocketServer<WS> : public SocketServerBase<WS> {
+ public:
+ SocketServer() : SocketServerBase<WS>(80) {}
+ protected:
+ void accept() override {
+ //Create new socket for this connection (stored in Connection::socket)
+ //Shared_ptr is used to pass temporary objects to the asynchronous functions
+ std::shared_ptr<Connection> connection(new Connection(new WS(*io_context)));
+
+ acceptor->async_accept(*connection->socket, [this, connection](const std::error_code& ec) {
+ //Immediately start accepting a new connection (if io_context hasn't been stopped)
+ if (ec != asio::error::operation_aborted)
+ accept();
+
+ if(!ec) {
+ asio::ip::tcp::no_delay option(true);
+ connection->socket->set_option(option);
+
+ read_handshake(connection);
+ }
+ });
+ }
+ };
+}
+#endif /* SERVER_WS_HPP */
diff --git a/src/lib/util/server_wss.hpp b/src/lib/util/server_wss.hpp
new file mode 100644
index 00000000000..000c00bf83d
--- /dev/null
+++ b/src/lib/util/server_wss.hpp
@@ -0,0 +1,77 @@
+// license:MIT
+// copyright-holders:Ole Christian Eidheim, Miodrag Milanovic
+#ifndef SERVER_WSS_HPP
+#define SERVER_WSS_HPP
+
+#include "server_ws.hpp"
+#include "asio/ssl.hpp"
+#include <openssl/ssl.h>
+#include <algorithm>
+
+namespace webpp {
+ using WSS = asio::ssl::stream<asio::ip::tcp::socket>;
+
+ template<>
+ class SocketServer<WSS> : public SocketServerBase<WSS> {
+ std::string session_id_context;
+ bool set_session_id_context = false;
+ public:
+ SocketServer(const std::string& cert_file, const std::string& private_key_file,
+ const std::string& verify_file=std::string()) : SocketServerBase<WSS>(443), context(asio::ssl::context::tlsv12) {
+
+ context.use_certificate_chain_file(cert_file);
+ context.use_private_key_file(private_key_file, asio::ssl::context::pem);
+
+ if (verify_file.size() > 0) {
+ context.load_verify_file(verify_file);
+ context.set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert |
+ asio::ssl::verify_client_once);
+ set_session_id_context=true;
+ }
+ }
+
+ void start() override {
+ if(set_session_id_context) {
+ // Creating session_id_context from address:port but reversed due to small SSL_MAX_SSL_SESSION_ID_LENGTH
+ session_id_context=std::to_string(config.port)+':';
+ session_id_context.append(config.address.rbegin(), config.address.rend());
+ SSL_CTX_set_session_id_context(context.native_handle(), reinterpret_cast<const unsigned char*>(session_id_context.data()),
+ std::min<size_t>(session_id_context.size(), SSL_MAX_SSL_SESSION_ID_LENGTH));
+ }
+ SocketServerBase::start();
+ }
+ protected:
+ asio::ssl::context context;
+
+ void accept() override {
+ //Create new socket for this connection (stored in Connection::socket)
+ //Shared_ptr is used to pass temporary objects to the asynchronous functions
+ std::shared_ptr<Connection> connection(new Connection(new WSS(*io_context, context)));
+
+ acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const std::error_code& ec) {
+ //Immediately start accepting a new connection (if io_context hasn't been stopped)
+ if (ec != asio::error::operation_aborted)
+ accept();
+
+ if(!ec) {
+ asio::ip::tcp::no_delay option(true);
+ connection->socket->lowest_layer().set_option(option);
+
+ //Set timeout on the following asio::ssl::stream::async_handshake
+ auto timer = get_timeout_timer(connection, config.timeout_request);
+ connection->socket->async_handshake(asio::ssl::stream_base::server,
+ [this, connection, timer](const std::error_code& ec) {
+ if(timer)
+ timer->cancel();
+ if(!ec)
+ read_handshake(connection);
+ });
+ }
+ });
+ }
+ };
+}
+
+
+#endif /* SERVER_WSS_HPP */
+
diff --git a/src/lib/util/sha1.hpp b/src/lib/util/sha1.hpp
new file mode 100644
index 00000000000..7befbd326f0
--- /dev/null
+++ b/src/lib/util/sha1.hpp
@@ -0,0 +1,181 @@
+// license:BSD-3-Clause
+// copyright-holders:Micael Hildenborg
+/*
+ Copyright (c) 2011, Micael Hildenborg
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Micael Hildenborg nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY Micael Hildenborg ''AS IS'' AND ANY
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL Micael Hildenborg BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SHA1_DEFINED
+#define SHA1_DEFINED
+
+namespace sha1 {
+
+namespace { // local
+
+// Rotate an integer value to left.
+inline unsigned int rol(unsigned int value, unsigned int steps) {
+ return ((value << steps) | (value >> (32 - steps)));
+}
+
+// Sets the first 16 integers in the buffert to zero.
+// Used for clearing the W buffert.
+inline void clearWBuffert(unsigned int * buffert)
+{
+ for (int pos = 16; --pos >= 0;)
+ {
+ buffert[pos] = 0;
+ }
+}
+
+inline void innerHash(unsigned int * result, unsigned int * w)
+{
+ unsigned int a = result[0];
+ unsigned int b = result[1];
+ unsigned int c = result[2];
+ unsigned int d = result[3];
+ unsigned int e = result[4];
+
+ int round = 0;
+
+ #define sha1macro(func,val) \
+ { \
+ const unsigned int t = rol(a, 5) + (func) + e + val + w[round]; \
+ e = d; \
+ d = c; \
+ c = rol(b, 30); \
+ b = a; \
+ a = t; \
+ }
+
+ while (round < 16)
+ {
+ sha1macro((b & c) | (~b & d), 0x5a827999)
+ ++round;
+ }
+ while (round < 20)
+ {
+ w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1);
+ sha1macro((b & c) | (~b & d), 0x5a827999)
+ ++round;
+ }
+ while (round < 40)
+ {
+ w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1);
+ sha1macro(b ^ c ^ d, 0x6ed9eba1)
+ ++round;
+ }
+ while (round < 60)
+ {
+ w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1);
+ sha1macro((b & c) | (b & d) | (c & d), 0x8f1bbcdc)
+ ++round;
+ }
+ while (round < 80)
+ {
+ w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1);
+ sha1macro(b ^ c ^ d, 0xca62c1d6)
+ ++round;
+ }
+
+ #undef sha1macro
+
+ result[0] += a;
+ result[1] += b;
+ result[2] += c;
+ result[3] += d;
+ result[4] += e;
+}
+
+} // namespace
+
+/// Calculate a SHA1 hash
+/**
+ * @param src points to any kind of data to be hashed.
+ * @param bytelength the number of bytes to hash from the src pointer.
+ * @param hash should point to a buffer of at least 20 bytes of size for storing
+ * the sha1 result in.
+ */
+inline void calc(void const * src, size_t bytelength, unsigned char * hash) {
+ // Init the result array.
+ unsigned int result[5] = { 0x67452301, 0xefcdab89, 0x98badcfe,
+ 0x10325476, 0xc3d2e1f0 };
+
+ // Cast the void src pointer to be the byte array we can work with.
+ unsigned char const * sarray = static_cast<unsigned char const *>(src);
+
+ // The reusable round buffer
+ unsigned int w[80];
+
+ // Loop through all complete 64byte blocks.
+
+ size_t endCurrentBlock;
+ size_t currentBlock = 0;
+
+ if (bytelength >= 64) {
+ size_t const endOfFullBlocks = bytelength - 64;
+
+ while (currentBlock <= endOfFullBlocks) {
+ endCurrentBlock = currentBlock + 64;
+
+ // Init the round buffer with the 64 byte block data.
+ for (int roundPos = 0; currentBlock < endCurrentBlock; currentBlock += 4)
+ {
+ // This line will swap endian on big endian and keep endian on
+ // little endian.
+ w[roundPos++] = static_cast<unsigned int>(sarray[currentBlock + 3])
+ | (static_cast<unsigned int>(sarray[currentBlock + 2]) << 8)
+ | (static_cast<unsigned int>(sarray[currentBlock + 1]) << 16)
+ | (static_cast<unsigned int>(sarray[currentBlock]) << 24);
+ }
+ innerHash(result, w);
+ }
+ }
+
+ // Handle the last and not full 64 byte block if existing.
+ endCurrentBlock = bytelength - currentBlock;
+ clearWBuffert(w);
+ size_t lastBlockBytes = 0;
+ for (;lastBlockBytes < endCurrentBlock; ++lastBlockBytes) {
+ w[lastBlockBytes >> 2] |= static_cast<unsigned int>(sarray[lastBlockBytes + currentBlock]) << ((3 - (lastBlockBytes & 3)) << 3);
+ }
+
+ w[lastBlockBytes >> 2] |= 0x80 << ((3 - (lastBlockBytes & 3)) << 3);
+ if (endCurrentBlock >= 56) {
+ innerHash(result, w);
+ clearWBuffert(w);
+ }
+ w[15] = bytelength << 3;
+ innerHash(result, w);
+
+ // Store hash in result pointer, and make sure we get in in the correct
+ // order on both endian models.
+ for (int hashByte = 20; --hashByte >= 0;) {
+ hash[hashByte] = (result[hashByte >> 2] >> (((3 - hashByte) & 0x3) << 3)) & 0xff;
+ }
+}
+
+} // namespace sha1
+
+#endif // SHA1_DEFINED