From bd0a98ba5a39e764feb66cf0f5e323b5537022fe Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 18 Dec 2018 12:14:09 -0500 Subject: [PATCH] lokinet-bootserv --- contrib/bootserv/.gitignore | 1 + .../config/lokinet-bootserv-nginx.conf | 29 +++ contrib/bootserv/config/lokinet-bootserv.ini | 4 + contrib/bootserv/makefile | 20 ++ contrib/bootserv/readme.md | 35 ++++ contrib/bootserv/src/cgi.cpp | 171 ++++++++++++++++++ contrib/bootserv/src/handler.hpp | 43 +++++ contrib/bootserv/src/lokinet-cgi.hpp | 31 ++++ contrib/bootserv/src/lokinet-config.cpp | 132 ++++++++++++++ contrib/bootserv/src/lokinet-config.hpp | 47 +++++ contrib/bootserv/src/lokinet-cron.cpp | 37 ++++ contrib/bootserv/src/lokinet-cron.hpp | 25 +++ contrib/bootserv/src/main.cpp | 60 ++++++ 13 files changed, 635 insertions(+) create mode 100644 contrib/bootserv/.gitignore create mode 100644 contrib/bootserv/config/lokinet-bootserv-nginx.conf create mode 100644 contrib/bootserv/config/lokinet-bootserv.ini create mode 100644 contrib/bootserv/makefile create mode 100644 contrib/bootserv/readme.md create mode 100644 contrib/bootserv/src/cgi.cpp create mode 100644 contrib/bootserv/src/handler.hpp create mode 100644 contrib/bootserv/src/lokinet-cgi.hpp create mode 100644 contrib/bootserv/src/lokinet-config.cpp create mode 100644 contrib/bootserv/src/lokinet-config.hpp create mode 100644 contrib/bootserv/src/lokinet-cron.cpp create mode 100644 contrib/bootserv/src/lokinet-cron.hpp create mode 100644 contrib/bootserv/src/main.cpp diff --git a/contrib/bootserv/.gitignore b/contrib/bootserv/.gitignore new file mode 100644 index 000000000..0c119ef3e --- /dev/null +++ b/contrib/bootserv/.gitignore @@ -0,0 +1 @@ +lokinet-bootserv \ No newline at end of file diff --git a/contrib/bootserv/config/lokinet-bootserv-nginx.conf b/contrib/bootserv/config/lokinet-bootserv-nginx.conf new file mode 100644 index 000000000..4fe24ee47 --- /dev/null +++ b/contrib/bootserv/config/lokinet-bootserv-nginx.conf @@ -0,0 +1,29 @@ +# replace your.server.tld with your server's fqdn + +server { + listen 80; + server_name your.server.tld; + location / { + return 302 https://your.server.tld$request_uri; + } + location /.well-known/acme-challenge { + root /var/www/letsencrypt; + } +} + +server { + listen 443 ssl; + server_name your.server.tld; + ssl_certificate /etc/letsencrypt/live/your.server.tld/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/your.server.tld/privkey.pem; + + location / { + root /var/www/lokinet-bootserv; + } + + location /bootstrap.signed { + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME /usr/local/bin/lokinet-bootserv; + fastcgi_pass unix://tmp/cgi.sock; + } +} \ No newline at end of file diff --git a/contrib/bootserv/config/lokinet-bootserv.ini b/contrib/bootserv/config/lokinet-bootserv.ini new file mode 100644 index 000000000..1c1eda45b --- /dev/null +++ b/contrib/bootserv/config/lokinet-bootserv.ini @@ -0,0 +1,4 @@ + +# set me to where the nodedb is for lokinet +#[nodedb] +#dir=/path/to/nodedb/ diff --git a/contrib/bootserv/makefile b/contrib/bootserv/makefile new file mode 100644 index 000000000..081016c40 --- /dev/null +++ b/contrib/bootserv/makefile @@ -0,0 +1,20 @@ + +SRC = $(sort $(wildcard src/*.cpp)) +OBJS = $(SRC:.cpp=.cpp.o) + +CGI_EXE = lokinet-bootserv + +CXX = clang++ + +all: build + +build: $(CGI_EXE) + +%.cpp.o: %.cpp + $(CXX) -g -std=c++17 -c -Wall -Werror -Wpedantic $^ -o $^.o + +$(CGI_EXE): $(OBJS) + $(CXX) -o $(CGI_EXE) $^ + +clean: + rm -f $(CGI_EXE) $(OBJS) diff --git a/contrib/bootserv/readme.md b/contrib/bootserv/readme.md new file mode 100644 index 000000000..572ba50f8 --- /dev/null +++ b/contrib/bootserv/readme.md @@ -0,0 +1,35 @@ +# lokinet-bootserv + +cgi executable for serving a random RC for bootstrap from a nodedb + +## configuring + +copy the example config (privileged) + + # cp configs/lokinet-bootserv.ini /usr/local/etc/lokinet-bootserv.ini + +edit config to have proper values, +specifically make sure the `[nodedb]` section has a `dir` value that points to a static copy of a healthy nodedb + +## building + +to build: + + $ make + +## installing (priviledged) + +install cgi binary: + + # cp lokinet-bootserv /usr/local/bin/lokinet-bootserv + +set up with nginx cgi: + + # cp configs/lokinet-bootserv-nginx.conf /etc/nginx/sites-available/lokinet-bootserv.conf + # ln -s /etc/nginx/sites-available/lokinet-bootserv.conf /etc/nginx/sites-enabled/ + +## maintainence + +add the following to crontab + + 0 0 * * * /usr/local/bin/lokinet-bootserv --cron diff --git a/contrib/bootserv/src/cgi.cpp b/contrib/bootserv/src/cgi.cpp new file mode 100644 index 000000000..46482ab7c --- /dev/null +++ b/contrib/bootserv/src/cgi.cpp @@ -0,0 +1,171 @@ +#include "lokinet-cgi.hpp" +#include +#include +#include +#include + +namespace lokinet +{ + namespace bootserv + { + CGIHandler::CGIHandler(std::ostream& o) : Handler(o) + { + } + + CGIHandler::~CGIHandler() + { + } + + int + CGIHandler::Exec(const Config& conf) + { + const char* e = getenv("REQUEST_METHOD"); + if(e == nullptr) + return ReportError("$REQUEST_METHOD not set"); + std::string_view method(e); + + if(method != "GET") + { + out << "Content-Type: text/plain" << std::endl; + out << "Status: 405 Method Not Allowed" << std::endl << std::endl; + return 0; + } + + std::string fname; + if(!conf.VisitSection( + "nodedb", [&](const Config::Section_t& sect) -> bool { + auto itr = sect.find("dir"); + if(itr == sect.end()) + return false; + fname = PickRandomFileInDir( + std::string(itr->second.data(), itr->second.size())); + return true; + })) + + return ReportError("bad values in nodedb section of config"); + if(fname.empty()) + { + // no files in nodedb + out << "Content-Type: text/plain" << std::endl; + out << "Status: 404 Not Found" << std::endl << std::endl; + return 0; + } + return ServeFile(fname.c_str(), "application/octect-stream"); + } + + std::string + CGIHandler::PickRandomFileInDir(std::string dirname) const + { + // collect files + std::list< std::string > files; + { + DIR* d = opendir(dirname.c_str()); + if(d == nullptr) + { + return ""; + }; + std::list< std::string > subdirs; + dirent* ent = nullptr; + while((ent = readdir(d))) + { + std::string_view f = ent->d_name; + if(f != "." && f != "..") + { + std::stringstream ss; + ss << dirname; + ss << '/'; + ss << f; + subdirs.emplace_back(ss.str()); + } + } + closedir(d); + for(const auto& subdir : subdirs) + { + d = opendir(subdir.c_str()); + if(d) + { + while((ent = readdir(d))) + { + std::string_view f; + f = ent->d_name; + if(f != "." && f != ".." + && f.find_last_of(".signed") != std::string_view::npos) + { + std::stringstream ss; + ss << subdir << "/" << f; + files.emplace_back(ss.str()); + } + } + closedir(d); + } + } + } + uint32_t randint; + { + std::basic_ifstream< uint32_t > randf("/dev/urandom"); + if(!randf.is_open()) + return ""; + randf.read(&randint, 1); + } + auto itr = files.begin(); + if(files.size() > 1) + std::advance(itr, randint % files.size()); + return *itr; + } + + int + CGIHandler::ServeFile(const char* fname, const char* contentType) const + { + std::ifstream f(fname); + if(f.is_open()) + { + f.seekg(0, std::ios::end); + auto sz = f.tellg(); + f.seekg(0, std::ios::beg); + if(sz) + { + out << "Content-Type: " << contentType << std::endl; + out << "Status: 200 OK" << std::endl; + out << "Content-Length: " << std::to_string(sz) << std::endl + << std::endl; + char buf[512] = {0}; + size_t r = 0; + while((r = f.readsome(buf, sizeof(buf))) > 0) + out.write(buf, r); + out << std::flush; + } + else + { + out << "Content-Type: text/plain" << std::endl; + out << "Status: 500 Internal Server Error" << std::endl << std::endl; + out << "could not serve '" << fname << "' as it is an empty file" + << std::endl; + } + } + else + { + out << "Content-Type: text/plain" << std::endl; + out << "Status: 404 Not Found" << std::endl << std::endl; + out << "could not serve '" << fname + << "' as it does not exist on the filesystem" << std::endl; + } + return 0; + } + + int + CGIHandler::ReportError(const char* err) + { + out << "Content-Type: text/plain" << std::endl; + out << "Status: 500 Internal Server Error" << std::endl << std::endl; + out << err << std::endl; + return 0; + } + + Handler_ptr + NewCGIHandler(std::ostream& out) + { + return std::make_unique< CGIHandler >(out); + } + + } // namespace bootserv +} // namespace lokinet diff --git a/contrib/bootserv/src/handler.hpp b/contrib/bootserv/src/handler.hpp new file mode 100644 index 000000000..7327e900c --- /dev/null +++ b/contrib/bootserv/src/handler.hpp @@ -0,0 +1,43 @@ +#ifndef LOKINET_BOOTSERV_HANDLER_HPP +#define LOKINET_BOOTSERV_HANDLER_HPP +#include +#include "lokinet-config.hpp" + +namespace lokinet +{ + namespace bootserv + { + struct Handler + { + Handler(std::ostream& o) : out(o){}; + + virtual ~Handler(){}; + + /// handle command + /// return exit code + virtual int + Exec(const Config& conf) = 0; + + /// report an error to system however that is done + /// return exit code + virtual int + ReportError(const char* err) = 0; + + protected: + std::ostream& out; + }; + + using Handler_ptr = std::unique_ptr< Handler >; + + /// create cgi handler + Handler_ptr + NewCGIHandler(std::ostream& out); + + /// create cron handler + Handler_ptr + NewCronHandler(std::ostream& out); + + } // namespace bootserv +} // namespace lokinet + +#endif diff --git a/contrib/bootserv/src/lokinet-cgi.hpp b/contrib/bootserv/src/lokinet-cgi.hpp new file mode 100644 index 000000000..032bf3d3d --- /dev/null +++ b/contrib/bootserv/src/lokinet-cgi.hpp @@ -0,0 +1,31 @@ +#ifndef BOOTSERV_LOKINET_CRON_HPP +#define BOOTSERV_LOKINET_CRON_HPP + +#include "handler.hpp" + +namespace lokinet +{ + namespace bootserv + { + struct CGIHandler final : public Handler + { + CGIHandler(std::ostream& o); + ~CGIHandler(); + + int + Exec(const Config& conf) override; + + int + ReportError(const char* err) override; + + int + ServeFile(const char* fname, const char* mime) const; + + std::string + PickRandomFileInDir(std::string dirname) const; + }; + + } // namespace bootserv +} // namespace lokinet + +#endif diff --git a/contrib/bootserv/src/lokinet-config.cpp b/contrib/bootserv/src/lokinet-config.cpp new file mode 100644 index 000000000..155fb66f7 --- /dev/null +++ b/contrib/bootserv/src/lokinet-config.cpp @@ -0,0 +1,132 @@ +#include "lokinet-config.hpp" +#include +#include +#include + +namespace lokinet +{ + namespace bootserv + { + const char* Config::DefaultPath = "/usr/local/etc/lokinet-bootserv.ini"; + + bool + Config::LoadFile(const char* fname) + { + { + std::ifstream f(fname); + if(!f.is_open()) + return false; + f.seekg(0, std::ios::end); + m_Data.resize(f.tellg()); + f.seekg(0, std::ios::beg); + if(m_Data.size() == 0) + return false; + f.read(m_Data.data(), m_Data.size()); + } + return Parse(); + } + + void + Config::Clear() + { + m_Config.clear(); + m_Data.clear(); + } + + bool + Config::Parse() + { + std::list< String_t > lines; + { + auto itr = m_Data.begin(); + // split into lines + while(itr != m_Data.end()) + { + auto beg = itr; + while(itr != m_Data.end() && *itr != '\n' && *itr != '\r') + ++itr; + lines.emplace_back(std::addressof(*beg), (itr - beg)); + if(itr == m_Data.end()) + break; + ++itr; + } + } + + String_t sectName; + + for(const auto& line : lines) + { + String_t realLine; + auto comment = line.find_first_of(';'); + if(comment == String_t::npos) + comment = line.find_first_of('#'); + if(comment == String_t::npos) + realLine = line; + else + realLine = line.substr(0, comment); + // blank or commented line? + if(realLine.size() == 0) + continue; + // find delimiters + auto sectOpenPos = realLine.find_first_of('['); + auto sectClosPos = realLine.find_first_of(']'); + auto kvDelim = realLine.find_first_of('='); + if(sectOpenPos != String_t::npos && sectClosPos != String_t::npos + && kvDelim == String_t::npos) + { + // section header + + // clamp whitespaces + ++sectOpenPos; + while(std::isspace(realLine[sectOpenPos]) + && sectOpenPos != sectClosPos) + ++sectOpenPos; + --sectClosPos; + while(std::isspace(realLine[sectClosPos]) + && sectClosPos != sectOpenPos) + --sectClosPos; + // set section name + sectName = realLine.substr(sectOpenPos, sectClosPos); + } + else if(kvDelim != String_t::npos) + { + // key value pair + String_t::size_type k_start = 0; + String_t::size_type k_end = kvDelim; + String_t::size_type v_start = kvDelim + 1; + String_t::size_type v_end = realLine.size() - 1; + // clamp whitespaces + while(std::isspace(realLine[k_start]) && k_start != kvDelim) + ++k_start; + while(std::isspace(realLine[k_end]) && k_end != k_start) + --k_end; + while(std::isspace(realLine[v_start]) && v_start != v_end) + ++v_start; + while(std::isspace(realLine[v_end])) + --v_end; + + // sect.k = v + String_t k = realLine.substr(k_start, k_end); + String_t v = realLine.substr(v_start, v_end); + Section_t& sect = m_Config[sectName]; + sect[k] = v; + } + else // malformed? + return false; + } + return true; + } + + bool + Config::VisitSection( + const char* name, + std::function< bool(const Section_t& sect) > visit) const + { + auto itr = m_Config.find(name); + if(itr == m_Config.end()) + return false; + return visit(itr->second); + } + + } // namespace bootserv +} // namespace lokinet diff --git a/contrib/bootserv/src/lokinet-config.hpp b/contrib/bootserv/src/lokinet-config.hpp new file mode 100644 index 000000000..caad9a2bc --- /dev/null +++ b/contrib/bootserv/src/lokinet-config.hpp @@ -0,0 +1,47 @@ +#ifndef LOKINET_BOOTSERV_CONFIG_HPP +#define LOKINET_BOOTSERV_CONFIG_HPP +#include +#include +#include +#include +#include + +namespace lokinet +{ + namespace bootserv + { + struct Config + { + using String_t = std::string_view; + using Section_t = std::unordered_map< String_t, String_t >; + using Config_impl_t = std::unordered_map< String_t, Section_t >; + + static const char* DefaultPath; + + /// clear config + void + Clear(); + + /// load config file for bootserv + /// return true on success + /// return false on error + bool + LoadFile(const char* fname); + + /// visit a section in config read only by name + /// return false if no section or value propagated from visitor + bool + VisitSection(const char* name, + std::function< bool(const Section_t&) > visit) const; + + private: + bool + Parse(); + + std::vector< char > m_Data; + Config_impl_t m_Config; + }; + } // namespace bootserv +} // namespace lokinet + +#endif diff --git a/contrib/bootserv/src/lokinet-cron.cpp b/contrib/bootserv/src/lokinet-cron.cpp new file mode 100644 index 000000000..dfca782a2 --- /dev/null +++ b/contrib/bootserv/src/lokinet-cron.cpp @@ -0,0 +1,37 @@ +#include "lokinet-cron.hpp" + +namespace lokinet +{ + namespace bootserv + { + CronHandler::CronHandler(std::ostream& o) : Handler(o) + { + } + + CronHandler::~CronHandler() + { + } + + int + CronHandler::Exec(const Config& conf) + { + // this runs the cron tasks + // TODO: implement me + return 0; + } + + int + CronHandler::ReportError(const char* err) + { + out << "error: " << err << std::endl; + return 1; + } + + Handler_ptr + NewCronHandler(std::ostream& out) + { + return std::make_unique< CronHandler >(out); + } + + } // namespace bootserv +} // namespace lokinet diff --git a/contrib/bootserv/src/lokinet-cron.hpp b/contrib/bootserv/src/lokinet-cron.hpp new file mode 100644 index 000000000..8bd9ba3c6 --- /dev/null +++ b/contrib/bootserv/src/lokinet-cron.hpp @@ -0,0 +1,25 @@ +#ifndef BOOTSERV_LOKINET_CRON_HPP +#define BOOTSERV_LOKINET_CRON_HPP + +#include "handler.hpp" + +namespace lokinet +{ + namespace bootserv + { + struct CronHandler final : public Handler + { + CronHandler(std::ostream& o); + ~CronHandler(); + + int + Exec(const Config& conf) override; + + int + ReportError(const char* err) override; + }; + + } // namespace bootserv +} // namespace lokinet + +#endif diff --git a/contrib/bootserv/src/main.cpp b/contrib/bootserv/src/main.cpp new file mode 100644 index 000000000..ad51d0f0b --- /dev/null +++ b/contrib/bootserv/src/main.cpp @@ -0,0 +1,60 @@ +#include "handler.hpp" +#include "lokinet-config.hpp" + +#include +#include +#include + +static int +printhelp(const char* exe) +{ + std::cout << "usage: " << exe << " [--cron] [--conf /path/to/alt/config.ini]" + << std::endl; + return 1; +} + +int +main(int argc, char* argv[]) +{ + bool RunCron = false; + + const char* confFile = lokinet::bootserv::Config::DefaultPath; + lokinet::bootserv::Config config; + + lokinet::bootserv::Handler_ptr handler; + + option longopts[] = {{"cron", no_argument, 0, 'C'}, + {"help", no_argument, 0, 'h'}, + {"conf", required_argument, 0, 'c'}, + {0, 0, 0, 0}}; + + int c = 0; + int index = 0; + while((c = getopt_long(argc, argv, "hCc:", longopts, &index)) != -1) + { + switch(c) + { + case 'h': + return printhelp(argv[0]); + case 'C': + RunCron = true; + break; + case 'c': + confFile = optarg; + break; + } + } + if(RunCron) + handler = lokinet::bootserv::NewCronHandler(std::cout); + else + handler = lokinet::bootserv::NewCGIHandler(std::cout); + + if(!config.LoadFile(confFile)) + { + std::stringstream ss; + ss << "failed to load " << confFile; + return handler->ReportError(ss.str().c_str()); + } + else + return handler->Exec(config); +}