mistserver/src/utils/util_certbot.cpp
2023-06-19 17:09:25 +02:00

174 lines
6.9 KiB
C++

/// \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 <mist/config.h>
#include <mist/defines.h>
#include <mist/stream.h>
#include <string>
/// 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 --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";
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());
Util::sendUDPApi(cmd);
Util::wait(500);
Util::sendUDPApi(cmd);
Util::wait(500);
Util::sendUDPApi(cmd);
}
}
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());
Util::sendUDPApi(cmd);
Util::wait(500);
Util::sendUDPApi(cmd);
Util::wait(500);
Util::sendUDPApi(cmd);
}
Util::wait(5000);
return 0;
}
// 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;
Util::sendUDPApi(cmd);
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...");
Util::sendUDPApi(cmd);
Util::wait(1000);
}
if (!counter){
FAIL_MSG("Timed out! Is " APPNAME " running, and is certbot being ran under the same system user " APPNAME " is running under?");
return 1;
}
INFO_MSG("Success!");
Util::wait(5000);
}else{
if (currConf.isMember("certbot") && 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;
Util::sendUDPApi(cmd);
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...");
Util::sendUDPApi(cmd);
Util::wait(1000);
}
if (!counter){
FAIL_MSG("Timed out! Is " APPNAME " running, and is certbot being ran under the same system user " APPNAME " is running under?");
return 1;
}
INFO_MSG("Success!");
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;
}