summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRoland Reichwein <mail@reichwein.it>2020-04-04 16:32:10 +0200
committerRoland Reichwein <mail@reichwein.it>2020-04-04 16:32:10 +0200
commit938fbe7a2f2f10a3abb530a9463e57fc20f40038 (patch)
tree62ee0c285c672b10a42b0690a011ede7a0bf00b6
parent95d5acc8c7e60255b19e7084e374eb26cc5d0ba3 (diff)
HTTP and HTTPs
-rw-r--r--Makefile9
-rw-r--r--TODO6
-rw-r--r--config.cpp30
-rw-r--r--config.h11
-rw-r--r--file.cpp46
-rw-r--r--file.h15
-rw-r--r--http.cpp141
-rw-r--r--http.h7
-rw-r--r--https.cpp558
-rw-r--r--https.h9
-rw-r--r--server.cpp10
-rw-r--r--server.h9
-rw-r--r--server_certificate.h69
-rw-r--r--webserver.conf13
-rw-r--r--webserver.cpp4
15 files changed, 749 insertions, 188 deletions
diff --git a/Makefile b/Makefile
index 65c3ada..ed35360 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ CXX=clang++
endif
ifeq ($(shell which $(CXX)),)
-#CXX=g++-9
+CXX=g++-9
endif
ifeq ($(CXXFLAGS),)
@@ -19,7 +19,7 @@ endif
# -fprofile-instr-generate -fcoverage-mapping
# gcc:--coverage
-CXXFLAGS+= -Wall -I.
+CXXFLAGS+= -Wall -I. -DVERSION=\"$(VERSION)\"
CXXFLAGS+= -pthread
ifeq ($(CXX),clang++-10)
@@ -58,9 +58,12 @@ endif
PROGSRC=\
config.cpp \
+ file.cpp \
http.cpp \
+ https.cpp \
http_debian10.cpp \
- plugin.cpp
+ plugin.cpp \
+ server.cpp
TESTSRC=\
test-webserver.cpp \
diff --git a/TODO b/TODO
index 575d33b..737d2f3 100644
--- a/TODO
+++ b/TODO
@@ -1,3 +1,7 @@
-Plugin: https://www.boost.org/doc/libs/1_72_0/doc/html/boost_dll/tutorial.html#boost_dll.tutorial.symbol_shadowing_problem__linux_
HTTP+HTTPS: https://www.boost.org/doc/libs/1_72_0/libs/beast/doc/html/beast/examples.html#beast.examples.servers
Certbot: https://certbot.eff.org/lets-encrypt/debianbuster-other
+Webbox
+Debian 10
+alternative hosts www, lists, ...
+
+Selective sites per Socket
diff --git a/config.cpp b/config.cpp
index d28a3c1..17376d0 100644
--- a/config.cpp
+++ b/config.cpp
@@ -20,8 +20,11 @@ void Config::readConfigfile(std::string filename)
// mandatory
m_user = tree.get<std::string>("webserver.user");
+
m_group = tree.get<std::string>("webserver.group");
+ m_threads = tree.get<int>("webserver.threads");
+
// optional entries
auto elements = tree.get_child_optional("webserver");
if (elements) {
@@ -71,11 +74,15 @@ void Config::readConfigfile(std::string filename)
socket_struct.port = x.second.data();
} else if (x.first == "protocol"s) {
if (x.second.data() == "http"s)
- socket_struct.protocol = HTTP;
+ socket_struct.protocol = SocketProtocol::HTTP;
else if (x.second.data() == "https"s)
- socket_struct.protocol = HTTPS;
+ socket_struct.protocol = SocketProtocol::HTTPS;
else
throw std::runtime_error("Unknown protocol: "s + x.second.data());
+ } else if (x.first == "certpath"s) {
+ socket_struct.cert_path = x.second.data();
+ } else if (x.first == "keypath"s) {
+ socket_struct.key_path = x.second.data();
} else
throw std::runtime_error("Unknown element: "s + x.first);
}
@@ -102,6 +109,11 @@ std::string Config::Group() const
return m_group;
}
+int Config::Threads() const
+{
+ return m_threads;
+}
+
const std::vector<std::string>& Config::PluginDirectories() const
{
return m_plugin_directories;
@@ -122,6 +134,8 @@ void Config::dump() const
std::cout << "=== Configuration ===========================" << std::endl;
std::cout << "User: " << m_user << std::endl;
std::cout << "Group: " << m_user << std::endl;
+
+ std::cout << "Threads: " << m_threads << std::endl;
std::cout << "Plugin Directories:";
for (const auto& dir: m_plugin_directories)
@@ -131,17 +145,21 @@ void Config::dump() const
for (const auto& site: m_sites) {
std::cout << "Site: " << site.name << ": " << site.host << std::endl;
if (site.paths.size() == 0)
- std::cout << " Warning: No paths configured." << std::endl;
+ std::cout << " Warning: No paths configured." << std::endl;
for (const auto& path: site.paths) {
- std::cout << " Path: " << path.requested << " -> " << ((path.type == Files) ? "files" : "plugin") << std::endl;
+ std::cout << " Path: " << path.requested << " -> " << ((path.type == Files) ? "files" : "plugin") << std::endl;
for (const auto& param: path.params) {
- std::cout << " " << param.first << ": " << param.second << std::endl;
+ std::cout << " " << param.first << ": " << param.second << std::endl;
}
}
}
for (const auto& socket: m_sockets) {
- std::cout << "Socket: " << socket.address << ":" << socket.port << " (" << (socket.protocol == HTTP ? "HTTP" : "HTTPS") << ")" << std::endl;
+ std::cout << "Socket: " << socket.address << ":" << socket.port << " (" << (socket.protocol == SocketProtocol::HTTP ? "HTTP" : "HTTPS") << ")" << std::endl;
+ if (socket.protocol == SocketProtocol::HTTPS) {
+ std::cout << " Key: " << socket.key_path.generic_string() << std::endl;
+ std::cout << " Cert: " << socket.cert_path.generic_string() << std::endl;
+ }
}
std::cout << "=============================================" << std::endl;
}
diff --git a/config.h b/config.h
index 4838ea6..98a99c0 100644
--- a/config.h
+++ b/config.h
@@ -1,9 +1,12 @@
#pragma once
+#include <filesystem>
#include <string>
#include <unordered_map>
#include <vector>
+namespace fs = std::filesystem;
+
enum PathType
{
Files, // serve files
@@ -24,7 +27,7 @@ struct Site
std::vector<Path> paths;
};
-enum SocketProtocol
+enum class SocketProtocol
{
HTTP,
HTTPS
@@ -35,6 +38,9 @@ struct Socket
std::string address;
std::string port;
SocketProtocol protocol;
+ std::vector<std::string> serve_sites; // if empty, serve all configured sites // TODO: implement
+ fs::path cert_path;
+ fs::path key_path;
};
class Config
@@ -45,6 +51,7 @@ class Config
std::string m_user;
std::string m_group;
+ int m_threads;
std::vector<std::string> m_plugin_directories;
std::vector<Site> m_sites;
std::vector<Socket> m_sockets;
@@ -56,6 +63,8 @@ class Config
std::string User() const;
std::string Group() const;
+ int Threads() const;
+
const std::vector<std::string>& PluginDirectories() const;
const std::vector<Site>& Sites() const;
const std::vector<Socket>& Sockets() const;
diff --git a/file.cpp b/file.cpp
new file mode 100644
index 0000000..47ab8be
--- /dev/null
+++ b/file.cpp
@@ -0,0 +1,46 @@
+#include "file.h"
+
+#include <fstream>
+
+namespace fs = std::filesystem;
+
+using namespace std::string_literals;
+
+std::string File::getFile(const fs::path& filename)
+{
+ std::ifstream file(filename.string(), std::ios::in | std::ios::binary | std::ios::ate);
+
+ if (file.is_open()) {
+ std::ifstream::pos_type fileSize = file.tellg();
+ file.seekg(0, std::ios::beg);
+
+ std::string bytes(fileSize, ' ');
+ file.read(reinterpret_cast<char*>(bytes.data()), fileSize);
+
+ return bytes;
+
+ } else {
+ throw std::runtime_error("Opening "s + filename.string() + " for reading");
+ }
+}
+
+void File::setFile(const fs::path& filename, const std::string& s)
+{
+ File::setFile(filename, s.data(), s.size());
+}
+
+void File::setFile(const fs::path& filename, const char* data, size_t size)
+{
+ std::ofstream file(filename.string(), std::ios::out | std::ios::binary);
+ if (file.is_open()) {
+ file.write(data, size);
+ } else {
+ throw std::runtime_error("Opening "s + filename.string() + " for writing");
+ }
+}
+
+void File::setFile(const fs::path& filename, const std::vector<uint8_t>& data)
+{
+ File::setFile(filename, reinterpret_cast<const char*>(data.data()), data.size());
+}
+
diff --git a/file.h b/file.h
new file mode 100644
index 0000000..e7e4cf6
--- /dev/null
+++ b/file.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <cstdint>
+#include <filesystem>
+#include <string>
+#include <vector>
+
+namespace File {
+
+std::string getFile(const std::filesystem::path& filename);
+void setFile(const std::filesystem::path& filename, const std::string& s);
+void setFile(const std::filesystem::path& filename, const char* data, size_t size);
+void setFile(const std::filesystem::path& filename, const std::vector<uint8_t>& data);
+
+}
diff --git a/http.cpp b/http.cpp
index 406b384..82e1a39 100644
--- a/http.cpp
+++ b/http.cpp
@@ -1,13 +1,11 @@
-#include <boost/beast/version.hpp>
-
-#if BOOST_VERSION == 107100
+#include "http.h"
-#include "server_certificate.h"
+#include "server.h"
+#include <boost/beast/version.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
-#include <boost/beast/ssl.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/strand.hpp>
#include <boost/config.hpp>
@@ -23,9 +21,10 @@
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
-namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
+namespace {
+
// Return a reasonable mime type based on the extension of a file.
beast::string_view
mime_type(beast::string_view path)
@@ -107,7 +106,7 @@ handle_request(
[&req](beast::string_view why)
{
http::response<http::string_body> res{http::status::bad_request, req.version()};
- res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
+ res.set(http::field::server, VersionString);
res.set(http::field::content_type, "text/html");
res.keep_alive(req.keep_alive());
res.body() = std::string(why);
@@ -120,7 +119,7 @@ handle_request(
[&req](beast::string_view target)
{
http::response<http::string_body> res{http::status::not_found, req.version()};
- res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
+ res.set(http::field::server, VersionString);
res.set(http::field::content_type, "text/html");
res.keep_alive(req.keep_alive());
res.body() = "The resource '" + std::string(target) + "' was not found.";
@@ -133,7 +132,7 @@ handle_request(
[&req](beast::string_view what)
{
http::response<http::string_body> res{http::status::internal_server_error, req.version()};
- res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
+ res.set(http::field::server, VersionString);
res.set(http::field::content_type, "text/html");
res.keep_alive(req.keep_alive());
res.body() = "An error occurred: '" + std::string(what) + "'";
@@ -177,7 +176,7 @@ handle_request(
if(req.method() == http::verb::head)
{
http::response<http::empty_body> res{http::status::ok, req.version()};
- res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
+ res.set(http::field::server, VersionString);
res.set(http::field::content_type, mime_type(path));
res.content_length(size);
res.keep_alive(req.keep_alive());
@@ -189,7 +188,7 @@ handle_request(
std::piecewise_construct,
std::make_tuple(std::move(body)),
std::make_tuple(http::status::ok, req.version())};
- res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
+ res.set(http::field::server, VersionString);
res.set(http::field::content_type, mime_type(path));
res.content_length(size);
res.keep_alive(req.keep_alive());
@@ -202,26 +201,6 @@ handle_request(
void
fail(beast::error_code ec, char const* what)
{
- // ssl::error::stream_truncated, also known as an SSL "short read",
- // indicates the peer closed the connection without performing the
- // required closing handshake (for example, Google does this to
- // improve performance). Generally this can be a security issue,
- // but if your communication protocol is self-terminated (as
- // it is with both HTTP and WebSocket) then you may simply
- // ignore the lack of close_notify.
- //
- // https://github.com/boostorg/beast/issues/38
- //
- // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
- //
- // When a short read would cut off the end of an HTTP message,
- // Beast returns the error beast::http::error::partial_message.
- // Therefore, if we see a short read here, it has occurred
- // after the message has been completed, so it is safe to ignore it.
-
- if(ec == net::ssl::error::stream_truncated)
- return;
-
std::cerr << what << ": " << ec.message() << "\n";
}
@@ -265,7 +244,7 @@ class session : public std::enable_shared_from_this<session>
}
};
- beast::ssl_stream<beast::tcp_stream> stream_;
+ beast::tcp_stream stream_;
beast::flat_buffer buffer_;
std::shared_ptr<std::string const> doc_root_;
http::request<http::string_body> req_;
@@ -273,13 +252,11 @@ class session : public std::enable_shared_from_this<session>
send_lambda lambda_;
public:
- // Take ownership of the socket
- explicit
+ // Take ownership of the stream
session(
tcp::socket&& socket,
- ssl::context& ctx,
std::shared_ptr<std::string const> const& doc_root)
- : stream_(std::move(socket), ctx)
+ : stream_(std::move(socket))
, doc_root_(doc_root)
, lambda_(*this)
{
@@ -293,35 +270,10 @@ public:
// on the I/O objects in this session. Although not strictly necessary
// for single-threaded contexts, this example code is written to be
// thread-safe by default.
- net::dispatch(
- stream_.get_executor(),
- beast::bind_front_handler(
- &session::on_run,
- shared_from_this()));
- }
-
- void
- on_run()
- {
- // Set the timeout.
- beast::get_lowest_layer(stream_).expires_after(
- std::chrono::seconds(30));
-
- // Perform the SSL handshake
- stream_.async_handshake(
- ssl::stream_base::server,
- beast::bind_front_handler(
- &session::on_handshake,
- shared_from_this()));
- }
-
- void
- on_handshake(beast::error_code ec)
- {
- if(ec)
- return fail(ec, "handshake");
-
- do_read();
+ net::dispatch(stream_.get_executor(),
+ beast::bind_front_handler(
+ &session::do_read,
+ shared_from_this()));
}
void
@@ -332,7 +284,7 @@ public:
req_ = {};
// Set the timeout.
- beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
+ stream_.expires_after(std::chrono::seconds(30));
// Read a request
http::async_read(stream_, buffer_, req_,
@@ -387,21 +339,9 @@ public:
void
do_close()
{
- // Set the timeout.
- beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
-
- // Perform the SSL shutdown
- stream_.async_shutdown(
- beast::bind_front_handler(
- &session::on_shutdown,
- shared_from_this()));
- }
-
- void
- on_shutdown(beast::error_code ec)
- {
- if(ec)
- return fail(ec, "shutdown");
+ // Send a TCP shutdown
+ beast::error_code ec;
+ stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
// At this point the connection is closed gracefully
}
@@ -413,19 +353,16 @@ public:
class listener : public std::enable_shared_from_this<listener>
{
net::io_context& ioc_;
- ssl::context& ctx_;
tcp::acceptor acceptor_;
std::shared_ptr<std::string const> doc_root_;
public:
listener(
net::io_context& ioc,
- ssl::context& ctx,
tcp::endpoint endpoint,
std::shared_ptr<std::string const> const& doc_root)
: ioc_(ioc)
- , ctx_(ctx)
- , acceptor_(ioc)
+ , acceptor_(net::make_strand(ioc))
, doc_root_(doc_root)
{
beast::error_code ec;
@@ -495,7 +432,6 @@ private:
// Create the session and run it
std::make_shared<session>(
std::move(socket),
- ctx_,
doc_root_)->run();
}
@@ -504,37 +440,26 @@ private:
}
};
+} // anonymous namespace
+
//------------------------------------------------------------------------------
-int http_server(int argc, char* argv[])
+namespace HTTP {
+
+int server(Config& config)
{
- // Check command line arguments.
- if (argc != 5)
- {
- std::cerr <<
- "Usage: http-server-async-ssl <address> <port> <doc_root> <threads>\n" <<
- "Example:\n" <<
- " http-server-async-ssl 0.0.0.0 8080 . 1\n";
- return EXIT_FAILURE;
- }
- auto const address = net::ip::make_address(argv[1]);
- auto const port = static_cast<unsigned short>(std::atoi(argv[2]));
- auto const doc_root = std::make_shared<std::string>(argv[3]);
- auto const threads = std::max<int>(1, std::atoi(argv[4]));
+ // TODO: Config
+ auto const address = net::ip::make_address(config.Sockets()[0].address);
+ auto const port = static_cast<unsigned short>(std::atoi(config.Sockets()[0].port.data()));
+ auto const doc_root = std::make_shared<std::string>(config.Sites()[0].paths[0].params.at("target"));
+ auto const threads = std::max<int>(1, config.Threads());
// The io_context is required for all I/O
net::io_context ioc{threads};
- // The SSL context is required, and holds certificates
- ssl::context ctx{ssl::context::tlsv12};
-
- // This holds the self-signed certificate used by the server
- load_server_certificate(ctx);
-
// Create and launch a listening port
std::make_shared<listener>(
ioc,
- ctx,
tcp::endpoint{address, port},
doc_root)->run();
@@ -552,4 +477,4 @@ int http_server(int argc, char* argv[])
return EXIT_SUCCESS;
}
-#endif
+} // namespace HTTP
diff --git a/http.h b/http.h
index 59d30e4..229d48e 100644
--- a/http.h
+++ b/http.h
@@ -1,4 +1,9 @@
#pragma once
-int http_server(int argc, char* argv[]);
+#include "config.h"
+namespace HTTP {
+
+int server(Config& config);
+
+} // namespace HTTP
diff --git a/https.cpp b/https.cpp
new file mode 100644
index 0000000..0c3b97b
--- /dev/null
+++ b/https.cpp
@@ -0,0 +1,558 @@
+#include "https.h"
+
+#include "server.h"
+
+#include <boost/beast/version.hpp>
+
+#if BOOST_VERSION == 107100
+
+#include "server_certificate.h"
+
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/version.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/asio/dispatch.hpp>
+#include <boost/asio/strand.hpp>
+#include <boost/config.hpp>
+#include <algorithm>
+#include <cstdlib>
+#include <functional>
+#include <iostream>
+#include <memory>
+#include <string>
+#include <thread>
+#include <vector>
+
+namespace beast = boost::beast; // from <boost/beast.hpp>
+namespace http = beast::http; // from <boost/beast/http.hpp>
+namespace net = boost::asio; // from <boost/asio.hpp>
+namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
+using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
+
+namespace {
+
+// Return a reasonable mime type based on the extension of a file.
+beast::string_view
+mime_type(beast::string_view path)
+{
+ using beast::iequals;
+ auto const ext = [&path]
+ {
+ auto const pos = path.rfind(".");
+ if(pos == beast::string_view::npos)
+ return beast::string_view{};
+ return path.substr(pos);
+ }();
+ if(iequals(ext, ".htm")) return "text/html";
+ if(iequals(ext, ".html")) return "text/html";
+ if(iequals(ext, ".php")) return "text/html";
+ if(iequals(ext, ".css")) return "text/css";
+ if(iequals(ext, ".txt")) return "text/plain";
+ if(iequals(ext, ".js")) return "application/javascript";
+ if(iequals(ext, ".json")) return "application/json";
+ if(iequals(ext, ".xml")) return "application/xml";
+ if(iequals(ext, ".swf")) return "application/x-shockwave-flash";
+ if(iequals(ext, ".flv")) return "video/x-flv";
+ if(iequals(ext, ".png")) return "image/png";
+ if(iequals(ext, ".jpe")) return "image/jpeg";
+ if(iequals(ext, ".jpeg")) return "image/jpeg";
+ if(iequals(ext, ".jpg")) return "image/jpeg";
+ if(iequals(ext, ".gif")) return "image/gif";
+ if(iequals(ext, ".bmp")) return "image/bmp";
+ if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon";
+ if(iequals(ext, ".tiff")) return "image/tiff";
+ if(iequals(ext, ".tif")) return "image/tiff";
+ if(iequals(ext, ".svg")) return "image/svg+xml";
+ if(iequals(ext, ".svgz")) return "image/svg+xml";
+ return "application/text";
+}
+
+// Append an HTTP rel-path to a local filesystem path.
+// The returned path is normalized for the platform.
+std::string
+path_cat(
+ beast::string_view base,
+ beast::string_view path)
+{
+ if(base.empty())
+ return std::string(path);
+ std::string result(base);
+#ifdef BOOST_MSVC
+ char constexpr path_separator = '\\';
+ if(result.back() == path_separator)
+ result.resize(result.size() - 1);
+ result.append(path.data(), path.size());
+ for(auto& c : result)
+ if(c == '/')
+ c = path_separator;
+#else
+ char constexpr path_separator = '/';
+ if(result.back() == path_separator)
+ result.resize(result.size() - 1);
+ result.append(path.data(), path.size());
+#endif
+ return result;
+}
+
+// This function produces an HTTP response for the given
+// request. The type of the response object depends on the
+// contents of the request, so the interface requires the
+// caller to pass a generic lambda for receiving the response.
+template<
+ class Body, class Allocator,
+ class Send>
+void
+handle_request(
+ beast::string_view doc_root,
+ http::request<Body, http::basic_fields<Allocator>>&& req,
+ Send&& send)
+{
+ // Returns a bad request response
+ auto const bad_request =
+ [&req](beast::string_view why)
+ {
+ http::response<http::string_body> res{http::status::bad_request, req.version()};
+ res.set(http::field::server, VersionString);
+ res.set(http::field::content_type, "text/html");
+ res.keep_alive(req.keep_alive());
+ res.body() = std::string(why);
+ res.prepare_payload();
+ return res;
+ };
+
+ // Returns a not found response
+ auto const not_found =
+ [&req](beast::string_view target)
+ {
+ http::response<http::string_body> res{http::status::not_found, req.version()};
+ res.set(http::field::server, VersionString);
+ res.set(http::field::content_type, "text/html");
+ res.keep_alive(req.keep_alive());
+ res.body() = "The resource '" + std::string(target) + "' was not found.";
+ res.prepare_payload();
+ return res;
+ };
+
+ // Returns a server error response
+ auto const server_error =
+ [&req](beast::string_view what)
+ {
+ http::response<http::string_body> res{http::status::internal_server_error, req.version()};
+ res.set(http::field::server, VersionString);
+ res.set(http::field::content_type, "text/html");
+ res.keep_alive(req.keep_alive());
+ res.body() = "An error occurred: '" + std::string(what) + "'";
+ res.prepare_payload();
+ return res;
+ };
+
+ // Make sure we can handle the method
+ if( req.method() != http::verb::get &&
+ req.method() != http::verb::head)
+ return send(bad_request("Unknown HTTP-method"));
+
+ // Request path must be absolute and not contain "..".
+ if( req.target().empty() ||
+ req.target()[0] != '/' ||
+ req.target().find("..") != beast::string_view::npos)
+ return send(bad_request("Illegal request-target"));
+
+ // Build the path to the requested file
+ std::string path = path_cat(doc_root, req.target());
+ if(req.target().back() == '/')
+ path.append("index.html");
+
+ // Attempt to open the file
+ beast::error_code ec;
+ http::file_body::value_type body;
+ body.open(path.c_str(), beast::file_mode::scan, ec);
+
+ // Handle the case where the file doesn't exist
+ if(ec == beast::errc::no_such_file_or_directory)
+ return send(not_found(req.target()));
+
+ // Handle an unknown error
+ if(ec)
+ return send(server_error(ec.message()));
+
+ // Cache the size since we need it after the move
+ auto const size = body.size();
+
+ // Respond to HEAD request
+ if(req.method() == http::verb::head)
+ {
+ http::response<http::empty_body> res{http::status::ok, req.version()};
+ res.set(http::field::server, VersionString);
+ res.set(http::field::content_type, mime_type(path));
+ res.content_length(size);
+ res.keep_alive(req.keep_alive());
+ return send(std::move(res));
+ }
+
+ // Respond to GET request
+ http::response<http::file_body> res{
+ std::piecewise_construct,
+ std::make_tuple(std::move(body)),
+ std::make_tuple(http::status::ok, req.version())};
+ res.set(http::field::server, VersionString);
+ res.set(http::field::content_type, mime_type(path));
+ res.content_length(size);
+ res.keep_alive(req.keep_alive());
+ return send(std::move(res));
+}
+
+//------------------------------------------------------------------------------
+
+// Report a failure
+void
+fail(beast::error_code ec, char const* what)
+{
+ // ssl::error::stream_truncated, also known as an SSL "short read",
+ // indicates the peer closed the connection without performing the
+ // required closing handshake (for example, Google does this to
+ // improve performance). Generally this can be a security issue,
+ // but if your communication protocol is self-terminated (as
+ // it is with both HTTP and WebSocket) then you may simply
+ // ignore the lack of close_notify.
+ //
+ // https://github.com/boostorg/beast/issues/38
+ //
+ // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
+ //
+ // When a short read would cut off the end of an HTTP message,
+ // Beast returns the error beast::http::error::partial_message.
+ // Therefore, if we see a short read here, it has occurred
+ // after the message has been completed, so it is safe to ignore it.
+
+ if(ec == net::ssl::error::stream_truncated)
+ return;
+
+ std::cerr << what << ": " << ec.message() << "\n";
+}
+
+// Handles an HTTP server connection
+class session : public std::enable_shared_from_this<session>
+{
+ // This is the C++11 equivalent of a generic lambda.
+ // The function object is used to send an HTTP message.
+ struct send_lambda
+ {
+ session& self_;
+
+ explicit
+ send_lambda(session& self)
+ : self_(self)
+ {
+ }
+
+ template<bool isRequest, class Body, class Fields>
+ void
+ operator()(http::message<isRequest, Body, Fields>&& msg) const
+ {
+ // The lifetime of the message has to extend
+ // for the duration of the async operation so
+ // we use a shared_ptr to manage it.
+ auto sp = std::make_shared<
+ http::message<isRequest, Body, Fields>>(std::move(msg));
+
+ // Store a type-erased version of the shared
+ // pointer in the class to keep it alive.
+ self_.res_ = sp;
+
+ // Write the response
+ http::async_write(
+ self_.stream_,
+ *sp,
+ beast::bind_front_handler(
+ &session::on_write,
+ self_.shared_from_this(),
+ sp->need_eof()));
+ }
+ };
+
+ beast::ssl_stream<beast::tcp_stream> stream_;
+ beast::flat_buffer buffer_;
+ std::shared_ptr<std::string const> doc_root_;
+ http::request<http::string_body> req_;
+ std::shared_ptr<void> res_;
+ send_lambda lambda_;
+
+public:
+ // Take ownership of the socket
+ explicit
+ session(
+ tcp::socket&& socket,
+ ssl::context& ctx,
+ std::shared_ptr<std::string const> const& doc_root)
+ : stream_(std::move(socket), ctx)
+ , doc_root_(doc_root)
+ , lambda_(*this)
+ {
+ }
+
+ // Start the asynchronous operation
+ void
+ run()
+ {
+ // We need to be executing within a strand to perform async operations
+ // on the I/O objects in this session. Although not strictly necessary
+ // for single-threaded contexts, this example code is written to be
+ // thread-safe by default.
+ net::dispatch(
+ stream_.get_executor(),
+ beast::bind_front_handler(
+ &session::on_run,
+ shared_from_this()));
+ }
+
+ void
+ on_run()
+ {
+ // Set the timeout.
+ beast::get_lowest_layer(stream_).expires_after(
+ std::chrono::seconds(30));
+
+ // Perform the SSL handshake
+ stream_.async_handshake(
+ ssl::stream_base::server,
+ beast::bind_front_handler(
+ &session::on_handshake,
+ shared_from_this()));
+ }
+
+ void
+ on_handshake(beast::error_code ec)
+ {
+ if(ec)
+ return fail(ec, "handshake");
+
+ do_read();
+ }
+
+ void
+ do_read()
+ {
+ // Make the request empty before reading,
+ // otherwise the operation behavior is undefined.
+ req_ = {};
+
+ // Set the timeout.
+ beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
+
+ // Read a request
+ http::async_read(stream_, buffer_, req_,
+ beast::bind_front_handler(
+ &session::on_read,
+ shared_from_this()));
+ }
+
+ void
+ on_read(
+ beast::error_code ec,
+ std::size_t bytes_transferred)
+ {
+ boost::ignore_unused(bytes_transferred);
+
+ // This means they closed the connection
+ if(ec == http::error::end_of_stream)
+ return do_close();
+
+ if(ec)
+ return fail(ec, "read");
+
+ // Send the response
+ handle_request(*doc_root_, std::move(req_), lambda_);
+ }
+
+ void
+ on_write(
+ bool close,
+ beast::error_code ec,
+ std::size_t bytes_transferred)
+ {
+ boost::ignore_unused(bytes_transferred);
+
+ if(ec)
+ return fail(ec, "write");
+
+ if(close)
+ {
+ // This means we should close the connection, usually because
+ // the response indicated the "Connection: close" semantic.
+ return do_close();
+ }
+
+ // We're done with the response so delete it
+ res_ = nullptr;
+
+ // Read another request
+ do_read();
+ }
+
+ void
+ do_close()
+ {
+ // Set the timeout.
+ beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
+
+ // Perform the SSL shutdown
+ stream_.async_shutdown(
+ beast::bind_front_handler(
+ &session::on_shutdown,
+ shared_from_this()));
+ }
+
+ void
+ on_shutdown(beast::error_code ec)
+ {
+ if(ec)
+ return fail(ec, "shutdown");
+
+ // At this point the connection is closed gracefully
+ }
+};
+
+//------------------------------------------------------------------------------
+
+// Accepts incoming connections and launches the sessions
+class listener : public std::enable_shared_from_this<listener>
+{
+ net::io_context& ioc_;
+ ssl::context& ctx_;
+ tcp::acceptor acceptor_;
+ std::shared_ptr<std::string const> doc_root_;
+
+public:
+ listener(
+ net::io_context& ioc,
+ ssl::context& ctx,
+ tcp::endpoint endpoint,
+ std::shared_ptr<std::string const> const& doc_root)
+ : ioc_(ioc)
+ , ctx_(ctx)
+ , acceptor_(ioc)
+ , doc_root_(doc_root)
+ {
+ beast::error_code ec;
+
+ // Open the acceptor
+ acceptor_.open(endpoint.protocol(), ec);
+ if(ec)
+ {
+ fail(ec, "open");
+ return;
+ }
+
+ // Allow address reuse
+ acceptor_.set_option(net::socket_base::reuse_address(true), ec);
+ if(ec)
+ {
+ fail(ec, "set_option");
+ return;
+ }
+
+ // Bind to the server address
+ acceptor_.bind(endpoint, ec);
+ if(ec)
+ {
+ fail(ec, "bind");
+ return;
+ }
+
+ // Start listening for connections
+ acceptor_.listen(
+ net::socket_base::max_listen_connections, ec);
+ if(ec)
+ {
+ fail(ec, "listen");
+ return;
+ }
+ }
+
+ // Start accepting incoming connections
+ void
+ run()
+ {
+ do_accept();
+ }
+
+private:
+ void
+ do_accept()
+ {
+ // The new connection gets its own strand
+ acceptor_.async_accept(
+ net::make_strand(ioc_),
+ beast::bind_front_handler(
+ &listener::on_accept,
+ shared_from_this()));
+ }
+
+ void
+ on_accept(beast::error_code ec, tcp::socket socket)
+ {
+ if(ec)
+ {
+ fail(ec, "accept");
+ }
+ else
+ {
+ // Create the session and run it
+ std::make_shared<session>(
+ std::move(socket),
+ ctx_,
+ doc_root_)->run();
+ }
+
+ // Accept another connection
+ do_accept();
+ }
+};
+
+} // anonymous namespace
+//------------------------------------------------------------------------------
+
+namespace HTTPS {
+
+int server(Config& config)
+{
+ // TODO: Config
+ auto const address = net::ip::make_address(config.Sockets()[0].address);
+ auto const port = static_cast<unsigned short>(std::atoi(config.Sockets()[0].port.data()));
+ auto const doc_root = std::make_shared<std::string>(config.Sites()[0].paths[0].params.at("target"));
+ auto const threads = std::max<int>(1, config.Threads());
+
+ // The io_context is required for all I/O
+ net::io_context ioc{threads};
+
+ // The SSL context is required, and holds certificates
+ ssl::context ctx{ssl::context::tlsv13};
+
+ // This holds the self-signed certificate used by the server
+ load_server_certificate(ctx, config.Sockets()[0].cert_path, config.Sockets()[0].key_path); // TODO: config
+
+ // Create and launch a listening port
+ std::make_shared<listener>(
+ ioc,
+ ctx,
+ tcp::endpoint{address, port},
+ doc_root)->run();
+
+ // Run the I/O service on the requested number of threads
+ std::vector<std::thread> v;
+ v.reserve(threads - 1);
+ for(auto i = threads - 1; i > 0; --i)
+ v.emplace_back(
+ [&ioc]
+ {
+ ioc.run();
+ });
+ ioc.run();
+
+ return EXIT_SUCCESS;
+}
+
+} // namespace HTTPS
+#endif
+
diff --git a/https.h b/https.h
new file mode 100644
index 0000000..3621f33
--- /dev/null
+++ b/https.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "config.h"
+
+namespace HTTPS {
+
+int server(Config& config);
+
+}
diff --git a/server.cpp b/server.cpp
new file mode 100644
index 0000000..2ad9bcd
--- /dev/null
+++ b/server.cpp
@@ -0,0 +1,10 @@
+#include "server.h"
+
+#include "http.h"
+#include "https.h"
+
+int server(Config& config)
+{
+ //return HTTP::server(config);
+ return HTTPS::server(config);
+}
diff --git a/server.h b/server.h
new file mode 100644
index 0000000..5ad21ff
--- /dev/null
+++ b/server.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "config.h"
+
+using namespace std::string_literals;
+
+static const std::string VersionString{ "Webserver "s + std::string{VERSION} };
+
+int server(Config& config);
diff --git a/server_certificate.h b/server_certificate.h
index a20110e..1dc12a4 100644
--- a/server_certificate.h
+++ b/server_certificate.h
@@ -1,12 +1,3 @@
-//
-// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com)
-//
-// Distributed under the Boost Software License, Version 1.0. (See accompanying
-// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
-//
-// Official repository: https://github.com/boostorg/beast
-//
-
#ifndef BOOST_BEAST_EXAMPLE_COMMON_SERVER_CERTIFICATE_HPP
#define BOOST_BEAST_EXAMPLE_COMMON_SERVER_CERTIFICATE_HPP
@@ -15,6 +6,9 @@
#include <cstddef>
#include <memory>
+#include "config.h"
+#include "file.h"
+
/* Load a signed certificate into the ssl context, and configure
the context for use with a server.
@@ -26,7 +20,7 @@
*/
inline
void
-load_server_certificate(boost::asio::ssl::context& ctx)
+load_server_certificate(boost::asio::ssl::context& ctx, fs::path cert_path, fs::path key_path)
{
/*
The certificate was generated from CMD.EXE on Windows 10 using:
@@ -35,59 +29,8 @@ load_server_certificate(boost::asio::ssl::context& ctx)
winpty openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 10000 -out cert.pem -subj "//C=US\ST=CA\L=Los Angeles\O=Beast\CN=www.example.com"
*/
- std::string const cert =
- "-----BEGIN CERTIFICATE-----\n"
- "MIIDaDCCAlCgAwIBAgIJAO8vBu8i8exWMA0GCSqGSIb3DQEBCwUAMEkxCzAJBgNV\n"
- "BAYTAlVTMQswCQYDVQQIDAJDQTEtMCsGA1UEBwwkTG9zIEFuZ2VsZXNPPUJlYXN0\n"
- "Q049d3d3LmV4YW1wbGUuY29tMB4XDTE3MDUwMzE4MzkxMloXDTQ0MDkxODE4Mzkx\n"
- "MlowSTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMS0wKwYDVQQHDCRMb3MgQW5n\n"
- "ZWxlc089QmVhc3RDTj13d3cuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA\n"
- "A4IBDwAwggEKAoIBAQDJ7BRKFO8fqmsEXw8v9YOVXyrQVsVbjSSGEs4Vzs4cJgcF\n"
- "xqGitbnLIrOgiJpRAPLy5MNcAXE1strVGfdEf7xMYSZ/4wOrxUyVw/Ltgsft8m7b\n"
- "Fu8TsCzO6XrxpnVtWk506YZ7ToTa5UjHfBi2+pWTxbpN12UhiZNUcrRsqTFW+6fO\n"
- "9d7xm5wlaZG8cMdg0cO1bhkz45JSl3wWKIES7t3EfKePZbNlQ5hPy7Pd5JTmdGBp\n"
- "yY8anC8u4LPbmgW0/U31PH0rRVfGcBbZsAoQw5Tc5dnb6N2GEIbq3ehSfdDHGnrv\n"
- "enu2tOK9Qx6GEzXh3sekZkxcgh+NlIxCNxu//Dk9AgMBAAGjUzBRMB0GA1UdDgQW\n"
- "BBTZh0N9Ne1OD7GBGJYz4PNESHuXezAfBgNVHSMEGDAWgBTZh0N9Ne1OD7GBGJYz\n"
- "4PNESHuXezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCmTJVT\n"
- "LH5Cru1vXtzb3N9dyolcVH82xFVwPewArchgq+CEkajOU9bnzCqvhM4CryBb4cUs\n"
- "gqXWp85hAh55uBOqXb2yyESEleMCJEiVTwm/m26FdONvEGptsiCmF5Gxi0YRtn8N\n"
- "V+KhrQaAyLrLdPYI7TrwAOisq2I1cD0mt+xgwuv/654Rl3IhOMx+fKWKJ9qLAiaE\n"
- "fQyshjlPP9mYVxWOxqctUdQ8UnsUKKGEUcVrA08i1OAnVKlPFjKBvk+r7jpsTPcr\n"
- "9pWXTO9JrYMML7d+XRSZA1n3856OqZDX4403+9FnXCvfcLZLLKTBvwwFgEFGpzjK\n"
- "UEVbkhd5qstF6qWK\n"
- "-----END CERTIFICATE-----\n";
-
- std::string const key =
- "-----BEGIN PRIVATE KEY-----\n"
- "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJ7BRKFO8fqmsE\n"
- "Xw8v9YOVXyrQVsVbjSSGEs4Vzs4cJgcFxqGitbnLIrOgiJpRAPLy5MNcAXE1strV\n"
- "GfdEf7xMYSZ/4wOrxUyVw/Ltgsft8m7bFu8TsCzO6XrxpnVtWk506YZ7ToTa5UjH\n"
- "fBi2+pWTxbpN12UhiZNUcrRsqTFW+6fO9d7xm5wlaZG8cMdg0cO1bhkz45JSl3wW\n"
- "KIES7t3EfKePZbNlQ5hPy7Pd5JTmdGBpyY8anC8u4LPbmgW0/U31PH0rRVfGcBbZ\n"
- "sAoQw5Tc5dnb6N2GEIbq3ehSfdDHGnrvenu2tOK9Qx6GEzXh3sekZkxcgh+NlIxC\n"
- "Nxu//Dk9AgMBAAECggEBAK1gV8uETg4SdfE67f9v/5uyK0DYQH1ro4C7hNiUycTB\n"
- "oiYDd6YOA4m4MiQVJuuGtRR5+IR3eI1zFRMFSJs4UqYChNwqQGys7CVsKpplQOW+\n"
- "1BCqkH2HN/Ix5662Dv3mHJemLCKUON77IJKoq0/xuZ04mc9csykox6grFWB3pjXY\n"
- "OEn9U8pt5KNldWfpfAZ7xu9WfyvthGXlhfwKEetOuHfAQv7FF6s25UIEU6Hmnwp9\n"
- "VmYp2twfMGdztz/gfFjKOGxf92RG+FMSkyAPq/vhyB7oQWxa+vdBn6BSdsfn27Qs\n"
- "bTvXrGe4FYcbuw4WkAKTljZX7TUegkXiwFoSps0jegECgYEA7o5AcRTZVUmmSs8W\n"
- "PUHn89UEuDAMFVk7grG1bg8exLQSpugCykcqXt1WNrqB7x6nB+dbVANWNhSmhgCg\n"
- "VrV941vbx8ketqZ9YInSbGPWIU/tss3r8Yx2Ct3mQpvpGC6iGHzEc/NHJP8Efvh/\n"
- "CcUWmLjLGJYYeP5oNu5cncC3fXUCgYEA2LANATm0A6sFVGe3sSLO9un1brA4zlZE\n"
- "Hjd3KOZnMPt73B426qUOcw5B2wIS8GJsUES0P94pKg83oyzmoUV9vJpJLjHA4qmL\n"
- "CDAd6CjAmE5ea4dFdZwDDS8F9FntJMdPQJA9vq+JaeS+k7ds3+7oiNe+RUIHR1Sz\n"
- "VEAKh3Xw66kCgYB7KO/2Mchesu5qku2tZJhHF4QfP5cNcos511uO3bmJ3ln+16uR\n"
- "GRqz7Vu0V6f7dvzPJM/O2QYqV5D9f9dHzN2YgvU9+QSlUeFK9PyxPv3vJt/WP1//\n"
- "zf+nbpaRbwLxnCnNsKSQJFpnrE166/pSZfFbmZQpNlyeIuJU8czZGQTifQKBgHXe\n"
- "/pQGEZhVNab+bHwdFTxXdDzr+1qyrodJYLaM7uFES9InVXQ6qSuJO+WosSi2QXlA\n"
- "hlSfwwCwGnHXAPYFWSp5Owm34tbpp0mi8wHQ+UNgjhgsE2qwnTBUvgZ3zHpPORtD\n"
- "23KZBkTmO40bIEyIJ1IZGdWO32q79nkEBTY+v/lRAoGBAI1rbouFYPBrTYQ9kcjt\n"
- "1yfu4JF5MvO9JrHQ9tOwkqDmNCWx9xWXbgydsn/eFtuUMULWsG3lNjfst/Esb8ch\n"
- "k5cZd6pdJZa4/vhEwrYYSuEjMCnRb0lUsm7TsHxQrUd6Fi/mUuFU/haC0o0chLq7\n"
- "pVOUFq5mW8p0zbtfHbjkgxyF\n"
- "-----END PRIVATE KEY-----\n";
-
+ std::string const cert = File::getFile(cert_path);
+ std::string const key = File::getFile(key_path);
std::string const dh =
"-----BEGIN DH PARAMETERS-----\n"
"MIIBCAKCAQEArzQc5mpm0Fs8yahDeySj31JZlwEphUdZ9StM2D8+Fo7TMduGtSi+\n"
diff --git a/webserver.conf b/webserver.conf
index 91ea90b..2036f67 100644
--- a/webserver.conf
+++ b/webserver.conf
@@ -1,6 +1,7 @@
<webserver>
<user>www-data</user>
<group>www-data</group>
+ <threads>10</threads>
<!--
<plugin-directory><a c="d">b<e>f</e></a>/usr/lib/webserver/plugins</plugin-directory>
<plugin-directory>/usr/local/lib/webserver/plugins</plugin-directory>
@@ -9,14 +10,16 @@
<sites>
<site>
<name>antcom.de</name>
- <host>antcom.de</host>
+ <host>lists.antcom.de</host>
<path requested="/" type="files">
- <target>/var/www/antcom.de</target>
+ <target>/home/ernie/homepage/test</target>
</path>
+ <!--
<path requested="/webbox" type="plugin">
<plugin>webbox</plugin>
<target>/var/lib/webbox</target>
</path>
+ -->
</site>
<!--
<site>
@@ -31,18 +34,22 @@
<sockets>
<socket>
<address>127.0.0.1</address>
- <port>80</port>
+ <port>8080</port>
<protocol>http</protocol>
+ <certpath>/home/ernie/code/webserver/fullchain.pem</certpath>
+ <keypath>/home/ernie/code/webserver/privkey.pem</keypath>
<!--
<site>antcom.de</site>
<site>reichwein.it</site>
-->
</socket>
+ <!--
<socket>
<address>127.0.0.1</address>
<port>443</port>
<protocol>https</protocol>
</socket>
+ -->
</sockets>
</webserver>
diff --git a/webserver.cpp b/webserver.cpp
index 3f7a2a3..dd06021 100644
--- a/webserver.cpp
+++ b/webserver.cpp
@@ -1,5 +1,5 @@
#include "config.h"
-#include "http.h"
+#include "server.h"
#include "plugin.h"
#include <exception>
@@ -40,7 +40,7 @@ int main(int argc, char* argv[])
if (!plugin_loader.validate_config())
throw std::runtime_error("Couldn't find all configured plugins.");
- return http_server(argc, argv);
+ return server(config);
} catch (const std::exception& ex) {
std::cout << "Error: " << ex.what() << std::endl;
return 1;