summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Makefile3
-rw-r--r--TODO3
-rw-r--r--http.cpp2
-rw-r--r--https.cpp18
-rw-r--r--plugins/websocket/Makefile55
-rw-r--r--plugins/websocket/websocket.cpp80
-rw-r--r--plugins/websocket/websocket.h29
-rw-r--r--response.cpp27
-rw-r--r--response.h7
-rw-r--r--tests/test-response.cpp2
-rw-r--r--tests/test-webserver.cpp168
-rw-r--r--websocket.h15
12 files changed, 341 insertions, 68 deletions
diff --git a/Makefile b/Makefile
index 2803d05..20b1072 100644
--- a/Makefile
+++ b/Makefile
@@ -12,7 +12,8 @@ PLUGINS= \
static-files \
statistics \
webbox \
- weblog
+ weblog \
+ websocket
CXXFLAGS+=-fPIE
CXXFLAGS+=-gdwarf-4
diff --git a/TODO b/TODO
index 53c7c5b..65d9195 100644
--- a/TODO
+++ b/TODO
@@ -1,5 +1,8 @@
Big file bug
+- dynamic plugin interface (file buffer, ...)
Websockets
+- forward subprotocol
+http+https=CRTP
FastCGI from command line
stats.png
diff --git a/http.cpp b/http.cpp
index 0a9c680..4738ad8 100644
--- a/http.cpp
+++ b/http.cpp
@@ -51,7 +51,7 @@ class session : public std::enable_shared_from_this<session>
void handle_request(::Server& server, request_type&& req)
{
stream_.expires_after(std::chrono::seconds(300)); // timeout on write by server much longer than read timeout from client
- auto sp = std::make_shared<response_type>(generate_response(req, server));
+ auto sp = std::make_shared<response_type>(response::generate_response(req, server));
res_ = sp;
diff --git a/https.cpp b/https.cpp
index ce3a6fd..5d19d5b 100644
--- a/https.cpp
+++ b/https.cpp
@@ -56,13 +56,13 @@ class session : public std::enable_shared_from_this<session>
beast::flat_buffer buffer_;
Server& m_server;
std::optional<http::request_parser<http::string_body>> parser_; // need to reset parser every time, no other mechanism currently
- http::request<http::string_body> req_;
+ request_type req_;
std::shared_ptr<response_type> res_; // std::shared_ptr<void>
- void handle_request(::Server& server, request_type&& req)
+ void handle_request()
{
beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(300)); // timeout on write by server much longer than read timeout from client
- auto sp = std::make_shared<response_type>(generate_response(req, server));
+ auto sp = std::make_shared<response_type>(response::generate_response(req_, m_server));
res_ = sp;
@@ -75,6 +75,13 @@ class session : public std::enable_shared_from_this<session>
shared_from_this(),
sp->need_eof()));
}
+
+ void handle_websocket()
+ {
+ beast::get_lowest_layer(stream_).expires_never();
+ std::make_shared<websocket_session>(ioc_, std::move(stream_), response::get_websocket_address(req_, m_server))->do_accept_in(parser_->release());
+ }
+
public:
// Take ownership of the socket
explicit
@@ -171,13 +178,12 @@ public:
if (websocket::is_upgrade(req_))
{
- beast::get_lowest_layer(stream_).expires_never();
- std::make_shared<websocket_session>(ioc_, std::move(stream_))->do_accept_in(parser_->release());
+ handle_websocket();
return;
}
// Send the response
- handle_request(m_server, std::move(req_));
+ handle_request();
}
void
diff --git a/plugins/websocket/Makefile b/plugins/websocket/Makefile
new file mode 100644
index 0000000..4e841a8
--- /dev/null
+++ b/plugins/websocket/Makefile
@@ -0,0 +1,55 @@
+include ../../common.mk
+
+PROJECTNAME=websocket
+
+CXXFLAGS+= -fvisibility=hidden -fPIC
+
+CXXFLAGS+= -I../..
+
+LDLIBS=\
+-lreichwein \
+-lboost_context \
+-lboost_coroutine \
+-lboost_program_options \
+-lboost_system \
+-lboost_thread \
+-lboost_filesystem \
+-lboost_regex \
+-lpthread \
+-lssl -lcrypto \
+-ldl
+
+PROGSRC=\
+ websocket.cpp
+
+SRC=$(PROGSRC)
+
+all: $(PROJECTNAME).so
+
+$(PROJECTNAME).so: $(SRC:.cpp=.o)
+ $(CXX) $(CXXFLAGS) $^ -shared $(LIBS) -o $@
+
+%.d: %.cpp
+ $(CXX) $(CXXFLAGS) -MM -MP -MF $@ -c $<
+
+%.o: %.cpp %.d
+ $(CXX) $(CXXFLAGS) -c $< -o $@
+
+# dependencies
+
+ADD_DEP=Makefile
+
+install:
+ mkdir -p $(DESTDIR)/usr/lib/webserver/plugins
+ cp $(PROJECTNAME).so $(DESTDIR)/usr/lib/webserver/plugins
+
+# misc ---------------------------------------------------
+
+debs: $(DISTROS)
+
+clean:
+ -rm -f *.o *.so *.d
+
+.PHONY: clean install all
+
+-include $(wildcard $(SRC:.cpp=.d))
diff --git a/plugins/websocket/websocket.cpp b/plugins/websocket/websocket.cpp
new file mode 100644
index 0000000..884f691
--- /dev/null
+++ b/plugins/websocket/websocket.cpp
@@ -0,0 +1,80 @@
+#include "websocket.h"
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/array.hpp>
+#include <boost/endian/conversion.hpp>
+#include <boost/coroutine2/coroutine.hpp>
+#include <boost/process.hpp>
+
+#include <algorithm>
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <unordered_map>
+
+using namespace std::string_literals;
+namespace bp = boost::process;
+namespace fs = std::filesystem;
+
+namespace {
+
+ // Used to return errors by generating response page and HTTP status code
+ std::string HttpStatus(std::string status, std::string message, std::function<plugin_interface_setter_type>& SetResponseHeader)
+ {
+ SetResponseHeader("status", status);
+ SetResponseHeader("content_type", "text/html");
+ return status + " " + message;
+ }
+
+} // anonymous namespace
+
+std::string websocket_plugin::name()
+{
+ return "websocket";
+}
+
+websocket_plugin::websocket_plugin()
+{
+ //std::cout << "Plugin constructor" << std::endl;
+}
+
+websocket_plugin::~websocket_plugin()
+{
+ //std::cout << "Plugin destructor" << std::endl;
+}
+
+std::string websocket_plugin::generate_page(
+ std::function<std::string(const std::string& key)>& GetServerParam,
+ std::function<std::string(const std::string& key)>& GetRequestParam, // request including body (POST...)
+ std::function<void(const std::string& key, const std::string& value)>& SetResponseHeader // to be added to result string
+)
+{
+ try {
+ // Request path must not contain "..".
+ std::string rel_target{GetRequestParam("rel_target")};
+ size_t query_pos{rel_target.find("?")};
+ if (query_pos != rel_target.npos)
+ rel_target = rel_target.substr(0, query_pos);
+
+ std::string target{GetRequestParam("target")};
+ if (rel_target.find("..") != std::string::npos) {
+ return HttpStatus("400", "Illegal request: "s + target, SetResponseHeader);
+ }
+
+ try {
+ return "<html>Dummy</html>";
+ } catch (const std::exception& ex) {
+ return HttpStatus("500", "Internal Server Error: "s + ex.what(), SetResponseHeader);
+ }
+
+ } catch (const std::exception& ex) {
+ return HttpStatus("500", "Unknown Error: "s + ex.what(), SetResponseHeader);
+ }
+}
+
+bool websocket_plugin::has_own_authentication()
+{
+ return false;
+}
+
diff --git a/plugins/websocket/websocket.h b/plugins/websocket/websocket.h
new file mode 100644
index 0000000..27218da
--- /dev/null
+++ b/plugins/websocket/websocket.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "../../plugin_interface.h"
+
+#include <boost/asio.hpp>
+
+#include <cstdint>
+#include <mutex>
+#include <set>
+
+class websocket_plugin: public webserver_plugin_interface
+{
+
+public:
+ websocket_plugin();
+ ~websocket_plugin();
+
+ std::string name() override;
+ std::string generate_page(
+ std::function<std::string(const std::string& key)>& GetServerParam,
+ std::function<std::string(const std::string& key)>& GetRequestParam, // request including body (POST...)
+ std::function<void(const std::string& key, const std::string& value)>& SetResponseHeader // to be added to result string
+ ) override;
+
+ bool has_own_authentication() override;
+};
+
+extern "C" BOOST_SYMBOL_EXPORT websocket_plugin webserver_plugin;
+websocket_plugin webserver_plugin;
diff --git a/response.cpp b/response.cpp
index 29176af..eeda8d0 100644
--- a/response.cpp
+++ b/response.cpp
@@ -42,7 +42,7 @@ public:
// GetTarget() == GetPluginPath() + GetRelativePath()
- const Path& GetPath() const {return m_path;}
+ const Path& GetPath() const {return m_path;} // GetPluginPath w/ configured params as struct
std::string GetPluginName() const {return m_path.params.at("plugin");} // can throw std::out_of_range
@@ -297,7 +297,7 @@ response_type handleAuth(RequestContext& req_ctx, response_type& res)
} // anonymous namespace
-response_type generate_response(request_type& req, Server& server)
+response_type response::generate_response(request_type& req, Server& server)
{
response_type res{http::status::ok, req.version()};
res.set(http::field::server, Server::VersionString);
@@ -334,3 +334,26 @@ response_type generate_response(request_type& req, Server& server)
}
+std::string response::get_websocket_address(request_type& req, Server& server)
+{
+ try {
+ std::cout << "DEBUG0" << std::endl;
+ std::cout << "DEBUG0: " << req.target() << std::endl;
+ RequestContext req_ctx{req, server}; // can throw std::out_of_range
+
+ std::cout << "DEBUG1" << std::endl;
+ if (req_ctx.GetPluginName() != "websocket") {
+ std::cout << "Bad plugin configured for websocket request: " << req_ctx.GetPluginName() << std::endl;
+ return {};
+ }
+
+ std::cout << "DEBUG2" << std::endl;
+ return req_ctx.GetDocRoot(); // Configured "path" in config: host:port for websocket
+ std::cout << "DEBUG3" << std::endl;
+
+ } catch (const std::exception& ex) {
+ std::cout << "No matching configured target websocket found: " << ex.what() << std::endl;
+ return {};
+ }
+}
+
diff --git a/response.h b/response.h
index ad3d2fc..46de9d9 100644
--- a/response.h
+++ b/response.h
@@ -13,4 +13,11 @@ namespace http = beast::http; // from <boost/beast/http.hpp>
typedef http::request<http::string_body> request_type;
typedef http::response<http::string_body> response_type;
+namespace response {
+
response_type generate_response(request_type& req, Server& server);
+
+// Get host:port e.g. reichwein.it:6543
+std::string get_websocket_address(request_type& req, Server& server);
+
+} // namespace
diff --git a/tests/test-response.cpp b/tests/test-response.cpp
index 3f83a6d..1c27bf0 100644
--- a/tests/test-response.cpp
+++ b/tests/test-response.cpp
@@ -22,7 +22,7 @@ public:
void teardown(){}
};
-BOOST_FIXTURE_TEST_CASE(response, ResponseFixture)
+BOOST_FIXTURE_TEST_CASE(response1, ResponseFixture)
{
}
diff --git a/tests/test-webserver.cpp b/tests/test-webserver.cpp
index 1c1e6cc..10f6dca 100644
--- a/tests/test-webserver.cpp
+++ b/tests/test-webserver.cpp
@@ -56,58 +56,9 @@ const fs::path testKeyFilename{"./testkey.pem"};
class WebserverProcess
{
-public:
- WebserverProcess(): m_pid{}
+ void init(const std::string& config)
{
- File::setFile(testConfigFilename, R"CONFIG(<webserver>
- <user>www-data</user>
- <group>www-data</group>
- <threads>10</threads>
- <statisticspath>stats.db</sttaisticspath>
- <plugin-directory>../plugins</plugin-directory>
- <sites>
- <site>
- <name>localhost</name>
- <host>ip6-localhost</host>
- <host>localhost</host>
- <host>127.0.0.1</host>
- <host>[::1]</host>
- <path requested="/">
- <plugin>static-files</plugin>
- <target>.</target>
- </path>
- <certpath>testchain.pem</certpath>
- <keypath>testkey.pem</keypath>
- </site>
- </sites>
- <sockets>
- <socket>
- <address>127.0.0.1</address>
- <port>8080</port>
- <protocol>http</protocol>
- <site>localhost</site>
- </socket>
- <socket>
- <address>::1</address>
- <port>8080</port>
- <protocol>http</protocol>
- <site>localhost</site>
- </socket>
- <socket>
- <address>127.0.0.1</address>
- <port>8081</port>
- <protocol>https</protocol>
- <site>localhost</site>
- </socket>
- <socket>
- <address>::1</address>
- <port>8081</port>
- <protocol>https</protocol>
- <site>localhost</site>
- </socket>
- </sockets>
-</webserver>
-)CONFIG");
+ File::setFile(testConfigFilename, config);
// test self signed certificate
File::setFile(testCertFilename, R"(-----BEGIN CERTIFICATE-----
@@ -161,6 +112,66 @@ VZTqPHmb+db0rFA3XlAg2A==
start();
}
+public:
+ WebserverProcess(const std::string& config): m_pid{}
+ {
+ init(config);
+ }
+
+ WebserverProcess(): m_pid{}
+ {
+ std::string config{R"CONFIG(<webserver>
+ <user>www-data</user>
+ <group>www-data</group>
+ <threads>10</threads>
+ <statisticspath>stats.db</statisticspath>
+ <plugin-directory>../plugins</plugin-directory>
+ <sites>
+ <site>
+ <name>localhost</name>
+ <host>ip6-localhost</host>
+ <host>localhost</host>
+ <host>127.0.0.1</host>
+ <host>[::1]</host>
+ <path requested="/">
+ <plugin>static-files</plugin>
+ <target>.</target>
+ </path>
+ <certpath>testchain.pem</certpath>
+ <keypath>testkey.pem</keypath>
+ </site>
+ </sites>
+ <sockets>
+ <socket>
+ <address>127.0.0.1</address>
+ <port>8080</port>
+ <protocol>http</protocol>
+ <site>localhost</site>
+ </socket>
+ <socket>
+ <address>::1</address>
+ <port>8080</port>
+ <protocol>http</protocol>
+ <site>localhost</site>
+ </socket>
+ <socket>
+ <address>127.0.0.1</address>
+ <port>8081</port>
+ <protocol>https</protocol>
+ <site>localhost</site>
+ </socket>
+ <socket>
+ <address>::1</address>
+ <port>8081</port>
+ <protocol>https</protocol>
+ <site>localhost</site>
+ </socket>
+ </sockets>
+</webserver>
+)CONFIG"};
+ init(config);
+ }
+
~WebserverProcess()
{
stop();
@@ -506,7 +517,7 @@ public:
try
{
auto const address = boost::asio::ip::make_address("::1");
- auto const port = static_cast<unsigned short>(9876);
+ auto const port = static_cast<unsigned short>(8765);
// The io_context is required for all I/O
boost::asio::io_context ioc{1};
@@ -558,7 +569,56 @@ private:
BOOST_FIXTURE_TEST_CASE(websocket, Fixture)
{
- WebserverProcess serverProcess;
+ std::string webserver_config{R"CONFIG(<webserver>
+ <user>www-data</user>
+ <group>www-data</group>
+ <threads>10</threads>
+ <statisticspath>stats.db</statisticspath>
+ <plugin-directory>../plugins</plugin-directory>
+ <sites>
+ <site>
+ <name>localhost</name>
+ <host>ip6-localhost</host>
+ <host>localhost</host>
+ <host>127.0.0.1</host>
+ <host>[::1]</host>
+ <path requested="/">
+ <plugin>websocket</plugin>
+ <target>::1:8765</target>
+ </path>
+ <certpath>testchain.pem</certpath>
+ <keypath>testkey.pem</keypath>
+ </site>
+ </sites>
+ <sockets>
+ <socket>
+ <address>127.0.0.1</address>
+ <port>8080</port>
+ <protocol>http</protocol>
+ <site>localhost</site>
+ </socket>
+ <socket>
+ <address>::1</address>
+ <port>8080</port>
+ <protocol>http</protocol>
+ <site>localhost</site>
+ </socket>
+ <socket>
+ <address>127.0.0.1</address>
+ <port>8081</port>
+ <protocol>https</protocol>
+ <site>localhost</site>
+ </socket>
+ <socket>
+ <address>::1</address>
+ <port>8081</port>
+ <protocol>https</protocol>
+ <site>localhost</site>
+ </socket>
+ </sockets>
+</webserver>
+)CONFIG"};
+ WebserverProcess serverProcess{webserver_config};
BOOST_REQUIRE(serverProcess.is_running());
WebsocketServerProcess websocketProcess;
@@ -598,6 +658,7 @@ BOOST_FIXTURE_TEST_CASE(websocket, Fixture)
// Update the host_ string. This will provide the value of the
// Host HTTP header during the WebSocket handshake.
// See https://tools.ietf.org/html/rfc7230#section-5.4
+ host = "[" + host + "]";
host += ':' + std::to_string(ep.port());
// Perform the SSL handshake
@@ -632,7 +693,6 @@ BOOST_FIXTURE_TEST_CASE(websocket, Fixture)
data = std::string(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
BOOST_CHECK_EQUAL(data, "request1: 1");
-
buffer.consume(buffer.size());
ws.write(boost::asio::buffer(std::string(text)));
diff --git a/websocket.h b/websocket.h
index 1611c45..85492f2 100644
--- a/websocket.h
+++ b/websocket.h
@@ -49,14 +49,23 @@ class websocket_session: public std::enable_shared_from_this<websocket_session>
std::string port_;
public:
- explicit websocket_session(boost::asio::io_context& ioc, beast::ssl_stream<beast::tcp_stream>&& stream):
+ explicit websocket_session(boost::asio::io_context& ioc, beast::ssl_stream<beast::tcp_stream>&& stream, const std::string& websocket_address):
ioc_(ioc),
resolver_(boost::asio::make_strand(ioc_)),
ws_in_(std::move(stream)),
ws_app_(boost::asio::make_strand(ioc_)),
- host_{"::1"},
- port_{"9876"}
+ host_{},
+ port_{}
{
+ // Parse websocket address host:port :
+
+ auto pos{websocket_address.find_last_of(':')};
+
+ if (pos == std::string::npos)
+ return;
+
+ host_ = websocket_address.substr(0, pos);
+ port_ = websocket_address.substr(pos + 1);
}
//