backend-app-api: move http options reading to lib + refactor

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-01-04 15:10:47 +01:00
parent 7695c5a44c
commit 31f20f5029
4 changed files with 158 additions and 99 deletions
@@ -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 <port> or <host>:<port>`,
);
}
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'),
},
};
}
@@ -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';
@@ -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;
};
@@ -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<T extends object>(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 <port> or <host>:<port>`,
);
}