Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiple values for HTTPS_PORT #3622

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions docs/administering/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
56 changes: 17 additions & 39 deletions src/sandstorm/config.c++
Original file line number Diff line number Diff line change
Expand Up @@ -70,31 +70,17 @@ auto parser = p::sequence(delimited<' '>(assignment), p::discardWhitespace, p::e

// =======================================================================================

kj::Array<uint> parsePorts(kj::Maybe<uint> httpsPort, kj::StringPtr portList) {
auto portsSplitOnComma = split(portList, ',');
size_t numHttpPorts = portsSplitOnComma.size();
size_t numHttpsPorts;
kj::Array<uint> result;

// If the configuration has a https port, then add it first.
KJ_IF_MAYBE(portNumber, httpsPort) {
numHttpsPorts = 1;
result = kj::heapArray<uint>(numHttpsPorts + numHttpPorts);
result[0] = *portNumber;
} else {
numHttpsPorts = 0;
result = kj::heapArray<uint>(numHttpsPorts + numHttpPorts);
}

kj::Array<uint> parsePortList(kj::StringPtr key, kj::StringPtr portListStr) {
auto portsSplitOnComma = split(portListStr, ',');
auto result = kj::heapArrayBuilder<uint>(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<UserIds> getUserIds(kj::StringPtr name) {
Expand Down Expand Up @@ -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<kj::String> maybePortValue = nullptr;

auto lines = splitLines(readAll(path));
for (auto& line: lines) {
auto equalsPos = KJ_ASSERT_NONNULL(line.findFirst('='), "Invalid config line", line);
Expand All @@ -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;
Expand Down Expand Up @@ -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<uint>(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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/sandstorm/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ struct UserIds {
};

struct Config {
kj::Maybe<uint> httpsPort;
kj::Array<uint> httpsPorts;
kj::Array<uint> ports;
uint mongoPort = 3001;
UserIds uids;
Expand Down
28 changes: 23 additions & 5 deletions src/sandstorm/gateway.c++
Original file line number Diff line number Diff line change
Expand Up @@ -935,9 +935,13 @@ void GatewayService::taskFailed(kj::Exception&& exception) {
// =======================================================================================

GatewayTlsManager::GatewayTlsManager(
kj::HttpServer& server, kj::NetworkAddress& smtpServer,
kj::Maybe<kj::StringPtr> privateKeyPassword, kj::PromiseFulfillerPair<void> readyPaf)
kj::HttpServer& server,
kj::HttpServer& altPortServer,
kj::NetworkAddress& smtpServer,
kj::Maybe<kj::StringPtr> privateKeyPassword,
kj::PromiseFulfillerPair<void> readyPaf)
: server(server),
altPortServer(altPortServer),
smtpServer(smtpServer),
privateKeyPassword(privateKeyPassword),
ready(readyPaf.promise.fork()),
Expand All @@ -950,6 +954,12 @@ kj::Promise<void> GatewayTlsManager::listenHttps(kj::ConnectionReceiver& port) {
});
}

kj::Promise<void> GatewayTlsManager::listenAltPortHttps(kj::ConnectionReceiver& port) {
return ready.addBranch().then([this, &port]() {
return listenAltPortLoop(port);
});
}

kj::Promise<void> GatewayTlsManager::listenSmtp(kj::ConnectionReceiver& port) {
return ready.addBranch().then([this, &port]() {
return listenSmtpLoop(port);
Expand Down Expand Up @@ -1018,12 +1028,20 @@ kj::Promise<void> GatewayTlsManager::subscribeKeys(GatewayRouter::Client gateway
}

kj::Promise<void> GatewayTlsManager::listenLoop(kj::ConnectionReceiver& port) {
return port.accept().then([this, &port](kj::Own<kj::AsyncIoStream>&& stream) {
return listenLoop(port, server);
}

kj::Promise<void> GatewayTlsManager::listenAltPortLoop(kj::ConnectionReceiver& port) {
return listenLoop(port, altPortServer);
}

kj::Promise<void> GatewayTlsManager::listenLoop(kj::ConnectionReceiver& port, kj::HttpServer& srv) {
return port.accept().then([this, &port, &srv](kj::Own<kj::AsyncIoStream>&& stream) {
KJ_IF_MAYBE(t, currentTls) {
auto tls = kj::addRef(**t);
tasks.add(tls->tls.wrapServer(kj::mv(stream))
.then([this](kj::Own<kj::AsyncIoStream>&& encrypted) {
return server.listenHttp(kj::mv(encrypted));
.then([&srv](kj::Own<kj::AsyncIoStream>&& encrypted) {
return srv.listenHttp(kj::mv(encrypted));
}).attach(kj::mv(tls)));
} else {
KJ_LOG(ERROR, "refused HTTPS connection because no TLS keys are configured");
Expand Down
15 changes: 12 additions & 3 deletions src/sandstorm/gateway.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<kj::StringPtr> privateKeyPassword)
: GatewayTlsManager(server, smtpServer, privateKeyPassword,
: GatewayTlsManager(server, altPortServer, smtpServer, privateKeyPassword,
kj::newPromiseAndFulfiller<void>()) {}
// Password, if provided, must remain valid while GatewayTlsManager exists.

Expand All @@ -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<void> listenAltPortHttps(kj::ConnectionReceiver& port);

kj::Promise<void> listenSmtp(kj::ConnectionReceiver& port);
kj::Promise<void> listenSmtps(kj::ConnectionReceiver& port);

Expand All @@ -217,6 +221,7 @@ class GatewayTlsManager: private kj::TaskSet::ErrorHandler {
};

kj::HttpServer& server;
kj::HttpServer& altPortServer;
kj::NetworkAddress& smtpServer;
kj::Maybe<kj::StringPtr> privateKeyPassword;

Expand All @@ -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<kj::StringPtr> privateKeyPassword,
kj::PromiseFulfillerPair<void> readyPaf);

kj::Promise<void> listenLoop(kj::ConnectionReceiver& port);
kj::Promise<void> listenLoop(kj::ConnectionReceiver& port, kj::HttpServer& srv);
kj::Promise<void> listenAltPortLoop(kj::ConnectionReceiver& port);
kj::Promise<void> listenSmtpLoop(kj::ConnectionReceiver& port);
kj::Promise<void> listenSmtpsLoop(kj::ConnectionReceiver& port);

Expand Down
45 changes: 27 additions & 18 deletions src/sandstorm/run-bundle.c++
Original file line number Diff line number Diff line change
Expand Up @@ -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<AltPortService>(
service, *headerTable, config.rootUrl, config.wildcardHost);
auto altPortServer = kj::heap<kj::HttpServer>(
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<void> promises = service.cleanupLoop()
.exclusiveJoin(tlsManager.subscribeKeys(kj::mv(router)))
Expand All @@ -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<AltPortService>(
service, *headerTable, config.rootUrl, config.wildcardHost);
auto altPortServer = kj::heap<kj::HttpServer>(
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the issue be here? If I can parse this, it means the altPortServer is only being reached if it's an HTTP port?

promises = promises.exclusiveJoin(promise.attach(kj::mv(listener)));
}
promises = promises.attach(kj::mv(altPortServer));
Expand Down Expand Up @@ -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.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fairly sure at this point this doesn't do anything; the sandcats logic doesn't look for this variable, and grepping around for it didn't come up with any uses.

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,
Expand All @@ -2628,7 +2637,7 @@ private:
kj::StringPtr scheme;
uint defaultPort;

if (config.httpsPort == nullptr) {
if (config.httpsPorts.size() == 0) {
scheme = "http://";
defaultPort = 80;
} else {
Expand Down