diff --git a/.gitignore b/.gitignore index 802473c..341fa4b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ debug/ release/ docs/api docs/conf.py +src/revision.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f6c4009..63ebbad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.22) -project(libzeep VERSION 6.0.13 LANGUAGES CXX) +project(libzeep VERSION 6.0.14 LANGUAGES CXX) list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) @@ -247,9 +247,9 @@ if(ZEEP_BUILD_LIBRARY) target_link_options(zeep PRIVATE -undefined dynamic_lookup) endif() - # Build debug library with d postfix - set(CMAKE_DEBUG_POSTFIX d) - set_target_properties(zeep PROPERTIES DEBUG_POSTFIX "d") + # # Build debug library with d postfix + # set(CMAKE_DEBUG_POSTFIX d) + # set_target_properties(zeep PROPERTIES DEBUG_POSTFIX "d") # Install rules install(TARGETS zeep diff --git a/changelog b/changelog index 540a9fc..669623c 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,8 @@ +Version 6.0.14 +- Added variant of http::daemon with forking a daemon but + no preforked children. Simply a single instance with + multiple threads. + Version 6.0.13 - Flush access log after each request. (replacing all instances of std::endl was a bit too drastic). diff --git a/include/zeep/http/daemon.hpp b/include/zeep/http/daemon.hpp index c6cbc8a..d587cd6 100644 --- a/include/zeep/http/daemon.hpp +++ b/include/zeep/http/daemon.hpp @@ -62,7 +62,7 @@ class daemon m_restart_time_window = within_nr_of_seconds; } - /// \brief Start the daemon, forking off in the background + /// \brief Start the daemon, forking off in the background with multiple preforked servers /// /// \param address The address to bind to /// \param port The port number to bind to @@ -74,6 +74,17 @@ class daemon int start(const std::string& address, uint16_t port, size_t nr_of_procs, size_t nr_of_threads, const std::string& run_as_user); + /// \brief Start the daemon, forking off in the background with single process + /// + /// \param address The address to bind to + /// \param port The port number to bind to + /// \param nr_of_threads The number of threads to pass to the server class + /// \param run_as_user The user to run the forked process. Daemons are usually + /// started as root and should drop their privileges as soon + /// as possible. + int start(const std::string& address, uint16_t port, size_t nr_of_threads, + const std::string& run_as_user); + /// \brief Stop a running daemon process. Returns 0 in case of successfully stopping a process. int stop(); @@ -92,8 +103,9 @@ class daemon private: - void daemonize(); + int daemonize(); void open_log_file(); + bool run_main_loop(const std::string& address, uint16_t port, size_t nr_of_procs, size_t nr_of_threads, const std::string& run_as_user); diff --git a/lib-http/src/daemon.cpp b/lib-http/src/daemon.cpp index c949905..fd57e3c 100644 --- a/lib-http/src/daemon.cpp +++ b/lib-http/src/daemon.cpp @@ -8,9 +8,9 @@ // Utilitie routines to build daemon processes #ifndef _WIN32 -#include -#include -#include +# include +# include +# include #endif #include @@ -124,7 +124,7 @@ bool daemon::pid_is_for_executable() return false; } -void daemon::daemonize() +int daemon::daemonize() { assert(false); } @@ -150,7 +150,7 @@ int daemon::start(const std::string &address, uint16_t port, size_t nr_of_procs, fs::path pidDir = fs::path(m_pid_file).parent_path(); if (not fs::is_directory(pidDir, ec)) fs::create_directories(pidDir, ec); - + if (ec) std::clog << "Creating directory for pid file failed: " << ec.message() << '\n'; @@ -197,8 +197,10 @@ int daemon::start(const std::string &address, uint16_t port, size_t nr_of_procs, throw std::runtime_error(std::string("Is server running already? ") + e.what()); } - daemonize(); - + int pid = daemonize(); + if (pid != 0) // parent process, we can simply exit now + _exit(0); + for (;;) { bool hupped = run_main_loop(address, port, nr_of_procs, nr_of_threads, run_as_user); @@ -207,14 +209,13 @@ int daemon::start(const std::string &address, uint16_t port, size_t nr_of_procs, std::clog << "Server was interrupted, will attempt to resume\n"; continue; } - + break; } - if (fs::exists(m_pid_file, ec)) fs::remove(m_pid_file, ec); - + if (ec) std::clog << "Removing pid file failed: " << ec.message() << '\n'; } @@ -222,6 +223,157 @@ int daemon::start(const std::string &address, uint16_t port, size_t nr_of_procs, return result; } +int daemon::start(const std::string &address, uint16_t port, size_t nr_of_threads, const std::string &run_as_user) +{ + int result = 0; + + if (pid_is_for_executable()) + { + std::clog << "Server is already running.\n"; + result = 1; + } + else + { + std::error_code ec; + + if (fs::exists(m_pid_file, ec)) + fs::remove(m_pid_file, ec); + + fs::path pidDir = fs::path(m_pid_file).parent_path(); + if (not fs::is_directory(pidDir, ec)) + fs::create_directories(pidDir, ec); + + if (ec) + std::clog << "Creating directory for pid file failed: " << ec.message() << '\n'; + + fs::path outLogDir = fs::path(m_stdout_log_file).parent_path(); + if (not fs::is_directory(outLogDir, ec)) + fs::create_directories(outLogDir, ec); + + if (ec) + std::clog << "Creating directory " << outLogDir << " for log files failed: " << ec.message() << '\n'; + + fs::path errLogDir = fs::path(m_stderr_log_file).parent_path(); + if (not fs::is_directory(errLogDir, ec)) + fs::create_directories(errLogDir, ec); + + if (ec) + std::clog << "Creating directory " << outLogDir << " for log files failed: " << ec.message() << '\n'; + + try + { + asio_ns::io_context io_context; + + asio_ns::ip::tcp::endpoint endpoint; + try + { + endpoint = asio_ns::ip::tcp::endpoint(asio_ns::ip::make_address(address), port); + } + catch (const std::exception &e) + { + asio_ns::ip::tcp::resolver resolver(io_context); + asio_ns::ip::tcp::resolver::query query(address, std::to_string(port)); + endpoint = *resolver.resolve(query); + } + + asio_ns::ip::tcp::acceptor acceptor(io_context); + acceptor.open(endpoint.protocol()); + acceptor.set_option(asio_ns::ip::tcp::acceptor::reuse_address(true)); + acceptor.bind(endpoint); + acceptor.listen(); + + acceptor.close(); + } + catch (exception &e) + { + throw std::runtime_error(std::string("Is server running already? ") + e.what()); + } + + int pid = daemonize(); + if (pid == 0) // Child process + { + // Start listening now that we still have the rights + + open_log_file(); + + std::clog << "starting server\n" + << "Listening to " << address << ':' << port << '\n'; + + signal_catcher sc; + sc.block(); + + std::unique_ptr server; + try + { + server.reset(m_factory()); + server->bind(address, port); + } + catch (const exception &e) + { + std::clog << "Failed to launch server: " << e.what() << '\n'; + exit(1); + } + + // Drop privileges + if (not run_as_user.empty()) + { + struct passwd *pw = getpwnam(run_as_user.c_str()); + if (pw == NULL) + { + std::clog << "Failed to set uid to " << run_as_user << ": " << strerror(errno) << '\n'; + exit(1); + } + + int ngroups = 0; + if (getgrouplist(pw->pw_name, pw->pw_gid, nullptr, &ngroups) == -1 and ngroups > 0) + { + std::vector groups(ngroups); + if (getgrouplist(pw->pw_name, pw->pw_gid, groups.data(), &ngroups) != -1 and + setgroups(ngroups, groups.data()) == -1) + { + std::clog << "Failed to set groups for " << run_as_user << ": " << strerror(errno) << '\n'; + exit(1); + } + } + + if (setgid(pw->pw_gid) < 0) + { + std::clog << "Failed to set gid for " << run_as_user << ": " << strerror(errno) << '\n'; + exit(1); + } + + if (setuid(pw->pw_uid) < 0) + { + std::clog << "Failed to set uid to " << run_as_user << ": " << strerror(errno) << '\n'; + exit(1); + } + } + + std::thread t([nr_of_threads, &server]() + { server->run(nr_of_threads); }); + + sc.unblock(); + sc.wait(); + + server->stop(); + + if (t.joinable()) + t.join(); + + if (fs::exists(m_pid_file, ec)) + fs::remove(m_pid_file, ec); + + if (ec) + std::clog << "Removing pid file failed: " << ec.message() << '\n'; + + // We're done. Exit + _exit(0); + } + } + + return result; +} + int daemon::stop() { int result = 1; @@ -296,7 +448,7 @@ int daemon::reload() return result; } -void daemon::daemonize() +int daemon::daemonize() { int pid = fork(); @@ -308,7 +460,7 @@ void daemon::daemonize() // exit the parent (=calling) process if (pid != 0) - _exit(0); + return pid; if (setsid() < 0) { @@ -348,6 +500,8 @@ void daemon::daemonize() // close stdin close(STDIN_FILENO); open("/dev/null", O_RDONLY); + + return 0; } void daemon::open_log_file() @@ -407,7 +561,7 @@ bool daemon::run_main_loop(const std::string &address, uint16_t port, size_t nr_ sigfillset(&new_mask); pthread_sigmask(SIG_BLOCK, &new_mask, &old_mask); - preforked_server server([=,this]() + preforked_server server([=, this]() { try { diff --git a/lib-http/test/http-test.cpp b/lib-http/test/http-test.cpp index 49220bb..491650a 100644 --- a/lib-http/test/http-test.cpp +++ b/lib-http/test/http-test.cpp @@ -170,6 +170,54 @@ BOOST_AUTO_TEST_CASE(webapp_7) t.join(); } +// Single process variant + +BOOST_AUTO_TEST_CASE(webapp_8) +{ + // start up a http server and stop it again + + zh::daemon d([]() { + auto s = new zh::server; + s->add_controller(new my_controller()); + return s; + }, "/tmp/libzeep-tests/zeep-http-test.pid", + "/tmp/libzeep-tests/zeep-http-test-access.log", + "/tmp/libzeep-tests/zeep-http-test-error.log"); + + std::random_device rng; + uint16_t port = 1024 + (rng() % 10240); + + d.start("::", port, 1, ""); + + std::clog << "started daemon for test_8 at port " << port << '\n'; + + using namespace std::chrono_literals; + std::this_thread::sleep_for(1s); + + try + { + auto reply = simple_request(port, "GET / HTTP/1.0\r\n\r\n"); + BOOST_TEST(reply.get_status() == zh::not_found); + + reply = simple_request(port, "XXX / HTTP/1.0\r\n\r\n"); + BOOST_TEST(reply.get_status() == zh::bad_request); + + reply = simple_request(port, "GET /test/one HTTP/1.0\r\n\r\n"); + BOOST_TEST(reply.get_status() == zh::ok); + + reply = simple_request(port, "GET /test/two HTTP/1.0\r\n\r\n"); + BOOST_TEST(reply.get_status() == zh::not_found); + } + catch (const std::exception& e) + { + std::clog << e.what() << '\n'; + BOOST_TEST_FAIL("Failed with exception"); + throw; + } + + d.stop(); +} + // authentication test BOOST_AUTO_TEST_CASE(server_with_security_1) { diff --git a/lib-xml/test/unit-test.cpp b/lib-xml/test/unit-test.cpp index 06afdd9..5e1217e 100644 --- a/lib-xml/test/unit-test.cpp +++ b/lib-xml/test/unit-test.cpp @@ -168,7 +168,7 @@ BOOST_AUTO_TEST_CASE(xml_container_and_iterators) zx::element e("test"); zx::element n("a"); - e.insert(e.begin(), move(n)); + e.insert(e.begin(), std::move(n)); e.back().set_content("aap "); e.emplace_back("b").set_content("noot "); @@ -345,7 +345,7 @@ BOOST_AUTO_TEST_CASE(xml_doc) zx::document doc; zx::element e("test", { { "a", "1" }, { "b", "2" } }); - doc.push_back(move(e)); + doc.push_back(std::move(e)); zx::document doc2(R"()");