Check config on load (#4170)

Co-authored-by: syuilo <syuilotan@yahoo.co.jp>
This commit is contained in:
Aya Morisawa 2019-02-06 22:44:55 +09:00 committed by syuilo
parent 41ba06a5e6
commit 96bc17aa10
19 changed files with 195 additions and 125 deletions

View File

@ -4,9 +4,10 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { URL } from 'url'; import { URL } from 'url';
import $ from 'cafy';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import { Source, Mixin } from './types';
import * as pkg from '../../package.json'; import * as pkg from '../../package.json';
import { fromNullable } from '../prelude/maybe';
/** /**
* Path of configuration directory * Path of configuration directory
@ -21,30 +22,148 @@ const path = process.env.NODE_ENV == 'test'
: `${dir}/default.yml`; : `${dir}/default.yml`;
export default function load() { 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); const url = validateUrl(config.url);
config.url = normalizeUrl(config.url);
mixin.host = url.host; if (typeof config.port !== 'number') {
mixin.hostname = url.hostname; throw 'You need to configure the port.';
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 (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<string>(x.ca)
})),
mongodb: {
host: config.mongodb.host as string,
port: config.mongodb.port as number,
db: config.mongodb.db as string,
user: fromNullable<string>(config.mongodb.user),
pass: fromNullable<string>(config.mongodb.pass)
},
redis: fromNullable(config.redis).map(x => ({
host: x.host as string,
port: x.port as number,
pass: fromNullable<string>(x.pass)
})),
elasticsearch: fromNullable(config.elasticsearch).map(x => ({
host: x.host as string,
port: x.port as number,
pass: fromNullable<string>(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<string>(config.drive.baseUrl),
config: config.drive.config
},
autoAdmin: typeof config.autoAdmin === 'boolean' ? config.autoAdmin as boolean : false,
proxy: fromNullable<string>(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) { function tryCreateUrl(url: string) {

View File

@ -1,64 +1,3 @@
/** import load from "./load";
*
*/
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;
};
autoAdmin?: boolean; export type Config = ReturnType<typeof load>;
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;

View File

