From 4c3dfa829fe2d8be93798415c80b5b63a31263e6 Mon Sep 17 00:00:00 2001 From: Thulinma Date: Mon, 9 Sep 2019 13:19:37 +0200 Subject: [PATCH 1/2] Implemented certbot helper utility --- CMakeLists.txt | 1 + src/controller/controller_api.cpp | 4 + src/controller/controller_connectors.cpp | 22 ++- src/controller/controller_connectors.h | 4 +- src/output/output_http_internal.cpp | 32 +++++ src/utils/util_certbot.cpp | 175 +++++++++++++++++++++++ 6 files changed, 224 insertions(+), 14 deletions(-) create mode 100644 src/utils/util_certbot.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 36648676..42c71cc6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,6 +275,7 @@ endmacro() makeUtil(RAX rax) makeUtil(AMF amf) +makeUtil(Certbot certbot) ######################################## # MistServer - Inputs # diff --git a/src/controller/controller_api.cpp b/src/controller/controller_api.cpp index 7827c712..669f4acd 100644 --- a/src/controller/controller_api.cpp +++ b/src/controller/controller_api.cpp @@ -478,6 +478,10 @@ void Controller::handleAPICommands(JSON::Value & Request, JSON::Value & Response if (Request["updateprotocol"].isArray() && Request["updateprotocol"].size() == 2){ jsonForEach(Controller::Storage["config"]["protocols"], it){ if ((*it).compareExcept(Request["updateprotocol"][0u], ignores)){ + //If the connector type didn't change, mark it as needing a reload + if ((*it)["connector"] == Request["updateprotocol"][1u]["connector"]){ + reloadProtocol(it.num()); + } (*it) = Request["updateprotocol"][1u]; } } diff --git a/src/controller/controller_connectors.cpp b/src/controller/controller_connectors.cpp index fcbe767e..8442b257 100644 --- a/src/controller/controller_connectors.cpp +++ b/src/controller/controller_connectors.cpp @@ -20,8 +20,13 @@ ///\brief Holds everything unique to the controller. namespace Controller { + static std::set needsReload; ///< List of connector indices that needs a reload static std::map currentConnectors; ///::iterator iter; - for (iter = currentConnectors.begin(); iter != currentConnectors.end(); iter++){ - if (iter->first.substr(0, protocol.size()) == protocol){ - Log("CONF", "Killing connector for update: " + iter->first); - Util::Procs::Stop(iter->second); - } - } - } - static inline void builPipedPart(JSON::Value & p, char * argarr[], int & argnum, const JSON::Value & argset){ jsonForEachConst(argset, it) { if (it->isMember("option")){ @@ -203,6 +196,11 @@ namespace Controller { runningConns.insert(myCmd); if (currentConnectors.count(myCmd) && Util::Procs::isActive(currentConnectors[myCmd])){ ( *ait)["online"] = 1; + //Reload connectors that need it + if (needsReload.count(ait.num())){ + kill(currentConnectors[myCmd], SIGUSR1); + needsReload.erase(ait.num()); + } }else{ ( *ait)["online"] = 0; } diff --git a/src/controller/controller_connectors.h b/src/controller/controller_connectors.h index 75e7da5c..52f72cae 100644 --- a/src/controller/controller_connectors.h +++ b/src/controller/controller_connectors.h @@ -2,8 +2,8 @@ namespace Controller { - /// Checks if the binary mentioned in the protocol argument is currently active, if so, restarts it. - void UpdateProtocol(std::string protocol); + /// Marks the given protocol as needing a reload (signal USR1) on next check + void reloadProtocol(size_t indice); /// Checks current protocol configuration, updates state of enabled connectors if neccesary. bool CheckProtocols(JSON::Value & p, const JSON::Value & capabilities); diff --git a/src/output/output_http_internal.cpp b/src/output/output_http_internal.cpp index 582b3581..7f4d94e1 100644 --- a/src/output/output_http_internal.cpp +++ b/src/output/output_http_internal.cpp @@ -122,6 +122,7 @@ namespace Mist { capa["url_match"].append("/embed_$.js"); capa["url_match"].append("/flashplayer.swf"); capa["url_match"].append("/oldflashplayer.swf"); + capa["url_prefix"] = "/.well-known/"; capa["optional"]["wrappers"]["name"] = "Active players"; capa["optional"]["wrappers"]["help"] = "Which players are attempted and in what order."; capa["optional"]["wrappers"]["default"] = ""; @@ -133,6 +134,12 @@ namespace Mist { capa["optional"]["wrappers"]["allowed"].append("flash_strobe"); capa["optional"]["wrappers"]["option"] = "--wrappers"; capa["optional"]["wrappers"]["short"] = "w"; + capa["optional"]["certbot"]["name"] = "Certbot validation token"; + capa["optional"]["certbot"]["help"] = "Automatically set by the MistUtilCertbot authentication hook for certbot. Not intended to be set manually."; + capa["optional"]["certbot"]["default"] = ""; + capa["optional"]["certbot"]["type"] = "str"; + capa["optional"]["certbot"]["option"] = "--certbot"; + capa["optional"]["certbot"]["short"] = "C"; cfg->addConnectorOptions(8080, capa); } @@ -447,6 +454,31 @@ namespace Mist { void OutHTTP::onHTTP(){ std::string method = H.method; + + //Handle certbot validations + if (H.url.substr(0, 28) == "/.well-known/acme-challenge/"){ + std::string cbToken = H.url.substr(28); + jsonForEach(config->getOption("certbot",true),it){ + if (it->asStringRef().substr(0, cbToken.size()+1) == cbToken+":"){ + H.Clean(); + H.SetHeader("Content-Type", "text/plain"); + H.SetHeader("Server", "MistServer/" PACKAGE_VERSION); + H.setCORSHeaders(); + H.SetBody(it->asStringRef().substr(cbToken.size()+1)); + H.SendResponse("200", "OK", myConn); + H.Clean(); + return; + } + } + H.Clean(); + H.SetHeader("Content-Type", "text/plain"); + H.SetHeader("Server", "MistServer/" PACKAGE_VERSION); + H.setCORSHeaders(); + H.SetBody("No matching validation found for token '" + cbToken + "'"); + H.SendResponse("404", "Not found", myConn); + H.Clean(); + return; + } if (H.url == "/crossdomain.xml"){ H.Clean(); diff --git a/src/utils/util_certbot.cpp b/src/utils/util_certbot.cpp new file mode 100644 index 00000000..cce6bf44 --- /dev/null +++ b/src/utils/util_certbot.cpp @@ -0,0 +1,175 @@ +/// \file util_certbot.cpp +/// Certbot integration utility +/// Intended to be ran like so: +//certbot certonly --manual --preferred-challenges=http --manual-auth-hook MistUtilCertbot --deploy-hook MistUtilCertbot -d yourdomain.example.com + + +//When called from --deploy-hook: +//RENEWED_LINEAGE: directory with the certificate +//RENEWED_DOMAINS: space-delimited list of domains + +//When called from --manual-auth-hook: +//CERTBOT_DOMAIN: The domain being authenticated +//CERTBOT_VALIDATION: The validation string +//CERTBOT_TOKEN: Resource name part of the HTTP-01 challenge + +#include +#include +#include +#include + +///Checks if port 80 is HTTP, returns the indice number (>= 0) if it is. +///Returns -1 if nothing is running on port 80. +///Returns -2 if the port is taken by another protocol (and prints a FAIL-level message). +///If found, sets currConf to the current configuration of the HTTP protocol. +int checkPort80(JSON::Value & currConf){ + Util::DTSCShmReader rCapa(SHM_CAPA); + DTSC::Scan conns = rCapa.getMember("connectors"); + Util::DTSCShmReader rProto(SHM_PROTO); + DTSC::Scan prtcls = rProto.getScan(); + unsigned int pro_cnt = prtcls.getSize(); + for (unsigned int i = 0; i < pro_cnt; ++i){ + std::string ctor = prtcls.getIndice(i).getMember("connector").asString(); + DTSC::Scan capa = conns.getMember(ctor); + uint16_t port = prtcls.getIndice(i).getMember("port").asInt(); + //get the default port if none is set + if (!port){ + port = capa.getMember("optional").getMember("port").getMember("default").asInt(); + } + //Found a port 80 entry? + if (port == 80){ + if (ctor == "HTTP" || ctor == "HTTP.exe"){ + currConf = prtcls.getIndice(i).asJSON(); + return i; + }else{ + FAIL_MSG("Found non-HTTP protocol %s on port 80; aborting! Please free up port 80 for HTTP", ctor.c_str()); + return -2; + } + } + } + return -1; +} + +int main(int argc, char **argv){ + Util::redirectLogsIfNeeded(); + Util::Config conf(argv[0]); + conf.parseArgs(argc, argv); + + //Handle --manual-auth-hook + if (getenv("CERTBOT_VALIDATION") && getenv("CERTBOT_TOKEN")){ + INFO_MSG("Detected '--manual-auth-hook' calling. Performing authentication."); + //Store certbot variables for later use + std::string cbValidation = getenv("CERTBOT_VALIDATION"); + std::string cbToken = getenv("CERTBOT_TOKEN"); + std::string cbCombo = cbToken+":"+cbValidation; + //Check Mist config, find HTTP output, check config + JSON::Value currConf; + int foundHTTP80 = checkPort80(currConf); + if (foundHTTP80 == -2){return 1;}//abort if port already taken by non-HTTP process + if (foundHTTP80 == -1){ + INFO_MSG("Nothing on port 80 found - adding HTTP connector on port 80 with correct config for certbot"); + JSON::Value cmd; + cmd["addprotocol"]["connector"] = "HTTP"; + cmd["addprotocol"]["port"] = 80; + cmd["addprotocol"]["certbot"] = cbCombo; + Socket::UDPConnection uSock; + uSock.SetDestination(UDP_API_HOST, UDP_API_PORT); + uSock.SendNow(cmd.toString()); + Util::wait(1000); + int counter = 10; + while (--counter && ((foundHTTP80 = checkPort80(currConf)) == -1 || currConf["certbot"].asStringRef() != cbCombo)){ + INFO_MSG("Waiting for Controller to pick up new config..."); + uSock.SendNow(cmd.toString()); + Util::wait(1000); + } + if (!counter){ + FAIL_MSG("Timed out!"); + return 1; + } + INFO_MSG("Success!"); + Util::wait(5000); + }else{ + if (currConf["certbot"].asStringRef() == cbCombo){ + INFO_MSG("Config already good - no changes needed"); + return 0; + } + INFO_MSG("Found HTTP on port 80; updating config..."); + JSON::Value cmd; + cmd["updateprotocol"].append(currConf); + cmd["updateprotocol"].append(currConf); + cmd["updateprotocol"][1u]["certbot"] = cbCombo; + Socket::UDPConnection uSock; + uSock.SetDestination(UDP_API_HOST, UDP_API_PORT); + uSock.SendNow(cmd.toString()); + Util::wait(1000); + int counter = 10; + while (--counter && ((foundHTTP80 = checkPort80(currConf)) == -1 || currConf["certbot"].asStringRef() != cbCombo)){ + INFO_MSG("Waiting for Controller to pick up new config..."); + uSock.SendNow(cmd.toString()); + Util::wait(1000); + } + if (!counter){ + FAIL_MSG("Timed out!"); + return 1; + } + INFO_MSG("Success!"); + Util::wait(5000); + } + return 0; + } + + //Handle --deploy-hook + if (getenv("RENEWED_LINEAGE")){ + INFO_MSG("Detected '--deploy-hook' calling. Installing certificate."); + std::string cbPath = getenv("RENEWED_LINEAGE"); + std::string cbCert = cbPath + "/fullchain.pem"; + std::string cbKey = cbPath + "/privkey.pem"; + Socket::UDPConnection uSock; + uSock.SetDestination(UDP_API_HOST, UDP_API_PORT); + Util::DTSCShmReader rProto(SHM_PROTO); + DTSC::Scan prtcls = rProto.getScan(); + unsigned int pro_cnt = prtcls.getSize(); + bool found = false; + for (unsigned int i = 0; i < pro_cnt; ++i){ + std::string ctor = prtcls.getIndice(i).getMember("connector").asString(); + if (ctor == "HTTPS"){ + found = true; + JSON::Value currConf = prtcls.getIndice(i).asJSON(); + JSON::Value cmd; + cmd["updateprotocol"].append(currConf); + cmd["updateprotocol"].append(currConf); + cmd["updateprotocol"][1u]["cert"] = cbCert; + cmd["updateprotocol"][1u]["key"] = cbKey; + INFO_MSG("Executing: %s", cmd.toString().c_str()); + uSock.SendNow(cmd.toString()); + Util::wait(500); + uSock.SendNow(cmd.toString()); + Util::wait(500); + uSock.SendNow(cmd.toString()); + } + } + if (!found){ + INFO_MSG("No HTTPS active; enabling on port 443"); + JSON::Value cmd; + cmd["addprotocol"]["connector"] = "HTTPS"; + cmd["addprotocol"]["port"] = 443; + cmd["addprotocol"]["cert"] = cbCert; + cmd["addprotocol"]["key"] = cbKey; + INFO_MSG("Executing: %s", cmd.toString().c_str()); + uSock.SendNow(cmd.toString()); + Util::wait(500); + uSock.SendNow(cmd.toString()); + Util::wait(500); + uSock.SendNow(cmd.toString()); + } + Util::wait(5000); + return 0; + } + + //Print usage message to help point users in the right direction + FAIL_MSG("This utility is meant to be ran by certbot, not by hand."); + FAIL_MSG("Sample usage: certbot certonly --manual --preferred-challenges=http --manual-auth-hook MistUtilCertbot --deploy-hook MistUtilCertbot -d yourdomain.example.com"); + WARN_MSG("Note: This utility will alter your MistServer configuration. If ran as deploy hook it will install the certificate generated by certbot to any already enabled HTTPS output or enable HTTPS on port 443 (if it was disabled). If ran as auth hook it will change the HTTP port to 80 (and enable HTTP if it wasn't enabled already) in order to perform the validation."); + return 1; +} + From 309d39eab70295187c74fd35d4b026ff9b313a78 Mon Sep 17 00:00:00 2001 From: Thulinma Date: Mon, 9 Sep 2019 13:31:20 +0200 Subject: [PATCH 2/2] Backported UDP API from Pro edition --- lib/defines.h | 9 +++++++++ src/controller/controller.cpp | 4 ++++ src/controller/controller_api.cpp | 26 ++++++++++++++++++++++++++ src/controller/controller_api.h | 1 + 4 files changed, 40 insertions(+) diff --git a/lib/defines.h b/lib/defines.h index 01988d20..7a6abfdc 100644 --- a/lib/defines.h +++ b/lib/defines.h @@ -157,3 +157,12 @@ static inline void show_stackframe(){} #define SIMUL_TRACKS 20 + +#ifndef UDP_API_HOST +#define UDP_API_HOST "localhost" +#endif + +#ifndef UDP_API_PORT +#define UDP_API_PORT 4242 +#endif + diff --git a/src/controller/controller.cpp b/src/controller/controller.cpp index 6db2e2c5..06c8b075 100644 --- a/src/controller/controller.cpp +++ b/src/controller/controller.cpp @@ -360,6 +360,8 @@ int main_loop(int argc, char **argv){ tthread::thread statsThread(Controller::SharedMemStats, &Controller::conf); // start monitoring thread tthread::thread monitorThread(statusMonitor, 0); + // start UDP API thread + tthread::thread UDPAPIThread(Controller::handleUDPAPI, 0); // start main loop while (Controller::conf.is_active){ @@ -380,6 +382,8 @@ int main_loop(int argc, char **argv){ statsThread.join(); HIGH_MSG("Joining monitor thread..."); monitorThread.join(); + HIGH_MSG("Joining UDP API thread..."); + UDPAPIThread.join(); // write config tthread::lock_guard guard(Controller::logMutex); Controller::writeConfigToDisk(); diff --git a/src/controller/controller_api.cpp b/src/controller/controller_api.cpp index 669f4acd..6346f037 100644 --- a/src/controller/controller_api.cpp +++ b/src/controller/controller_api.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "controller_api.h" #include "controller_storage.h" #include "controller_streams.h" @@ -360,6 +361,31 @@ int Controller::handleAPIConnection(Socket::Connection & conn){ return 0; } +void Controller::handleUDPAPI(void * np){ + Socket::UDPConnection uSock(true); + if (!uSock.bind(UDP_API_PORT, UDP_API_HOST)){ + FAIL_MSG("Could not open local API UDP socket - not all functionality will be available"); + return; + } + Util::Procs::socketList.insert(uSock.getSock()); + while (Controller::conf.is_active){ + if (uSock.Receive()){ + MEDIUM_MSG("UDP API: %s", uSock.data); + JSON::Value Request = JSON::fromString(uSock.data, uSock.data_len); + Request["minimal"] = true; + JSON::Value Response; + if (Request.isObject()){ + tthread::lock_guard guard(configMutex); + handleAPICommands(Request, Response); + }else{ + WARN_MSG("Invalid API command received over UDP: %s", uSock.data); + } + }else{ + Util::sleep(500); + } + } +} + /// Local-only helper function that checks for duplicate protocols and removes them static void removeDuplicateProtocols(){ JSON::Value & P = Controller::Storage["config"]["protocols"]; diff --git a/src/controller/controller_api.h b/src/controller/controller_api.h index e274b178..6c348716 100644 --- a/src/controller/controller_api.h +++ b/src/controller/controller_api.h @@ -8,4 +8,5 @@ namespace Controller { int handleAPIConnection(Socket::Connection & conn); void handleAPICommands(JSON::Value & Request, JSON::Value & Response); void handleWebSocket(HTTP::Parser & H, Socket::Connection & C); + void handleUDPAPI(void * np); }