diff --git a/scripts/src/emu.lua b/scripts/src/emu.lua index 76cdb16427c..44b67a338ae 100644 --- a/scripts/src/emu.lua +++ b/scripts/src/emu.lua @@ -39,6 +39,7 @@ includedirs { files { MAME_DIR .. "src/emu/emu.h", MAME_DIR .. "src/emu/main.h", + MAME_DIR .. "src/emu/main.cpp", MAME_DIR .. "src/emu/gamedrv.h", MAME_DIR .. "src/emu/hashfile.cpp", MAME_DIR .. "src/emu/hashfile.h", diff --git a/scripts/src/lib.lua b/scripts/src/lib.lua index 51b92d098d2..e102ba3cdcc 100644 --- a/scripts/src/lib.lua +++ b/scripts/src/lib.lua @@ -31,6 +31,7 @@ project "utils" MAME_DIR .. "src/lib/util/avhuff.h", MAME_DIR .. "src/lib/util/aviio.cpp", MAME_DIR .. "src/lib/util/aviio.h", + MAME_DIR .. "src/lib/util/base64.hpp", MAME_DIR .. "src/lib/util/bitmap.cpp", MAME_DIR .. "src/lib/util/bitmap.h", MAME_DIR .. "src/lib/util/cdrom.cpp", @@ -41,6 +42,10 @@ project "utils" MAME_DIR .. "src/lib/util/chdcd.h", MAME_DIR .. "src/lib/util/chdcodec.cpp", MAME_DIR .. "src/lib/util/chdcodec.h", + MAME_DIR .. "src/lib/util/client_http.hpp", + MAME_DIR .. "src/lib/util/client_https.hpp", + MAME_DIR .. "src/lib/util/client_ws.hpp", + MAME_DIR .. "src/lib/util/client_wss.hpp", MAME_DIR .. "src/lib/util/corealloc.h", MAME_DIR .. "src/lib/util/corefile.cpp", MAME_DIR .. "src/lib/util/corefile.h", @@ -48,6 +53,7 @@ project "utils" MAME_DIR .. "src/lib/util/corestr.h", MAME_DIR .. "src/lib/util/coreutil.cpp", MAME_DIR .. "src/lib/util/coreutil.h", + MAME_DIR .. "src/lib/util/crypto.hpp", MAME_DIR .. "src/lib/util/delegate.cpp", MAME_DIR .. "src/lib/util/delegate.h", MAME_DIR .. "src/lib/util/flac.cpp", @@ -71,14 +77,21 @@ project "utils" MAME_DIR .. "src/lib/util/options.h", MAME_DIR .. "src/lib/util/palette.cpp", MAME_DIR .. "src/lib/util/palette.h", + MAME_DIR .. "src/lib/util/path_to_regex.cpp", + MAME_DIR .. "src/lib/util/path_to_regex.h", MAME_DIR .. "src/lib/util/plaparse.cpp", MAME_DIR .. "src/lib/util/plaparse.h", MAME_DIR .. "src/lib/util/png.cpp", MAME_DIR .. "src/lib/util/png.h", MAME_DIR .. "src/lib/util/pool.cpp", MAME_DIR .. "src/lib/util/pool.h", + MAME_DIR .. "src/lib/util/server_http.hpp", + MAME_DIR .. "src/lib/util/server_https.hpp", + MAME_DIR .. "src/lib/util/server_ws.hpp", + MAME_DIR .. "src/lib/util/server_wss.hpp", MAME_DIR .. "src/lib/util/sha1.cpp", MAME_DIR .. "src/lib/util/sha1.h", + MAME_DIR .. "src/lib/util/sha1.hpp", MAME_DIR .. "src/lib/util/strformat.cpp", MAME_DIR .. "src/lib/util/strformat.h", MAME_DIR .. "src/lib/util/timeconv.cpp", diff --git a/src/emu/emuopts.cpp b/src/emu/emuopts.cpp index 0e5305f33d1..b6463a99f9d 100644 --- a/src/emu/emuopts.cpp +++ b/src/emu/emuopts.cpp @@ -203,6 +203,12 @@ const options_entry emu_options::s_option_entries[] = { OPTION_PLUGIN, nullptr, OPTION_STRING, "list of plugins to enable" }, { OPTION_NO_PLUGIN, nullptr, OPTION_STRING, "list of plugins to disable" }, { OPTION_LANGUAGE ";lang", "English", OPTION_STRING, "display language" }, + + { nullptr, nullptr, OPTION_HEADER, "HTTP SERVER OPTIONS" }, + { OPTION_HTTP, "0", OPTION_BOOLEAN, "HTTP server enable" }, + { OPTION_HTTP_PORT, "8080", OPTION_INTEGER, "HTTP server port" }, + { OPTION_HTTP_ROOT, "web", OPTION_STRING, "HTTP server document root" }, + { nullptr } }; diff --git a/src/emu/emuopts.h b/src/emu/emuopts.h index d8983101a3a..2046661146b 100644 --- a/src/emu/emuopts.h +++ b/src/emu/emuopts.h @@ -190,6 +190,10 @@ #define OPTION_LANGUAGE "language" +#define OPTION_HTTP "http" +#define OPTION_HTTP_PORT "http_port" +#define OPTION_HTTP_ROOT "http_root" + //************************************************************************** // TYPE DEFINITIONS //************************************************************************** @@ -381,6 +385,11 @@ public: const char *language() const { return value(OPTION_LANGUAGE); } + // Web server specific optopns + bool http() const { return value(OPTION_HTTP); } + short http_port() const { return int_value(OPTION_HTTP_PORT); } + const char *http_root() const { return value(OPTION_HTTP_ROOT); } + // cache frequently used options in members void update_cached_options(); diff --git a/src/emu/machine.cpp b/src/emu/machine.cpp index 91999a8307e..6785a1ccfed 100644 --- a/src/emu/machine.cpp +++ b/src/emu/machine.cpp @@ -84,6 +84,9 @@ #include "network.h" #include "ui/uimain.h" #include +#include "server_http.hpp" +#include "rapidjson/include/rapidjson/writer.h" +#include "rapidjson/include/rapidjson/stringbuffer.h" #if defined(EMSCRIPTEN) #include @@ -332,6 +335,8 @@ int running_machine::run(bool quiet) if (m_saveload_schedule != SLS_NONE) handle_saveload(); + export_http_api(); + // run the CPUs until a reset or exit m_hard_reset_pending = false; while ((!m_hard_reset_pending && !m_exit_pending) || m_saveload_schedule != SLS_NONE) @@ -1176,7 +1181,30 @@ running_machine::logerror_callback_item::logerror_callback_item(logerror_callbac { } +void running_machine::export_http_api() +{ + m_manager.http_server()->on_get("/api/machine", [this](auto response, auto request) + { + rapidjson::StringBuffer s; + rapidjson::Writer writer(s); + writer.StartObject(); + writer.Key("name"); + writer.String(m_basename.c_str()); + writer.Key("devices"); + writer.StartArray(); + + device_iterator iter(root_device()); + for (device_t &device : iter) + writer.String(device.tag()); + + writer.EndArray(); + writer.EndObject(); + + response->type("application/json"); + response->status(200).send(s.GetString()); + }); +} //************************************************************************** // SYSTEM TIME diff --git a/src/emu/machine.h b/src/emu/machine.h index cac9a3f38b6..2b14edfcd64 100644 --- a/src/emu/machine.h +++ b/src/emu/machine.h @@ -239,6 +239,7 @@ public: void add_logerror_callback(logerror_callback callback); void set_ui_active(bool active) { m_ui_active = active; } void debug_break(); + void export_http_api(); // TODO: Do saves and loads still require scheduling? void immediate_save(const char *filename); diff --git a/src/emu/main.cpp b/src/emu/main.cpp new file mode 100644 index 00000000000..07043960027 --- /dev/null +++ b/src/emu/main.cpp @@ -0,0 +1,186 @@ +// license:BSD-3-Clause +// copyright-holders:Nicola Salmoria, Aaron Giles +/*************************************************************************** + +main.cpp + +Controls execution of the core MAME system. + +***************************************************************************/ + +#include "emu.h" +#include "emuopts.h" +#include "main.h" +#include "server_http.hpp" +#include + +const static struct mapping +{ + const char* extension; + const char* mime_type; +} mappings[] = +{ + { "aac", "audio/aac" }, + { "aat", "application/font-sfnt" }, + { "aif", "audio/x-aif" }, + { "arj", "application/x-arj-compressed" }, + { "asf", "video/x-ms-asf" }, + { "avi", "video/x-msvideo" }, + { "bmp", "image/bmp" }, + { "cff", "application/font-sfnt" }, + { "css", "text/css" }, + { "csv", "text/csv" }, + { "doc", "application/msword" }, + { "eps", "application/postscript" }, + { "exe", "application/octet-stream" }, + { "gif", "image/gif" }, + { "gz", "application/x-gunzip" }, + { "htm", "text/html" }, + { "html", "text/html" }, + { "ico", "image/x-icon" }, + { "ief", "image/ief" }, + { "jpeg", "image/jpeg" }, + { "jpg", "image/jpeg" }, + { "jpm", "image/jpm" }, + { "jpx", "image/jpx" }, + { "js", "application/javascript" }, + { "json", "application/json" }, + { "m3u", "audio/x-mpegurl" }, + { "m4v", "video/x-m4v" }, + { "mid", "audio/x-midi" }, + { "mov", "video/quicktime" }, + { "mp3", "audio/mpeg" }, + { "mp4", "video/mp4" }, + { "mpeg", "video/mpeg" }, + { "mpg", "video/mpeg" }, + { "oga", "audio/ogg" }, + { "ogg", "audio/ogg" }, + { "ogv", "video/ogg" }, + { "otf", "application/font-sfnt" }, + { "pct", "image/x-pct" }, + { "pdf", "application/pdf" }, + { "pfr", "application/font-tdpfr" }, + { "pict", "image/pict" }, + { "png", "image/png" }, + { "ppt", "application/x-mspowerpoint" }, + { "ps", "application/postscript" }, + { "qt", "video/quicktime" }, + { "ra", "audio/x-pn-realaudio" }, + { "ram", "audio/x-pn-realaudio" }, + { "rar", "application/x-arj-compressed" }, + { "rgb", "image/x-rgb" }, + { "rtf", "application/rtf" }, + { "sgm", "text/sgml" }, + { "shtm", "text/html" }, + { "shtml", "text/html" }, + { "sil", "application/font-sfnt" }, + { "svg", "image/svg+xml" }, + { "swf", "application/x-shockwave-flash" }, + { "tar", "application/x-tar" }, + { "tgz", "application/x-tar-gz" }, + { "tif", "image/tiff" }, + { "tiff", "image/tiff" }, + { "torrent", "application/x-bittorrent" }, + { "ttf", "application/font-sfnt" }, + { "txt", "text/plain" }, + { "wav", "audio/x-wav" }, + { "webm", "video/webm" }, + { "woff", "application/font-woff" }, + { "wrl", "model/vrml" }, + { "xhtml", "application/xhtml+xml" }, + { "xls", "application/x-msexcel" }, + { "xml", "text/xml" }, + { "xsl", "application/xml" }, + { "xslt", "application/xml" }, + { "zip", "application/x-zip-compressed" } +}; + +static std::string extension_to_type(const std::string& extension) +{ + for (mapping m : mappings) + { + if (m.extension == extension) + { + return m.mime_type; + } + } + + return "text/plain"; +} + +machine_manager::machine_manager(emu_options& options, osd_interface& osd) + : m_osd(osd), + m_options(options), + m_machine(nullptr), + m_io_context(std::make_shared()) +{ +} + +machine_manager::~machine_manager() +{ + if (options().http()) + { + m_server->stop(); + } + m_server_thread.join(); +} + +void machine_manager::start_http_server() +{ + if (options().http()) + { + m_server = std::make_unique(); + m_server->m_config.port = options().http_port(); + m_server->set_io_context(m_io_context); + std::string doc_root = options().http_root(); + + m_server->on_get([this, doc_root](auto response, auto request) { + std::string path = request->path; + // If path ends in slash (i.e. is a directory) then add "index.html". + if (path[path.size() - 1] == '/') + { + path += "index.html"; + } + + std::size_t last_qmark_pos = path.find_last_of("?"); + if (last_qmark_pos != std::string::npos) + path = path.substr(0, last_qmark_pos - 1); + + // Determine the file extension. + std::size_t last_slash_pos = path.find_last_of("/"); + std::size_t last_dot_pos = path.find_last_of("."); + std::string extension; + if (last_dot_pos != std::string::npos && last_dot_pos > last_slash_pos) + { + extension = path.substr(last_dot_pos + 1); + } + + // Open the file to send back. + std::string full_path = doc_root + path; + std::ifstream is(full_path.c_str(), std::ios::in | std::ios::binary); + if (!is) + { + response->status(400).send("Error"); + } + + // Fill out the reply to be sent to the client. + std::string content; + char buf[512]; + while (is.read(buf, sizeof(buf)).gcount() > 0) + content.append(buf, size_t(is.gcount())); + + response->type(extension_to_type(extension)); + response->status(200).send(content); + + }); + m_server->start(); + } +} + +void machine_manager::start_context() +{ + m_server_thread = std::thread([this]() { + m_io_context->run(); + }); +} + diff --git a/src/emu/main.h b/src/emu/main.h index 12f0f1a9462..2ad90a50885 100644 --- a/src/emu/main.h +++ b/src/emu/main.h @@ -65,15 +65,23 @@ public: // ======================> machine_manager class ui_manager; +namespace asio +{ + class io_context; +} +namespace webpp +{ + class http_server; +} class machine_manager { DISABLE_COPYING(machine_manager); protected: // construction/destruction - machine_manager(emu_options &options, osd_interface &osd) : m_osd(osd), m_options(options), m_machine(nullptr) { } + machine_manager(emu_options& options, osd_interface& osd); public: - virtual ~machine_manager() { } + virtual ~machine_manager(); osd_interface &osd() const { return m_osd; } emu_options &options() const { return m_options; } @@ -87,10 +95,17 @@ public: virtual void ui_initialize(running_machine& machine) { } virtual void update_machine() { } + + void start_http_server(); + void start_context(); + webpp::http_server* http_server() const { return m_server.get(); } protected: osd_interface & m_osd; // reference to OSD system emu_options & m_options; // reference to options running_machine * m_machine; + std::shared_ptr m_io_context; + std::unique_ptr m_server; + std::thread m_server_thread; }; diff --git a/src/frontend/mame/clifront.cpp b/src/frontend/mame/clifront.cpp index ff7e371c438..fd785ef8139 100644 --- a/src/frontend/mame/clifront.cpp +++ b/src/frontend/mame/clifront.cpp @@ -309,9 +309,13 @@ int cli_frontend::execute(int argc, char **argv) load_translation(m_options); + manager->start_http_server(); + manager->start_luaengine(); - start_execution(manager, argc, argv, option_errors); + manager->start_context(); + + start_execution(manager, argc, argv, option_errors); } // handle exceptions of various types catch (emu_fatalerror &fatal) 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 + +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(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(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(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(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 +#include +#include +#include + +#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 hasher; + seed ^= hasher(std::tolower(c)) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } + return seed; + } +}; +#endif + +namespace webpp { + template + class Client; + + template + class ClientBase { + public: + virtual ~ClientBase() {} + + class Response { + friend class ClientBase; + friend class Client; + public: + std::string http_version, status_code; + + std::istream content; + + std::unordered_multimap header; + + private: + asio::streambuf content_buffer; + + Response(): content(&content_buffer) {} + }; + + class Config { + friend class ClientBase; + 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 request(const std::string& request_type, const std::string& path="/", const std::string content="", + const std::map& header=std::map()) { + auto corrected_path=path; + if(corrected_path=="") + corrected_path="/"; + if (!config.proxy_server.empty() && std::is_same::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 lock(socket_mutex); + socket = nullptr; + throw std::system_error(ec); + } + }); + } + } + else { + std::lock_guard lock(socket_mutex); + socket = nullptr; + throw std::system_error(ec); + } + }); + io_context.reset(); + io_context.run(); + return request_read(); + } + + std::shared_ptr request(const std::string& request_type, const std::string& path, std::iostream& content, + const std::map& header=std::map()) { + auto corrected_path=path; + if(corrected_path=="") + corrected_path="/"; + if (!config.proxy_server.empty() && std::is_same::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 lock(socket_mutex); + socket = nullptr; + throw std::system_error(ec); + } + }); + io_context.reset(); + io_context.run(); + + return request_read(); + } + void close() { + std::lock_guard 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; + 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 parse_host_port(const std::string &host_port, unsigned short default_port) const + { + std::pair 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(stoul(host_port.substr(host_end+1))); + } + return parsed_host_port; + } + + virtual void connect()=0; + + std::shared_ptr get_timeout_timer() { + if (config.timeout == 0) + return nullptr; + + auto timer = std::make_shared(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) const { + std::string line; + getline(response->content, line); + size_t version_end=line.find(' '); + if(version_end!=std::string::npos) { + if(5http_version=line.substr(5, version_end-5); + if((version_end+1)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)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 request_read() { + std::shared_ptr 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 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 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, 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(response->content_buffer.size() - bytes_transferred); + + auto post_process = [this, &response, &streambuf, length] { + std::ostream stream(&streambuf); + if (length > 0) { + std::vector buffer(static_cast(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 lock(socket_mutex); + socket = nullptr; + throw std::system_error(ec); + } + }); + } + else + post_process(); + } + else { + std::lock_guard lock(socket_mutex); + socket = nullptr; + throw std::system_error(ec); + } + }); + } + }; + + template + class Client : public ClientBase { + public: + Client(const std::string& host_port, unsigned short default_port) + : ClientBase(host_port, default_port) + { + } + }; + + using HTTP = asio::ip::tcp::socket; + + template<> + class Client : public ClientBase { + 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 query; + if (config.proxy_server.empty()) + query = std::make_unique(host, std::to_string(port)); + else { + auto proxy_host_port = parse_host_port(config.proxy_server, 8080); + query = std::make_unique(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 lock(socket_mutex); + socket = std::make_unique(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 lock(socket_mutex); + socket=nullptr; + throw std::system_error(ec); + } + }); + } + else { + std::lock_guard lock(socket_mutex); + socket=nullptr; + throw std::system_error(ec); + } + }); + io_context.reset(); + io_context.run(); + } + } + }; + + class http_client : public Client { + public: + explicit http_client(const std::string& server_port_path) : Client::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; + + template<> + class Client : public ClientBase { + 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 query; + if (config.proxy_server.empty()) { + query = std::make_unique(host, std::to_string(port)); + } + else { + auto proxy_host_port = parse_host_port(config.proxy_server, 8080); + query = std::make_unique(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 lock(socket_mutex); + socket = std::make_unique(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 lock(socket_mutex); + socket = nullptr; + throw std::system_error(ec); + } + }); + } + else { + std::lock_guard 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 lock(socket_mutex); + socket = nullptr; + throw std::system_error(ec); + } + }); + io_context.reset(); + io_context.run(); + + std::shared_ptr 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 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 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 lock(socket_mutex); + socket = nullptr; + throw std::system_error(ec); + } + }); + io_context.reset(); + io_context.run(); + } + } + }; + + class https_client : public Client { + 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::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 +#include +#include +#include +#include +#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 hasher; + seed ^= hasher(std::tolower(c)) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } + return seed; + } +}; +#endif + +namespace webpp { + template + class SocketClient; + + template + class SocketClientBase { + public: + virtual ~SocketClientBase() { connection.reset(); } + + class SendStream : public std::iostream { + friend class SocketClientBase; + asio::streambuf streambuf; + public: + SendStream(): std::iostream(&streambuf) {} + size_t size() const { + return streambuf.size(); + } + }; + + class Connection { + friend class SocketClientBase; + friend class SocketClient; + + public: + std::unordered_multimap 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 &send_stream, const std::function &callback) : + send_stream(send_stream), callback(callback) {} + std::shared_ptr send_stream; + std::function callback; + }; + + std::unique_ptr socket; + + asio::io_context::strand strand; + + std::list 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 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; + + class Message : public std::istream { + friend class SocketClientBase; + + 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 on_open; + std::function)> on_message; + std::function on_close; + std::function on_error; + + void start() { + if(!io_context) { + io_context=std::make_shared(); + internal_io_context=true; + } + + if(io_context->stopped()) + io_context->reset(); + + if(!resolver) + resolver= std::make_unique(*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 &message_stream, const std::function& callback=nullptr, + unsigned char fin_rsv_opcode=129) { + //Create mask + std::vector mask; + mask.resize(4); + std::uniform_int_distribution dist(0,255); + std::random_device rd; + for(int c=0;c<4;c++) { + mask[c]=static_cast(dist(rd)); + } + + auto send_stream = std::make_shared(); + + 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(127+128)); + } + else { + num_bytes=2; + send_stream->put(static_cast(126+128)); + } + + for(int c=num_bytes-1;c>=0;c--) { + send_stream->put((static_cast(length) >> (8 * c)) % 256); + } + } + else + send_stream->put(static_cast(length+128)); + + for(int c=0;c<4;c++) { + send_stream->put(mask[c]); + } + + for(size_t c=0;cput(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& 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(); + + 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 io_context; + protected: + const std::string ws_magic_string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + bool internal_io_context=false; + std::unique_ptr 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(stoul(host_port_path.substr(host_end + 1))); + else + port=static_cast(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(); + + 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 dist(0,255); + std::random_device rd; + for(int c=0;c<16;c++) + nonce[c]=static_cast(dist(rd)); + + auto nonce_base64 = std::make_shared(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(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)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) { + 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 first_bytes; + first_bytes.resize(2); + message->read(reinterpret_cast(&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 length_bytes; + length_bytes.resize(2); + message->read(reinterpret_cast(&length_bytes[0]), 2); + + size_t length=0; + int num_bytes=2; + for(int c=0;clength=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 length_bytes; + length_bytes.resize(8); + message->read(reinterpret_cast(&length_bytes[0]), 8); + + size_t length=0; + int num_bytes=8; + for(int c=0;clength=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) { + 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(); + send(empty_send_stream, nullptr, message->fin_rsv_opcode+1); + } + else if(on_message) { + on_message(message); + } + + //Next message + std::shared_ptr next_message(new Message()); + read_message(next_message); + } + else if(on_error) + on_error(ec); + }); + } + }; + + template + class SocketClient : public SocketClientBase { + public: + SocketClient(const std::string& host_port_path, unsigned short default_port) + : SocketClientBase(host_port_path, default_port) + { + } + }; + + using WS = asio::ip::tcp::socket; + + template<> + class SocketClient : public SocketClientBase { + 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(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; + + template<> + class SocketClient : public SocketClientBase { + 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(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(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 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 +#include +#include + +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; +using Tokens = std::vector; +using Options = std::map; + + /** + * 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 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 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 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 +#include +#include +#include +#include +#include +#include + +#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 hasher; + seed ^= hasher(std::tolower(c)) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } + return seed; + } +}; +#endif +namespace webpp { + template + class Server; + + template + class ServerBase { + public: + virtual ~ServerBase() {} + + class Response { + friend class ServerBase; + + asio::streambuf m_streambuf; + + std::shared_ptr m_socket; + std::ostream m_ostream; + std::stringstream m_header; + explicit Response(const std::shared_ptr &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() { return m_socket; } + }; + + class Content : public std::istream { + friend class ServerBase; + 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; + friend class Server; + public: + std::string method, path, http_version; + + Content content; + + std::unordered_multimap header; + + path2regex::Keys keys; + std::map 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; + + 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 ®ex_str) : std::regex(reg), str(regex_str) {} + bool operator<(const regex_orderable &rhs) const { + return str, std::shared_ptr)>; + + public: + template void on_get(std::string regex, T&& func) { std::lock_guard 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 void on_get(T&& func) { std::lock_guard lock(m_resource_mutex); m_default_resource["GET"] = func; } + template void on_post(std::string regex, T&& func) { std::lock_guard 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 void on_post(T&& func) { std::lock_guard lock(m_resource_mutex); m_default_resource["POST"] = func; } + template void on_put(std::string regex, T&& func) { std::lock_guard 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 void on_put(T&& func) { std::lock_guard lock(m_resource_mutex); m_default_resource["PUT"] = func; } + template void on_patch(std::string regex, T&& func) { std::lock_guard 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 void on_patch(T&& func) { std::lock_guard lock(m_resource_mutex); m_default_resource["PATCH"] = func; } + template void on_delete(std::string regex, T&& func) { std::lock_guard 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 void on_delete(T&& func) { std::lock_guard lock(m_resource_mutex); m_default_resource["DELETE"] = func; } + + void remove_handler(std::string regex) + { + std::lock_guard 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::Request>, const std::error_code&)> on_error; + + std::function socket, std::shared_ptr::Request>)> on_upgrade; + private: + /// Warning: do not add or remove resources after start() is called + std::map>> m_resource; + + std::map m_default_resource; + + std::mutex m_resource_mutex; + public: + virtual void start() { + if(!m_io_context) + m_io_context=std::make_shared(); + + 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(*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, const std::function& 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 new_io_context) + { + m_io_context = new_io_context; + m_external_context = true; + } + protected: + std::shared_ptr m_io_context; + bool m_external_context; + std::unique_ptr acceptor; + std::vector threads; + + ServerBase(unsigned short port) : m_config(port), m_external_context(false) {} + + virtual void accept()=0; + + std::shared_ptr get_timeout_timer(const std::shared_ptr &socket, long seconds) { + if(seconds==0) + return nullptr; + auto timer = std::make_shared(*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) { + //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(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) 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)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, const std::shared_ptr &request) { + std::lock_guard 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(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, const std::shared_ptr &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(new Response(socket), [this, request, timer](Response *response_ptr) { + auto response=std::shared_ptr(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 Server : public ServerBase { + public: + Server(unsigned short port, size_t num_threads, long timeout_request, long timeout_send_or_receive) + : ServerBase(port, num_threads, timeout_request, timeout_send_or_receive) + { + } + }; + + using HTTP = asio::ip::tcp::socket; + + template<> + class Server : public ServerBase { + public: + Server() : ServerBase::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(*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(new Request(*socket)), ec); + }); + } + }; + + class http_server : public Server { + public: + http_server() : Server::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 +#include + +namespace webpp { + using HTTPS = asio::ssl::stream; + + template<> + class Server : public ServerBase { + 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::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(session_id_context.data()), + std::min(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(*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(new Request(*socket)), ec); + }); + } + else if(on_error) + on_error(std::shared_ptr(new Request(*socket)), ec); + }); + } + }; + + class https_server : public Server { + public: + explicit https_server(const std::string& cert_file, const std::string& private_key_file, const std::string& verify_file = std::string()) : Server::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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 hasher; + seed ^= hasher(std::tolower(c)) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } + return seed; + } +}; +#endif + +namespace webpp { + template + class SocketServer; + + template + class SocketServerBase { + public: + virtual ~SocketServerBase() {} + + class SendStream : public std::ostream { + friend class SocketServerBase; + + asio::streambuf streambuf; + public: + SendStream(): std::ostream(&streambuf) {} + size_t size() const { + return streambuf.size(); + } + }; + + class Connection { + friend class SocketServerBase; + friend class SocketServer; + + public: + explicit Connection(const std::shared_ptr &socket) : remote_endpoint_port(0), socket(socket), strand(socket->get_io_service()), closed(false) { } + + std::string method, path, http_version; + + std::unordered_multimap 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 &header_stream, const std::shared_ptr &message_stream, + const std::function &callback) : + header_stream(header_stream), message_stream(message_stream), callback(callback) {} + std::shared_ptr header_stream; + std::shared_ptr message_stream; + std::function callback; + }; + + std::shared_ptr socket; + + asio::io_context::strand strand; + + std::list send_queue; + + void send_from_queue(const std::shared_ptr &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 closed; + + std::unique_ptr 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; + + 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; + std::unordered_set > connections; + std::mutex connections_mutex; + + public: + std::function)> on_open; + std::function, std::shared_ptr)> on_message; + std::function, int, const std::string&)> on_close; + std::function, const std::error_code&)> on_error; + + std::unordered_set > get_connections() { + std::lock_guard lock(connections_mutex); + auto copy=connections; + return copy; + } + }; + + class Config { + friend class SocketServerBase; + + 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 ®ex_cstr) : std::regex(path2regex::path_to_regex(regex_cstr, keys)), str(regex_cstr) {} + bool operator<(const regex_orderable &rhs) const { + return str endpoint; + + virtual void start() { + if(!io_context) + io_context=std::make_shared(); + + 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(*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, const std::shared_ptr &message_stream, + const std::function& callback=nullptr, + unsigned char fin_rsv_opcode=129) const { + if(fin_rsv_opcode!=136) + timer_idle_reset(connection); + + auto header_stream = std::make_shared(); + + 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(length) >> (8 * c)) % 256); + } + } + else + header_stream->put(static_cast(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, int status, const std::string& reason="", + const std::function& 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(); + + 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 > get_connections() { + std::unordered_set > all_connections; + for(auto& e: endpoint) { + std::lock_guard 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::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) { + auto read_buffer=std::make_shared(); + 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 io_context; + protected: + const std::string ws_magic_string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + std::unique_ptr acceptor; + + std::vector threads; + + SocketServerBase(unsigned short port) : + config(port) {} + + virtual void accept()=0; + + std::shared_ptr get_timeout_timer(const std::shared_ptr &connection, size_t seconds) { + if (seconds == 0) + return nullptr; + auto timer = std::make_shared(connection->socket->get_io_service()); + timer->expires_at(std::chrono::system_clock::now() + std::chrono::seconds(static_cast(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->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(); + + //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, 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)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)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, const std::shared_ptr &read_buffer) { + //Find path- and method-match, and generate response + for (auto ®ex_endpoint : endpoint) { + std::smatch path_match; + if(std::regex_match(connection->path, path_match, regex_endpoint.first)) { + auto write_buffer = std::make_shared(); + 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, ®ex_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, 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, + const std::shared_ptr &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 first_bytes; + first_bytes.resize(2); + stream.read(reinterpret_cast(&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 length_bytes; + length_bytes.resize(2); + stream.read(reinterpret_cast(&length_bytes[0]), 2); + + size_t length=0; + int num_bytes=2; + for(int c=0;csocket, *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 length_bytes; + length_bytes.resize(8); + stream.read(reinterpret_cast(&length_bytes[0]), 8); + + size_t length=0; + int num_bytes=8; + for(int c=0;c &connection, const std::shared_ptr &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 mask; + mask.resize(4); + raw_message_data.read(reinterpret_cast(&mask[0]), 4); + + std::shared_ptr 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=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(); + 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, Endpoint& endpoint) { + timer_idle_init(connection); + + { + std::lock_guard lock(endpoint.connections_mutex); + endpoint.connections.insert(connection); + } + + if(endpoint.on_open) + endpoint.on_open(connection); + } + + void connection_close(const std::shared_ptr &connection, Endpoint& endpoint, int status, const std::string& reason) const { + timer_idle_cancel(connection); + + { + std::lock_guard 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, Endpoint& endpoint, const std::error_code& ec) const { + timer_idle_cancel(connection); + + { + std::lock_guard 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) { + if(config.timeout_idle>0) { + connection->timer_idle= std::make_unique(connection->socket->get_io_service()); + connection->timer_idle->expires_from_now(std::chrono::seconds(static_cast(config.timeout_idle))); + timer_idle_expired_function(connection); + } + } + void timer_idle_reset(const std::shared_ptr &connection) const { + if(config.timeout_idle>0 && connection->timer_idle->expires_from_now(std::chrono::seconds(static_cast(config.timeout_idle)))>0) + timer_idle_expired_function(connection); + } + void timer_idle_cancel(const std::shared_ptr &connection) const { + if(config.timeout_idle>0) + connection->timer_idle->cancel(); + } + + void timer_idle_expired_function(const std::shared_ptr &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 SocketServer : public SocketServerBase { + public: + SocketServer(unsigned short port, size_t timeout_request, size_t timeout_idle) + : SocketServerBase(port, timeout_request, timeout_idle) + { + } + }; + + using WS = asio::ip::tcp::socket; + + template<> + class SocketServer : public SocketServerBase { + public: + SocketServer() : SocketServerBase(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(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 +#include + +namespace webpp { + using WSS = asio::ssl::stream; + + template<> + class SocketServer : public SocketServerBase { + 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(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(session_id_context.data()), + std::min(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(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(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(sarray[currentBlock + 3]) + | (static_cast(sarray[currentBlock + 2]) << 8) + | (static_cast(sarray[currentBlock + 1]) << 16) + | (static_cast(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(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 diff --git a/web/LICENSE b/web/LICENSE new file mode 100644 index 00000000000..4d9210d10e1 --- /dev/null +++ b/web/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2016, MAME Development Team +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 bsd3 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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. diff --git a/web/README.md b/web/README.md new file mode 100644 index 00000000000..ed61ca5da20 --- /dev/null +++ b/web/README.md @@ -0,0 +1,5 @@ +# **web** # + +Cointains web application part of MAME + +Licensed under [The BSD 3-Clause License](http://opensource.org/licenses/BSD-3-Clause)