Merge branch '3.0' of https://github.com/calzoneman/sync into 3.0
[KisSync.git] / src / config.js
blob0fed443c8dcd81a2cea486a17d3b144b0684e921
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');
13 var defaults = {
14     mysql: {
15         server: "localhost",
16         port: 3306,
17         database: "cytube3",
18         user: "cytube3",
19         password: "",
20         "pool-size": 10
21     },
22     listen: [
23         {
24             ip: "0.0.0.0",
25             port: 8080,
26             http: true,
27         },
28         {
29             ip: "0.0.0.0",
30             port: 1337,
31             io: true
32         }
33     ],
34     http: {
35         "default-port": 8080,
36         "root-domain": "localhost",
37         "alt-domains": ["127.0.0.1"],
38         minify: false,
39         "max-age": "7d",
40         gzip: true,
41         "gzip-threshold": 1024,
42         "cookie-secret": "change-me",
43         index: {
44             "max-entries": 50
45         },
46         "trust-proxies": [
47             "loopback"
48         ]
49     },
50     https: {
51         enabled: false,
52         domain: "https://localhost",
53         "default-port": 8443,
54         keyfile: "localhost.key",
55         passphrase: "",
56         certfile: "localhost.cert",
57         cafile: "",
58         ciphers: "HIGH:!DSS:!aNULL@STRENGTH"
59     },
60     io: {
61         domain: "http://localhost",
62         "default-port": 1337,
63         "ip-connection-limit": 10,
64         cors: {
65             "allowed-origins": []
66         }
67     },
68     "youtube-v3-key": "",
69     "channel-blacklist": [],
70     "channel-path": "r",
71     "channel-save-interval": 5,
72     "max-channels-per-user": 5,
73     "max-accounts-per-ip": 5,
74     "guest-login-delay": 60,
75     aliases: {
76         "purge-interval": 3600000,
77         "max-age": 2592000000
78     },
79     "vimeo-workaround": false,
80     "html-template": {
81         title: "CyTube Beta", description: "Free, open source synchtube"
82     },
83     "reserved-names": {
84         usernames: ["^(.*?[-_])?admin(istrator)?([-_].*)?$", "^(.*?[-_])?owner([-_].*)?$"],
85         channels: ["^(.*?[-_])?admin(istrator)?([-_].*)?$", "^(.*?[-_])?owner([-_].*)?$"],
86         pagetitles: []
87     },
88     "contacts": [],
89     "aggressive-gc": false,
90     playlist: {
91         "max-items": 4000,
92         "update-interval": 5
93     },
94     ffmpeg: {
95         enabled: false,
96         "ffprobe-exec": "ffprobe"
97     },
98     "link-domain-blacklist": [],
99     setuid: {
100         enabled: false,
101         "group": "users",
102         "user": "nobody",
103         "timeout": 15
104     },
105     "service-socket": {
106         enabled: false,
107         socket: "service.sock"
108     },
109     "twitch-client-id": null,
110     poll: {
111         "max-options": 50
112     }
116  * Merges a config object with the defaults, warning about missing keys
117  */
118 function merge(obj, def, path) {
119     for (var key in def) {
120         if (key in obj) {
121             if (typeof obj[key] === "object") {
122                 merge(obj[key], def[key], path + "." + key);
123             }
124         } else {
125             LOGGER.warn("Missing config key " + (path + "." + key) +
126                         "; using default: " + JSON.stringify(def[key]));
127             obj[key] = def[key];
128         }
129     }
132 var cfg = defaults;
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
140  */
141 exports.load = function (file) {
142     let absPath = path.join(__dirname, "..", file);
143     try {
144         cfg = YAML.load(absPath);
145     } catch (e) {
146         if (e.code === "ENOENT") {
147             throw new Error(`No such file: ${absPath}`);
148         } else {
149             throw new Error(`Invalid config file ${absPath}: ${e}`);
150         }
151     }
153     if (cfg == null) {
154         throw new Error("Configuration parser returned null");
155     }
157     if (cfg.mail) {
158         LOGGER.error(
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.'
163         );
164     }
166     merge(cfg, defaults, "config");
168     preprocessConfig(cfg);
169     LOGGER.info("Loaded configuration from " + file);
171     loadCamoConfig();
172     loadPrometheusConfig();
173     loadEmailConfig();
174     loadCaptchaConfig();
177 function checkLoadConfig(configClass, filename) {
178     try {
179         return loadFromToml(
180             configClass,
181             path.resolve(__dirname, '..', 'conf', filename)
182         );
183     } catch (error) {
184         if (error.code === 'ENOENT') {
185             return null;
186         }
188         if (typeof error.line !== 'undefined') {
189             LOGGER.error(`Error in conf/${filename}: ${error} (line ${error.line})`);
190         } else {
191             LOGGER.error(`Error loading conf/${filename}: ${error.stack}`);
192         }
193     }
196 function loadCamoConfig() {
197     const conf = checkLoadConfig(CamoConfig, 'camo.toml');
199     if (conf === null) {
200         LOGGER.info('No camo configuration found, chat images will not be proxied.');
201         camoConfig = new CamoConfig();
202     } else {
203         camoConfig = conf;
204         const enabled = camoConfig.isEnabled() ? 'ENABLED' : 'DISABLED';
205         LOGGER.info(`Loaded camo configuration from conf/camo.toml.  Camo is ${enabled}`);
206     }
209 function loadPrometheusConfig() {
210     const conf = checkLoadConfig(PrometheusConfig, 'prometheus.toml');
212     if (conf === null) {
213         LOGGER.info('No prometheus configuration found, defaulting to disabled');
214         prometheusConfig = new PrometheusConfig();
215     } else {
216         prometheusConfig = conf;
217         const enabled = prometheusConfig.isEnabled() ? 'ENABLED' : 'DISABLED';
218         LOGGER.info(
219             'Loaded prometheus configuration from conf/prometheus.toml.  ' +
220             `Prometheus listener is ${enabled}`
221         );
222     }
225 function loadEmailConfig() {
226     const conf = checkLoadConfig(EmailConfig, 'email.toml');
228     if (conf === null) {
229         LOGGER.info('No email configuration found, defaulting to disabled');
230         emailConfig = new EmailConfig();
231     } else {
232         emailConfig = conf;
233         LOGGER.info('Loaded email configuration from conf/email.toml.');
234     }
237 function loadCaptchaConfig() {
238     const conf = checkLoadConfig(Object, 'captcha.toml');
240     if (conf === null) {
241         LOGGER.info('No captcha configuration found, defaulting to disabled');
242         captchaConfig.load();
243     } else {
244         captchaConfig.load(conf);
245         LOGGER.info('Loaded captcha configuration from conf/captcha.toml.');
246     }
249 // I'm sorry
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)) {
254         LOGGER.warn(
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)",
258             root
259         );
260     }
261     if (/^http/.test(root)) {
262         LOGGER.warn(
263             "root-domain '%s' should not contain http:// or https://, removing it",
264             root
265         );
266         root = root.replace(/^https?:\/\//, "");
267     }
268     if (/:\d+$/.test(root)) {
269         LOGGER.warn(
270             "root-domain '%s' should not contain a trailing port, removing it",
271             root
272         );
273         root = root.replace(/:\d+$/, "");
274     }
275     root = root.replace(/^\.*/, "");
276     cfg.http["root-domain"] = root;
277     if (root.indexOf(".") !== -1 && !net.isIP(root)) {
278         root = "." + root;
279     }
280     cfg.http["root-domain-dotted"] = root;
282     // Debug
283     if (process.env.DEBUG === "1" || process.env.DEBUG === "true") {
284         cfg.debug = true;
285     } else {
286         cfg.debug = false;
287     }
289     // Strip trailing slashes from domains
290     cfg.https.domain = cfg.https.domain.replace(/\/*$/, "");
292     // Socket.IO URLs
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];
299         if (!srv.ip) {
300             srv.ip = "0.0.0.0";
301         }
302         if (!srv.io) {
303             continue;
304         }
306         if (srv.ip === "") {
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"];
311             }
312             continue;
313         }
315         if (net.isIPv4(srv.ip) || srv.ip === "::") {
316             if (srv.https && !cfg.io["ipv4-ssl"]) {
317                 if (srv.url) {
318                     cfg.io["ipv4-ssl"] = srv.url;
319                 } else {
320                     cfg.io["ipv4-ssl"] = cfg.https["domain"] + ":" + srv.port;
321                 }
322             } else if (!cfg.io["ipv4-nossl"]) {
323                 if (srv.url) {
324                     cfg.io["ipv4-nossl"] = srv.url;
325                 } else {
326                     cfg.io["ipv4-nossl"] = cfg.io["domain"] + ":" + srv.port;
327                 }
328             }
329         }
330         if (net.isIPv6(srv.ip) || srv.ip === "::") {
331             if (srv.https && !cfg.io["ipv6-ssl"]) {
332                 if (!srv.url) {
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) + ")");
338                 } else {
339                     cfg.io["ipv6-ssl"] = srv.url;
340                 }
341             } else if (!cfg.io["ipv6-nossl"]) {
342                 if (!srv.url) {
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) + ")");
348                 } else {
349                     cfg.io["ipv6-nossl"] = srv.url;
350                 }
351             }
352         }
353     }
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"])) {
359         LOGGER.warn(
360             "socket.io is bound to localhost, this server will be inaccessible " +
361             "from other computers!"
362         );
363     }
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");
370         } else {
371             reserved[key] = false;
372         }
373     }
375     /* Convert channel blacklist to a hashtable */
376     var tbl = {};
377     cfg["channel-blacklist"].forEach(function (c) {
378         tbl[c.toLowerCase()] = true;
379     });
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
386     }
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");
391     } else {
392         // Match nothing
393         cfg["link-domain-blacklist-regex"] = new RegExp("$x^", "gi");
394     }
396     if (cfg["youtube-v3-key"]) {
397         require("@cytube/mediaquery/lib/provider/youtube").setApiKey(
398                 cfg["youtube-v3-key"]);
399     } else {
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.");
404     }
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"]);
411     } else {
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");
416     }
418     // Remove calzoneman from contact config (old default)
419     cfg.contacts = cfg.contacts.filter(contact => {
420         return contact.name !== 'calzoneman';
421     });
423     if (!cfg.io.throttle) {
424         cfg.io.throttle = {};
425     }
427     cfg.io.throttle = Object.assign({
428         'in-rate-limit': Infinity
429     }, cfg.io.throttle);
430     cfg.io.throttle = Object.assign({
431         'bucket-capacity': cfg.io.throttle['in-rate-limit']
432     }, cfg.io.throttle);
434     if (!cfg['channel-storage']) {
435         cfg['channel-storage'] = { type: undefined };
436     }
438     return cfg;
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
446  */
447 exports.get = function (key) {
448     var obj = cfg;
449     var keylist = key.split(".");
450     var current = keylist.shift();
451     var path = current;
452     while (keylist.length > 0) {
453         if (!(current in obj)) {
454             throw new Error("Nonexistant config key '" + path + "." + current + "'");
455         }
456         obj = obj[current];
457         current = keylist.shift();
458         path += "." + current;
459     }
461     return obj[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
469  */
470 exports.set = function (key, value) {
471     var obj = cfg;
472     var keylist = key.split(".");
473     var current = keylist.shift();
474     var path = current;
475     while (keylist.length > 0) {
476         if (!(current in obj)) {
477             throw new Error("Nonexistant config key '" + path + "." + current + "'");
478         }
479         obj = obj[current];
480         current = keylist.shift();
481         path += "." + current;
482     }
484     obj[current] = value;
487 exports.getCamoConfig = function getCamoConfig() {
488     return camoConfig;
491 exports.getPrometheusConfig = function getPrometheusConfig() {
492     return prometheusConfig;
495 exports.getEmailConfig = function getEmailConfig() {
496     return emailConfig;
499 exports.getCaptchaConfig = function getCaptchaConfig() {
500     return captchaConfig;