diff --git a/packages/backend-app-api/src/lib/http/config.ts b/packages/backend-app-api/src/lib/http/config.ts new file mode 100644 index 0000000000..933bab42a0 --- /dev/null +++ b/packages/backend-app-api/src/lib/http/config.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { HttpServerOptions } from './types'; + +const DEFAULT_PORT = 7007; +const DEFAULT_HOST = ''; + +/** + * Reads {@link HttpServerOptions} from a {@link @backstage/config#Config} object. + * + * @public + * @remarks + * + * The provided configuration object should contain the `listen` and + * additional keys directly. + * + * @example + * ```ts + * const opts = readHttpServerOptions(config.getConfig('backend')); + * ``` + */ +export function readHttpServerOptions(config: Config): HttpServerOptions { + return { + listen: readHttpListenOptions(config), + https: readHttpsOptions(config), + }; +} + +function readHttpListenOptions(config: Config): HttpServerOptions['listen'] { + const listen = config.get('listen'); + if (typeof listen === 'string') { + const parts = listen.split(':'); + const port = parseInt(parts[parts.length - 1], 10); + if (!isNaN(port)) { + if (parts.length === 1) { + return { port, host: DEFAULT_HOST }; + } + if (parts.length === 2) { + return { host: parts[0], port }; + } + } + throw new Error( + `Unable to parse listen address ${listen}, expected or :`, + ); + } + + return { + port: config.getOptionalNumber('listen.port') ?? DEFAULT_PORT, + host: config.getOptionalString('listen.host') ?? DEFAULT_HOST, + }; +} + +function readHttpsOptions(config: Config): HttpServerOptions['https'] { + const https = config.getOptional('https'); + if (https === true) { + const baseUrl = config.getString('baseUrl'); + let hostname; + try { + hostname = new URL(baseUrl).hostname; + } catch (error) { + throw new Error(`Invalid baseUrl "${baseUrl}"`); + } + + return { certificate: { type: 'generated', hostname } }; + } + + const cc = config.getOptionalConfig('https'); + if (!cc) { + return undefined; + } + + return { + certificate: { + type: 'plain', + cert: cc.getString('certificate.cert'), + key: cc.getString('certificate.key'), + }, + }; +} diff --git a/packages/backend-app-api/src/lib/http/index.ts b/packages/backend-app-api/src/lib/http/index.ts new file mode 100644 index 0000000000..0717194f75 --- /dev/null +++ b/packages/backend-app-api/src/lib/http/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { readHttpServerOptions } from './config'; +export type { HttpServerOptions, HttpServerCertificateOptions } from './types'; diff --git a/packages/backend-app-api/src/lib/http/types.ts b/packages/backend-app-api/src/lib/http/types.ts new file mode 100644 index 0000000000..a33415ec83 --- /dev/null +++ b/packages/backend-app-api/src/lib/http/types.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Options for starting up an HTTP server. + * + * @public + */ +export type HttpServerOptions = { + listen: { + port: number; + host: string; + }; + https?: { + certificate: HttpServerCertificateOptions; + }; +}; + +/** + * Options for configuring HTTPS for an HTTP server. + * + * @public + */ +export type HttpServerCertificateOptions = + | { + type: 'plain'; + key: string; + cert: string; + } + | { + type: 'generated'; + hostname: string; + }; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts index 93cc12b7d4..a147b67ea1 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts @@ -52,49 +52,6 @@ type CustomOrigin = ( callback: (err: Error | null, origin?: StaticOrigin) => void, ) => void; -/** - * Reads some base options out of a config object. - * - * @param config - The root of a backend config object - * @returns A base options object - * - * @example - * ```json - * { - * baseUrl: "http://localhost:7007", - * listen: "0.0.0.0:7007" - * } - * ``` - */ -export function readBaseOptions(config: Config): BaseOptions { - if (typeof config.get('listen') === 'string') { - // TODO(freben): Expand this to support more addresses and perhaps optional - const { host, port } = parseListenAddress(config.getString('listen')); - - return removeUnknown({ - listenPort: port, - listenHost: host, - }); - } - - const port = config.getOptional('listen.port'); - if ( - typeof port !== 'undefined' && - typeof port !== 'number' && - typeof port !== 'string' - ) { - throw new Error( - `Invalid type in config for key 'backend.listen.port', got ${typeof port}, wanted string or number`, - ); - } - - return removeUnknown({ - listenPort: port, - listenHost: config.getOptionalString('listen.host'), - baseUrl: config.getOptionalString('baseUrl'), - }); -} - /** * Attempts to read a CORS options object from the root of a config object. * @@ -165,49 +122,6 @@ export function readCspOptions( return result; } -/** - * Attempts to read a https settings object from the root of a config object. - * - * @param config - The root of a backend config object - * @returns A https settings object, or undefined if not specified - * - * @example - * ```json - * { - * https: { - * certificate: ... - * } - * } - * ``` - */ -export function readHttpsSettings(config: Config): HttpsSettings | undefined { - const https = config.getOptional('https'); - if (https === true) { - const baseUrl = config.getString('baseUrl'); - let hostname; - try { - hostname = new URL(baseUrl).hostname; - } catch (error) { - throw new Error(`Invalid backend.baseUrl "${baseUrl}"`); - } - - return { certificate: { hostname } }; - } - - const cc = config.getOptionalConfig('https'); - if (!cc) { - return undefined; - } - - const certificateConfig = cc.get('certificate'); - - const cfg = { - certificate: certificateConfig, - }; - - return removeUnknown(cfg as HttpsSettings); -} - function getOptionalStringOrStrings( config: Config, key: string, @@ -269,16 +183,3 @@ function removeUnknown(obj: T): T { Object.entries(obj).filter(([, v]) => v !== undefined), ) as T; } - -function parseListenAddress(value: string): { host?: string; port?: number } { - const parts = value.split(':'); - if (parts.length === 1) { - return { port: parseInt(parts[0], 10) }; - } - if (parts.length === 2) { - return { host: parts[0], port: parseInt(parts[1], 10) }; - } - throw new Error( - `Unable to parse listen address ${value}, expected or :`, - ); -}