diff --git a/docs/administering/config-file.md b/docs/administering/config-file.md index 0e5c4363c..039caee54 100644 --- a/docs/administering/config-file.md +++ b/docs/administering/config-file.md @@ -51,11 +51,14 @@ typically a static publishing website, then serve a normal response. ### HTTPS_PORT -A port number for Sandstorm to bind on and listen for HTTPS. On a default install, if port 443 -was available and the user chose to use Sandcats, this is 443. However, this may be set for any -Sandstorm-managed TLS configuration, including automated renewals of certificates with Let's Encrypt -with a supported DNS provider or a manually-uploaded certificate. If this config option is missing, -Sandstorm's built-in HTTPS server is disabled. +Like `PORT`, but these ports are served using HTTPS. If both `PORT` and `HTTPS_PORT` are specified, +the first `HTTPS_PORT` "First port" as described above, and all values in `PORT` are "alternate +ports." + +On a default install, if port 443 was available and the user chose to use Sandcats, this is 443. +However, this may be set for any Sandstorm-managed TLS configuration, including automated renewals +of certificates with Let's Encrypt with a supported DNS provider or a manually-uploaded certificate. +If this config option is missing, Sandstorm's built-in HTTPS server is disabled. If Sandstorm is started as root, Sandstorm binds to this port as root, allowing it to use low-numbered ports. The socket is passed-through to code that does not run as root. @@ -66,9 +69,6 @@ Example: HTTPS_PORT=443 ``` -A HTTPS_PORT is automatically treated as the first port, in the context of "first port" vs. -"alternate ports." - ### SMTP_LISTEN_PORT A port number on which Sandstorm will bind, listening for inbound email. By default, 30025; if diff --git a/src/sandstorm/config.c++ b/src/sandstorm/config.c++ index d7d037809..8e99005a6 100644 --- a/src/sandstorm/config.c++ +++ b/src/sandstorm/config.c++ @@ -70,31 +70,17 @@ auto parser = p::sequence(delimited<' '>(assignment), p::discardWhitespace, p::e // ======================================================================================= -kj::Array parsePorts(kj::Maybe httpsPort, kj::StringPtr portList) { - auto portsSplitOnComma = split(portList, ','); - size_t numHttpPorts = portsSplitOnComma.size(); - size_t numHttpsPorts; - kj::Array result; - - // If the configuration has a https port, then add it first. - KJ_IF_MAYBE(portNumber, httpsPort) { - numHttpsPorts = 1; - result = kj::heapArray(numHttpsPorts + numHttpPorts); - result[0] = *portNumber; - } else { - numHttpsPorts = 0; - result = kj::heapArray(numHttpsPorts + numHttpPorts); - } - +kj::Array parsePortList(kj::StringPtr key, kj::StringPtr portListStr) { + auto portsSplitOnComma = split(portListStr, ','); + auto result = kj::heapArrayBuilder(portsSplitOnComma.size()); for (size_t i = 0; i < portsSplitOnComma.size(); i++) { KJ_IF_MAYBE(portNumber, parseUInt(trim(portsSplitOnComma[i]), 10)) { - result[i + numHttpsPorts] = *portNumber; + result.add(*portNumber); } else { - KJ_FAIL_REQUIRE("invalid config value PORT", portList); + KJ_FAIL_REQUIRE("invalid config value ", key, portListStr); } } - - return kj::mv(result); + return result.finish(); } kj::Maybe getUserIds(kj::StringPtr name) { @@ -170,10 +156,6 @@ Config readConfig(const char *path, bool parseUids) { config.uids.uid = getuid(); config.uids.gid = getgid(); - // Store the PORT and HTTPS_PORT values in variables here so we can - // process them at the end. - kj::Maybe maybePortValue = nullptr; - auto lines = splitLines(readAll(path)); for (auto& line: lines) { auto equalsPos = KJ_ASSERT_NONNULL(line.findFirst('='), "Invalid config line", line); @@ -190,13 +172,9 @@ Config readConfig(const char *path, bool parseUids) { } } } else if (key == "HTTPS_PORT") { - KJ_IF_MAYBE(p, parseUInt(value, 10)) { - config.httpsPort = *p; - } else { - KJ_FAIL_REQUIRE("invalid config value HTTPS_PORT", value); - } + config.httpsPorts = parsePortList(key, value); } else if (key == "PORT") { - maybePortValue = kj::mv(value); + config.ports = parsePortList(key, value); } else if (key == "MONGO_PORT") { KJ_IF_MAYBE(p, parseUInt(value, 10)) { config.mongoPort = *p; @@ -280,16 +258,16 @@ Config readConfig(const char *path, bool parseUids) { } } - // Now process the PORT setting, since the actual value in config.ports - // depends on if HTTPS_PORT was provided at any point in reading the - // config file. - // - // Outer KJ_IF_MAYBE so we only run this code if the config file contained - // a PORT= declaration. - KJ_IF_MAYBE(portValue, maybePortValue) { - auto ports = parsePorts(config.httpsPort, *portValue); - config.ports = kj::mv(ports); + // config.ports should actually include the HTTPS_PORTs as well + // (and they should come first): + auto allPorts = kj::heapArrayBuilder(config.ports.size() + config.httpsPorts.size()); + for(uint port : config.httpsPorts) { + allPorts.add(port); + } + for(uint port : config.ports) { + allPorts.add(port); } + config.ports = allPorts.finish(); return config; } diff --git a/src/sandstorm/config.h b/src/sandstorm/config.h index 38e64d169..ec1fd5d46 100644 --- a/src/sandstorm/config.h +++ b/src/sandstorm/config.h @@ -12,7 +12,7 @@ struct UserIds { }; struct Config { - kj::Maybe httpsPort; + kj::Array httpsPorts; kj::Array ports; uint mongoPort = 3001; UserIds uids; diff --git a/src/sandstorm/gateway.c++ b/src/sandstorm/gateway.c++ index 36e113c9d..20c9244d9 100644 --- a/src/sandstorm/gateway.c++ +++ b/src/sandstorm/gateway.c++ @@ -935,9 +935,13 @@ void GatewayService::taskFailed(kj::Exception&& exception) { // ======================================================================================= GatewayTlsManager::GatewayTlsManager( - kj::HttpServer& server, kj::NetworkAddress& smtpServer, - kj::Maybe privateKeyPassword, kj::PromiseFulfillerPair readyPaf) + kj::HttpServer& server, + kj::HttpServer& altPortServer, + kj::NetworkAddress& smtpServer, + kj::Maybe privateKeyPassword, + kj::PromiseFulfillerPair readyPaf) : server(server), + altPortServer(altPortServer), smtpServer(smtpServer), privateKeyPassword(privateKeyPassword), ready(readyPaf.promise.fork()), @@ -950,6 +954,12 @@ kj::Promise GatewayTlsManager::listenHttps(kj::ConnectionReceiver& port) { }); } +kj::Promise GatewayTlsManager::listenAltPortHttps(kj::ConnectionReceiver& port) { + return ready.addBranch().then([this, &port]() { + return listenAltPortLoop(port); + }); +} + kj::Promise GatewayTlsManager::listenSmtp(kj::ConnectionReceiver& port) { return ready.addBranch().then([this, &port]() { return listenSmtpLoop(port); @@ -1018,12 +1028,20 @@ kj::Promise GatewayTlsManager::subscribeKeys(GatewayRouter::Client gateway } kj::Promise GatewayTlsManager::listenLoop(kj::ConnectionReceiver& port) { - return port.accept().then([this, &port](kj::Own&& stream) { + return listenLoop(port, server); +} + +kj::Promise GatewayTlsManager::listenAltPortLoop(kj::ConnectionReceiver& port) { + return listenLoop(port, altPortServer); +} + +kj::Promise GatewayTlsManager::listenLoop(kj::ConnectionReceiver& port, kj::HttpServer& srv) { + return port.accept().then([this, &port, &srv](kj::Own&& stream) { KJ_IF_MAYBE(t, currentTls) { auto tls = kj::addRef(**t); tasks.add(tls->tls.wrapServer(kj::mv(stream)) - .then([this](kj::Own&& encrypted) { - return server.listenHttp(kj::mv(encrypted)); + .then([&srv](kj::Own&& encrypted) { + return srv.listenHttp(kj::mv(encrypted)); }).attach(kj::mv(tls))); } else { KJ_LOG(ERROR, "refused HTTPS connection because no TLS keys are configured"); diff --git a/src/sandstorm/gateway.h b/src/sandstorm/gateway.h index c89c1b4d1..b5423b6e6 100644 --- a/src/sandstorm/gateway.h +++ b/src/sandstorm/gateway.h @@ -187,9 +187,11 @@ class GatewayTlsManager: private kj::TaskSet::ErrorHandler { // Manages TLS keys and connections. public: - GatewayTlsManager(kj::HttpServer& server, kj::NetworkAddress& smtpServer, + GatewayTlsManager(kj::HttpServer& server, + kj::HttpServer& altPortServer, + kj::NetworkAddress& smtpServer, kj::Maybe privateKeyPassword) - : GatewayTlsManager(server, smtpServer, privateKeyPassword, + : GatewayTlsManager(server, altPortServer, smtpServer, privateKeyPassword, kj::newPromiseAndFulfiller()) {} // Password, if provided, must remain valid while GatewayTlsManager exists. @@ -199,6 +201,8 @@ class GatewayTlsManager: private kj::TaskSet::ErrorHandler { // // No connections will be accepted until setKeys() has been called at least once. + kj::Promise listenAltPortHttps(kj::ConnectionReceiver& port); + kj::Promise listenSmtp(kj::ConnectionReceiver& port); kj::Promise listenSmtps(kj::ConnectionReceiver& port); @@ -217,6 +221,7 @@ class GatewayTlsManager: private kj::TaskSet::ErrorHandler { }; kj::HttpServer& server; + kj::HttpServer& altPortServer; kj::NetworkAddress& smtpServer; kj::Maybe privateKeyPassword; @@ -229,11 +234,15 @@ class GatewayTlsManager: private kj::TaskSet::ErrorHandler { kj::TaskSet tasks; - GatewayTlsManager(kj::HttpServer& server, kj::NetworkAddress& smtpServer, + GatewayTlsManager(kj::HttpServer& server, + kj::HttpServer& altPortServer, + kj::NetworkAddress& smtpServer, kj::Maybe privateKeyPassword, kj::PromiseFulfillerPair readyPaf); kj::Promise listenLoop(kj::ConnectionReceiver& port); + kj::Promise listenLoop(kj::ConnectionReceiver& port, kj::HttpServer& srv); + kj::Promise listenAltPortLoop(kj::ConnectionReceiver& port); kj::Promise listenSmtpLoop(kj::ConnectionReceiver& port); kj::Promise listenSmtpsLoop(kj::ConnectionReceiver& port); diff --git a/src/sandstorm/run-bundle.c++ b/src/sandstorm/run-bundle.c++ index 18ad4d920..585854683 100644 --- a/src/sandstorm/run-bundle.c++ +++ b/src/sandstorm/run-bundle.c++ @@ -2498,8 +2498,17 @@ private: auto shellSmptConn = fdBundle.consumeClient(FdBundle::SHELL_SMTP, *io.lowLevelProvider); kj::CapabilityStreamNetworkAddress shellSmtpAddr(*io.provider, *shellSmptConn); - GatewayTlsManager tlsManager(server, shellSmtpAddr, config.privateKeyPassword - .map([](const kj::String& str) -> kj::StringPtr { return str; })); + auto altPortService = kj::heap( + service, *headerTable, config.rootUrl, config.wildcardHost); + auto altPortServer = kj::heap( + io.provider->getTimer(), *headerTable, *altPortService); + altPortServer = altPortServer.attach(kj::mv(altPortService)); + GatewayTlsManager tlsManager(server, + *altPortServer, + shellSmtpAddr, + config.privateKeyPassword.map( + [](const kj::String& str) -> kj::StringPtr { return str; } + )); kj::Promise promises = service.cleanupLoop() .exclusiveJoin(tlsManager.subscribeKeys(kj::mv(router))) @@ -2509,28 +2518,32 @@ private: KJ_FAIL_REQUIRE("backend died; gateway aborting too"); })); + auto isHttpsPort = [&](uint port) -> bool { + for(uint httpsPort : config.httpsPorts) { + if (httpsPort == port) { + return true; + } + } + return false; + }; + // Listen on main port. if (config.ports.size() > 0) { auto port = config.ports[0]; auto listener = fdBundle.consume(port, *io.lowLevelProvider); - bool isHttps = false; - KJ_IF_MAYBE(p, config.httpsPort) { - isHttps = port == *p; - } - auto promise = isHttps ? tlsManager.listenHttps(*listener) : server.listenHttp(*listener); + auto promise = isHttpsPort(port) + ? tlsManager.listenHttps(*listener) + : server.listenHttp(*listener); promises = promises.exclusiveJoin(promise.attach(kj::mv(listener))); } if (config.ports.size() > 1) { // Listen on other ports. - auto altPortService = kj::heap( - service, *headerTable, config.rootUrl, config.wildcardHost); - auto altPortServer = kj::heap( - io.provider->getTimer(), *headerTable, *altPortService); - altPortServer = altPortServer.attach(kj::mv(altPortService)); for (auto port: config.ports.slice(1, config.ports.size())) { auto listener = fdBundle.consume(port, *io.lowLevelProvider); - auto promise = altPortServer->listenHttp(*listener); + auto promise = isHttpsPort(port) + ? tlsManager.listenAltPortHttps(*listener) + : altPortServer->listenHttp(*listener); promises = promises.exclusiveJoin(promise.attach(kj::mv(listener))); } promises = promises.attach(kj::mv(altPortServer)); @@ -2611,10 +2624,6 @@ private: KJ_SYSCALL(setenv("HTTP_GATEWAY", "local", true)); KJ_SYSCALL(setenv("PORT", kj::str(config.ports[0]).cStr(), true)); - KJ_IF_MAYBE(httpsPort, config.httpsPort) { - // TODO(cleanup): At this point, all this does is tell Sandcats to refresh certs. - KJ_SYSCALL(setenv("HTTPS_PORT", kj::str(*httpsPort).cStr(), true)); - } KJ_SYSCALL(setenv("MONGO_URL", kj::str("mongodb://", authPrefix, "127.0.0.1:", config.mongoPort, @@ -2628,7 +2637,7 @@ private: kj::StringPtr scheme; uint defaultPort; - if (config.httpsPort == nullptr) { + if (config.httpsPorts.size() == 0) { scheme = "http://"; defaultPort = 80; } else {