Skip to content
Merged
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
4 changes: 4 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ module.exports = {
},

https: {
// Disable the app's own TLS listener where TLS terminates at the platform
// edge (e.g. a managed host that routes plain HTTP to $PORT). Enabled by
// default so local and self-hosted runs keep their HTTPS listener.
enabled: process.env.TLS_ENABLED !== 'false',
port: Number(process.env.TLS_PORT) || 8443,
keyPath: process.env.TLS_KEY,
certPath: process.env.TLS_CERT,
Expand Down
1 change: 1 addition & 0 deletions config/serverConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ const config = require('./index');
module.exports = {
httpPort: config.http.port,
httpsPort: config.https.port,
httpsEnabled: config.https.enabled,
};
36 changes: 27 additions & 9 deletions src/server/startApplicationRuntime.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const {
* initializeDescriptionJobStoreFn?: Function,
* initializeRateLimitStoreProviderFn?: Function,
* loadTlsCredentialsFn?: Function,
* serverPorts?: { httpPort?: number, httpsPort?: number },
* serverPorts?: { httpPort?: number, httpsPort?: number, httpsEnabled?: boolean },
* startServerFn?: Function,
* }} [params] - runtime dependencies
* @returns {Promise<object>}
Expand Down Expand Up @@ -81,18 +81,36 @@ const startApplicationRuntime = async ({
rateLimitStoreProvider,
runtimeState,
});
const tlsCredentials = await loadTlsCredentialsFn();
// Skip the app's own TLS listener where TLS terminates at the platform edge
// (httpsEnabled === false). This avoids loading certificates that do not
// exist there and binds only the plain HTTP listener the platform probes.
const httpsEnabled = serverPorts.httpsEnabled !== false;
const httpServer = createHttpServerFn(app);
const httpsServer = createHttpsServerFn(app, () => tlsCredentials);
/** @type {import('node:http').Server[]} */
const servers = [httpServer];
/** @type {import('node:http').Server | undefined} */
let httpsServer;

await Promise.all([
startServerFn(httpServer, serverPorts.httpPort, appLogger),
startServerFn(httpsServer, serverPorts.httpsPort, appLogger),
]);
// Load certificates before binding any listener so a TLS failure aborts the
// whole boot rather than leaving a half-open HTTP socket behind.
if (httpsEnabled) {
const tlsCredentials = await loadTlsCredentialsFn();
const createdHttpsServer = createHttpsServerFn(app, () => tlsCredentials);
httpsServer = createdHttpsServer;
servers.push(createdHttpsServer);
}

/** @type {Array<Promise<unknown>>} */
const listeners = [startServerFn(httpServer, serverPorts.httpPort, appLogger)];
if (httpsServer) {
listeners.push(startServerFn(httpsServer, serverPorts.httpsPort, appLogger));
}

await Promise.all(listeners);
runtimeState.markReady();

shutdown = gracefulShutdownFn(
[httpServer, httpsServer],
servers,
appLogger,
runtimeState,
processRef,
Expand All @@ -107,7 +125,7 @@ const startApplicationRuntime = async ({
descriptionJobStore,
rateLimitStoreProvider,
runtimeState,
servers: [httpServer, httpsServer],
servers,
shutdown,
};
} catch (error) {
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/config/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,23 @@ describe('Unit | Config | Index', () => {
});

expect(config.https).toEqual({
enabled: true,
port: 9443,
keyPath: 'tls-key',
certPath: 'tls-cert',
});
});

it('disables the HTTPS listener when TLS_ENABLED is "false"', () => {
const config = loadConfig({
overrides: {
TLS_ENABLED: 'false',
},
});

expect(config.https.enabled).toBe(false);
});

it('allows auth tokens to stay configured while auth is explicitly disabled', () => {
const config = loadConfig({
overrides: {
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/server/startApplicationRuntime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,48 @@ describe('Unit | Server | Start Application Runtime', () => {
);
});

it('starts only the HTTP server when TLS is terminated at the edge', async () => {
const app = { name: 'express-app' };
const httpServer = { kind: 'http' };
const shutdown = jest.fn();
const createHttpsServerFn = jest.fn();
const loadTlsCredentialsFn = jest.fn();
const startServerFn = jest.fn().mockResolvedValue(undefined);
const gracefulShutdownFn = jest.fn(() => shutdown);

const result = await startApplicationRuntime({
appLogger: { info: jest.fn() },
config: { env: 'production' },
createAppFn: jest.fn(() => ({ app })),
createHttpServerFn: jest.fn(() => httpServer),
createHttpsServerFn,
gracefulShutdownFn,
initializeDescriptionJobStoreFn: jest.fn(() => ({ close: jest.fn() })),
initializeRateLimitStoreProviderFn: jest.fn(() => ({
close: jest.fn(),
createStore: jest.fn(),
kind: 'memory',
})),
loadTlsCredentialsFn,
processRef: createProcessRef(),
serverPorts: { httpPort: 8080, httpsPort: 8443, httpsEnabled: false },
startServerFn,
});

expect(result.servers).toEqual([httpServer]);
expect(loadTlsCredentialsFn).not.toHaveBeenCalled();
expect(createHttpsServerFn).not.toHaveBeenCalled();
expect(startServerFn).toHaveBeenCalledTimes(1);
expect(startServerFn).toHaveBeenCalledWith(httpServer, 8080, expect.any(Object));
expect(gracefulShutdownFn).toHaveBeenCalledWith(
[httpServer],
expect.any(Object),
result.runtimeState,
expect.any(EventEmitter),
[expect.any(Function), expect.any(Function)],
);
});

it('cleans up fatal handlers when bootstrap fails before servers are ready', async () => {
const processRef = createProcessRef();
const removeListenerSpy = jest.spyOn(processRef, 'off');
Expand Down
Loading