From 96bc17aa1014983d5e6bf8b4c05d898156995a0d Mon Sep 17 00:00:00 2001 From: Aya Morisawa Date: Wed, 6 Feb 2019 22:44:55 +0900 Subject: [PATCH] Check config on load (#4170) Co-authored-by: syuilo --- src/config/load.ts | 157 +++++++++++++++--- src/config/types.ts | 65 +------- src/db/elasticsearch.ts | 7 +- src/db/mongodb.ts | 4 +- src/db/redis.ts | 12 +- src/index.ts | 2 +- src/prelude/maybe.ts | 10 ++ src/queue/index.ts | 14 +- src/remote/activitypub/resolver.ts | 2 +- src/server/api/endpoints/meta.ts | 6 +- .../api/endpoints/users/recommendation.ts | 2 +- src/server/api/streaming.ts | 4 +- src/server/index.ts | 15 +- src/server/proxy/proxy-media.ts | 2 +- src/server/web/index.ts | 4 +- src/services/drive/add-file.ts | 5 +- src/services/drive/upload-from-url.ts | 2 +- src/services/note/create.ts | 2 +- src/tools/move-drive-files.ts | 5 +- 19 files changed, 195 insertions(+), 125 deletions(-) diff --git a/src/config/load.ts b/src/config/load.ts index 5bb01f3d41..29e4a66f91 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -4,9 +4,10 @@ import * as fs from 'fs'; import { URL } from 'url'; +import $ from 'cafy'; import * as yaml from 'js-yaml'; -import { Source, Mixin } from './types'; import * as pkg from '../../package.json'; +import { fromNullable } from '../prelude/maybe'; /** * Path of configuration directory @@ -21,30 +22,148 @@ const path = process.env.NODE_ENV == 'test' : `${dir}/default.yml`; export default function load() { - const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source; + const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')); - const mixin = {} as Mixin; + if (typeof config.url !== 'string') { + throw 'You need to configure the URL.'; + } const url = validateUrl(config.url); - config.url = normalizeUrl(config.url); - mixin.host = url.host; - mixin.hostname = url.hostname; - mixin.scheme = url.protocol.replace(/:$/, ''); - mixin.ws_scheme = mixin.scheme.replace('http', 'ws'); - mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`; - mixin.api_url = `${mixin.scheme}://${mixin.host}/api`; - mixin.auth_url = `${mixin.scheme}://${mixin.host}/auth`; - mixin.dev_url = `${mixin.scheme}://${mixin.host}/dev`; - mixin.docs_url = `${mixin.scheme}://${mixin.host}/docs`; - mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`; - mixin.status_url = `${mixin.scheme}://${mixin.host}/status`; - mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`; - mixin.user_agent = `Misskey/${pkg.version} (${config.url})`; + if (typeof config.port !== 'number') { + throw 'You need to configure the port.'; + } - if (config.autoAdmin == null) config.autoAdmin = false; + if (config.https != null) { + if (typeof config.https.key !== 'string') { + throw 'You need to configure the https key.'; + } + if (typeof config.https.cert !== 'string') { + throw 'You need to configure the https cert.'; + } + } - return Object.assign(config, mixin); + if (config.mongodb == null) { + throw 'You need to configure the MongoDB.'; + } + + if (typeof config.mongodb.host !== 'string') { + throw 'You need to configure the MongoDB host.'; + } + + if (typeof config.mongodb.port !== 'number') { + throw 'You need to configure the MongoDB port.'; + } + + if (typeof config.mongodb.db !== 'string') { + throw 'You need to configure the MongoDB database name.'; + } + + if (config.drive == null) { + throw 'You need to configure the drive.'; + } + + if (typeof config.drive.storage !== 'string') { + throw 'You need to configure the drive storage type.'; + } + + if (!$.str.or(['db', 'minio']).ok(config.drive.storage)) { + throw 'Unrecognized drive storage type is specified.'; + } + + if (config.drive.storage === 'minio') { + if (typeof config.drive.storage.bucket !== 'string') { + throw 'You need to configure the minio bucket.'; + } + + if (typeof config.drive.storage.prefix !== 'string') { + throw 'You need to configure the minio prefix.'; + } + + if (config.drive.storage.prefix.config == null) { + throw 'You need to configure the minio.'; + } + } + + if (config.redis != null) { + if (typeof config.redis.host !== 'string') { + throw 'You need to configure the Redis host.'; + } + + if (typeof config.redis.port !== 'number') { + throw 'You need to configure the Redis port.'; + } + } + + if (config.elasticsearch != null) { + if (typeof config.elasticsearch.host !== 'string') { + throw 'You need to configure the Elasticsearch host.'; + } + + if (typeof config.elasticsearch.port !== 'number') { + throw 'You need to configure the Elasticsearch port.'; + } + } + + const source = { + url: normalizeUrl(config.url as string), + port: config.port as number, + https: fromNullable(config.https).map(x => ({ + key: x.key as string, + cert: x.cert as string, + ca: fromNullable(x.ca) + })), + mongodb: { + host: config.mongodb.host as string, + port: config.mongodb.port as number, + db: config.mongodb.db as string, + user: fromNullable(config.mongodb.user), + pass: fromNullable(config.mongodb.pass) + }, + redis: fromNullable(config.redis).map(x => ({ + host: x.host as string, + port: x.port as number, + pass: fromNullable(x.pass) + })), + elasticsearch: fromNullable(config.elasticsearch).map(x => ({ + host: x.host as string, + port: x.port as number, + pass: fromNullable(x.pass) + })), + disableHsts: typeof config.disableHsts === 'boolean' ? config.disableHsts as boolean : false, + drive: { + storage: config.drive.storage as string, + bucket: config.drive.bucket as string, + prefix: config.drive.prefix as string, + baseUrl: fromNullable(config.drive.baseUrl), + config: config.drive.config + }, + autoAdmin: typeof config.autoAdmin === 'boolean' ? config.autoAdmin as boolean : false, + proxy: fromNullable(config.proxy), + clusterLimit: typeof config.clusterLimit === 'number' ? config.clusterLimit as number : Infinity, + }; + + const host = url.host; + const scheme = url.protocol.replace(/:$/, ''); + const ws_scheme = scheme.replace('http', 'ws'); + + const mixin = { + host: url.host, + hostname: url.hostname, + scheme: scheme, + ws_scheme: ws_scheme, + ws_url: `${ws_scheme}://${host}`, + api_url: `${scheme}://${host}/api`, + auth_url: `${scheme}://${host}/auth`, + dev_url: `${scheme}://${host}/dev`, + docs_url: `${scheme}://${host}/docs`, + stats_url: `${scheme}://${host}/stats`, + status_url: `${scheme}://${host}/status`, + drive_url: `${scheme}://${host}/files`, + user_agent: `Misskey/${pkg.version} (${config.url})` + }; + + return Object.assign(source, mixin); } function tryCreateUrl(url: string) { diff --git a/src/config/types.ts b/src/config/types.ts index 2ce9c0c80d..b133e64c25 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,64 +1,3 @@ -/** - * ユーザーが設定する必要のある情報 - */ -export type Source = { - repository_url?: string; - feedback_url?: string; - url: string; - port: number; - https?: { [x: string]: string }; - disableHsts?: boolean; - mongodb: { - host: string; - port: number; - db: string; - user: string; - pass: string; - }; - redis: { - host: string; - port: number; - pass: string; - }; - elasticsearch: { - host: string; - port: number; - pass: string; - }; - drive?: { - storage: string; - bucket?: string; - prefix?: string; - baseUrl?: string; - config?: any; - }; +import load from "./load"; - autoAdmin?: boolean; - - proxy?: string; - - accesslog?: string; - - clusterLimit?: number; -}; - -/** - * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報 - */ -export type Mixin = { - host: string; - hostname: string; - scheme: string; - ws_scheme: string; - api_url: string; - ws_url: string; - auth_url: string; - docs_url: string; - stats_url: string; - status_url: string; - dev_url: string; - drive_url: string; - user_agent: string; -}; - -export type Config = Source & Mixin; +export type Config = ReturnType; diff --git a/src/db/elasticsearch.ts b/src/db/elasticsearch.ts index cbe6afbbb9..68ad736b25 100644 --- a/src/db/elasticsearch.ts +++ b/src/db/elasticsearch.ts @@ -42,9 +42,10 @@ const index = { }; // Init ElasticSearch connection -const client = config.elasticsearch ? new elasticsearch.Client({ - host: `${config.elasticsearch.host}:${config.elasticsearch.port}` -}) : null; + +const client = config.elasticsearch.map(({ host, port }) => { + return new elasticsearch.Client({ host: `${host}:${port}` }); +}).getOrElse(null); if (client) { // Send a HEAD request diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts index dedb289ce9..3e7d40fde7 100644 --- a/src/db/mongodb.ts +++ b/src/db/mongodb.ts @@ -1,7 +1,7 @@ import config from '../config'; -const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null; -const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null; +const u = config.mongodb.user.map(x => encodeURIComponent(x)).getOrElse(null); +const p = config.mongodb.pass.map(x => encodeURIComponent(x)).getOrElse(null); const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; diff --git a/src/db/redis.ts b/src/db/redis.ts index 48e3f4e43e..4193ac7e7b 100644 --- a/src/db/redis.ts +++ b/src/db/redis.ts @@ -1,10 +1,8 @@ import * as redis from 'redis'; import config from '../config'; -export default config.redis ? redis.createClient( - config.redis.port, - config.redis.host, - { - auth_pass: config.redis.pass - } -) : null; +export default config.redis.map(({ host, port, pass }) => { + return redis.createClient(port, host, { + auth_pass: pass.getOrElse(null) + }); +}).getOrElse(null); diff --git a/src/index.ts b/src/index.ts index 6983ec722e..13f2d0b7d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -228,7 +228,7 @@ async function init(): Promise { return config; } -async function spawnWorkers(limit: number = Infinity) { +async function spawnWorkers(limit: number) { const workers = Math.min(limit, os.cpus().length); bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); await Promise.all([...Array(workers)].map(spawnWorker)); diff --git a/src/prelude/maybe.ts b/src/prelude/maybe.ts index f9ac95c0b5..857fc80195 100644 --- a/src/prelude/maybe.ts +++ b/src/prelude/maybe.ts @@ -1,5 +1,7 @@ export interface Maybe { isJust(): this is Just; + map(f: (x: T) => S): Maybe; + getOrElse(x: T): T; } export type Just = Maybe & { @@ -9,6 +11,8 @@ export type Just = Maybe & { export function just(value: T): Just { return { isJust: () => true, + getOrElse: (_: T) => value, + map: (f: (x: T) => S) => just(f(value)), get: () => value }; } @@ -16,5 +20,11 @@ export function just(value: T): Just { export function nothing(): Maybe { return { isJust: () => false, + getOrElse: (value: T) => value, + map: (_: (x: T) => S) => nothing() }; } + +export function fromNullable(value: T): Maybe { + return value == null ? nothing() : just(value); +} diff --git a/src/queue/index.ts b/src/queue/index.ts index 5d3baa8243..28768bf38f 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -8,17 +8,17 @@ import handler from './processors'; import { queueLogger } from './logger'; const enableQueue = !program.disableQueue; -const queueAvailable = config.redis != null; +const queueAvailable = config.redis.isJust(); const queue = initializeQueue(); function initializeQueue() { - if (queueAvailable) { + return config.redis.map(({ port, host, pass }) => { return new Queue('misskey', { redis: { - port: config.redis.port, - host: config.redis.host, - password: config.redis.pass + port: port, + host: host, + password: pass.getOrElse(null) }, removeOnSuccess: true, @@ -27,9 +27,7 @@ function initializeQueue() { sendEvents: false, storeJobs: false }); - } else { - return null; - } + }).getOrElse(null); } export function deliver(user: ILocalUser, content: any, to: any) { diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 049e645e47..cae37cacd4 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -57,7 +57,7 @@ export default class Resolver { const object = await request({ url: value, - proxy: config.proxy, + proxy: config.proxy.getOrElse(null), timeout: this.timeout, headers: { 'User-Agent': config.user_agent, diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 91cb095c92..d08d0b0734 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -46,7 +46,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { description: instance.description, langs: instance.langs, - secure: config.https != null, + secure: config.https.isJust(), machine: os.hostname(), os: os.platform(), node: process.version, @@ -83,9 +83,9 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { registration: !instance.disableRegistration, localTimeLine: !instance.disableLocalTimeline, globalTimeLine: !instance.disableGlobalTimeline, - elasticsearch: config.elasticsearch ? true : false, + elasticsearch: config.elasticsearch.isJust(), recaptcha: instance.enableRecaptcha, - objectStorage: config.drive && config.drive.storage === 'minio', + objectStorage: config.drive.storage === 'minio', twitter: instance.enableTwitterIntegration, github: instance.enableGithubIntegration, discord: instance.enableDiscordIntegration, diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index e3a03888b6..d07c4b08f0 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -50,7 +50,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { request({ url: url, - proxy: config.proxy, + proxy: config.proxy.getOrElse(null), timeout: timeout, json: true, followRedirect: true, diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index f8f3c0ff4a..d70cec03dc 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -23,10 +23,10 @@ module.exports = (server: http.Server) => { let ev: EventEmitter; - if (config.redis) { + if (config.redis.isJust()) { // Connect to Redis const subscriber = redis.createClient( - config.redis.port, config.redis.host); + config.redis.get().port, config.redis.get().host); subscriber.subscribe('misskey'); diff --git a/src/server/index.ts b/src/server/index.ts index 0e1c701050..df252a3575 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -96,13 +96,14 @@ app.use(router.routes()); app.use(mount(require('./web'))); function createServer() { - if (config.https) { - const certs: any = {}; - for (const k of Object.keys(config.https)) { - certs[k] = fs.readFileSync(config.https[k]); - } - certs['allowHTTP1'] = true; - return http2.createSecureServer(certs, app.callback()) as https.Server; + if (config.https.isJust()) { + const opts = { + key: fs.readFileSync(config.https.get().key), + cert: fs.readFileSync(config.https.get().cert), + ...config.https.get().ca.map(path => ({ ca: fs.readFileSync(path) })).getOrElse({}), + allowHTTP1: true + }; + return http2.createSecureServer(opts, app.callback()) as https.Server; } else { return http.createServer(app.callback()); } diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts index 3f234a727d..95491b6141 100644 --- a/src/server/proxy/proxy-media.ts +++ b/src/server/proxy/proxy-media.ts @@ -69,7 +69,7 @@ async function fetch(url: string, path: string) { const req = request({ url: requestUrl, - proxy: config.proxy, + proxy: config.proxy.getOrElse(null), timeout: 10 * 1000, headers: { 'User-Agent': config.user_agent diff --git a/src/server/web/index.ts b/src/server/web/index.ts index 3f2e1ed19f..d8a8d75c7a 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -31,7 +31,9 @@ const app = new Koa(); app.use(views(__dirname + '/views', { extension: 'pug', options: { - config + config: { + url: config.url + } } })); diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index 31902b2425..b19794b8e2 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -50,8 +50,9 @@ async function save(path: string, name: string, type: string, hash: string, size if (type === 'image/webp') ext = '.webp'; } - const baseUrl = config.drive.baseUrl - || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; + const baseUrl = config.drive.baseUrl.getOrElse( + `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }` + ); // for original const key = `${config.drive.prefix}/${uuid.v4()}${ext}`; diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts index 8daecbadbc..c5c9763d5d 100644 --- a/src/services/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -55,7 +55,7 @@ export default async ( const req = request({ url: requestUrl, - proxy: config.proxy, + proxy: config.proxy.getOrElse(null), timeout: 10 * 1000, headers: { 'User-Agent': config.user_agent diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 9ccf3be9e8..6afdcbbfbd 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -500,7 +500,7 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str } function index(note: INote) { - if (note.text == null || config.elasticsearch == null) return; + if (note.text == null || !config.elasticsearch.isJust()) return; es.index({ index: 'misskey', diff --git a/src/tools/move-drive-files.ts b/src/tools/move-drive-files.ts index 8a1e944503..263e5e125d 100644 --- a/src/tools/move-drive-files.ts +++ b/src/tools/move-drive-files.ts @@ -37,8 +37,9 @@ async function job(file: IDriveFile): Promise { const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`; const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`; - const baseUrl = config.drive.baseUrl - || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; + const baseUrl = config.drive.baseUrl.getOrElse( + `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }` + ); const bucket = await getDriveFileBucket(); const readable = bucket.openDownloadStream(file._id);