1 var path = require("path");
2 var net = require("net");
3 var YAML = require("yamljs");
5 import { loadFromToml } from './configuration/configloader';
6 import { CamoConfig } from './configuration/camoconfig';
7 import { PrometheusConfig } from './configuration/prometheusconfig';
8 import { EmailConfig } from './configuration/emailconfig';
9 import { CaptchaConfig } from './configuration/captchaconfig';
11 const LOGGER = require('@calzoneman/jsli')('config');
36 "root-domain": "localhost",
37 "alt-domains": ["127.0.0.1"],
41 "gzip-threshold": 1024,
42 "cookie-secret": "change-me",
52 domain: "https://localhost",
54 keyfile: "localhost.key",
56 certfile: "localhost.cert",
58 ciphers: "HIGH:!DSS:!aNULL@STRENGTH"
61 domain: "http://localhost",
63 "ip-connection-limit": 10,
69 "channel-blacklist": [],
71 "channel-save-interval": 5,
72 "max-channels-per-user": 5,
73 "max-accounts-per-ip": 5,
74 "guest-login-delay": 60,
76 "purge-interval": 3600000,
79 "vimeo-workaround": false,
81 title: "CyTube Beta", description: "Free, open source synchtube"
84 usernames: ["^(.*?[-_])?admin(istrator)?([-_].*)?$", "^(.*?[-_])?owner([-_].*)?$"],
85 channels: ["^(.*?[-_])?admin(istrator)?([-_].*)?$", "^(.*?[-_])?owner([-_].*)?$"],
89 "aggressive-gc": false,
96 "ffprobe-exec": "ffprobe"
98 "link-domain-blacklist": [],
107 socket: "service.sock"
109 "twitch-client-id": null,
116 * Merges a config object with the defaults, warning about missing keys
118 function merge(obj, def, path) {
119 for (var key in def) {
121 if (typeof obj[key] === "object") {
122 merge(obj[key], def[key], path + "." + key);
125 LOGGER.warn("Missing config key " + (path + "." + key) +
126 "; using default: " + JSON.stringify(def[key]));
133 let camoConfig = new CamoConfig();
134 let prometheusConfig = new PrometheusConfig();
135 let emailConfig = new EmailConfig();
136 let captchaConfig = new CaptchaConfig();
139 * Initializes the configuration from the given YAML file
141 exports.load = function (file) {
142 let absPath = path.join(__dirname, "..", file);
144 cfg = YAML.load(absPath);
146 if (e.code === "ENOENT") {
147 throw new Error(`No such file: ${absPath}`);
149 throw new Error(`Invalid config file ${absPath}: ${e}`);
154 throw new Error("Configuration parser returned null");
159 'Old style mail configuration found in config.yaml. ' +
160 'Email will not be delivered unless you copy conf/example/email.toml ' +
161 'to conf/email.toml and edit it to your liking. ' +
162 'To remove this warning, delete the "mail:" block in config.yaml.'
166 merge(cfg, defaults, "config");
168 preprocessConfig(cfg);
169 LOGGER.info("Loaded configuration from " + file);
172 loadPrometheusConfig();
177 function checkLoadConfig(configClass, filename) {
181 path.resolve(__dirname, '..', 'conf', filename)
184 if (error.code === 'ENOENT') {
188 if (typeof error.line !== 'undefined') {
189 LOGGER.error(`Error in conf/${filename}: ${error} (line ${error.line})`);
191 LOGGER.error(`Error loading conf/${filename}: ${error.stack}`);
196 function loadCamoConfig() {
197 const conf = checkLoadConfig(CamoConfig, 'camo.toml');
200 LOGGER.info('No camo configuration found, chat images will not be proxied.');
201 camoConfig = new CamoConfig();
204 const enabled = camoConfig.isEnabled() ? 'ENABLED' : 'DISABLED';
205 LOGGER.info(`Loaded camo configuration from conf/camo.toml. Camo is ${enabled}`);
209 function loadPrometheusConfig() {
210 const conf = checkLoadConfig(PrometheusConfig, 'prometheus.toml');
213 LOGGER.info('No prometheus configuration found, defaulting to disabled');
214 prometheusConfig = new PrometheusConfig();
216 prometheusConfig = conf;
217 const enabled = prometheusConfig.isEnabled() ? 'ENABLED' : 'DISABLED';
219 'Loaded prometheus configuration from conf/prometheus.toml. ' +
220 `Prometheus listener is ${enabled}`
225 function loadEmailConfig() {
226 const conf = checkLoadConfig(EmailConfig, 'email.toml');
229 LOGGER.info('No email configuration found, defaulting to disabled');
230 emailConfig = new EmailConfig();
233 LOGGER.info('Loaded email configuration from conf/email.toml.');
237 function loadCaptchaConfig() {
238 const conf = checkLoadConfig(Object, 'captcha.toml');
241 LOGGER.info('No captcha configuration found, defaulting to disabled');
242 captchaConfig.load();
244 captchaConfig.load(conf);
245 LOGGER.info('Loaded captcha configuration from conf/captcha.toml.');
250 function preprocessConfig(cfg) {
251 // Root domain should start with a . for cookies
252 var root = cfg.http["root-domain"];
253 if (/127\.0\.0\.1|localhost/.test(root)) {
255 "Detected 127.0.0.1 or localhost in root-domain '%s'. This server " +
256 "will not work from other computers! Set root-domain to the domain " +
257 "the website will be accessed from (e.g. example.com)",
261 if (/^http/.test(root)) {
263 "root-domain '%s' should not contain http:// or https://, removing it",
266 root = root.replace(/^https?:\/\//, "");
268 if (/:\d+$/.test(root)) {
270 "root-domain '%s' should not contain a trailing port, removing it",
273 root = root.replace(/:\d+$/, "");
275 root = root.replace(/^\.*/, "");
276 cfg.http["root-domain"] = root;
277 if (root.indexOf(".") !== -1 && !net.isIP(root)) {
280 cfg.http["root-domain-dotted"] = root;
283 if (process.env.DEBUG === "1" || process.env.DEBUG === "true") {
289 // Strip trailing slashes from domains
290 cfg.https.domain = cfg.https.domain.replace(/\/*$/, "");
293 cfg.io["ipv4-nossl"] = "";
294 cfg.io["ipv4-ssl"] = "";
295 cfg.io["ipv6-nossl"] = "";
296 cfg.io["ipv6-ssl"] = "";
297 for (var i = 0; i < cfg.listen.length; i++) {
298 var srv = cfg.listen[i];
307 if (srv.port === cfg.io["default-port"]) {
308 cfg.io["ipv4-nossl"] = cfg.io["domain"] + ":" + cfg.io["default-port"];
309 } else if (srv.port === cfg.https["default-port"]) {
310 cfg.io["ipv4-ssl"] = cfg.https["domain"] + ":" + cfg.https["default-port"];
315 if (net.isIPv4(srv.ip) || srv.ip === "::") {
316 if (srv.https && !cfg.io["ipv4-ssl"]) {
318 cfg.io["ipv4-ssl"] = srv.url;
320 cfg.io["ipv4-ssl"] = cfg.https["domain"] + ":" + srv.port;
322 } else if (!cfg.io["ipv4-nossl"]) {
324 cfg.io["ipv4-nossl"] = srv.url;
326 cfg.io["ipv4-nossl"] = cfg.io["domain"] + ":" + srv.port;
330 if (net.isIPv6(srv.ip) || srv.ip === "::") {
331 if (srv.https && !cfg.io["ipv6-ssl"]) {
333 LOGGER.error("Config Error: no URL defined for IPv6 " +
334 "Socket.IO listener! Ignoring this listener " +
335 "because the Socket.IO client cannot connect to " +
336 "a raw IPv6 address.");
337 LOGGER.error("(Listener was: " + JSON.stringify(srv) + ")");
339 cfg.io["ipv6-ssl"] = srv.url;
341 } else if (!cfg.io["ipv6-nossl"]) {
343 LOGGER.error("Config Error: no URL defined for IPv6 " +
344 "Socket.IO listener! Ignoring this listener " +
345 "because the Socket.IO client cannot connect to " +
346 "a raw IPv6 address.");
347 LOGGER.error("(Listener was: " + JSON.stringify(srv) + ")");
349 cfg.io["ipv6-nossl"] = srv.url;
355 cfg.io["ipv4-default"] = cfg.io["ipv4-ssl"] || cfg.io["ipv4-nossl"];
356 cfg.io["ipv6-default"] = cfg.io["ipv6-ssl"] || cfg.io["ipv6-nossl"];
358 if (/127\.0\.0\.1|localhost/.test(cfg.io["ipv4-default"])) {
360 "socket.io is bound to localhost, this server will be inaccessible " +
361 "from other computers!"
365 // Generate RegExps for reserved names
366 var reserved = cfg["reserved-names"];
367 for (var key in reserved) {
368 if (reserved[key] && reserved[key].length > 0) {
369 reserved[key] = new RegExp(reserved[key].join("|"), "i");
371 reserved[key] = false;
375 /* Convert channel blacklist to a hashtable */
377 cfg["channel-blacklist"].forEach(function (c) {
378 tbl[c.toLowerCase()] = true;
380 cfg["channel-blacklist"] = tbl;
382 /* Check channel path */
383 if(!/^[-\w]+$/.test(cfg["channel-path"])){
384 LOGGER.error("Channel paths may only use the same characters as usernames and channel names.");
385 process.exit(78); // sysexits.h for bad config
388 if (cfg["link-domain-blacklist"].length > 0) {
389 cfg["link-domain-blacklist-regex"] = new RegExp(
390 cfg["link-domain-blacklist"].join("|").replace(/\./g, "\\."), "gi");
393 cfg["link-domain-blacklist-regex"] = new RegExp("$x^", "gi");
396 if (cfg["youtube-v3-key"]) {
397 require("@cytube/mediaquery/lib/provider/youtube").setApiKey(
398 cfg["youtube-v3-key"]);
400 LOGGER.warn("No YouTube v3 API key set. YouTube links will " +
401 "not work. See youtube-v3-key in config.template.yaml and " +
402 "https://developers.google.com/youtube/registering_an_application for " +
403 "information on registering an API key.");
406 if (cfg["twitch-client-id"]) {
407 require("@cytube/mediaquery/lib/provider/twitch-vod").setClientID(
408 cfg["twitch-client-id"]);
409 require("@cytube/mediaquery/lib/provider/twitch-clip").setClientID(
410 cfg["twitch-client-id"]);
412 LOGGER.warn("No Twitch Client ID set. Twitch VOD links will " +
413 "not work. See twitch-client-id in config.template.yaml and " +
414 "https://github.com/justintv/Twitch-API/blob/master/authentication.md#developer-setup" +
415 "for more information on registering a client ID");
418 // Remove calzoneman from contact config (old default)
419 cfg.contacts = cfg.contacts.filter(contact => {
420 return contact.name !== 'calzoneman';
423 if (!cfg.io.throttle) {
424 cfg.io.throttle = {};
427 cfg.io.throttle = Object.assign({
428 'in-rate-limit': Infinity
430 cfg.io.throttle = Object.assign({
431 'bucket-capacity': cfg.io.throttle['in-rate-limit']
434 if (!cfg['channel-storage']) {
435 cfg['channel-storage'] = { type: undefined };
442 * Retrieves a configuration value with the given key
444 * Accepts a dot-separated key for nested values, e.g. "http.port"
445 * Throws an error if a nonexistant key is requested
447 exports.get = function (key) {
449 var keylist = key.split(".");
450 var current = keylist.shift();
452 while (keylist.length > 0) {
453 if (!(current in obj)) {
454 throw new Error("Nonexistant config key '" + path + "." + current + "'");
457 current = keylist.shift();
458 path += "." + current;
465 * Sets a configuration value with the given key
467 * Accepts a dot-separated key for nested values, e.g. "http.port"
468 * Throws an error if a nonexistant key is requested
470 exports.set = function (key, value) {
472 var keylist = key.split(".");
473 var current = keylist.shift();
475 while (keylist.length > 0) {
476 if (!(current in obj)) {
477 throw new Error("Nonexistant config key '" + path + "." + current + "'");
480 current = keylist.shift();
481 path += "." + current;
484 obj[current] = value;
487 exports.getCamoConfig = function getCamoConfig() {
491 exports.getPrometheusConfig = function getPrometheusConfig() {
492 return prometheusConfig;
495 exports.getEmailConfig = function getEmailConfig() {
499 exports.getCaptchaConfig = function getCaptchaConfig() {
500 return captchaConfig;