Added initial HTTP/HTTPS webserver/websocket server support (nw)

This commit is contained in:
Miodrag Milanovic 2017-01-04 16:15:57 +01:00
parent 47c4f47bd4
commit 63e3f48775
24 changed files with 3549 additions and 3 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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 }
};

View File

@ -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();

View File

@ -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

View File

@ -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
View 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();
});
}

View File

@ -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;
};

View File

@ -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
View 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_

View 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 */

View 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
View 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 */

View 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
View 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 */

View 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

View 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

View 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 &regex_str) : std::regex(reg), str(regex_str) {}
bool operator<(const regex_orderable &rhs) const {
return str<rhs.str;
}
std::string getstr() const { return str; }
};
using http_handler = std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)>;
public:
template<class T> void on_get(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg,regex)]["GET"] = std::make_tuple(std::move(keys), func); }
template<class T> void on_get(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["GET"] = func; }
template<class T> void on_post(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg, regex)]["POST"] = std::make_tuple(std::move(keys), func); }
template<class T> void on_post(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["POST"] = func; }
template<class T> void on_put(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg, regex)]["PUT"] = std::make_tuple(std::move(keys), func); }
template<class T> void on_put(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["PUT"] = func; }
template<class T> void on_patch(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg, regex)]["PATCH"] = std::make_tuple(std::move(keys), func); }
template<class T> void on_patch(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["PATCH"] = func; }
template<class T> void on_delete(std::string regex, T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); path2regex::Keys keys; auto reg = path2regex::path_to_regex(regex, keys); m_resource[regex_orderable(reg, regex)]["DELETE"] = std::make_tuple(std::move(keys), func); }
template<class T> void on_delete(T&& func) { std::lock_guard<std::mutex> lock(m_resource_mutex); m_default_resource["DELETE"] = func; }
void remove_handler(std::string regex)
{
std::lock_guard<std::mutex> lock(m_resource_mutex);
for (auto &it = m_resource.begin(); it != m_resource.end(); ++it)
{
if (it->first.getstr() == regex)
{
m_resource.erase(it);
break;
}
}
}
std::function<void(std::shared_ptr<typename ServerBase<socket_type>::Request>, const std::error_code&)> on_error;
std::function<void(std::shared_ptr<socket_type> socket, std::shared_ptr<typename ServerBase<socket_type>::Request>)> on_upgrade;
private:
/// Warning: do not add or remove resources after start() is called
std::map<regex_orderable, std::map<std::string, std::tuple<path2regex::Keys, http_handler>>> m_resource;
std::map<std::string, http_handler> m_default_resource;
std::mutex m_resource_mutex;
public:
virtual void start() {
if(!m_io_context)
m_io_context=std::make_shared<asio::io_context>();
if(m_io_context->stopped())
m_io_context.reset();
asio::ip::tcp::endpoint endpoint;
if(m_config.address.size()>0)
endpoint=asio::ip::tcp::endpoint(asio::ip::make_address(m_config.address), m_config.port);
else
endpoint=asio::ip::tcp::endpoint(asio::ip::tcp::v4(), m_config.port);
if(!acceptor)
acceptor= std::make_unique<asio::ip::tcp::acceptor>(*m_io_context);
acceptor->open(endpoint.protocol());
acceptor->set_option(asio::socket_base::reuse_address(m_config.reuse_address));
acceptor->bind(endpoint);
acceptor->listen();
accept();
if (!m_external_context)
m_io_context->run();
}
void stop() const
{
acceptor->close();
if (!m_external_context)
m_io_context->stop();
}
///Use this function if you need to recursively send parts of a longer message
void send(const std::shared_ptr<Response> &response, const std::function<void(const std::error_code&)>& callback=nullptr) const {
asio::async_write(*response->socket(), response->m_streambuf, [this, response, callback](const std::error_code& ec, size_t /*bytes_transferred*/) {
if(callback)
callback(ec);
});
}
void set_io_context(std::shared_ptr<asio::io_context> new_io_context)
{
m_io_context = new_io_context;
m_external_context = true;
}
protected:
std::shared_ptr<asio::io_context> m_io_context;
bool m_external_context;
std::unique_ptr<asio::ip::tcp::acceptor> acceptor;
std::vector<std::thread> threads;
ServerBase(unsigned short port) : m_config(port), m_external_context(false) {}
virtual void accept()=0;
std::shared_ptr<asio::system_timer> get_timeout_timer(const std::shared_ptr<socket_type> &socket, long seconds) {
if(seconds==0)
return nullptr;
auto timer = std::make_shared<asio::system_timer>(*m_io_context);
timer->expires_at(std::chrono::system_clock::now() + std::chrono::seconds(seconds));
timer->async_wait([socket](const std::error_code& ec){
if(!ec) {
std::error_code newec = ec;
socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, newec);
socket->lowest_layer().close();
}
});
return timer;
}
void read_request_and_content(const std::shared_ptr<socket_type> &socket) {
//Create new streambuf (Request::streambuf) for async_read_until()
//shared_ptr is used to pass temporary objects to the asynchronous functions
std::shared_ptr<Request> request(new Request(*socket));
//Set timeout on the following asio::async-read or write function
auto timer = get_timeout_timer(socket, m_config.timeout_request);
asio::async_read_until(*socket, request->streambuf, "\r\n\r\n",
[this, socket, request, timer](const std::error_code& ec, size_t bytes_transferred) {
if(timer)
timer->cancel();
if(!ec) {
//request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs:
//"After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter"
//The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the
//streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content).
size_t num_additional_bytes=request->streambuf.size()-bytes_transferred;
if (!parse_request(request))
return;
//If content, read that as well
auto it = request->header.find("Content-Length");
if (it != request->header.end()) {
unsigned long long content_length;
try {
content_length = stoull(it->second);
}
catch (const std::exception &) {
if (on_error)
on_error(request, std::error_code(EPROTO, std::generic_category()));
return;
}
if (content_length > num_additional_bytes) {
//Set timeout on the following asio::async-read or write function
auto timer2 = get_timeout_timer(socket, m_config.timeout_content);
asio::async_read(*socket, request->streambuf,
asio::transfer_exactly(size_t(content_length) - num_additional_bytes),
[this, socket, request, timer2]
(const std::error_code& ec, size_t /*bytes_transferred*/) {
if (timer2)
timer2->cancel();
if (!ec)
find_resource(socket, request);
else if (on_error)
on_error(request, ec);
});
}
else {
find_resource(socket, request);
}
}
else {
find_resource(socket, request);
}
}
else if (on_error)
on_error(request, ec);
});
}
bool parse_request(const std::shared_ptr<Request> &request) const {
std::string line;
getline(request->content, line);
size_t method_end;
if((method_end=line.find(' '))!=std::string::npos) {
size_t path_end;
if((path_end=line.find(' ', method_end+1))!=std::string::npos) {
request->method=line.substr(0, method_end);
request->path=line.substr(method_end+1, path_end-method_end-1);
size_t protocol_end;
if((protocol_end=line.find('/', path_end+1))!=std::string::npos) {
if(line.compare(path_end+1, protocol_end-path_end-1, "HTTP")!=0)
return false;
request->http_version=line.substr(protocol_end+1, line.size()-protocol_end-2);
}
else
return false;
getline(request->content, line);
size_t param_end;
while((param_end=line.find(':'))!=std::string::npos) {
size_t value_start=param_end+1;
if((value_start)<line.size()) {
if(line[value_start]==' ')
value_start++;
if(value_start<line.size())
request->header.emplace(line.substr(0, param_end), line.substr(value_start, line.size() - value_start - 1));
}
getline(request->content, line);
}
}
else
return false;
}
else
return false;
return true;
}
void find_resource(const std::shared_ptr<socket_type> &socket, const std::shared_ptr<Request> &request) {
std::lock_guard<std::mutex> lock(m_resource_mutex);
//Upgrade connection
if(on_upgrade) {
auto it=request->header.find("Upgrade");
if(it!=request->header.end()) {
on_upgrade(socket, request);
return;
}
}
//Find path- and method-match, and call write_response
for(auto& regex_method : m_resource) {
auto it = regex_method.second.find(request->method);
if (it != regex_method.second.end()) {
std::smatch sm_res;
if (std::regex_match(request->path, sm_res, regex_method.first)) {
request->keys = std::get<0>(it->second);
for (size_t i = 0; i < request->keys.size(); i++) {
request->params.insert(std::pair<std::string, std::string>(request->keys[i].name, sm_res[i + 1]));
}
write_response(socket, request, std::get<1>(it->second));
return;
}
}
}
auto it=m_default_resource.find(request->method);
if(it!=m_default_resource.end()) {
write_response(socket, request, it->second);
}
}
void write_response(const std::shared_ptr<socket_type> &socket, const std::shared_ptr<Request> &request, http_handler& resource_function) {
//Set timeout on the following asio::async-read or write function
auto timer = get_timeout_timer(socket, m_config.timeout_content);
auto response=std::shared_ptr<Response>(new Response(socket), [this, request, timer](Response *response_ptr) {
auto response=std::shared_ptr<Response>(response_ptr);
send(response, [this, response, request, timer](const std::error_code& ec) {
if (timer)
timer->cancel();
if (!ec) {
float http_version;
try {
http_version = stof(request->http_version);
}
catch (const std::exception &) {
if (on_error)
on_error(request, std::error_code(EPROTO, std::generic_category()));
return;
}
auto range = request->header.equal_range("Connection");
case_insensitive_equals check;
for (auto it = range.first; it != range.second; ++it) {
if (check(it->second, "close"))
return;
}
if (http_version > 1.05)
read_request_and_content(response->socket());
}
else if (on_error)
on_error(request, ec);
});
});
try {
resource_function(response, request);
}
catch(const std::exception &) {
if (on_error)
on_error(request, std::error_code(EPROTO, std::generic_category()));
}
}
};
template<class socket_type>
class Server : public ServerBase<socket_type> {
public:
Server(unsigned short port, size_t num_threads, long timeout_request, long timeout_send_or_receive)
: ServerBase<socket_type>(port, num_threads, timeout_request, timeout_send_or_receive)
{
}
};
using HTTP = asio::ip::tcp::socket;
template<>
class Server<HTTP> : public ServerBase<HTTP> {
public:
Server() : ServerBase<HTTP>::ServerBase(80) {}
protected:
void accept() override {
//Create new socket for this connection
//Shared_ptr is used to pass temporary objects to the asynchronous functions
auto socket = std::make_shared<HTTP>(*m_io_context);
acceptor->async_accept(*socket, [this, socket](const std::error_code& ec){
//Immediately start accepting a new connection (if io_context hasn't been stopped)
if (ec != asio::error::operation_aborted)
accept();
if(!ec) {
asio::ip::tcp::no_delay option(true);
socket->set_option(option);
read_request_and_content(socket);
} else if (on_error)
on_error(std::shared_ptr<Request>(new Request(*socket)), ec);
});
}
};
class http_server : public Server<HTTP> {
public:
http_server() : Server<HTTP>::Server() {}
};
}
#endif /* SERVER_HTTP_HPP */

