feat: built-in ssl support

- CORE_HTTPS is now CORE_RETURN_HTTPS
- SSL_(KEY/CERT/ALLOW_HTTP1)
This commit is contained in:
diced 2022-12-07 19:40:54 -08:00
parent eadfa09570
commit c21d8f837e
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
9 changed files with 61 additions and 21 deletions

View file

@ -1,5 +1,5 @@
export interface ConfigCore { export interface ConfigCore {
https: boolean; return_https: boolean;
secret: string; secret: string;
host: string; host: string;
port: number; port: number;
@ -134,6 +134,12 @@ export interface ConfigExif {
remove_gps: boolean; remove_gps: boolean;
} }
export interface ConfigSsl {
allow_http1: boolean;
key: string;
cert: string;
}
export interface Config { export interface Config {
core: ConfigCore; core: ConfigCore;
uploader: ConfigUploader; uploader: ConfigUploader;
@ -147,4 +153,5 @@ export interface Config {
chunks: ConfigChunks; chunks: ConfigChunks;
mfa: ConfigMfa; mfa: ConfigMfa;
exif: ConfigExif; exif: ConfigExif;
ssl: ConfigSsl;
} }

View file

@ -1,10 +1,11 @@
import { parse } from 'dotenv'; import { parse } from 'dotenv';
import { expand } from 'dotenv-expand'; import { expand } from 'dotenv-expand';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
import { resolve } from 'path';
import Logger from '../logger'; import Logger from '../logger';
import { humanToBytes } from '../utils/bytes'; 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<string, any> { function isObject(value: any): value is Record<string, any> {
return typeof value === 'object' && value !== null; return typeof value === 'object' && value !== null;
@ -55,7 +56,7 @@ export default function readConfig() {
} }
const maps = [ const maps = [
map('CORE_HTTPS', 'boolean', 'core.https'), map('CORE_RETURN_HTTPS', 'boolean', 'core.return_https'),
map('CORE_SECRET', 'string', 'core.secret'), map('CORE_SECRET', 'string', 'core.secret'),
map('CORE_HOST', 'string', 'core.host'), map('CORE_HOST', 'string', 'core.host'),
map('CORE_PORT', 'number', 'core.port'), map('CORE_PORT', 'number', 'core.port'),
@ -150,6 +151,10 @@ export default function readConfig() {
map('EXIF_ENABLED', 'boolean', 'exif.enabled'), map('EXIF_ENABLED', 'boolean', 'exif.enabled'),
map('EXIF_REMOVE_GPS', 'boolean', 'exif.remove_gps'), 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 = {}; const config = {};
@ -186,6 +191,10 @@ export default function readConfig() {
parsed = humanToBytes(value) ?? undefined; parsed = humanToBytes(value) ?? undefined;
if (!parsed) logger.debug(`Unable to parse ${map.env}=${value}`); 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; break;
default: default:
parsed = value; parsed = value;

View file

@ -23,7 +23,7 @@ const discord_content = s
const validator = s.object({ const validator = s.object({
core: s.object({ core: s.object({
https: s.boolean.default(false), return_https: s.boolean.default(false),
secret: s.string.lengthGreaterThanOrEqual(8), secret: s.string.lengthGreaterThanOrEqual(8),
host: s.string.default('0.0.0.0'), host: s.string.default('0.0.0.0'),
port: s.number.default(3000), port: s.number.default(3000),
@ -206,6 +206,13 @@ const validator = s.object({
enabled: false, enabled: false,
remove_gps: 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 { export default function validate(config): Config {

View file

@ -25,7 +25,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
return { return {
redirect: discord_auth.oauth_url( redirect: discord_auth.oauth_url(
config.oauth.discord_client_id, config.oauth.discord_client_id,
`${config.core.https ? 'https' : 'http'}://${host}`, `${config.core.return_https ? 'https' : 'http'}://${host}`,
state state
), ),
}; };
@ -35,7 +35,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
client_secret: config.oauth.discord_client_secret, client_secret: config.oauth.discord_client_secret,
code, code,
grant_type: 'authorization_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', scope: 'identify',
}); });

View file

@ -24,7 +24,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
return { return {
redirect: google_auth.oauth_url( redirect: google_auth.oauth_url(
config.oauth.google_client_id, config.oauth.google_client_id,
`${config.core.https ? 'https' : 'http'}://${host}`, `${config.core.return_https ? 'https' : 'http'}://${host}`,
state state
), ),
}; };
@ -33,7 +33,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
code, code,
client_id: config.oauth.google_client_id, client_id: config.oauth.google_client_id,
client_secret: config.oauth.google_client_secret, 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', grant_type: 'authorization_code',
}); });

View file

@ -58,14 +58,14 @@ async function handler(req: NextApiReq, res: NextApiRes) {
await sendShorten( await sendShorten(
user, user,
url, 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 req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id
}` }`
); );
} }
return res.json({ 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 req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id
}`, }`,
}); });

View file

@ -179,7 +179,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
); );
} else { } else {
response.files.push( 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 zconfig.uploader.route === '/' ? '' : zconfig.uploader.route
}/${invis ? invis.invis : file.file}` }/${invis ? invis.invis : file.file}`
); );
@ -189,7 +189,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
await sendUpload( await sendUpload(
user, user,
file, 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 invis ? invis.invis : file.file
}` }`
); );
@ -311,7 +311,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
); );
} else { } else {
response.files.push( 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 zconfig.uploader.route === '/' ? '' : zconfig.uploader.route
}/${invis ? invis.invis : image.file}` }/${invis ? invis.invis : image.file}`
); );
@ -323,7 +323,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
await sendUpload( await sendUpload(
user, user,
image, 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
}`
); );
} }

View file

@ -35,7 +35,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
error: 'oauth token expired', error: 'oauth token expired',
redirect_uri: discord_auth.oauth_url( redirect_uri: discord_auth.oauth_url(
config.oauth.discord_client_id, 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', error: 'oauth token expired',
redirect_uri: discord_auth.oauth_url( redirect_uri: discord_auth.oauth_url(
config.oauth.discord_client_id, 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', error: 'oauth token expired',
redirect_uri: google_auth.oauth_url( redirect_uri: google_auth.oauth_url(
config.oauth.google_client_id, 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', error: 'oauth token expired',
redirect_uri: google_auth.oauth_url( redirect_uri: google_auth.oauth_url(
config.oauth.google_client_id, config.oauth.google_client_id,
`${config.core.https ? 'https' : 'http'}://${req.headers.host}` `${config.core.return_https ? 'https' : 'http'}://${req.headers.host}`
), ),
}); });
} }

