diff options
author | Roland Reichwein <mail@reichwein.it> | 2023-01-12 20:00:40 +0100 |
---|---|---|
committer | Roland Reichwein <mail@reichwein.it> | 2023-01-12 20:00:40 +0100 |
commit | 702d32b41c1c4f496dba046c2017cb5b907e55cd (patch) | |
tree | 271a48cc1dc9ef1d2fcc846ed4bebbcdd24242e2 | |
parent | 124646fe2a31b7211d12fb043fcb760cbe7313b0 (diff) |
FCGI test
-rw-r--r-- | TODO | 1 | ||||
-rw-r--r-- | debian/control | 2 | ||||
-rw-r--r-- | debian/webserver.example-redirect.conf | 28 | ||||
-rw-r--r-- | tests/Makefile | 16 | ||||
-rw-r--r-- | tests/fastcgiprocess.cpp | 115 | ||||
-rw-r--r-- | tests/fastcgiprocess.h | 28 | ||||
-rw-r--r-- | tests/fcgi1.cpp | 89 | ||||
-rw-r--r-- | tests/helper.cpp | 228 | ||||
-rw-r--r-- | tests/helper.h | 56 | ||||
-rw-r--r-- | tests/test-webserver.cpp | 692 | ||||
-rw-r--r-- | tests/webserverprocess.cpp | 221 | ||||
-rw-r--r-- | tests/webserverprocess.h | 30 | ||||
-rw-r--r-- | tests/websocketserverprocess.cpp | 205 | ||||
-rw-r--r-- | tests/websocketserverprocess.h | 53 | ||||
-rw-r--r-- | webserver.conf | 3 |
15 files changed, 1174 insertions, 593 deletions
@@ -2,7 +2,6 @@ example conf files: - php test: - FCGI -- Redirect Big file bug - dynamic plugin interface (file buffer, ...) diff --git a/debian/control b/debian/control index 2ffe331..f2cb6b4 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: webserver Section: httpd Priority: optional Maintainer: Roland Reichwein <mail@reichwein.it> -Build-Depends: debhelper (>= 12), libssl-dev, libboost-all-dev | libboost1.71-all-dev, clang | g++, llvm | g++, lld | g++, libc++-dev | g++, libc++abi-dev | g++, uglifyjs, python3-pkg-resources, htmlmin, cleancss, libreichwein-dev, gcovr, libfmt-dev, pkg-config +Build-Depends: debhelper (>= 12), libssl-dev, libboost-all-dev | libboost1.71-all-dev, clang | g++, llvm | g++, lld | g++, libc++-dev | g++, libc++abi-dev | g++, uglifyjs, python3-pkg-resources, htmlmin, cleancss, libreichwein-dev, gcovr, libfmt-dev, pkg-config, libfcgi-dev Standards-Version: 4.5.0 Homepage: http://www.reichwein.it/webserver/ diff --git a/debian/webserver.example-redirect.conf b/debian/webserver.example-redirect.conf new file mode 100644 index 0000000..689dfa8 --- /dev/null +++ b/debian/webserver.example-redirect.conf @@ -0,0 +1,28 @@ +<webserver> + <user>www-data</user> + <group>www-data</group> + <threads>10</threads> + <statisticspath>/var/lib/webserver/stats.db</statisticspath> + <plugin-directory>/usr/lib/webserver/plugins</plugin-directory> + <sites> + <site> + <name>localhost</name> + <host>localhost</host> + <host>[::1]</host> + <path requested="/redirect1"> + <plugin>redirect</plugin> + <target>https://www.antcom.de/</target> + <STATUS_CODE>301</STATUS_CODE> + <MESSAGE>Redirecting to antcom.de ...</MESSAGE> + </path> + </site> + </sites> + <sockets> + <socket> + <address>::1</address> + <port>8080</port> + <protocol>http</protocol> + <site>localhost</site> + </socket> + </sockets> +</webserver> 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 + + + diff --git a/webserver.conf b/webserver.conf index 53b48ce..a00549f 100644 --- a/webserver.conf +++ b/webserver.conf @@ -65,7 +65,8 @@ </path> <path requested="/php"> <plugin>fcgi</plugin> - <target>/run/php/php-fpm.sock</target> + <!-- <target>/run/php/php-fpm.sock</target> --> + <target>127.0.0.1:9021</target> </path> <path requested="/cgi-bin/admin/echo.fcgi"> |