View 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
View 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 &regex_cstr) : std::regex(path2regex::path_to_regex(regex_cstr, keys)), str(regex_cstr) {}
bool operator<(const regex_orderable &rhs) const {
return str<rhs.str;
}
};
public:
/// Warning: do not add or remove endpoints after start() is called
std::map<regex_orderable, Endpoint> endpoint;
virtual void start() {
if(!io_context)
io_context=std::make_shared<asio::io_context>();
if(io_context->stopped())
io_context->reset();
asio::ip::tcp::endpoint endpoint;
if(config.address.size()>0)
endpoint=asio::ip::tcp::endpoint(asio::ip::address::from_string(config.address), config.port);
else
endpoint=asio::ip::tcp::endpoint(asio::ip::tcp::v4(), config.port);
if(!acceptor)
acceptor= std::make_unique<asio::ip::tcp::acceptor>(*io_context);
acceptor->open(endpoint.protocol());
acceptor->set_option(asio::socket_base::reuse_address(config.reuse_address));
acceptor->bind(endpoint);
acceptor->listen();
accept();
io_context->run();
}
void stop() {
acceptor->close();
io_context->stop();
for(auto& p: endpoint)
p.second.connections.clear();
}
///fin_rsv_opcode: 129=one fragment, text, 130=one fragment, binary, 136=close connection.
///See http://tools.ietf.org/html/rfc6455#section-5.2 for more information
void send(const std::shared_ptr<Connection> &connection, const std::shared_ptr<SendStream> &message_stream,
const std::function<void(const std::error_code&)>& callback=nullptr,
unsigned char fin_rsv_opcode=129) const {
if(fin_rsv_opcode!=136)
timer_idle_reset(connection);
auto header_stream = std::make_shared<SendStream>();
size_t length=message_stream->size();
header_stream->put(fin_rsv_opcode);
//unmasked (first length byte<128)
if(length>=126) {
int num_bytes;
if(length>0xffff) {
num_bytes=8;
header_stream->put(127);
}
else {
num_bytes=2;
header_stream->put(126);
}
for(int c=num_bytes-1;c>=0;c--) {
header_stream->put((static_cast<unsigned long long>(length) >> (8 * c)) % 256);
}
}
else
header_stream->put(static_cast<unsigned char>(length));
connection->strand.post([this, connection, header_stream, message_stream, callback]() {
connection->send_queue.emplace_back(header_stream, message_stream, callback);
if(connection->send_queue.size()==1)
connection->send_from_queue(connection);
});
}
void send_close(const std::shared_ptr<Connection> &connection, int status, const std::string& reason="",
const std::function<void(const std::error_code&)>& callback=nullptr) const {
//Send close only once (in case close is initiated by server)
if(connection->closed)
return;
connection->closed=true;
auto send_stream=std::make_shared<SendStream>();
send_stream->put(status>>8);
send_stream->put(status%256);
*send_stream << reason;
//fin_rsv_opcode=136: message close
send(connection, send_stream, callback, 136);
}
std::unordered_set<std::shared_ptr<Connection> > get_connections() {
std::unordered_set<std::shared_ptr<Connection> > all_connections;
for(auto& e: endpoint) {
std::lock_guard<std::mutex> lock(e.second.connections_mutex);
all_connections.insert(e.second.connections.begin(), e.second.connections.end());
}
return all_connections;
}
/**
* Upgrades a request, from for instance Simple-Web-Server, to a WebSocket connection.
* The parameters are moved to the Connection object.
* See also Server::on_upgrade in the Simple-Web-Server project.
* The socket's io_service is used, thus running start() is not needed.
*
* Example use:
* server.on_upgrade=[&socket_server] (auto socket, auto request) {
* auto connection=std::make_shared<SimpleWeb::SocketServer<SimpleWeb::WS>::Connection>(socket);
* connection->method=std::move(request->method);
* connection->path=std::move(request->path);
* connection->http_version=std::move(request->http_version);
* connection->header=std::move(request->header);
* connection->remote_endpoint_address=std::move(request->remote_endpoint_address);
* connection->remote_endpoint_port=request->remote_endpoint_port;
* socket_server.upgrade(connection);
* }
*/
void upgrade(const std::shared_ptr<Connection> &connection) {
auto read_buffer=std::make_shared<asio::streambuf>();
write_handshake(connection, read_buffer);
}
/// If you have your own asio::io_context, store its pointer here before running start().
/// You might also want to set config.num_threads to 0.
std::shared_ptr<asio::io_context> io_context;
protected:
const std::string ws_magic_string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
std::unique_ptr<asio::ip::tcp::acceptor> acceptor;
std::vector<std::thread> threads;
SocketServerBase(unsigned short port) :
config(port) {}
virtual void accept()=0;
std::shared_ptr<asio::system_timer> get_timeout_timer(const std::shared_ptr<Connection> &connection, size_t seconds) {
if (seconds == 0)
return nullptr;
auto timer = std::make_shared<asio::system_timer>(connection->socket->get_io_service());
timer->expires_at(std::chrono::system_clock::now() + std::chrono::seconds(static_cast<long>(seconds)));
timer->async_wait([connection](const std::error_code& ec){
if(!ec) {
connection->socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both);
connection->socket->lowest_layer().close();
}
});
return timer;
}
void read_handshake(const std::shared_ptr<Connection> &connection) {
connection->read_remote_endpoint_data();
//Create new read_buffer for async_read_until()
//Shared_ptr is used to pass temporary objects to the asynchronous functions
auto read_buffer = std::make_shared<asio::streambuf>();
//Set timeout on the following asio::async-read or write function
auto timer = get_timeout_timer(connection, config.timeout_request);
asio::async_read_until(*connection->socket, *read_buffer, "\r\n\r\n",
[this, connection, read_buffer, timer]
(const std::error_code& ec, size_t /*bytes_transferred*/) {
if(timer)
timer->cancel();
if(!ec) {
//Convert to istream to extract string-lines
std::istream stream(read_buffer.get());
parse_handshake(connection, stream);
write_handshake(connection, read_buffer);
}
});
}
void parse_handshake(const std::shared_ptr<Connection> &connection, std::istream& stream) const {
std::string line;
getline(stream, line);
size_t method_end;
if((method_end=line.find(' '))!=std::string::npos) {
size_t path_end;
if((path_end=line.find(' ', method_end+1))!=std::string::npos) {
connection->method=line.substr(0, method_end);
connection->path=line.substr(method_end+1, path_end-method_end-1);
if((path_end+6)<line.size())
connection->http_version=line.substr(path_end+6, line.size()-(path_end+6)-1);
else
connection->http_version="1.1";
getline(stream, line);
size_t param_end;
while((param_end=line.find(':'))!=std::string::npos) {
size_t value_start=param_end+1;
if((value_start)<line.size()) {
if(line[value_start]==' ')
value_start++;
if(value_start<line.size())
connection->header.emplace(line.substr(0, param_end), line.substr(value_start, line.size()-value_start-1));
}
getline(stream, line);
}
}
}
}
void write_handshake(const std::shared_ptr<Connection> &connection, const std::shared_ptr<asio::streambuf> &read_buffer) {
//Find path- and method-match, and generate response
for (auto &regex_endpoint : endpoint) {
std::smatch path_match;
if(std::regex_match(connection->path, path_match, regex_endpoint.first)) {
auto write_buffer = std::make_shared<asio::streambuf>();
std::ostream handshake(write_buffer.get());
if(generate_handshake(connection, handshake)) {
connection->path_match=std::move(path_match);
//Capture write_buffer in lambda so it is not destroyed before async_write is finished
asio::async_write(*connection->socket, *write_buffer,
[this, connection, write_buffer, read_buffer, &regex_endpoint]
(const std::error_code& ec, size_t /*bytes_transferred*/) {
if(!ec) {
connection_open(connection, regex_endpoint.second);
read_message(connection, read_buffer, regex_endpoint.second);
}
else
connection_error(connection, regex_endpoint.second, ec);
});
}
return;
}
}
}
bool generate_handshake(const std::shared_ptr<Connection> &connection, std::ostream& handshake) const {
auto header_it = connection->header.find("Sec-WebSocket-Key");
if (header_it == connection->header.end())
return false;
auto sha1=sha1_encode(header_it->second + ws_magic_string);
handshake << "HTTP/1.1 101 Web Socket Protocol Handshake\r\n";
handshake << "Upgrade: websocket\r\n";
handshake << "Connection: Upgrade\r\n";
handshake << "Sec-WebSocket-Accept: " << base64_encode(sha1) << "\r\n";
handshake << "\r\n";
return true;
}
void read_message(const std::shared_ptr<Connection> &connection,
const std::shared_ptr<asio::streambuf> &read_buffer, Endpoint& endpoint) const {
asio::async_read(*connection->socket, *read_buffer, asio::transfer_exactly(2),
[this, connection, read_buffer, &endpoint]
(const std::error_code& ec, size_t bytes_transferred) {
if(!ec) {
if(bytes_transferred==0) { //TODO: why does this happen sometimes?
read_message(connection, read_buffer, endpoint);
return;
}
std::istream stream(read_buffer.get());
std::vector<unsigned char> first_bytes;
first_bytes.resize(2);
stream.read(reinterpret_cast<char*>(&first_bytes[0]), 2);
unsigned char fin_rsv_opcode=first_bytes[0];
//Close connection if unmasked message from client (protocol error)
if(first_bytes[1]<128) {
const std::string reason("message from client not masked");
send_close(connection, 1002, reason, [this, connection](const std::error_code& /*ec*/) {});
connection_close(connection, endpoint, 1002, reason);
return;
}
size_t length=(first_bytes[1]&127);
if(length==126) {
//2 next bytes is the size of content
asio::async_read(*connection->socket, *read_buffer, asio::transfer_exactly(2),
[this, connection, read_buffer, &endpoint, fin_rsv_opcode]
(const std::error_code& ec, size_t /*bytes_transferred*/) {
if(!ec) {
std::istream stream(read_buffer.get());
std::vector<unsigned char> length_bytes;
length_bytes.resize(2);
stream.read(reinterpret_cast<char*>(&length_bytes[0]), 2);
size_t length=0;
int num_bytes=2;
for(int c=0;c<num_bytes;c++)
length+=length_bytes[c]<<(8*(num_bytes-1-c));
read_message_content(connection, read_buffer, length, endpoint, fin_rsv_opcode);
}
else
connection_error(connection, endpoint, ec);
});
}
else if(length==127) {
//8 next bytes is the size of content
asio::async_read(*connection->socket, *read_buffer, asio::transfer_exactly(8),
[this, connection, read_buffer, &endpoint, fin_rsv_opcode]
(const std::error_code& ec, size_t /*bytes_transferred*/) {
if(!ec) {
std::istream stream(read_buffer.get());
std::vector<unsigned char> length_bytes;
length_bytes.resize(8);
stream.read(reinterpret_cast<char*>(&length_bytes[0]), 8);
size_t length=0;
int num_bytes=8;
for(int c=0;c<num_bytes;c++)
length+=length_bytes[c]<<(8*(num_bytes-1-c));
read_message_content(connection, read_buffer, length, endpoint, fin_rsv_opcode);
}
else
connection_error(connection, endpoint, ec);
});
}
else
read_message_content(connection, read_buffer, length, endpoint, fin_rsv_opcode);
}
else
connection_error(connection, endpoint, ec);
});
}
void read_message_content(const std::shared_ptr<Connection> &connection, const std::shared_ptr<asio::streambuf> &read_buffer,
size_t length, Endpoint& endpoint, unsigned char fin_rsv_opcode) const {
asio::async_read(*connection->socket, *read_buffer, asio::transfer_exactly(4+length),
[this, connection, read_buffer, length, &endpoint, fin_rsv_opcode]
(const std::error_code& ec, size_t /*bytes_transferred*/) {
if(!ec) {
std::istream raw_message_data(read_buffer.get());
//Read mask
std::vector<unsigned char> mask;
mask.resize(4);
raw_message_data.read(reinterpret_cast<char*>(&mask[0]), 4);
std::shared_ptr<Message> message(new Message());
message->length=length;
message->fin_rsv_opcode=fin_rsv_opcode;
std::ostream message_data_out_stream(&message->streambuf);
for(size_t c=0;c<length;c++) {
message_data_out_stream.put(raw_message_data.get()^mask[c%4]);
}
//If connection close
if((fin_rsv_opcode&0x0f)==8) {
int status=0;
if(length>=2) {
unsigned char byte1=message->get();
unsigned char byte2=message->get();
status=(byte1<<8)+byte2;
}
auto reason=message->string();
send_close(connection, status, reason, [this, connection](const std::error_code& /*ec*/) {});
connection_close(connection, endpoint, status, reason);
return;
}
else {
//If ping
if((fin_rsv_opcode&0x0f)==9) {
//send pong
auto empty_send_stream=std::make_shared<SendStream>();
send(connection, empty_send_stream, nullptr, fin_rsv_opcode+1);
}
else if(endpoint.on_message) {
timer_idle_reset(connection);
endpoint.on_message(connection, message);
}
//Next message
read_message(connection, read_buffer, endpoint);
}
}
else
connection_error(connection, endpoint, ec);
});
}
void connection_open(const std::shared_ptr<Connection> &connection, Endpoint& endpoint) {
timer_idle_init(connection);
{
std::lock_guard<std::mutex> lock(endpoint.connections_mutex);
endpoint.connections.insert(connection);
}
if(endpoint.on_open)
endpoint.on_open(connection);
}
void connection_close(const std::shared_ptr<Connection> &connection, Endpoint& endpoint, int status, const std::string& reason) const {
timer_idle_cancel(connection);
{
std::lock_guard<std::mutex> lock(endpoint.connections_mutex);
endpoint.connections.erase(connection);
}
if(endpoint.on_close)
endpoint.on_close(connection, status, reason);
}
void connection_error(const std::shared_ptr<Connection> &connection, Endpoint& endpoint, const std::error_code& ec) const {
timer_idle_cancel(connection);
{
std::lock_guard<std::mutex> lock(endpoint.connections_mutex);
endpoint.connections.erase(connection);
}
if(endpoint.on_error) {
std::error_code ec_tmp=ec;
endpoint.on_error(connection, ec_tmp);
}
}
void timer_idle_init(const std::shared_ptr<Connection> &connection) {
if(config.timeout_idle>0) {
connection->timer_idle= std::make_unique<asio::system_timer>(connection->socket->get_io_service());
connection->timer_idle->expires_from_now(std::chrono::seconds(static_cast<unsigned long>(config.timeout_idle)));
timer_idle_expired_function(connection);
}
}
void timer_idle_reset(const std::shared_ptr<Connection> &connection) const {
if(config.timeout_idle>0 && connection->timer_idle->expires_from_now(std::chrono::seconds(static_cast<unsigned long>(config.timeout_idle)))>0)
timer_idle_expired_function(connection);
}
void timer_idle_cancel(const std::shared_ptr<Connection> &connection) const {
if(config.timeout_idle>0)
connection->timer_idle->cancel();
}
void timer_idle_expired_function(const std::shared_ptr<Connection> &connection) const {
connection->timer_idle->async_wait([this, connection](const std::error_code& ec){
if(!ec)
send_close(connection, 1000, "idle timeout"); //1000=normal closure
});
}
};
template<class socket_type>
class SocketServer : public SocketServerBase<socket_type> {
public:
SocketServer(unsigned short port, size_t timeout_request, size_t timeout_idle)
: SocketServerBase<socket_type>(port, timeout_request, timeout_idle)
{
}
};
using WS = asio::ip::tcp::socket;
template<>
class SocketServer<WS> : public SocketServerBase<WS> {
public:
SocketServer() : SocketServerBase<WS>(80) {}
protected:
void accept() override {
//Create new socket for this connection (stored in Connection::socket)
//Shared_ptr is used to pass temporary objects to the asynchronous functions
std::shared_ptr<Connection> connection(new Connection(new WS(*io_context)));
acceptor->async_accept(*connection->socket, [this, connection](const std::error_code& ec) {
//Immediately start accepting a new connection (if io_context hasn't been stopped)
if (ec != asio::error::operation_aborted)
accept();
if(!ec) {
asio::ip::tcp::no_delay option(true);
connection->socket->set_option(option);
read_handshake(connection);
}
});
}
};
}
#endif /* SERVER_WS_HPP */

View 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
View 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
View 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
View 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)