summaryrefslogtreecommitdiffhomepage
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/Makefile16
-rw-r--r--tests/fastcgiprocess.cpp115
-rw-r--r--tests/fastcgiprocess.h28
-rw-r--r--tests/fcgi1.cpp89
-rw-r--r--tests/helper.cpp228
-rw-r--r--tests/helper.h56
-rw-r--r--tests/test-webserver.cpp692
-rw-r--r--tests/webserverprocess.cpp221
-rw-r--r--tests/webserverprocess.h30
-rw-r--r--tests/websocketserverprocess.cpp205
-rw-r--r--tests/websocketserverprocess.h53
11 files changed, 1143 insertions, 590 deletions
diff --git a/tests/Makefile b/tests/Makefile
index c04cbcd..8df5a45 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -17,7 +17,7 @@ CXXFLAGS+= -I. -I.. -fPIE
CXXTESTFLAGS=
-CXXFLAGS+=$(shell pkg-config --cflags fmt)
+CXXFLAGS+=$(shell pkg-config --cflags fmt fcgi)
LIBS+=\
-lreichwein \
@@ -29,7 +29,7 @@ LIBS+=\
-lpthread \
-lssl -lcrypto \
-ldl \
-$(shell pkg-config --libs fmt)
+$(shell pkg-config --libs fmt fcgi)
LDFLAGS+=-pie
@@ -57,10 +57,13 @@ TESTSRC=\
test-server.cpp \
test-statistics.cpp \
test-webserver.cpp \
+ fastcgiprocess.cpp \
+ helper.cpp \
+ webserverprocess.cpp \
+ websocketserverprocess.cpp
- $(PROGSRC)
+build: $(PROJECTNAME) fcgi1
-build: $(PROJECTNAME)
ifeq ($(CXXTYPE),clang++)
LLVM_PROFILE_FILE="$(PROJECTNAME).profraw" ./$(PROJECTNAME)
$(LLVMPROFDATA) merge -sparse $(PROJECTNAME).profraw -o $(PROJECTNAME).profdata
@@ -73,6 +76,9 @@ endif
$(PROJECTNAME): $(TESTSRC:.cpp=.o) $(UNITS:.cpp=.o)
$(CXX) $(LDFLAGS) $^ $(LDLIBS) $(LIBS) -o $@
+fcgi1: fcgi1.o
+ $(CXX) $(LDFLAGS) $^ $(LDLIBS) $(LIBS) -o $@
+
dep: $(TESTSRC:.cpp=.d)
%.d: %.cpp
@@ -109,7 +115,7 @@ ADD_DEP=Makefile
# misc ---------------------------------------------------
clean:
- -rm -f *.o *.a *.d $(PROJECTNAME) *.gcda *.gcno *.profraw *.profdata *.gcov
+ -rm -f *.o *.a *.d $(PROJECTNAME) *.gcda *.gcno *.profraw *.profdata *.gcov fcgi1
.PHONY: clean all install
diff --git a/tests/fastcgiprocess.cpp b/tests/fastcgiprocess.cpp
new file mode 100644
index 0000000..53b9d04
--- /dev/null
+++ b/tests/fastcgiprocess.cpp
@@ -0,0 +1,115 @@
+#include "fastcgiprocess.h"
+
+#include <boost/algorithm/string.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/beast/version.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/buffers_iterator.hpp>
+#include <boost/asio/connect.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ssl/error.hpp>
+#include <boost/asio/ssl/stream.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/xml_parser.hpp>
+
+#include <signal.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#include <libreichwein/file.h>
+#include <libreichwein/process.h>
+
+#include "helper.h"
+
+using namespace std::string_literals;
+namespace fs = std::filesystem;
+namespace pt = boost::property_tree;
+using namespace boost::unit_test;
+using namespace Reichwein;
+
+#define FCGI_LISTENSOCK_FILENO 0
+
+FastCGIProcess::FastCGIProcess(const std::filesystem::path& path, const std::string& host, unsigned short port):
+ m_pid{},
+ m_command{path.generic_string()},
+ m_host{host},
+ m_port{port}
+{
+ start();
+}
+
+FastCGIProcess::~FastCGIProcess()
+{
+ stop();
+}
+
+void FastCGIProcess::start()
+{
+ if (m_pid != 0)
+ throw std::runtime_error("Process already running, so it can't be started");
+
+ m_pid = fork();
+ if (m_pid < 0)
+ throw std::runtime_error("Fork unsuccessful.");
+
+ if (m_pid == 0) { // child process branch
+ try {
+ boost::asio::io_context ioc;
+ boost::asio::ip::tcp::resolver resolver(ioc);
+ auto const results = resolver.resolve(m_host.c_str(), std::to_string(m_port).c_str());
+ if (results.begin() == results.end())
+ std::runtime_error("no resolve result");
+ boost::asio::ip::tcp::endpoint endpoint{*results.begin()};
+ boost::asio::ip::tcp::acceptor acceptor(ioc);
+ acceptor.open(endpoint.protocol());
+ acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
+ acceptor.bind(endpoint);
+ acceptor.listen();
+ int fd{acceptor.native_handle()};
+
+ if (fd != FCGI_LISTENSOCK_FILENO) {
+ close(FCGI_LISTENSOCK_FILENO);
+ dup2(fd, FCGI_LISTENSOCK_FILENO);
+ close(fd);
+ }
+
+ execl(m_command.c_str(), m_command.c_str(), (const char*)nullptr);
+ } catch (const std::exception& ex) {
+ std::cout << "FastCGI process error: " << ex.what() << std::endl;
+ }
+ exit(0);
+ }
+
+ // wait for server to start up
+ wait_for_pid_listening_on(m_pid, m_port);
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+}
+
+void FastCGIProcess::stop()
+{
+ if (m_pid == 0)
+ throw std::runtime_error("Process not running, so it can't be stopped");
+
+ if (kill(m_pid, SIGTERM) != 0)
+ throw std::runtime_error("Unable to kill process");
+
+ if (int result = waitpid(m_pid, NULL, 0); result != m_pid)
+ throw std::runtime_error("waitpid returned "s + std::to_string(result));
+
+ m_pid = 0;
+}
+
+bool FastCGIProcess::is_running()
+{
+ if (m_pid == 0)
+ return false;
+
+ return Reichwein::Process::is_running(m_pid);
+}
+
diff --git a/tests/fastcgiprocess.h b/tests/fastcgiprocess.h
new file mode 100644
index 0000000..ce7bf74
--- /dev/null
+++ b/tests/fastcgiprocess.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <filesystem>
+#include <string>
+
+#include <ext/stdio_filebuf.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+class FastCGIProcess
+{
+public:
+ FastCGIProcess(const std::filesystem::path& path, const std::string& host, unsigned short port);
+ ~FastCGIProcess();
+ bool is_running();
+
+private:
+ void start();
+ void stop();
+
+ pid_t m_pid;
+ std::string m_command;
+ std::string m_host;
+ unsigned short m_port;
+};
diff --git a/tests/fcgi1.cpp b/tests/fcgi1.cpp
new file mode 100644
index 0000000..f7d2837
--- /dev/null
+++ b/tests/fcgi1.cpp
@@ -0,0 +1,89 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <dirent.h>
+#include <sys/types.h>
+
+#include <fcgiapp.h>
+
+#include <chrono>
+#include <iostream>
+#include <functional>
+#include <filesystem>
+#include <mutex>
+#include <regex>
+#include <string>
+#include <thread>
+#include <unordered_map>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/property_tree/xml_parser.hpp>
+
+#include "config.h"
+
+namespace pt = boost::property_tree;
+using namespace std::string_literals;
+namespace fs = std::filesystem;
+
+// the actual main() for testability
+int main(int argc, char* argv[])
+{
+ int result = FCGX_Init();
+ if (result != 0) { // error on init
+ fprintf(stderr, "Error: FCGX_Init()\n");
+ return 1;
+ }
+
+ result = FCGX_IsCGI();
+ if (result) {
+ fprintf(stderr, "Error: No FCGI environment available.\n");
+ return 1;
+ }
+
+ FCGX_Request request;
+ result = FCGX_InitRequest(&request, 0, 0);
+ if (result != 0) {
+ fprintf(stderr, "Error: FCGX_InitRequest()\n");
+ return 1;
+ }
+
+ std::cout << "FGCI app running. Accepting connections." << std::endl;
+ while (FCGX_Accept_r(&request) >= 0) {
+ try {
+ char* method = FCGX_GetParam("REQUEST_METHOD", request.envp);
+
+ // POST for server actions, changes
+ if (!strcmp(method, "POST") ||!strcmp(method, "GET") ) {
+ size_t contentLength { std::stoul(FCGX_GetParam("CONTENT_LENGTH", request.envp)) };
+ std::string postData(contentLength, '\0'); // contentLength number of bytes, initialize with 0
+ if (FCGX_GetStr(postData.data(), contentLength, request.in) != static_cast<int>(contentLength)) {
+ throw std::runtime_error("Bad data read: Content length mismatch.\r\n");
+ }
+ // postData contains POST data
+ std::string contentType(FCGX_GetParam("CONTENT_TYPE", request.envp));
+ postData = "returning data of " + contentType + ": " + postData;
+
+ FCGX_PutS("Content-Type: text/plain\r\n", request.out);
+ FCGX_FPrintF(request.out, "Content-Length: %d\r\n\r\n", postData.size());
+ FCGX_PutStr(postData.c_str(), postData.size(), request.out);
+
+ } else {
+ throw std::runtime_error("Unsupported method.\r\n");
+ }
+ } catch (const std::runtime_error& ex) {
+ FCGX_PutS("Status: 500 Internal Server Error\r\n", request.out);
+ FCGX_PutS("Content-Type: text/html\r\n\r\n", request.out);
+ FCGX_FPrintF(request.out, "Error: %s\r\n", ex.what());
+ } catch (const std::exception& ex) {
+ FCGX_PutS("Status: 500 Internal Server Error\r\n", request.out);
+ FCGX_PutS("Content-Type: text/html\r\n\r\n", request.out);
+ FCGX_FPrintF(request.out, "Unknown exception: %s\r\n", ex.what());
+ }
+ }
+
+ std::cout << "FGCI app exiting." << std::endl;
+ return 0;
+}
+
diff --git a/tests/helper.cpp b/tests/helper.cpp
new file mode 100644
index 0000000..644b9ca
--- /dev/null
+++ b/tests/helper.cpp
@@ -0,0 +1,228 @@
+#include "helper.h"
+
+using namespace std::string_literals;
+namespace fs = std::filesystem;
+namespace pt = boost::property_tree;
+using namespace boost::unit_test;
+using namespace Reichwein;
+
+const fs::path testConfigFilename{"./webserver.conf"};
+const fs::path testCertFilename{"./testchain.pem"};
+const fs::path testKeyFilename{"./testkey.pem"};
+
+// tcp: tcp or tcp6
+bool tcp_is_pid_listening_on(const std::string& tcp, pid_t pid, int port)
+{
+ std::string filename{fmt::format("/proc/{}/net/{}", pid, tcp)};
+ std::ifstream f{filename, std::ios::in};
+ // e.g.:
+ // sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
+ // 0: 00000000:C799 00000000:0000 0A 00000000:00000000 00:00000000 00000000 107 0 21869 1 00000000335416a4 100 0 0 10 0
+ std::string s;
+ std::getline(f, s); // skip head line
+ while (std::getline(f, s)) {
+ boost::algorithm::trim_left(s);
+
+ size_t pos_space1{s.find(' ')};
+ if (pos_space1 == std::string::npos)
+ throw std::runtime_error("Expected first space in " + filename);
+
+ size_t pos_colon1{s.find(':', pos_space1 + 1)};
+ if (pos_colon1 == std::string::npos)
+ throw std::runtime_error("Expected first colon in " + filename);
+
+ size_t pos_space2{s.find(' ', pos_colon1 + 1)};
+ if (pos_space2 == std::string::npos)
+ throw std::runtime_error("Expected second space in " + filename);
+
+ std::string port_s{s.substr(pos_colon1 + 1, pos_space2 - (pos_colon1 + 1))};
+ auto current_port{std::stoul(port_s, nullptr, 16)};
+ if (current_port != port)
+ continue;
+
+ // now, we are in a line related to matching local port
+
+ size_t pos_space3{s.find(' ', pos_space2 + 1)};
+ if (pos_space3 == std::string::npos)
+ throw std::runtime_error("Expected third space in " + filename);
+
+ size_t pos_space4{s.find(' ', pos_space3 + 1)};
+ if (pos_space4 == std::string::npos)
+ throw std::runtime_error("Expected fourth space in " + filename);
+
+ std::string state_s{s.substr(pos_space3 + 1, pos_space4 - (pos_space3 + 1))};
+ if (state_s == "0A") // listening state TCP_LISTEN, from net/tcp_states.h
+ return true;
+ }
+
+ return false; // not found
+}
+
+bool is_pid_listening_on(pid_t pid, int port)
+{
+ return tcp_is_pid_listening_on("tcp", pid, port) || tcp_is_pid_listening_on("tcp6", pid, port);
+}
+
+void wait_for_pid_listening_on(pid_t pid, int port)
+{
+ while (!is_pid_listening_on(pid, port)) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ }
+}
+
+// returns -1 if no port found in config
+int port_from_config(const std::string& config)
+{
+ pt::ptree tree;
+ std::istringstream stream{config};
+ pt::read_xml(stream, tree);
+ try {
+ return tree.get<int>("webserver.sockets.socket.port");
+ } catch(...) {
+ return -1;
+ }
+}
+
+std::pair<std::string,std::string> HTTP(const std::string& target, bool ipv6, bool HTTP11, boost::beast::http::verb method)
+{
+ auto const host = ipv6 ? "::1" : "127.0.0.1";
+ auto const port = "8080";
+ int version = HTTP11 ? 11 : 10;
+
+ // The io_context is required for all I/O
+ boost::asio::io_context ioc;
+
+ // These objects perform our I/O
+ boost::asio::ip::tcp::resolver resolver(ioc);
+ boost::beast::tcp_stream stream(ioc);
+
+ // Look up the domain name
+ auto const results = resolver.resolve(host, port);
+
+ // Make the connection on the IP address we get from a lookup
+ stream.connect(results);
+
+ // Set up an HTTP GET request message
+ boost::beast::http::request<boost::beast::http::string_body> req;
+ req.method(method);
+ req.target(target);
+ req.version(version);
+ req.set(boost::beast::http::field::host, host == "::1"s ? "["s + host + "]"s : host);
+ req.set(boost::beast::http::field::user_agent, "Webserver Testsuite");
+
+ // Send the HTTP request to the remote host
+ boost::beast::http::write(stream, req);
+
+ // This buffer is used for reading and must be persisted
+ boost::beast::flat_buffer buffer;
+
+ // Declare a container to hold the response
+ boost::beast::http::response<boost::beast::http::dynamic_body> res;
+
+ // Receive the HTTP response
+ boost::beast::http::read(stream, buffer, res);
+
+ // Return value
+ std::ostringstream header_stream;
+ header_stream << res.base();
+ std::ostringstream body_stream;
+ body_stream << boost::beast::buffers_to_string(res.body().data());
+
+ // Gracefully close the socket
+ boost::beast::error_code ec;
+ stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
+
+ // not_connected happens sometimes
+ // so don't bother reporting it.
+ //
+ if (ec && ec != boost::beast::errc::not_connected)
+ throw boost::beast::system_error{ec};
+
+ return {header_stream.str(), body_stream.str()};
+}
+
+void load_root_certificates(boost::asio::ssl::context& ctx)
+{
+ std::string cert_chain{File::getFile(testCertFilename)};
+ ctx.add_certificate_authority(boost::asio::buffer(cert_chain.data(), cert_chain.size()));
+}
+
+std::pair<std::string,std::string> HTTPS(const std::string& target, bool ipv6, bool HTTP11, boost::beast::http::verb method)
+{
+ auto const host = ipv6 ? "::1" : "127.0.0.1";
+ auto const port = "8081";
+ int version = HTTP11 ? 11 : 10;
+
+ // The io_context is required for all I/O
+ boost::asio::io_context ioc;
+
+ // The SSL context is required, and holds certificates
+ boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv13_client);
+
+ // This holds the root certificate used for verification
+ load_root_certificates(ctx);
+
+ // Verify the remote server's certificate
+ ctx.set_verify_mode(boost::asio::ssl::verify_peer);
+
+ // These objects perform our I/O
+ boost::asio::ip::tcp::resolver resolver(ioc);
+ boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
+
+ // Set SNI Hostname (many hosts need this to handshake successfully)
+ if (!SSL_set_tlsext_host_name(stream.native_handle(), host))
+ {
+ boost::beast::error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
+ throw boost::beast::system_error{ec};
+ }
+
+ // Look up the domain name
+ auto const results = resolver.resolve(host, port);
+
+ // Make the connection on the IP address we get from a lookup
+ boost::beast::get_lowest_layer(stream).connect(results);
+
+ // Perform the SSL handshake
+ stream.handshake(boost::asio::ssl::stream_base::client);
+
+ // Set up an HTTP GET request message
+ boost::beast::http::request<boost::beast::http::string_body> req;
+ req.method(method);
+ req.target(target);
+ req.version(version);
+ req.set(boost::beast::http::field::host, host == "::1"s ? "["s + host + "]"s : host);
+ req.set(boost::beast::http::field::user_agent, "Webserver Testsuite");
+
+ // Send the HTTP request to the remote host
+ boost::beast::http::write(stream, req);
+
+ // This buffer is used for reading and must be persisted
+ boost::beast::flat_buffer buffer;
+
+ // Declare a container to hold the response
+ boost::beast::http::response<boost::beast::http::dynamic_body> res;
+
+ // Receive the HTTP response
+ boost::beast::http::read(stream, buffer, res);
+
+ // Return value
+ std::ostringstream header_stream;
+ header_stream << res.base();
+ std::ostringstream body_stream;
+ body_stream << boost::beast::buffers_to_string(res.body().data());
+
+ // Gracefully close the stream
+ boost::beast::error_code ec;
+ stream.shutdown(ec);
+ if (ec == boost::asio::error::eof)
+ {
+ // Rationale:
+ // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
+ ec = {};
+ }
+ if (ec)
+ throw boost::beast::system_error{ec};
+
+ return {header_stream.str(), body_stream.str()};
+}
+
diff --git a/tests/helper.h b/tests/helper.h
new file mode 100644
index 0000000..eba74cd
--- /dev/null
+++ b/tests/helper.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <boost/test/data/dataset.hpp>
+#include <boost/test/data/monomorphic.hpp>
+#include <boost/test/data/test_case.hpp>
+
+#include <boost/algorithm/string.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/beast/version.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/buffers_iterator.hpp>
+#include <boost/asio/connect.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ssl/error.hpp>
+#include <boost/asio/ssl/stream.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/xml_parser.hpp>
+
+#include <fmt/core.h>
+
+#include <chrono>
+#include <exception>
+#include <filesystem>
+#include <iostream>
+#include <memory>
+#include <mutex>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <thread>
+
+#include <ext/stdio_filebuf.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#include <libreichwein/file.h>
+#include <libreichwein/process.h>
+
+extern const std::filesystem::path testConfigFilename;
+extern const std::filesystem::path testCertFilename;
+extern const std::filesystem::path testKeyFilename;
+
+bool tcp_is_pid_listening_on(const std::string& tcp, pid_t pid, int port);
+bool is_pid_listening_on(pid_t pid, int port);
+void wait_for_pid_listening_on(pid_t pid, int port);
+int port_from_config(const std::string& config);
+void load_root_certificates(boost::asio::ssl::context& ctx);
+std::pair<std::string,std::string> HTTP(const std::string& target, bool ipv6 = true, bool HTTP11 = true, boost::beast::http::verb method = boost::beast::http::verb::get);
+std::pair<std::string,std::string> HTTPS(const std::string& target, bool ipv6 = true, bool HTTP11 = true, boost::beast::http::verb method = boost::beast::http::verb::get);
diff --git a/tests/test-webserver.cpp b/tests/test-webserver.cpp
index 85859df..714cdf2 100644
--- a/tests/test-webserver.cpp
+++ b/tests/test-webserver.cpp
@@ -36,7 +36,6 @@
#include <string>
#include <thread>
-#include <ext/stdio_filebuf.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -49,430 +48,17 @@
#include "webserver.h"
#include "response.h"
+#include "fastcgiprocess.h"
+#include "helper.h"
+#include "webserverprocess.h"
+#include "websocketserverprocess.h"
+
using namespace std::string_literals;
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
using namespace boost::unit_test;
using namespace Reichwein;
-const fs::path testConfigFilename{"./webserver.conf"};
-const fs::path testCertFilename{"./testchain.pem"};
-const fs::path testKeyFilename{"./testkey.pem"};
-
-// tcp: tcp or tcp6
-bool tcp_is_pid_listening_on(const std::string& tcp, pid_t pid, int port)
-{
- std::string filename{fmt::format("/proc/{}/net/{}", pid, tcp)};
- std::ifstream f{filename, std::ios::in};
- // e.g.:
- // sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
- // 0: 00000000:C799 00000000:0000 0A 00000000:00000000 00:00000000 00000000 107 0 21869 1 00000000335416a4 100 0 0 10 0
- std::string s;
- std::getline(f, s); // skip head line
- while (std::getline(f, s)) {
- boost::algorithm::trim_left(s);
-
- size_t pos_space1{s.find(' ')};
- if (pos_space1 == std::string::npos)
- throw std::runtime_error("Expected first space in " + filename);
-
- size_t pos_colon1{s.find(':', pos_space1 + 1)};
- if (pos_colon1 == std::string::npos)
- throw std::runtime_error("Expected first colon in " + filename);
-
- size_t pos_space2{s.find(' ', pos_colon1 + 1)};
- if (pos_space2 == std::string::npos)
- throw std::runtime_error("Expected second space in " + filename);
-
- std::string port_s{s.substr(pos_colon1 + 1, pos_space2 - (pos_colon1 + 1))};
- auto current_port{std::stoul(port_s, nullptr, 16)};
- if (current_port != port)
- continue;
-
- // now, we are in a line related to matching local port
-
- size_t pos_space3{s.find(' ', pos_space2 + 1)};
- if (pos_space3 == std::string::npos)
- throw std::runtime_error("Expected third space in " + filename);
-
- size_t pos_space4{s.find(' ', pos_space3 + 1)};
- if (pos_space4 == std::string::npos)
- throw std::runtime_error("Expected fourth space in " + filename);
-
- std::string state_s{s.substr(pos_space3 + 1, pos_space4 - (pos_space3 + 1))};
- if (state_s == "0A") // listening state TCP_LISTEN, from net/tcp_states.h
- return true;
- }
-
- return false; // not found
-}
-
-bool is_pid_listening_on(pid_t pid, int port)
-{
- return tcp_is_pid_listening_on("tcp", pid, port) || tcp_is_pid_listening_on("tcp6", pid, port);
-}
-
-void wait_for_pid_listening_on(pid_t pid, int port)
-{
- while (!is_pid_listening_on(pid, port)) {
- std::this_thread::sleep_for(std::chrono::milliseconds(10));
- }
-}
-
-// returns -1 if no port found in config
-int port_from_config(const std::string& config)
-{
- pt::ptree tree;
- std::istringstream stream{config};
- pt::read_xml(stream, tree);
- try {
- return tree.get<int>("webserver.sockets.socket.port");
- } catch(...) {
- return -1;
- }
-}
-
-class WebserverProcess
-{
- void init(const std::string& config)
- {
- m_config = config;
- File::setFile(testConfigFilename, config);
-
- // test self signed certificate
- File::setFile(testCertFilename, R"(-----BEGIN CERTIFICATE-----
-MIIC4zCCAcugAwIBAgIUeS9y+EsFWxf+foEx6SJ/R56rmX8wDQYJKoZIhvcNAQEL
-BQAwADAgFw0yMzAxMDYxNzIwNTFaGA8yMDUwMDUyNDE3MjA1MVowADCCASIwDQYJ
-KoZIhvcNAQEBBQADggEPADCCAQoCggEBALiZSICAcXng9j7zAb873U4TpuzvRVfh
-xS3gEhxqNPs6+ZQ43nAxDSdafzfGxpTkElTt/REj4oEOLw+QWI/jfbe4gDRDzf6V
-ij0fVuzp02JtJSS+dNrLv17NufBydOyD8oDrPehVrPlrZQhhkYMvLHAim+wikT2O
-s0es2R+avixxAZvx5EYgHba9T7R/pC/lA4BI3lEbVKjDA83hZvjPH1YdK+RYQS2g
-Jygdhe8qOSswXIwFAF3MMBpwRD3mz+vAJZP3lpBGsn+asO6Xd/5cjC8msgomS8Ji
-c9DMMNlrE1WU73wVG9n0OJcke2XEtzARVKJLlBPsug4oxDev6O4GakkCAwEAAaNT
-MFEwHQYDVR0OBBYEFE4i7Gtyn30qpIkH6f0/wuFA45pjMB8GA1UdIwQYMBaAFE4i
-7Gtyn30qpIkH6f0/wuFA45pjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
-BQADggEBAIlGv4b2yLmTOrXOPNst2y3J+GiRvuMKoAfDt5KLxUhbCmPgJzGDWn0l
-60xXBX/t2uo3dQa9yAIW64RqhEQX7uja/7B3PmJZlgF7+owvT8OZA4+UN1lLUvY4
-V7mUzuKuqo5jcX8EmZnHrJ4TGZ0dXbT1hAUgqIjnDChjWyvs4B9zZL5FTisPUic7
-MU+FcpKJ5M6iJ150d9hzLiwmJyPLkW5Grq0Jh22njUQwWW2vIMn4cA3CyS64+oi2
-DNnDgde3mYxXL8Oki7CbeCTpmUXcBHmQtWOvKZPCsOzMF4moTLC4DdElvOpwKCAK
-ABd6rubkarwvDV7wEo1eSuAHPZ/KhGo=
------END CERTIFICATE-----
-)");
- File::setFile(testKeyFilename, R"(-----BEGIN PRIVATE KEY-----
-MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC4mUiAgHF54PY+
-8wG/O91OE6bs70VX4cUt4BIcajT7OvmUON5wMQ0nWn83xsaU5BJU7f0RI+KBDi8P
-kFiP4323uIA0Q83+lYo9H1bs6dNibSUkvnTay79ezbnwcnTsg/KA6z3oVaz5a2UI
-YZGDLyxwIpvsIpE9jrNHrNkfmr4scQGb8eRGIB22vU+0f6Qv5QOASN5RG1SowwPN
-4Wb4zx9WHSvkWEEtoCcoHYXvKjkrMFyMBQBdzDAacEQ95s/rwCWT95aQRrJ/mrDu
-l3f+XIwvJrIKJkvCYnPQzDDZaxNVlO98FRvZ9DiXJHtlxLcwEVSiS5QT7LoOKMQ3
-r+juBmpJAgMBAAECggEACjs5suCbmYAb2d2VZlRitdP+Q6HX37D0YTBrBI7o6JdK
-U7oqrwBy/JBGHpDqewBgmTs3FGr/H/zJpDTRickXs9X6qhrreQ2wA6b/5gHoPMt0
-nHKfbqyOCuq/YGmxnBXMnDNdoynTfGAE0af5vRIBZiYu6vG4B9fHzURR5O/qVDNn
-WqJ+2Y8AAf4mJDCBEvJz2RaZwSq788i/d8oTSeCk93TDF+GHhUq6ymkORACj76ws
-8ohfoNQIG1VhdcTK2GOQjqctEFUm54t9N1nxD6VavMu5DlSSVsPTDbYuE5U4cy3T
-ThDtoYJgwz5KRflklwl3xoDJVx3B5wMUaqviRp6l0QKBgQC6MPG+EV2drsHwG0Wg
-gnP4uCSuFAfWBlHAQyZv5PMQBNfM8YjyMyL+O7cggGNJSOZr/X30EqoBe+LXrL3X
-Gtix8F1Ed0fbAarAgxIwq8MktzmastDq4XS+zwYPZ7UTbmbqvT3VYPga4Sh90fyY
-nPJpqZvhvGzQX22yeHS7vTSQOQKBgQD9z36EIYMuLl0HJK6gfjGHsy/Rx7bw1TmP
-aHmuF8Ra7rpDSOym0ImKWTOLEoLlQUsMz/FuVLCGP/ACjMFKsqh3Zy/0hVJOMDMR
-Z+ODT28Hcz4AMcTYDvcTYd70HhhZL+/eFCVk8Nk164saMuhifAkOgvwfaYs0m3ue
-S9jxlZKKkQKBgHgBzf6k8MMOfaAF4/XVv2wDPFkbPgW74vtaDK84UVX02ScWUx9Q
-yHA3Cwye09/LZgEazREA6qS0NfyvMVkwy5S9CVB01VKam3UjxhiqzMegdTd5o+CQ
-WpAVnaFWRcb1dM4+FVmv+5pPn6qhKv8uwaxLDtcLfNM9ftX2f77176g5AoGADWtQ
-DBpdfi6TWpJU7UVexwbxS00c3gTYAz4J2OuGxSwECxSq9nLmIrtunza+VvKpziac
-ZDH0F1UAEpJwkct6Xr3E6k+2N04TFSOCAupLO4CbUZVQDABWjd7J0+xXaze+neZA
-x+J4CYLHmv4ADVzzeaHxRJPm+UQTOB5YfQVkdxECgYBv3QuUMiBGKWgeheP4nAFU
-SVgqGBQwAtqb5DR1YVJ4LFPt+jyrQMby6mqSlzENYcidSP3Ogn22CvST+bAjbf6D
-D/ae1zeOHBls00ILHANv1Z/hXcEkiKnZdeP6O43xBfCS+Lps5daXgUbC0kw2R09S
-VZTqPHmb+db0rFA3XlAg2A==
------END PRIVATE KEY-----
-)");
- 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();
- fs::remove(testConfigFilename);
- fs::remove(testCertFilename);
- fs::remove(testKeyFilename);
- }
-
- void start()
- {
- if (m_pid != 0)
- throw std::runtime_error("Process already running, so it can't be started");
-
- // connect stdout of new child process to stream of parent, via pipe
- m_pid = fork();
- if (m_pid < 0)
- throw std::runtime_error("Fork unsuccessful.");
-
- if (m_pid == 0) { // child process branch
- char* argv[] = {(char*)"webserver", (char*)"-c", (char*)"./webserver.conf"};
- webserver(sizeof(argv) / sizeof(char*), argv);
- exit(0);
- }
-
- // wait for server to start up
- if (int port{port_from_config(m_config)}; port >= 0)
- wait_for_pid_listening_on(m_pid, port);
- std::this_thread::sleep_for(std::chrono::milliseconds(10));
- }
-
- void stop()
- {
- if (m_pid == 0)
- throw std::runtime_error("Process not running, so it can't be stopped");
-
- if (kill(m_pid, SIGTERM) != 0)
- throw std::runtime_error("Unable to kill process");
-
- if (int result = waitpid(m_pid, NULL, 0); result != m_pid)
- throw std::runtime_error("waitpid returned "s + std::to_string(result));
-
- m_pid = 0;
- m_is = 0;
- m_filebuf = 0;
- }
-
- bool is_running()
- {
- if (m_pid == 0)
- return false;
-
- return Reichwein::Process::is_running(m_pid);
- }
-
- std::string output()
- {
- if (!is_running())
- throw std::runtime_error("No output/stdout available from webserver since it is not running");
-
- if (!m_is)
- throw std::runtime_error("Webserver stdout stream not initialized.");
-
- std::stringstream result;
- std::string buffer(static_cast<std::string::size_type>(1024), '\0');
- int size{};
- while ((size = m_is->readsome(buffer.data(), buffer.size())) > 0)
- result << buffer.substr(0, size);
- return result.str();
- }
-
-private:
- pid_t m_pid;
- std::string m_config;
-
- // child stdout
- std::shared_ptr<__gnu_cxx::stdio_filebuf<char>> m_filebuf;
- std::shared_ptr<std::istream> m_is;
-}; // class WebserverProcess
-
-std::pair<std::string,std::string> HTTP(const std::string& target, bool ipv6 = true, bool HTTP11 = true, boost::beast::http::verb method = boost::beast::http::verb::get)
-{
- auto const host = ipv6 ? "::1" : "127.0.0.1";
- auto const port = "8080";
- int version = HTTP11 ? 11 : 10;
-
- // The io_context is required for all I/O
- boost::asio::io_context ioc;
-
- // These objects perform our I/O
- boost::asio::ip::tcp::resolver resolver(ioc);
- boost::beast::tcp_stream stream(ioc);
-
- // Look up the domain name
- auto const results = resolver.resolve(host, port);
-
- // Make the connection on the IP address we get from a lookup
- stream.connect(results);
-
- // Set up an HTTP GET request message
- boost::beast::http::request<boost::beast::http::string_body> req;
- req.method(method);
- req.target(target);
- req.version(version);
- req.set(boost::beast::http::field::host, host == "::1"s ? "["s + host + "]"s : host);
- req.set(boost::beast::http::field::user_agent, "Webserver Testsuite");
-
- // Send the HTTP request to the remote host
- boost::beast::http::write(stream, req);
-
- // This buffer is used for reading and must be persisted
- boost::beast::flat_buffer buffer;
-
- // Declare a container to hold the response
- boost::beast::http::response<boost::beast::http::dynamic_body> res;
-
- // Receive the HTTP response
- boost::beast::http::read(stream, buffer, res);
-
- // Return value
- std::ostringstream header_stream;
- header_stream << res.base();
- std::ostringstream body_stream;
- body_stream << boost::beast::buffers_to_string(res.body().data());
-
- // Gracefully close the socket
- boost::beast::error_code ec;
- stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
-
- // not_connected happens sometimes
- // so don't bother reporting it.
- //
- if (ec && ec != boost::beast::errc::not_connected)
- throw boost::beast::system_error{ec};
-
- return {header_stream.str(), body_stream.str()};
-}
-
-void load_root_certificates(boost::asio::ssl::context& ctx)
-{
- std::string cert_chain{File::getFile(testCertFilename)};
- ctx.add_certificate_authority(boost::asio::buffer(cert_chain.data(), cert_chain.size()));
-}
-
-std::pair<std::string,std::string> HTTPS(const std::string& target, bool ipv6 = true, bool HTTP11 = true, boost::beast::http::verb method = boost::beast::http::verb::get)
-{
- auto const host = ipv6 ? "::1" : "127.0.0.1";
- auto const port = "8081";
- int version = HTTP11 ? 11 : 10;
-
- // The io_context is required for all I/O
- boost::asio::io_context ioc;
-
- // The SSL context is required, and holds certificates
- boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv13_client);
-
- // This holds the root certificate used for verification
- load_root_certificates(ctx);
-
- // Verify the remote server's certificate
- ctx.set_verify_mode(boost::asio::ssl::verify_peer);
-
- // These objects perform our I/O
- boost::asio::ip::tcp::resolver resolver(ioc);
- boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
-
- // Set SNI Hostname (many hosts need this to handshake successfully)
- if (!SSL_set_tlsext_host_name(stream.native_handle(), host))
- {
- boost::beast::error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
- throw boost::beast::system_error{ec};
- }
-
- // Look up the domain name
- auto const results = resolver.resolve(host, port);
-
- // Make the connection on the IP address we get from a lookup
- boost::beast::get_lowest_layer(stream).connect(results);
-
- // Perform the SSL handshake
- stream.handshake(boost::asio::ssl::stream_base::client);
-
- // Set up an HTTP GET request message
- boost::beast::http::request<boost::beast::http::string_body> req;
- req.method(method);
- req.target(target);
- req.version(version);
- req.set(boost::beast::http::field::host, host == "::1"s ? "["s + host + "]"s : host);
- req.set(boost::beast::http::field::user_agent, "Webserver Testsuite");
-
- // Send the HTTP request to the remote host
- boost::beast::http::write(stream, req);
-
- // This buffer is used for reading and must be persisted
- boost::beast::flat_buffer buffer;
-
- // Declare a container to hold the response
- boost::beast::http::response<boost::beast::http::dynamic_body> res;
-
- // Receive the HTTP response
- boost::beast::http::read(stream, buffer, res);
-
- // Return value
- std::ostringstream header_stream;
- header_stream << res.base();
- std::ostringstream body_stream;
- body_stream << boost::beast::buffers_to_string(res.body().data());
-
- // Gracefully close the stream
- boost::beast::error_code ec;
- stream.shutdown(ec);
- if (ec == boost::asio::error::eof)
- {
- // Rationale:
- // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
- ec = {};
- }
- if (ec)
- throw boost::beast::system_error{ec};
-
- return {header_stream.str(), body_stream.str()};
-}
-
class Fixture
{
public:
@@ -522,172 +108,6 @@ BOOST_DATA_TEST_CASE_F(Fixture, http_get_file_not_found, data::make({false, true
BOOST_REQUIRE_EQUAL(response.second, method == boost::beast::http::verb::head ? "" : "404 Not found: /webserver.confSUFFIX");
}
-// Test server
-class WebsocketServerProcess
-{
- // shared data between Unix processes
- struct shared_data_t {
- std::mutex mutex; // for synchronization between processes (!)
- char subprotocol[1024]{}; // instead of std::string since std::string allocates data on heap
- char target[1024]{};
- };
-
-public:
- WebsocketServerProcess()
- {
- // RAII pattern for shared memory allocation/deallocation
- m_shared = std::unique_ptr<shared_data_t, std::function<void(shared_data_t*)>>(
- (shared_data_t*)mmap(NULL, sizeof(shared_data_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0),
- [this](shared_data_t*){munmap(m_shared.get(), sizeof(shared_data_t));});
- start();
- }
-
- ~WebsocketServerProcess()
- {
- stop();
- }
-
- void do_session(boost::asio::ip::tcp::socket socket)
- {
- try
- {
- // Construct the stream by moving in the socket
- boost::beast::websocket::stream<boost::asio::ip::tcp::socket> ws{std::move(socket)};
-
- // Set a decorator to change the Server of the handshake
- ws.set_option(boost::beast::websocket::stream_base::decorator(
- [](boost::beast::websocket::response_type& res)
- {
- res.set(boost::beast::http::field::server,
- std::string("Reichwein.IT Test Websocket Server"));
- }));
-
- boost::beast::http::request_parser<boost::beast::http::string_body> parser;
- request_type req;
- boost::beast::flat_buffer buffer;
-
- boost::beast::http::read(ws.next_layer(), buffer, parser);
- req = parser.get();
- {
- std::lock_guard lock{m_shared->mutex};
- strncpy(m_shared->subprotocol, std::string{req[http::field::sec_websocket_protocol]}.data(), sizeof(m_shared->subprotocol));
- strncpy(m_shared->target, std::string{req.target()}.data(), sizeof(m_shared->target));
- }
-
- ws.accept(req);
-
- for(;;)
- {
- boost::beast::flat_buffer buffer;
-
- ws.read(buffer);
-
- // Reply with <request>: <counter>
- ws.text(ws.got_text());
- std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
- data += ": " + std::to_string(m_count++);
- buffer.consume(buffer.size());
- boost::beast::ostream(buffer) << data;
- ws.write(buffer.data());
- }
- }
- catch(boost::beast::system_error const& se)
- {
- // This indicates that the session was closed
- if(se.code() != boost::beast::websocket::error::closed)
- std::cerr << "Error: " << se.code().message() << std::endl;
- }
- catch(std::exception const& e)
- {
- std::cerr << "Error: " << e.what() << std::endl;
- }
- }
-
- bool is_running()
- {
- if (m_pid == 0)
- return false;
-
- return Reichwein::Process::is_running(m_pid);
- }
-
- void start()
- {
- if (m_pid != 0)
- throw std::runtime_error("Process already running, so it can't be started");
-
- // connect stdout of new child process to stream of parent, via pipe
- m_pid = fork();
- if (m_pid < 0)
- throw std::runtime_error("Fork unsuccessful.");
-
- if (m_pid == 0) { // child process branch
- try
- {
- auto const address = boost::asio::ip::make_address("::1");
- auto const port = static_cast<unsigned short>(8765);
-
- // The io_context is required for all I/O
- boost::asio::io_context ioc{1};
-
- // The acceptor receives incoming connections
- boost::asio::ip::tcp::acceptor acceptor{ioc, {address, port}};
- for(;;)
- {
- // This will receive the new connection
- boost::asio::ip::tcp::socket socket{ioc};
-
- // Block until we get a connection
- acceptor.accept(socket);
-
- // Launch the session, transferring ownership of the socket
- std::thread(
- &WebsocketServerProcess::do_session, this,
- std::move(socket)).detach();
- }
- }
- catch (const std::exception& e)
- {
- std::cerr << "Error: " << e.what() << std::endl;
- }
- exit(0);
- }
-
- wait_for_pid_listening_on(m_pid, 8765);
- }
-
- void stop()
- {
- if (!is_running())
- throw std::runtime_error("Process not running, so it can't be stopped");
-
- if (kill(m_pid, SIGTERM) != 0)
- throw std::runtime_error("Unable to kill process");
-
- if (int result = waitpid(m_pid, NULL, 0); result != m_pid)
- throw std::runtime_error("waitpid returned "s + std::to_string(result));
-
- m_pid = 0;
- }
-
- std::string subprotocol()
- {
- std::lock_guard lock{m_shared->mutex};
- return m_shared->subprotocol;
- }
-
- std::string target()
- {
- std::lock_guard lock{m_shared->mutex};
- return m_shared->target;
- }
-
-private:
- int m_pid{};
- int m_count{};
- std::unique_ptr<shared_data_t, std::function<void(shared_data_t*)>> m_shared;
-}; // class WebsocketServerProcess
-
BOOST_FIXTURE_TEST_CASE(websocket_ssl, Fixture)
{
std::string webserver_config{R"CONFIG(<webserver>
@@ -1127,3 +547,105 @@ BOOST_FIXTURE_TEST_CASE(incomplete_config, Fixture)
std::this_thread::sleep_for(std::chrono::milliseconds(50));
BOOST_REQUIRE_EQUAL(serverProcess.is_running(), false);
}
+
+BOOST_FIXTURE_TEST_CASE(http_redirect, Fixture)
+{
+ 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>localhost</host>
+ <host>[::1]</host>
+ <path requested="/redirect1">
+ <plugin>redirect</plugin>
+ <target>https://www.reichwein.it/</target>
+ <STATUS_CODE>301</STATUS_CODE>
+ <MESSAGE>Redirecting to reichwein.it ...</MESSAGE>
+ </path>
+ </site>
+ </sites>
+ <sockets>
+ <socket>
+ <address>::1</address>
+ <port>8080</port>
+ <protocol>http</protocol>
+ <site>localhost</site>
+ </socket>
+ </sockets>
+</webserver>)CONFIG"};
+ WebserverProcess serverProcess{webserver_config};
+ BOOST_REQUIRE(serverProcess.is_running());
+
+ auto result {HTTP("/redirect1")};
+ BOOST_CHECK_EQUAL(result.first, fmt::format(
+"HTTP/1.1 301 Moved Permanently\r\n"
+"Server: Reichwein.IT Webserver {}\r\n"
+"Location: https://www.reichwein.it/\r\n"
+"Content-Type: text/html\r\n"
+"Content-Length: {}\r\n"
+"\r\n"
+ , VERSION, result.second.size()));
+ BOOST_CHECK_EQUAL(result.second, "301 Redirecting to reichwein.it ...");
+
+ result = HTTP("/redirect1/path1");
+ BOOST_CHECK_EQUAL(result.first, fmt::format(
+"HTTP/1.1 301 Moved Permanently\r\n"
+"Server: Reichwein.IT Webserver {}\r\n"
+"Location: https://www.reichwein.it/\r\n"
+"Content-Type: text/html\r\n"
+"Content-Length: {}\r\n"
+"\r\n"
+ , VERSION, result.second.size()));
+ BOOST_CHECK_EQUAL(result.second, "301 Redirecting to reichwein.it ...");
+}
+
+BOOST_FIXTURE_TEST_CASE(http_fcgi, Fixture)
+{
+ 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>localhost</host>
+ <host>[::1]</host>
+ <path requested="/fcgi">
+ <plugin>fcgi</plugin>
+ <target>127.0.0.1:8765</target>
+ </path>
+ </site>
+ </sites>
+ <sockets>
+ <socket>
+ <address>::1</address>
+ <port>8080</port>
+ <protocol>http</protocol>
+ <site>localhost</site>
+ </socket>
+ </sockets>
+</webserver>)CONFIG"};
+ WebserverProcess serverProcess{webserver_config};
+ BOOST_REQUIRE(serverProcess.is_running());
+
+ FastCGIProcess fcgiProcess("./fcgi1", "127.0.0.1", 8765);
+ BOOST_REQUIRE(fcgiProcess.is_running());
+
+ auto result {HTTP("/fcgi/abc")};
+ BOOST_CHECK_EQUAL(result.first, fmt::format(
+"HTTP/1.1 200 OK\r\n"
+"Server: Reichwein.IT Webserver {}\r\n"
+"Content-Type: text/plain\r\n"
+"Content-Length: {}\r\n"
+"\r\n"
+ , VERSION, result.second.size()));
+ BOOST_CHECK_EQUAL(result.second, "returning data of : ");
+}
+
diff --git a/tests/webserverprocess.cpp b/tests/webserverprocess.cpp
new file mode 100644
index 0000000..c91275b
--- /dev/null
+++ b/tests/webserverprocess.cpp
@@ -0,0 +1,221 @@
+#include "webserverprocess.h"
+
+#include <boost/test/data/dataset.hpp>
+#include <boost/test/data/monomorphic.hpp>
+#include <boost/test/data/test_case.hpp>
+
+#include <boost/algorithm/string.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/beast/version.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/buffers_iterator.hpp>
+#include <boost/asio/connect.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ssl/error.hpp>
+#include <boost/asio/ssl/stream.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/xml_parser.hpp>
+
+#include <fmt/core.h>
+
+#include <chrono>
+#include <exception>
+#include <filesystem>
+#include <iostream>
+#include <memory>
+#include <mutex>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <thread>
+
+#include <ext/stdio_filebuf.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#include <libreichwein/file.h>
+#include <libreichwein/process.h>
+
+#include "webserver.h"
+#include "helper.h"
+
+using namespace std::string_literals;
+namespace fs = std::filesystem;
+namespace pt = boost::property_tree;
+using namespace boost::unit_test;
+using namespace Reichwein;
+
+void WebserverProcess::init(const std::string& config)
+{
+ m_config = config;
+ File::setFile(testConfigFilename, config);
+
+ // test self signed certificate
+ File::setFile(testCertFilename, R"(-----BEGIN CERTIFICATE-----
+MIIC4zCCAcugAwIBAgIUeS9y+EsFWxf+foEx6SJ/R56rmX8wDQYJKoZIhvcNAQEL
+BQAwADAgFw0yMzAxMDYxNzIwNTFaGA8yMDUwMDUyNDE3MjA1MVowADCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBALiZSICAcXng9j7zAb873U4TpuzvRVfh
+xS3gEhxqNPs6+ZQ43nAxDSdafzfGxpTkElTt/REj4oEOLw+QWI/jfbe4gDRDzf6V
+ij0fVuzp02JtJSS+dNrLv17NufBydOyD8oDrPehVrPlrZQhhkYMvLHAim+wikT2O
+s0es2R+avixxAZvx5EYgHba9T7R/pC/lA4BI3lEbVKjDA83hZvjPH1YdK+RYQS2g
+Jygdhe8qOSswXIwFAF3MMBpwRD3mz+vAJZP3lpBGsn+asO6Xd/5cjC8msgomS8Ji
+c9DMMNlrE1WU73wVG9n0OJcke2XEtzARVKJLlBPsug4oxDev6O4GakkCAwEAAaNT
+MFEwHQYDVR0OBBYEFE4i7Gtyn30qpIkH6f0/wuFA45pjMB8GA1UdIwQYMBaAFE4i
+7Gtyn30qpIkH6f0/wuFA45pjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
+BQADggEBAIlGv4b2yLmTOrXOPNst2y3J+GiRvuMKoAfDt5KLxUhbCmPgJzGDWn0l
+60xXBX/t2uo3dQa9yAIW64RqhEQX7uja/7B3PmJZlgF7+owvT8OZA4+UN1lLUvY4
+V7mUzuKuqo5jcX8EmZnHrJ4TGZ0dXbT1hAUgqIjnDChjWyvs4B9zZL5FTisPUic7
+MU+FcpKJ5M6iJ150d9hzLiwmJyPLkW5Grq0Jh22njUQwWW2vIMn4cA3CyS64+oi2
+DNnDgde3mYxXL8Oki7CbeCTpmUXcBHmQtWOvKZPCsOzMF4moTLC4DdElvOpwKCAK
+ABd6rubkarwvDV7wEo1eSuAHPZ/KhGo=
+-----END CERTIFICATE-----
+)");
+ File::setFile(testKeyFilename, R"(-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC4mUiAgHF54PY+
+8wG/O91OE6bs70VX4cUt4BIcajT7OvmUON5wMQ0nWn83xsaU5BJU7f0RI+KBDi8P
+kFiP4323uIA0Q83+lYo9H1bs6dNibSUkvnTay79ezbnwcnTsg/KA6z3oVaz5a2UI
+YZGDLyxwIpvsIpE9jrNHrNkfmr4scQGb8eRGIB22vU+0f6Qv5QOASN5RG1SowwPN
+4Wb4zx9WHSvkWEEtoCcoHYXvKjkrMFyMBQBdzDAacEQ95s/rwCWT95aQRrJ/mrDu
+l3f+XIwvJrIKJkvCYnPQzDDZaxNVlO98FRvZ9DiXJHtlxLcwEVSiS5QT7LoOKMQ3
+r+juBmpJAgMBAAECggEACjs5suCbmYAb2d2VZlRitdP+Q6HX37D0YTBrBI7o6JdK
+U7oqrwBy/JBGHpDqewBgmTs3FGr/H/zJpDTRickXs9X6qhrreQ2wA6b/5gHoPMt0
+nHKfbqyOCuq/YGmxnBXMnDNdoynTfGAE0af5vRIBZiYu6vG4B9fHzURR5O/qVDNn
+WqJ+2Y8AAf4mJDCBEvJz2RaZwSq788i/d8oTSeCk93TDF+GHhUq6ymkORACj76ws
+8ohfoNQIG1VhdcTK2GOQjqctEFUm54t9N1nxD6VavMu5DlSSVsPTDbYuE5U4cy3T
+ThDtoYJgwz5KRflklwl3xoDJVx3B5wMUaqviRp6l0QKBgQC6MPG+EV2drsHwG0Wg
+gnP4uCSuFAfWBlHAQyZv5PMQBNfM8YjyMyL+O7cggGNJSOZr/X30EqoBe+LXrL3X
+Gtix8F1Ed0fbAarAgxIwq8MktzmastDq4XS+zwYPZ7UTbmbqvT3VYPga4Sh90fyY
+nPJpqZvhvGzQX22yeHS7vTSQOQKBgQD9z36EIYMuLl0HJK6gfjGHsy/Rx7bw1TmP
+aHmuF8Ra7rpDSOym0ImKWTOLEoLlQUsMz/FuVLCGP/ACjMFKsqh3Zy/0hVJOMDMR
+Z+ODT28Hcz4AMcTYDvcTYd70HhhZL+/eFCVk8Nk164saMuhifAkOgvwfaYs0m3ue
+S9jxlZKKkQKBgHgBzf6k8MMOfaAF4/XVv2wDPFkbPgW74vtaDK84UVX02ScWUx9Q
+yHA3Cwye09/LZgEazREA6qS0NfyvMVkwy5S9CVB01VKam3UjxhiqzMegdTd5o+CQ
+WpAVnaFWRcb1dM4+FVmv+5pPn6qhKv8uwaxLDtcLfNM9ftX2f77176g5AoGADWtQ
+DBpdfi6TWpJU7UVexwbxS00c3gTYAz4J2OuGxSwECxSq9nLmIrtunza+VvKpziac
+ZDH0F1UAEpJwkct6Xr3E6k+2N04TFSOCAupLO4CbUZVQDABWjd7J0+xXaze+neZA
+x+J4CYLHmv4ADVzzeaHxRJPm+UQTOB5YfQVkdxECgYBv3QuUMiBGKWgeheP4nAFU
+SVgqGBQwAtqb5DR1YVJ4LFPt+jyrQMby6mqSlzENYcidSP3Ogn22CvST+bAjbf6D
+D/ae1zeOHBls00ILHANv1Z/hXcEkiKnZdeP6O43xBfCS+Lps5daXgUbC0kw2R09S
+VZTqPHmb+db0rFA3XlAg2A==
+-----END PRIVATE KEY-----
+)");
+ start();
+}
+
+WebserverProcess::WebserverProcess(const std::string& config): m_pid{}
+{
+ init(config);
+}
+
+WebserverProcess::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::~WebserverProcess()
+{
+ stop();
+ fs::remove(testConfigFilename);
+ fs::remove(testCertFilename);
+ fs::remove(testKeyFilename);
+}
+
+void WebserverProcess::start()
+{
+ if (m_pid != 0)
+ throw std::runtime_error("Process already running, so it can't be started");
+
+ m_pid = fork();
+ if (m_pid < 0)
+ throw std::runtime_error("Fork unsuccessful.");
+
+ if (m_pid == 0) { // child process branch
+ char* argv[] = {(char*)"webserver", (char*)"-c", (char*)"./webserver.conf"};
+ webserver(sizeof(argv) / sizeof(char*), argv);
+ exit(0);
+ }
+
+ // wait for server to start up
+ if (int port{port_from_config(m_config)}; port >= 0)
+ wait_for_pid_listening_on(m_pid, port);
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+}
+
+void WebserverProcess::stop()
+{
+ if (m_pid == 0)
+ throw std::runtime_error("Process not running, so it can't be stopped");
+
+ if (kill(m_pid, SIGTERM) != 0)
+ throw std::runtime_error("Unable to kill process");
+
+ if (int result = waitpid(m_pid, NULL, 0); result != m_pid)
+ throw std::runtime_error("waitpid returned "s + std::to_string(result));
+
+ m_pid = 0;
+}
+
+bool WebserverProcess::is_running()
+{
+ if (m_pid == 0)
+ return false;
+
+ return Reichwein::Process::is_running(m_pid);
+}
+
diff --git a/tests/webserverprocess.h b/tests/webserverprocess.h
new file mode 100644
index 0000000..5593c70
--- /dev/null
+++ b/tests/webserverprocess.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <string>
+
+#include <ext/stdio_filebuf.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+class WebserverProcess
+{
+private:
+ void init(const std::string& config);
+public:
+ WebserverProcess(const std::string& config);
+ WebserverProcess();
+ ~WebserverProcess();
+ void start();
+ void stop();
+ bool is_running();
+ std::string output();
+
+private:
+ pid_t m_pid;
+ std::string m_config;
+
+}; // class WebserverProcess
+
diff --git a/tests/websocketserverprocess.cpp b/tests/websocketserverprocess.cpp
new file mode 100644
index 0000000..89a50ee
--- /dev/null
+++ b/tests/websocketserverprocess.cpp
@@ -0,0 +1,205 @@
+#include "websocketserverprocess.h"
+
+#include <boost/test/data/dataset.hpp>
+#include <boost/test/data/monomorphic.hpp>
+#include <boost/test/data/test_case.hpp>
+
+#include <boost/algorithm/string.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/beast/version.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/buffers_iterator.hpp>
+#include <boost/asio/connect.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ssl/error.hpp>
+#include <boost/asio/ssl/stream.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/xml_parser.hpp>
+
+#include <fmt/core.h>
+
+#include <chrono>
+#include <exception>
+#include <filesystem>
+#include <iostream>
+#include <memory>
+#include <mutex>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <thread>
+
+#include <ext/stdio_filebuf.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#include <libreichwein/file.h>
+#include <libreichwein/process.h>
+
+#include "webserver.h"
+#include "response.h"
+
+#include "helper.h"
+
+using namespace std::string_literals;
+namespace fs = std::filesystem;
+namespace pt = boost::property_tree;
+using namespace boost::unit_test;
+using namespace Reichwein;
+
+WebsocketServerProcess::WebsocketServerProcess()
+{
+ // RAII pattern for shared memory allocation/deallocation
+ m_shared = std::unique_ptr<shared_data_t, std::function<void(shared_data_t*)>>(
+ (shared_data_t*)mmap(NULL, sizeof(shared_data_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0),
+ [this](shared_data_t*){munmap(m_shared.get(), sizeof(shared_data_t));});
+ start();
+}
+
+WebsocketServerProcess::~WebsocketServerProcess()
+{
+ stop();
+}
+
+void WebsocketServerProcess::do_session(boost::asio::ip::tcp::socket socket)
+{
+ try
+ {
+ // Construct the stream by moving in the socket
+ boost::beast::websocket::stream<boost::asio::ip::tcp::socket> ws{std::move(socket)};
+
+ // Set a decorator to change the Server of the handshake
+ ws.set_option(boost::beast::websocket::stream_base::decorator(
+ [](boost::beast::websocket::response_type& res)
+ {
+ res.set(boost::beast::http::field::server,
+ std::string("Reichwein.IT Test Websocket Server"));
+ }));
+
+ boost::beast::http::request_parser<boost::beast::http::string_body> parser;
+ request_type req;
+ boost::beast::flat_buffer buffer;
+
+ boost::beast::http::read(ws.next_layer(), buffer, parser);
+ req = parser.get();
+ {
+ std::lock_guard lock{m_shared->mutex};
+ strncpy(m_shared->subprotocol, std::string{req[http::field::sec_websocket_protocol]}.data(), sizeof(m_shared->subprotocol));
+ strncpy(m_shared->target, std::string{req.target()}.data(), sizeof(m_shared->target));
+ }
+
+ ws.accept(req);
+
+ for(;;)
+ {
+ boost::beast::flat_buffer buffer;
+
+ ws.read(buffer);
+
+ // Reply with <request>: <counter>
+ ws.text(ws.got_text());
+ std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
+ data += ": " + std::to_string(m_count++);
+ buffer.consume(buffer.size());
+ boost::beast::ostream(buffer) << data;
+ ws.write(buffer.data());
+ }
+ }
+ catch(boost::beast::system_error const& se)
+ {
+ // This indicates that the session was closed
+ if(se.code() != boost::beast::websocket::error::closed)
+ std::cerr << "Error: " << se.code().message() << std::endl;
+ }
+ catch(std::exception const& e)
+ {
+ std::cerr << "Error: " << e.what() << std::endl;
+ }
+}
+
+bool WebsocketServerProcess::is_running()
+{
+ if (m_pid == 0)
+ return false;
+
+ return Reichwein::Process::is_running(m_pid);
+}
+
+void WebsocketServerProcess::start()
+{
+ if (m_pid != 0)
+ throw std::runtime_error("Process already running, so it can't be started");
+
+ // connect stdout of new child process to stream of parent, via pipe
+ m_pid = fork();
+ if (m_pid < 0)
+ throw std::runtime_error("Fork unsuccessful.");
+
+ if (m_pid == 0) { // child process branch
+ try
+ {
+ auto const address = boost::asio::ip::make_address("::1");
+ auto const port = static_cast<unsigned short>(8765);
+
+ // The io_context is required for all I/O
+ boost::asio::io_context ioc{1};
+
+ // The acceptor receives incoming connections
+ boost::asio::ip::tcp::acceptor acceptor{ioc, {address, port}};
+ for(;;)
+ {
+ // This will receive the new connection
+ boost::asio::ip::tcp::socket socket{ioc};
+
+ // Block until we get a connection
+ acceptor.accept(socket);
+
+ // Launch the session, transferring ownership of the socket
+ std::thread(
+ &WebsocketServerProcess::do_session, this,
+ std::move(socket)).detach();
+ }
+ }
+ catch (const std::exception& e)
+ {
+ std::cerr << "Error: " << e.what() << std::endl;
+ }
+ exit(0);
+ }
+
+ wait_for_pid_listening_on(m_pid, 8765);
+}
+
+void WebsocketServerProcess::stop()
+{
+ if (!is_running())
+ throw std::runtime_error("Process not running, so it can't be stopped");
+
+ if (kill(m_pid, SIGTERM) != 0)
+ throw std::runtime_error("Unable to kill process");
+
+ if (int result = waitpid(m_pid, NULL, 0); result != m_pid)
+ throw std::runtime_error("waitpid returned "s + std::to_string(result));
+
+ m_pid = 0;
+}
+
+std::string WebsocketServerProcess::subprotocol()
+{
+ std::lock_guard lock{m_shared->mutex};
+ return m_shared->subprotocol;
+}
+
+std::string WebsocketServerProcess::target()
+{
+ std::lock_guard lock{m_shared->mutex};
+ return m_shared->target;
+}
+
diff --git a/tests/websocketserverprocess.h b/tests/websocketserverprocess.h
new file mode 100644
index 0000000..74d7064
--- /dev/null
+++ b/tests/websocketserverprocess.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <boost/algorithm/string.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/beast/version.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/buffers_iterator.hpp>
+#include <boost/asio/connect.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ssl/error.hpp>
+#include <boost/asio/ssl/stream.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/xml_parser.hpp>
+
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <string>
+
+class WebsocketServerProcess
+{
+ // shared data between Unix processes
+ struct shared_data_t {
+ std::mutex mutex; // for synchronization between processes (!)
+ char subprotocol[1024]{}; // instead of std::string since std::string allocates data on heap
+ char target[1024]{};
+ };
+
+public:
+ WebsocketServerProcess();
+ ~WebsocketServerProcess();
+
+ bool is_running();
+ void start();
+ void stop();
+ std::string subprotocol();
+ std::string target();
+
+private:
+ void do_session(boost::asio::ip::tcp::socket socket);
+
+private:
+ int m_pid{};
+ int m_count{};
+ std::unique_ptr<shared_data_t, std::function<void(shared_data_t*)>> m_shared;
+}; // class WebsocketServerProcess
+
+
+