View file

@ -4,8 +4,8 @@ import datasource from '../lib/datasource';
import Logger from '../lib/logger'; import Logger from '../lib/logger';
import { getStats } from './util'; import { getStats } from './util';
import fastify, { FastifyInstance } from 'fastify'; import fastify, { FastifyInstance, FastifyServerOptions } from 'fastify';
import { createReadStream, existsSync } from 'fs'; import { createReadStream, existsSync, readFileSync } from 'fs';
import dbFileDecorator from './decorators/dbFile'; import dbFileDecorator from './decorators/dbFile';
import notFound from './decorators/notFound'; import notFound from './decorators/notFound';
import postFileDecorator from './decorators/postFile'; import postFileDecorator from './decorators/postFile';
@ -24,7 +24,7 @@ import urlsRoute, { urlsRouteOnResponse } from './routes/urls';
const dev = process.env.NODE_ENV === 'development'; const dev = process.env.NODE_ENV === 'development';
const logger = Logger.get('server'); const logger = Logger.get('server');
const server = fastify(); const server = fastify(genFastifyOpts());
if (dev) { if (dev) {
server.addHook('onRoute', (opts) => { server.addHook('onRoute', (opts) => {
@ -197,3 +197,18 @@ async function clearInvites(this: FastifyInstance) {
logger.child('invites').debug(`deleted ${count} used invites`); 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;
}