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