@ -42,9 +42,10 @@ const index = {
}; };
// Init ElasticSearch connection // Init ElasticSearch connection
const client = config.elasticsearch ? new elasticsearch.Client({
host: `${config.elasticsearch.host}:${config.elasticsearch.port}` const client = config.elasticsearch.map(({ host, port }) => {
}) : null; return new elasticsearch.Client({ host: `${host}:${port}` });
}).getOrElse(null);
if (client) { if (client) {
// Send a HEAD request // Send a HEAD request

View File

@ -1,7 +1,7 @@
import config from '../config'; import config from '../config';
const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null; const u = config.mongodb.user.map(x => encodeURIComponent(x)).getOrElse(null);
const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : 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}`; const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;

View File

@ -1,10 +1,8 @@
import * as redis from 'redis'; import * as redis from 'redis';
import config from '../config'; import config from '../config';
export default config.redis ? redis.createClient( export default config.redis.map(({ host, port, pass }) => {
config.redis.port, return redis.createClient(port, host, {
config.redis.host, auth_pass: pass.getOrElse(null)
{ });
auth_pass: config.redis.pass }).getOrElse(null);
}
) : null;

View File

@ -228,7 +228,7 @@ async function init(): Promise<Config> {
return config; return config;
} }
async function spawnWorkers(limit: number = Infinity) { async function spawnWorkers(limit: number) {
const workers = Math.min(limit, os.cpus().length); const workers = Math.min(limit, os.cpus().length);
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
await Promise.all([...Array(workers)].map(spawnWorker)); await Promise.all([...Array(workers)].map(spawnWorker));

View File

@ -1,5 +1,7 @@
export interface Maybe<T> { export interface Maybe<T> {
isJust(): this is Just<T>; isJust(): this is Just<T>;
map<S>(f: (x: T) => S): Maybe<S>;
getOrElse(x: T): T;
} }
export type Just<T> = Maybe<T> & { export type Just<T> = Maybe<T> & {
@ -9,6 +11,8 @@ export type Just<T> = Maybe<T> & {
export function just<T>(value: T): Just<T> { export function just<T>(value: T): Just<T> {
return { return {
isJust: () => true, isJust: () => true,
getOrElse: (_: T) => value,
map: <S>(f: (x: T) => S) => just(f(value)),
get: () => value get: () => value
}; };
} }
@ -16,5 +20,11 @@ export function just<T>(value: T): Just<T> {
export function nothing<T>(): Maybe<T> { export function nothing<T>(): Maybe<T> {
return { return {
isJust: () => false, isJust: () => false,
getOrElse: (value: T) => value,
map: <S>(_: (x: T) => S) => nothing<S>()
}; };
} }
export function fromNullable<T>(value: T): Maybe<T> {
return value == null ? nothing() : just(value);
}

View File

@ -8,17 +8,17 @@ import handler from './processors';
import { queueLogger } from './logger'; import { queueLogger } from './logger';
const enableQueue = !program.disableQueue; const enableQueue = !program.disableQueue;
const queueAvailable = config.redis != null; const queueAvailable = config.redis.isJust();
const queue = initializeQueue(); const queue = initializeQueue();
function initializeQueue() { function initializeQueue() {
if (queueAvailable) { return config.redis.map(({ port, host, pass }) => {
return new Queue('misskey', { return new Queue('misskey', {
redis: { redis: {
port: config.redis.port, port: port,
host: config.redis.host, host: host,
password: config.redis.pass password: pass.getOrElse(null)
}, },
removeOnSuccess: true, removeOnSuccess: true,
@ -27,9 +27,7 @@ function initializeQueue() {
sendEvents: false, sendEvents: false,
storeJobs: false storeJobs: false
}); });
} else { }).getOrElse(null);
return null;
}
} }
export function deliver(user: ILocalUser, content: any, to: any) { export function deliver(user: ILocalUser, content: any, to: any) {

View File

@ -57,7 +57,7 @@ export default class Resolver {
const object = await request({ const object = await request({
url: value, url: value,
proxy: config.proxy, proxy: config.proxy.getOrElse(null),
timeout: this.timeout, timeout: this.timeout,
headers: { headers: {
'User-Agent': config.user_agent, 'User-Agent': config.user_agent,

View File

@ -46,7 +46,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
description: instance.description, description: instance.description,
langs: instance.langs, langs: instance.langs,
secure: config.https != null, secure: config.https.isJust(),
machine: os.hostname(), machine: os.hostname(),
os: os.platform(), os: os.platform(),
node: process.version, node: process.version,
@ -83,9 +83,9 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
registration: !instance.disableRegistration, registration: !instance.disableRegistration,
localTimeLine: !instance.disableLocalTimeline, localTimeLine: !instance.disableLocalTimeline,
globalTimeLine: !instance.disableGlobalTimeline, globalTimeLine: !instance.disableGlobalTimeline,
elasticsearch: config.elasticsearch ? true : false, elasticsearch: config.elasticsearch.isJust(),
recaptcha: instance.enableRecaptcha, recaptcha: instance.enableRecaptcha,
objectStorage: config.drive && config.drive.storage === 'minio', objectStorage: config.drive.storage === 'minio',
twitter: instance.enableTwitterIntegration, twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration, github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration, discord: instance.enableDiscordIntegration,

View File

@ -50,7 +50,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
request({ request({
url: url, url: url,
proxy: config.proxy, proxy: config.proxy.getOrElse(null),
timeout: timeout, timeout: timeout,
json: true, json: true,
followRedirect: true, followRedirect: true,

View File

@ -23,10 +23,10 @@ module.exports = (server: http.Server) => {
let ev: EventEmitter; let ev: EventEmitter;
if (config.redis) { if (config.redis.isJust()) {
// Connect to Redis // Connect to Redis
const subscriber = redis.createClient( const subscriber = redis.createClient(
config.redis.port, config.redis.host); config.redis.get().port, config.redis.get().host);
subscriber.subscribe('misskey'); subscriber.subscribe('misskey');

View File

@ -96,13 +96,14 @@ app.use(router.routes());
app.use(mount(require('./web'))); app.use(mount(require('./web')));
function createServer() { function createServer() {
if (config.https) { if (config.https.isJust()) {
const certs: any = {}; const opts = {
for (const k of Object.keys(config.https)) { key: fs.readFileSync(config.https.get().key),
certs[k] = fs.readFileSync(config.https[k]); cert: fs.readFileSync(config.https.get().cert),
} ...config.https.get().ca.map<any>(path => ({ ca: fs.readFileSync(path) })).getOrElse({}),
certs['allowHTTP1'] = true; allowHTTP1: true
return http2.createSecureServer(certs, app.callback()) as https.Server; };
return http2.createSecureServer(opts, app.callback()) as https.Server;
} else { } else {
return http.createServer(app.callback()); return http.createServer(app.callback());
} }

View File

@ -69,7 +69,7 @@ async function fetch(url: string, path: string) {
const req = request({ const req = request({
url: requestUrl, url: requestUrl,
proxy: config.proxy, proxy: config.proxy.getOrElse(null),
timeout: 10 * 1000, timeout: 10 * 1000,
headers: { headers: {
'User-Agent': config.user_agent 'User-Agent': config.user_agent

View File

@ -31,7 +31,9 @@ const app = new Koa();
app.use(views(__dirname + '/views', { app.use(views(__dirname + '/views', {
extension: 'pug', extension: 'pug',
options: { options: {
config config: {
url: config.url
}
} }
})); }));

View File

@ -50,8 +50,9 @@ async function save(path: string, name: string, type: string, hash: string, size
if (type === 'image/webp') ext = '.webp'; if (type === 'image/webp') ext = '.webp';
} }
const baseUrl = config.drive.baseUrl 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 }`; `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`
);
// for original // for original
const key = `${config.drive.prefix}/${uuid.v4()}${ext}`; const key = `${config.drive.prefix}/${uuid.v4()}${ext}`;

View File

@ -55,7 +55,7 @@ export default async (
const req = request({ const req = request({
url: requestUrl, url: requestUrl,
proxy: config.proxy, proxy: config.proxy.getOrElse(null),
timeout: 10 * 1000, timeout: 10 * 1000,
headers: { headers: {
'User-Agent': config.user_agent 'User-Agent': config.user_agent

View File

@ -500,7 +500,7 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
} }
function index(note: INote) { function index(note: INote) {
if (note.text == null || config.elasticsearch == null) return; if (note.text == null || !config.elasticsearch.isJust()) return;
es.index({ es.index({
index: 'misskey', index: 'misskey',

View File

@ -37,8 +37,9 @@ async function job(file: IDriveFile): Promise<any> {
const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`; const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`;
const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`; const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`;
const baseUrl = config.drive.baseUrl 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 }`; `${ 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 bucket = await getDriveFileBucket();
const readable = bucket.openDownloadStream(file._id); const readable = bucket.openDownloadStream(file._id);