From b3246a88d5fa9cf006fd42aed0b1db669961dffc Mon Sep 17 00:00:00 2001 From: tecc Date: Sat, 24 Sep 2022 15:55:11 +0200 Subject: [PATCH 1/7] change(env): Improved environment parsing getEnvRaw: `getEnvRaw` is essentially the same as `getEnv`, but much more safe as it doesn't automatically convert types - parsing is left to the consuming code. parseBool: `parseBool` is a much more accepting function for parsing booleans. `true`, `1`, `yes`, and `on` are treated as `true`, and conversely, `false`, `0`, `no`, and `off` are treated as `false`. --- libs/server/config.ts | 55 +++++++++++++++++++------------------------ libs/shared/env.ts | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/libs/server/config.ts b/libs/server/config.ts index 571d3647a..e64506fbb 100644 --- a/libs/server/config.ts +++ b/libs/server/config.ts @@ -1,5 +1,5 @@ import yaml from 'js-yaml'; -import { getEnv } from 'libs/shared/env'; +import * as env from 'libs/shared/env'; import { existsSync, readFileSync } from 'fs'; export type BasicUser = { username: string; password: string }; @@ -40,7 +40,7 @@ export interface Configuration { let loaded: Configuration | undefined = undefined; export function loadConfig() { - const configFile = String(getEnv('CONFIG_FILE', './notea.yml')); + const configFile = env.getEnvRaw('CONFIG_FILE', false) ?? './notea.yml'; let baseConfig: Configuration = {} as Configuration; if (existsSync(configFile)) { @@ -48,11 +48,11 @@ export function loadConfig() { baseConfig = yaml.load(data) as Configuration; } - const disablePassword = getEnv('DISABLE_PASSWORD', undefined); + const disablePassword = env.parseBool(env.getEnvRaw('DISABLE_PASSWORD', false), false); let auth: AuthConfiguration; - if (disablePassword === undefined || !disablePassword) { - const envPassword = getEnv('PASSWORD', undefined, false); + if (!disablePassword) { + const envPassword = env.getEnvRaw('PASSWORD', false); if (baseConfig.auth === undefined) { if (envPassword === undefined) { throw new Error('Authentication undefined'); @@ -98,47 +98,40 @@ export function loadConfig() { // for now, this works { store.detectCredentials ??= true; - store.accessKey = getEnv( + store.accessKey = env.getEnvRaw( 'STORE_ACCESS_KEY', - store.accessKey, - !store.detectCredentials - )?.toString(); - store.secretKey = getEnv( + !store.detectCredentials || !store.accessKey + ) ?? store.accessKey; + store.secretKey = env.getEnvRaw( 'STORE_SECRET_KEY', - store.secretKey, - !store.detectCredentials - )?.toString(); - store.bucket = getEnv( + !store.detectCredentials || !store.secretKey + ) ?? store.secretKey; + store.bucket = env.getEnvRaw( 'STORE_BUCKET', - store.bucket ?? 'notea', false - ).toString(); - store.forcePathStyle = getEnv( + ) ?? 'notea'; + store.forcePathStyle = env.parseBool(env.getEnvRaw( 'STORE_FORCE_PATH_STYLE', - store.forcePathStyle ?? false, !store.forcePathStyle - ); - store.endpoint = getEnv( + )) ?? store.forcePathStyle; + store.endpoint = env.getEnvRaw( 'STORE_END_POINT', - store.endpoint, - !store.endpoint - ); - store.region = getEnv( + store.endpoint == null + ) ?? store.endpoint; + store.region = env.getEnvRaw( 'STORE_REGION', - store.region ?? 'us-east-1', false - ).toString(); - store.prefix = getEnv( + ) ?? store.region ?? 'us-east-1'; + store.prefix = env.getEnvRaw( 'STORE_PREFIX', - store.prefix ?? '', - false - ); + false, + ) ?? store.prefix ?? ''; } loaded = { auth, store, - baseUrl: getEnv('BASE_URL')?.toString() ?? baseConfig.baseUrl, + baseUrl: env.getEnvRaw('BASE_URL', false) ?? baseConfig.baseUrl, }; } diff --git a/libs/shared/env.ts b/libs/shared/env.ts index 5564321b3..bb9cb5acc 100644 --- a/libs/shared/env.ts +++ b/libs/shared/env.ts @@ -44,3 +44,45 @@ export function getEnv( return result as unknown as T; } + +export function getEnvRaw(env: AllowedEnvs, required: true): string; +export function getEnvRaw(env: AllowedEnvs, required?: false): string | undefined; +export function getEnvRaw(env: AllowedEnvs, required?: boolean): string | undefined; +export function getEnvRaw(env: AllowedEnvs, required: boolean = false): string | undefined { + const value = process.env[env]; + + if (value == null) { + if (required) { + throw new Error(`[Notea] ${env} is undefined`); + } else { + return undefined; + } + } + + return String(value).toLocaleLowerCase(); +} + +export function parseBool(str: string, invalid?: boolean): boolean; +export function parseBool(str: null | undefined): undefined; +export function parseBool(str: string | null | undefined, invalid: boolean): boolean; +export function parseBool(str: string | null | undefined, invalid?: boolean): boolean | undefined; +export function parseBool(str: string | null | undefined, invalid?: boolean): boolean | undefined { + if (str == null) { + return invalid ?? undefined; + } + switch (str.toLowerCase()) { + case "true": + case "1": + case "yes": + case "on": + return true; + case "false": + case "0": + case "no": + case "off": + return false; + default: + if (invalid == null) throw new Error("Invalid boolean: " + str); + else return invalid; + } +} \ No newline at end of file From d82c71070b697b69ec457fbf4aa495c888657ed1 Mon Sep 17 00:00:00 2001 From: tecc Date: Sat, 24 Sep 2022 15:58:43 +0200 Subject: [PATCH 2/7] change(config): Add `proxyAttachments` config value. proxyAttachments: `proxyAttachments` is the equivalent of `DIRECT_RESPONSE_ATTACHMENT`. [...file].ts: Replaced direct usage of `getEnv` with new config system. --- libs/server/config.ts | 2 ++ pages/api/file/[...file].ts | 21 +++++++++------------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/libs/server/config.ts b/libs/server/config.ts index e64506fbb..0f3f495e7 100644 --- a/libs/server/config.ts +++ b/libs/server/config.ts @@ -26,6 +26,7 @@ export interface S3StoreConfiguration { region: string; forcePathStyle: boolean; prefix: string; + proxyAttachments: boolean; } export type StoreConfiguration = S3StoreConfiguration; @@ -126,6 +127,7 @@ export function loadConfig() { 'STORE_PREFIX', false, ) ?? store.prefix ?? ''; + store.proxyAttachments = env.parseBool(env.getEnvRaw('DIRECT_RESPONSE_ATTACHMENT', false), store.proxyAttachments ?? false); } loaded = { diff --git a/pages/api/file/[...file].ts b/pages/api/file/[...file].ts index a5c892fd1..bba52883c 100644 --- a/pages/api/file/[...file].ts +++ b/pages/api/file/[...file].ts @@ -2,7 +2,7 @@ import { api } from 'libs/server/connect'; import { useReferrer } from 'libs/server/middlewares/referrer'; import { useStore } from 'libs/server/middlewares/store'; import { getPathFileByName } from 'libs/server/note-path'; -import { getEnv } from 'libs/shared/env'; +import { config } from 'libs/server/config'; // On aliyun `X-Amz-Expires` must be less than 604800 seconds const expires = 86400; @@ -24,9 +24,7 @@ export default api() `public, max-age=${expires}, s-maxage=${expires}, stale-while-revalidate=${expires}` ); - const directed = getEnv('DIRECT_RESPONSE_ATTACHMENT', false); - - if (directed) { + if (config().store.proxyAttachments) { const { buffer, contentType } = await req.state.store.getObjectAndMeta(objectPath); @@ -35,15 +33,14 @@ export default api() } res.send(buffer); - return; - } + } else { + const signUrl = await req.state.store.getSignUrl(objectPath, expires); - const signUrl = await req.state.store.getSignUrl(objectPath, expires); + if (signUrl) { + res.redirect(signUrl); + return; + } - if (signUrl) { - res.redirect(signUrl); - return; + res.redirect('/404'); } - - res.redirect('/404'); }); From 777f95834925113b34ae57b25a1cd9de4f5c0d6b Mon Sep 17 00:00:00 2001 From: tecc Date: Sat, 24 Sep 2022 16:00:33 +0200 Subject: [PATCH 3/7] docs(config): More documentation for `config/compatibility.yml` forcePathStyle: Added missing field of `forcePathStyle`. proxyAttachments: Added documentation for `proxyAttachments`. --- config/compatibility.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/compatibility.yml b/config/compatibility.yml index 92b30e29a..7a015ebce 100644 --- a/config/compatibility.yml +++ b/config/compatibility.yml @@ -22,6 +22,9 @@ store: # secretKey is the same as STORE_SECRET_KEY; must be specified if env doesn't secretKey: YOUR_SECRET_KEY # forcePathStyle is the same as STORE_FORCE_PATH_STYLE; defaults to false + forcePathStyle: true + # proxyAttachments is the same as DIRECT_RESPONSE_ATTACHMENT; defaults to false + proxyAttachments: true # baseUrl is the same as BASE_URL; not required baseUrl: https://example.com From c83f55219da197984a004918b8e665eedabba320 Mon Sep 17 00:00:00 2001 From: tecc Date: Sat, 24 Sep 2022 16:16:09 +0200 Subject: [PATCH 4/7] change(config): Add `server` field to config server: The `server` field will just contain things related to the server. I dunno how to explain it, but it's for that. useSecureCookies: `server.useSecureCookies` is the equivalent of `COOKIE_SECURE`. config/compatibility.yml: Add documentation for the new `server` field. --- config/compatibility.yml | 7 ++++++- libs/server/config.ts | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/config/compatibility.yml b/config/compatibility.yml index 7a015ebce..75127a524 100644 --- a/config/compatibility.yml +++ b/config/compatibility.yml @@ -26,5 +26,10 @@ store: # proxyAttachments is the same as DIRECT_RESPONSE_ATTACHMENT; defaults to false proxyAttachments: true +# Server configuration +server: + # useSecureConfig is the same as COOKIE_SECURE; if in production (which is for end-users), it will be true, else it'll be false + useSecureConfig: true + # baseUrl is the same as BASE_URL; not required -baseUrl: https://example.com +baseUrl: https://example.com \ No newline at end of file diff --git a/libs/server/config.ts b/libs/server/config.ts index 0f3f495e7..5578c6801 100644 --- a/libs/server/config.ts +++ b/libs/server/config.ts @@ -31,9 +31,15 @@ export interface S3StoreConfiguration { export type StoreConfiguration = S3StoreConfiguration; +export interface ServerConfiguration { + useSecureCookies: boolean; + // TODO: Move baseUrl to here +} + export interface Configuration { auth: AuthConfiguration; store: StoreConfiguration; + server: ServerConfiguration; baseUrl?: string; } @@ -130,9 +136,20 @@ export function loadConfig() { store.proxyAttachments = env.parseBool(env.getEnvRaw('DIRECT_RESPONSE_ATTACHMENT', false), store.proxyAttachments ?? false); } + let server: ServerConfiguration; + if (!baseConfig.server) { + server = {} as ServerConfiguration; + } else { + server = baseConfig.server; + } + { + server.useSecureCookies = env.parseBool(env.getEnvRaw('COOKIE_SECURE', false), process.env.NODE_ENV === 'production'); + } + loaded = { auth, store, + server, baseUrl: env.getEnvRaw('BASE_URL', false) ?? baseConfig.baseUrl, }; } From 5fcd9eabbe14e65d47a73cea0e9ceb0d89733006 Mon Sep 17 00:00:00 2001 From: tecc Date: Sat, 24 Sep 2022 16:21:24 +0200 Subject: [PATCH 5/7] change(env): Mark `getEnv` as deprecated deprecation: Well, it is. `getEnv` is too direct. It's not really a good system. --- libs/shared/env.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/shared/env.ts b/libs/shared/env.ts index bb9cb5acc..8c42e6c2a 100644 --- a/libs/shared/env.ts +++ b/libs/shared/env.ts @@ -14,6 +14,9 @@ type AllowedEnvs = | 'STORE_PREFIX' | 'CONFIG_FILE'; +/** + * @deprecated This function should not be used. Prefer the `config()` system. + */ export function getEnv( env: AllowedEnvs, defaultValue?: any, From 6fb14e611f3ac3d63390f925a952e83292c47de4 Mon Sep 17 00:00:00 2001 From: tecc Date: Sat, 24 Sep 2022 16:21:51 +0200 Subject: [PATCH 6/7] change(env): Replace remaining usages of `getEnv` --- libs/server/middlewares/csrf.ts | 4 ++-- libs/server/middlewares/session.ts | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/libs/server/middlewares/csrf.ts b/libs/server/middlewares/csrf.ts index 9ee8f8e3c..99a27e53a 100644 --- a/libs/server/middlewares/csrf.ts +++ b/libs/server/middlewares/csrf.ts @@ -1,13 +1,13 @@ import Tokens from 'csrf'; import { CSRF_HEADER_KEY } from 'libs/shared/const'; -import { getEnv } from 'libs/shared/env'; import md5 from 'md5'; import { ApiNext, ApiRequest, ApiResponse, SSRMiddleware } from '../connect'; +import { BasicAuthConfiguration, config } from 'libs/server/config'; const tokens = new Tokens(); // generate CSRF secret -const csrfSecret = md5('CSRF' + getEnv('PASSWORD')); +const csrfSecret = md5('CSRF' + (config().auth as BasicAuthConfiguration).password); export const getCsrfToken = () => tokens.create(csrfSecret); diff --git a/libs/server/middlewares/session.ts b/libs/server/middlewares/session.ts index fe406a46f..eabfef4d8 100644 --- a/libs/server/middlewares/session.ts +++ b/libs/server/middlewares/session.ts @@ -1,16 +1,13 @@ import { ironSession } from 'next-iron-session'; import md5 from 'md5'; -import { getEnv } from 'libs/shared/env'; +import { BasicAuthConfiguration, config } from 'libs/server/config'; const sessionOptions = { cookieName: 'notea-auth', - password: md5('notea' + getEnv('PASSWORD')), + password: md5('notea' + (config().auth as BasicAuthConfiguration).password), // NOTE(tecc): in the future, if this field becomes null, it will be an issue // if your localhost is served on http:// then disable the secure flag cookieOptions: { - secure: getEnv( - 'COOKIE_SECURE', - process.env.NODE_ENV === 'production' - ), + secure: config().server.useSecureCookies, }, }; From ad662afade3e460cbe679e4b02dd1811f081fa80 Mon Sep 17 00:00:00 2001 From: tecc Date: Sat, 24 Sep 2022 16:26:31 +0200 Subject: [PATCH 7/7] change(env): Move `config().baseUrl` to `config().server.baseUrl` config/compatibility.yml: Updated the example to reflect how the config actually works. move-to-server: It feels nicer to have it inside the `server` field. --- config/compatibility.yml | 5 ++--- libs/server/config.ts | 7 +++---- libs/server/middlewares/note.ts | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/config/compatibility.yml b/config/compatibility.yml index 75127a524..7401c1ced 100644 --- a/config/compatibility.yml +++ b/config/compatibility.yml @@ -30,6 +30,5 @@ store: server: # useSecureConfig is the same as COOKIE_SECURE; if in production (which is for end-users), it will be true, else it'll be false useSecureConfig: true - -# baseUrl is the same as BASE_URL; not required -baseUrl: https://example.com \ No newline at end of file + # baseUrl is the same as BASE_URL; not required + baseUrl: https://example.com \ No newline at end of file diff --git a/libs/server/config.ts b/libs/server/config.ts index 5578c6801..5d4fcaea5 100644 --- a/libs/server/config.ts +++ b/libs/server/config.ts @@ -33,14 +33,13 @@ export type StoreConfiguration = S3StoreConfiguration; export interface ServerConfiguration { useSecureCookies: boolean; - // TODO: Move baseUrl to here + baseUrl?: string; } export interface Configuration { auth: AuthConfiguration; store: StoreConfiguration; server: ServerConfiguration; - baseUrl?: string; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -144,13 +143,13 @@ export function loadConfig() { } { server.useSecureCookies = env.parseBool(env.getEnvRaw('COOKIE_SECURE', false), process.env.NODE_ENV === 'production'); + server.baseUrl = env.getEnvRaw('BASE_URL', false) ?? baseConfig.server.baseUrl; } loaded = { auth, store, - server, - baseUrl: env.getEnvRaw('BASE_URL', false) ?? baseConfig.baseUrl, + server }; } diff --git a/libs/server/middlewares/note.ts b/libs/server/middlewares/note.ts index db5d77aa5..6e5ea6026 100644 --- a/libs/server/middlewares/note.ts +++ b/libs/server/middlewares/note.ts @@ -32,7 +32,7 @@ export const applyNote: (id: string) => SSRMiddleware = req.props = { ...req.props, ...props, - baseURL: config()?.baseUrl || 'http://' + req.headers.host, + baseURL: config().server.baseUrl ?? 'http://' + req.headers.host, }; next();