From c21d8f837e14cc9d300f8b259e3fac9ccb79733b Mon Sep 17 00:00:00 2001 From: diced Date: Wed, 7 Dec 2022 19:40:54 -0800 Subject: [PATCH] feat: built-in ssl support - CORE_HTTPS is now CORE_RETURN_HTTPS - SSL_(KEY/CERT/ALLOW_HTTP1) --- src/lib/config/Config.ts | 9 ++++++++- src/lib/config/readConfig.ts | 13 +++++++++++-- src/lib/config/validateConfig.ts | 9 ++++++++- src/pages/api/auth/oauth/discord.ts | 4 ++-- src/pages/api/auth/oauth/google.ts | 4 ++-- src/pages/api/shorten.ts | 4 ++-- src/pages/api/upload.ts | 10 ++++++---- src/pages/api/user/index.ts | 8 ++++---- src/server/index.ts | 21 ++++++++++++++++++--- 9 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/lib/config/Config.ts b/src/lib/config/Config.ts index 8cedd10..421e8b4 100644 --- a/src/lib/config/Config.ts +++ b/src/lib/config/Config.ts @@ -1,5 +1,5 @@ export interface ConfigCore { - https: boolean; + return_https: boolean; secret: string; host: string; port: number; @@ -134,6 +134,12 @@ export interface ConfigExif { remove_gps: boolean; } +export interface ConfigSsl { + allow_http1: boolean; + key: string; + cert: string; +} + export interface Config { core: ConfigCore; uploader: ConfigUploader; @@ -147,4 +153,5 @@ export interface Config { chunks: ConfigChunks; mfa: ConfigMfa; exif: ConfigExif; + ssl: ConfigSsl; } diff --git a/src/lib/config/readConfig.ts b/src/lib/config/readConfig.ts index 9721276..5d2b898 100644 --- a/src/lib/config/readConfig.ts +++ b/src/lib/config/readConfig.ts @@ -1,10 +1,11 @@ import { parse } from 'dotenv'; import { expand } from 'dotenv-expand'; import { existsSync, readFileSync } from 'fs'; +import { resolve } from 'path'; import Logger from '../logger'; import { humanToBytes } from '../utils/bytes'; -export type ValueType = 'string' | 'number' | 'boolean' | 'array' | 'json-array' | 'human-to-byte'; +export type ValueType = 'string' | 'number' | 'boolean' | 'array' | 'json-array' | 'human-to-byte' | 'path'; function isObject(value: any): value is Record { return typeof value === 'object' && value !== null; @@ -55,7 +56,7 @@ export default function readConfig() { } const maps = [ - map('CORE_HTTPS', 'boolean', 'core.https'), + map('CORE_RETURN_HTTPS', 'boolean', 'core.return_https'), map('CORE_SECRET', 'string', 'core.secret'), map('CORE_HOST', 'string', 'core.host'), map('CORE_PORT', 'number', 'core.port'), @@ -150,6 +151,10 @@ export default function readConfig() { map('EXIF_ENABLED', 'boolean', 'exif.enabled'), map('EXIF_REMOVE_GPS', 'boolean', 'exif.remove_gps'), + + map('SSL_KEY', 'path', 'ssl.key'), + map('SSL_CERT', 'path', 'ssl.cert'), + map('SSL_ALLOW_HTTP1', 'boolean', 'ssl.allow_http1'), ]; const config = {}; @@ -186,6 +191,10 @@ export default function readConfig() { parsed = humanToBytes(value) ?? undefined; if (!parsed) logger.debug(`Unable to parse ${map.env}=${value}`); + break; + case 'path': + parsed = resolve(value); + if (!existsSync(parsed)) logger.debug(`Unable to find ${map.env}=${value} (path does not exist)`); break; default: parsed = value; diff --git a/src/lib/config/validateConfig.ts b/src/lib/config/validateConfig.ts index d0a8baa..5d6fdbb 100644 --- a/src/lib/config/validateConfig.ts +++ b/src/lib/config/validateConfig.ts @@ -23,7 +23,7 @@ const discord_content = s const validator = s.object({ core: s.object({ - https: s.boolean.default(false), + return_https: s.boolean.default(false), secret: s.string.lengthGreaterThanOrEqual(8), host: s.string.default('0.0.0.0'), port: s.number.default(3000), @@ -206,6 +206,13 @@ const validator = s.object({ enabled: false, remove_gps: false, }), + ssl: s + .object({ + key: s.string, + cert: s.string, + allow_http1: s.boolean.default(false), + }) + .optional.nullish.default(null), }); export default function validate(config): Config { diff --git a/src/pages/api/auth/oauth/discord.ts b/src/pages/api/auth/oauth/discord.ts index 8a19b0d..ccd8594 100644 --- a/src/pages/api/auth/oauth/discord.ts +++ b/src/pages/api/auth/oauth/discord.ts @@ -25,7 +25,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi return { redirect: discord_auth.oauth_url( config.oauth.discord_client_id, - `${config.core.https ? 'https' : 'http'}://${host}`, + `${config.core.return_https ? 'https' : 'http'}://${host}`, state ), }; @@ -35,7 +35,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi client_secret: config.oauth.discord_client_secret, code, grant_type: 'authorization_code', - redirect_uri: `${config.core.https ? 'https' : 'http'}://${host}/api/auth/oauth/discord`, + redirect_uri: `${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/discord`, scope: 'identify', }); diff --git a/src/pages/api/auth/oauth/google.ts b/src/pages/api/auth/oauth/google.ts index 5f8c2f9..ad2a577 100644 --- a/src/pages/api/auth/oauth/google.ts +++ b/src/pages/api/auth/oauth/google.ts @@ -24,7 +24,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi return { redirect: google_auth.oauth_url( config.oauth.google_client_id, - `${config.core.https ? 'https' : 'http'}://${host}`, + `${config.core.return_https ? 'https' : 'http'}://${host}`, state ), }; @@ -33,7 +33,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi code, client_id: config.oauth.google_client_id, client_secret: config.oauth.google_client_secret, - redirect_uri: `${config.core.https ? 'https' : 'http'}://${host}/api/auth/oauth/google`, + redirect_uri: `${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/google`, grant_type: 'authorization_code', }); diff --git a/src/pages/api/shorten.ts b/src/pages/api/shorten.ts index a920ce5..af4ce0f 100644 --- a/src/pages/api/shorten.ts +++ b/src/pages/api/shorten.ts @@ -58,14 +58,14 @@ async function handler(req: NextApiReq, res: NextApiRes) { await sendShorten( user, url, - `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${ + `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${ req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id }` ); } return res.json({ - url: `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${ + url: `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${ req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id }`, }); diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts index 97166e1..d17228d 100644 --- a/src/pages/api/upload.ts +++ b/src/pages/api/upload.ts @@ -179,7 +179,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { ); } else { response.files.push( - `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${ + `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}${ zconfig.uploader.route === '/' ? '' : zconfig.uploader.route }/${invis ? invis.invis : file.file}` ); @@ -189,7 +189,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { await sendUpload( user, file, - `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}/r/${ + `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}/r/${ invis ? invis.invis : file.file }` ); @@ -311,7 +311,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { ); } else { response.files.push( - `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${ + `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}${ zconfig.uploader.route === '/' ? '' : zconfig.uploader.route }/${invis ? invis.invis : image.file}` ); @@ -323,7 +323,9 @@ async function handler(req: NextApiReq, res: NextApiRes) { await sendUpload( user, image, - `${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}/r/${invis ? invis.invis : image.file}` + `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}/r/${ + invis ? invis.invis : image.file + }` ); } diff --git a/src/pages/api/user/index.ts b/src/pages/api/user/index.ts index 908b14f..8c0cf87 100644 --- a/src/pages/api/user/index.ts +++ b/src/pages/api/user/index.ts @@ -35,7 +35,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { error: 'oauth token expired', redirect_uri: discord_auth.oauth_url( config.oauth.discord_client_id, - `${config.core.https ? 'https' : 'http'}://${req.headers.host}` + `${config.core.return_https ? 'https' : 'http'}://${req.headers.host}` ), }); } @@ -59,7 +59,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { error: 'oauth token expired', redirect_uri: discord_auth.oauth_url( config.oauth.discord_client_id, - `${config.core.https ? 'https' : 'http'}://${req.headers.host}` + `${config.core.return_https ? 'https' : 'http'}://${req.headers.host}` ), }); } @@ -90,7 +90,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { error: 'oauth token expired', redirect_uri: google_auth.oauth_url( config.oauth.google_client_id, - `${config.core.https ? 'https' : 'http'}://${req.headers.host}` + `${config.core.return_https ? 'https' : 'http'}://${req.headers.host}` ), }); } @@ -113,7 +113,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { error: 'oauth token expired', redirect_uri: google_auth.oauth_url( config.oauth.google_client_id, - `${config.core.https ? 'https' : 'http'}://${req.headers.host}` + `${config.core.return_https ? 'https' : 'http'}://${req.headers.host}` ), }); } diff --git a/src/server/index.ts b/src/server/index.ts index e04f208..50e96e3 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,8 +4,8 @@ import datasource from '../lib/datasource'; import Logger from '../lib/logger'; import { getStats } from './util'; -import fastify, { FastifyInstance } from 'fastify'; -import { createReadStream, existsSync } from 'fs'; +import fastify, { FastifyInstance, FastifyServerOptions } from 'fastify'; +import { createReadStream, existsSync, readFileSync } from 'fs'; import dbFileDecorator from './decorators/dbFile'; import notFound from './decorators/notFound'; import postFileDecorator from './decorators/postFile'; @@ -24,7 +24,7 @@ import urlsRoute, { urlsRouteOnResponse } from './routes/urls'; const dev = process.env.NODE_ENV === 'development'; const logger = Logger.get('server'); -const server = fastify(); +const server = fastify(genFastifyOpts()); if (dev) { server.addHook('onRoute', (opts) => { @@ -197,3 +197,18 @@ async function clearInvites(this: FastifyInstance) { logger.child('invites').debug(`deleted ${count} used invites`); } + +function genFastifyOpts(): FastifyServerOptions { + const opts = {}; + + if (config.ssl?.cert && config.ssl?.key) { + opts['https'] = { + key: readFileSync(config.ssl.key), + cert: readFileSync(config.ssl.cert), + }; + + if (config.ssl?.allow_http1) opts['https']['allowHTTP1'] = true; + } + + return opts; +}