mirror of
https://github.com/Spengreb/sync.git
synced 2026-05-15 12:02:06 +00:00
Camo: https://github.com/atmos/camo. This has a couple advantages over just allowing images to be dumped as-is: - Prevents mixed-content warnings by allowing the server to proxy HTTP images to an HTTPS camo instance - Protects users' privacy by not exposing their browser directly to the image host - Allows the camo proxy to intercept and reject bad image sources (URLs that are not actually images, gigapixel-sized images likely to DoS users' browsers, etc.) Whitelisting specific domains is supported for cases where the source is known to be trustworthy.
479 lines
14 KiB
JavaScript
479 lines
14 KiB
JavaScript
var fs = require("fs");
|
|
var path = require("path");
|
|
var nodemailer = require("nodemailer");
|
|
var net = require("net");
|
|
var YAML = require("yamljs");
|
|
|
|
import { LoggerFactory } from '@calzoneman/jsli';
|
|
import { loadFromToml } from 'cytube-common/lib/configuration/configloader';
|
|
import { CamoConfig } from './configuration/camoconfig';
|
|
|
|
const LOGGER = LoggerFactory.getLogger('config');
|
|
|
|
var defaults = {
|
|
mysql: {
|
|
server: "localhost",
|
|
port: 3306,
|
|
database: "cytube3",
|
|
user: "cytube3",
|
|
password: "",
|
|
"pool-size": 10
|
|
},
|
|
listen: [
|
|
{
|
|
ip: "0.0.0.0",
|
|
port: 8080,
|
|
http: true,
|
|
},
|
|
{
|
|
ip: "0.0.0.0",
|
|
port: 1337,
|
|
io: true
|
|
}
|
|
],
|
|
http: {
|
|
domain: "http://localhost",
|
|
"default-port": 8080,
|
|
"root-domain": "localhost",
|
|
"alt-domains": ["127.0.0.1"],
|
|
minify: false,
|
|
"max-age": "7d",
|
|
gzip: true,
|
|
"gzip-threshold": 1024,
|
|
"cookie-secret": "change-me",
|
|
index: {
|
|
"max-entries": 50
|
|
}
|
|
},
|
|
https: {
|
|
enabled: false,
|
|
domain: "https://localhost",
|
|
"default-port": 8443,
|
|
keyfile: "localhost.key",
|
|
passphrase: "",
|
|
certfile: "localhost.cert",
|
|
cafile: "",
|
|
ciphers: "HIGH:!DSS:!aNULL@STRENGTH",
|
|
redirect: true
|
|
},
|
|
io: {
|
|
domain: "http://localhost",
|
|
"default-port": 1337,
|
|
"ip-connection-limit": 10,
|
|
"per-message-deflate": false
|
|
},
|
|
mail: {
|
|
enabled: false,
|
|
/* the key "config" is omitted because the format depends on the
|
|
service the owner is configuring for nodemailer */
|
|
"from-address": "some.user@gmail.com",
|
|
"from-name": "CyTube Services"
|
|
},
|
|
"youtube-v3-key": "",
|
|
"channel-save-interval": 5,
|
|
"max-channels-per-user": 5,
|
|
"max-accounts-per-ip": 5,
|
|
"guest-login-delay": 60,
|
|
stats: {
|
|
interval: 3600000,
|
|
"max-age": 86400000
|
|
},
|
|
aliases: {
|
|
"purge-interval": 3600000,
|
|
"max-age": 2592000000
|
|
},
|
|
"vimeo-workaround": false,
|
|
"vimeo-oauth": {
|
|
enabled: false,
|
|
"consumer-key": "",
|
|
secret: ""
|
|
},
|
|
"html-template": {
|
|
title: "CyTube Beta", description: "Free, open source synchtube"
|
|
},
|
|
"reserved-names": {
|
|
usernames: ["^(.*?[-_])?admin(istrator)?([-_].*)?$", "^(.*?[-_])?owner([-_].*)?$"],
|
|
channels: ["^(.*?[-_])?admin(istrator)?([-_].*)?$", "^(.*?[-_])?owner([-_].*)?$"],
|
|
pagetitles: []
|
|
},
|
|
"contacts": [],
|
|
"aggressive-gc": false,
|
|
playlist: {
|
|
"max-items": 4000,
|
|
"update-interval": 5
|
|
},
|
|
"channel-blacklist": [],
|
|
ffmpeg: {
|
|
enabled: false,
|
|
"ffprobe-exec": "ffprobe"
|
|
},
|
|
"link-domain-blacklist": [],
|
|
setuid: {
|
|
enabled: false,
|
|
"group": "users",
|
|
"user": "nobody",
|
|
"timeout": 15
|
|
},
|
|
"channel-storage": {
|
|
type: "file"
|
|
},
|
|
"service-socket": {
|
|
enabled: false,
|
|
socket: "service.sock"
|
|
},
|
|
"google-drive": {
|
|
"html5-hack-enabled": false
|
|
},
|
|
"twitch-client-id": null,
|
|
poll: {
|
|
"max-options": 50
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Merges a config object with the defaults, warning about missing keys
|
|
*/
|
|
function merge(obj, def, path) {
|
|
for (var key in def) {
|
|
if (key in obj) {
|
|
if (typeof obj[key] === "object") {
|
|
merge(obj[key], def[key], path + "." + key);
|
|
}
|
|
} else {
|
|
LOGGER.warn("Missing config key " + (path + "." + key) +
|
|
"; using default: " + JSON.stringify(def[key]));
|
|
obj[key] = def[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
var cfg = defaults;
|
|
let camoConfig = new CamoConfig();
|
|
|
|
/**
|
|
* Initializes the configuration from the given YAML file
|
|
*/
|
|
exports.load = function (file) {
|
|
try {
|
|
cfg = YAML.load(path.join(__dirname, "..", file));
|
|
} catch (e) {
|
|
if (e.code === "ENOENT") {
|
|
LOGGER.info(file + " does not exist, assuming default configuration");
|
|
cfg = defaults;
|
|
return;
|
|
} else {
|
|
LOGGER.error("Error loading config file " + file + ": ");
|
|
LOGGER.error(e);
|
|
if (e.stack) {
|
|
LOGGER.error(e.stack);
|
|
}
|
|
cfg = defaults;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (cfg == null) {
|
|
LOGGER.info(file + " is an Invalid configuration file, " +
|
|
"assuming default configuration");
|
|
cfg = defaults;
|
|
return;
|
|
}
|
|
|
|
var mailconfig = {};
|
|
if (cfg.mail && cfg.mail.config) {
|
|
mailconfig = cfg.mail.config;
|
|
delete cfg.mail.config;
|
|
}
|
|
merge(cfg, defaults, "config");
|
|
cfg.mail.config = mailconfig;
|
|
|
|
preprocessConfig(cfg);
|
|
LOGGER.info("Loaded configuration from " + file);
|
|
|
|
loadCamoConfig();
|
|
};
|
|
|
|
function loadCamoConfig() {
|
|
try {
|
|
camoConfig = loadFromToml(CamoConfig,
|
|
path.resolve(__dirname, '..', 'conf', 'camo.toml'));
|
|
const enabled = camoConfig.isEnabled() ? 'ENABLED' : 'DISABLED';
|
|
LOGGER.info(`Loaded camo configuration from conf/camo.toml. Camo is ${enabled}`);
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
LOGGER.info('No camo configuration found, chat images will not be proxied.');
|
|
camoConfig = new CamoConfig();
|
|
return;
|
|
}
|
|
|
|
if (typeof error.line !== 'undefined') {
|
|
LOGGER.error(`Error in conf/camo.toml: ${error} (line ${error.line})`);
|
|
} else {
|
|
LOGGER.error(`Error loading conf/camo.toml: ${error.stack}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// I'm sorry
|
|
function preprocessConfig(cfg) {
|
|
/* Detect 3.0.0-style config and warng the user about it */
|
|
if ("host" in cfg.http || "port" in cfg.http || "port" in cfg.https) {
|
|
LOGGER.warn("The method of specifying which IP/port to bind has "+
|
|
"changed. The config loader will try to handle this "+
|
|
"automatically, but you should read config.template.yaml "+
|
|
"and change your config.yaml to the new format.");
|
|
cfg.listen = [
|
|
{
|
|
ip: cfg.http.host || "0.0.0.0",
|
|
port: cfg.http.port,
|
|
http: true
|
|
},
|
|
{
|
|
ip: cfg.http.host || "0.0.0.0",
|
|
port: cfg.io.port,
|
|
io: true
|
|
}
|
|
];
|
|
|
|
if (cfg.https.enabled) {
|
|
cfg.listen.push(
|
|
{
|
|
ip: cfg.http.host || "0.0.0.0",
|
|
port: cfg.https.port,
|
|
https: true,
|
|
io: true
|
|
}
|
|
);
|
|
}
|
|
|
|
cfg.http["default-port"] = cfg.http.port;
|
|
cfg.https["default-port"] = cfg.https.port;
|
|
cfg.io["default-port"] = cfg.io.port;
|
|
}
|
|
// Root domain should start with a . for cookies
|
|
var root = cfg.http["root-domain"];
|
|
root = root.replace(/^\.*/, "");
|
|
cfg.http["root-domain"] = root;
|
|
if (root.indexOf(".") !== -1 && !net.isIP(root)) {
|
|
root = "." + root;
|
|
}
|
|
cfg.http["root-domain-dotted"] = root;
|
|
|
|
// Setup nodemailer
|
|
cfg.mail.nodemailer = nodemailer.createTransport(
|
|
cfg.mail.config
|
|
);
|
|
|
|
// Debug
|
|
if (process.env.DEBUG === "1" || process.env.DEBUG === "true") {
|
|
cfg.debug = true;
|
|
} else {
|
|
cfg.debug = false;
|
|
}
|
|
|
|
// Strip trailing slashes from domains
|
|
cfg.http.domain = cfg.http.domain.replace(/\/*$/, "");
|
|
cfg.https.domain = cfg.https.domain.replace(/\/*$/, "");
|
|
|
|
// HTTP/HTTPS domains with port numbers
|
|
if (!cfg.http["full-address"]) {
|
|
var httpfa = cfg.http.domain;
|
|
if (cfg.http["default-port"] !== 80) {
|
|
httpfa += ":" + cfg.http["default-port"];
|
|
}
|
|
cfg.http["full-address"] = httpfa;
|
|
}
|
|
|
|
if (!cfg.https["full-address"]) {
|
|
var httpsfa = cfg.https.domain;
|
|
if (cfg.https["default-port"] !== 443) {
|
|
httpsfa += ":" + cfg.https["default-port"];
|
|
}
|
|
cfg.https["full-address"] = httpsfa;
|
|
}
|
|
|
|
|
|
// Socket.IO URLs
|
|
cfg.io["ipv4-nossl"] = "";
|
|
cfg.io["ipv4-ssl"] = "";
|
|
cfg.io["ipv6-nossl"] = "";
|
|
cfg.io["ipv6-ssl"] = "";
|
|
for (var i = 0; i < cfg.listen.length; i++) {
|
|
var srv = cfg.listen[i];
|
|
if (!srv.ip) {
|
|
srv.ip = "0.0.0.0";
|
|
}
|
|
if (!srv.io) {
|
|
continue;
|
|
}
|
|
|
|
if (srv.ip === "") {
|
|
if (srv.port === cfg.io["default-port"]) {
|
|
cfg.io["ipv4-nossl"] = cfg.io["domain"] + ":" + cfg.io["default-port"];
|
|
} else if (srv.port === cfg.https["default-port"]) {
|
|
cfg.io["ipv4-ssl"] = cfg.https["domain"] + ":" + cfg.https["default-port"];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (net.isIPv4(srv.ip) || srv.ip === "::") {
|
|
if (srv.https && !cfg.io["ipv4-ssl"]) {
|
|
if (srv.url) {
|
|
cfg.io["ipv4-ssl"] = srv.url;
|
|
} else {
|
|
cfg.io["ipv4-ssl"] = cfg.https["domain"] + ":" + srv.port;
|
|
}
|
|
} else if (!cfg.io["ipv4-nossl"]) {
|
|
if (srv.url) {
|
|
cfg.io["ipv4-nossl"] = srv.url;
|
|
} else {
|
|
cfg.io["ipv4-nossl"] = cfg.io["domain"] + ":" + srv.port;
|
|
}
|
|
}
|
|
}
|
|
if (net.isIPv6(srv.ip) || srv.ip === "::") {
|
|
if (srv.https && !cfg.io["ipv6-ssl"]) {
|
|
if (!srv.url) {
|
|
LOGGER.error("Config Error: no URL defined for IPv6 " +
|
|
"Socket.IO listener! Ignoring this listener " +
|
|
"because the Socket.IO client cannot connect to " +
|
|
"a raw IPv6 address.");
|
|
LOGGER.error("(Listener was: " + JSON.stringify(srv) + ")");
|
|
} else {
|
|
cfg.io["ipv6-ssl"] = srv.url;
|
|
}
|
|
} else if (!cfg.io["ipv6-nossl"]) {
|
|
if (!srv.url) {
|
|
LOGGER.error("Config Error: no URL defined for IPv6 " +
|
|
"Socket.IO listener! Ignoring this listener " +
|
|
"because the Socket.IO client cannot connect to " +
|
|
"a raw IPv6 address.");
|
|
LOGGER.error("(Listener was: " + JSON.stringify(srv) + ")");
|
|
} else {
|
|
cfg.io["ipv6-nossl"] = srv.url;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
cfg.io["ipv4-default"] = cfg.io["ipv4-ssl"] || cfg.io["ipv4-nossl"];
|
|
cfg.io["ipv6-default"] = cfg.io["ipv6-ssl"] || cfg.io["ipv6-nossl"];
|
|
|
|
// sioconfig
|
|
// TODO this whole thing is messy, need to redo how the socket address is sent
|
|
var sioconfigjson = {
|
|
"ipv4-nossl": cfg.io["ipv4-nossl"],
|
|
"ipv4-ssl": cfg.io["ipv4-ssl"],
|
|
"ipv6-nossl": cfg.io["ipv6-nossl"],
|
|
"ipv6-ssl": cfg.io["ipv6-ssl"]
|
|
};
|
|
|
|
var sioconfig = JSON.stringify(sioconfigjson);
|
|
sioconfig = "var IO_URLS=" + sioconfig + ";";
|
|
cfg.sioconfigjson = sioconfigjson;
|
|
cfg.sioconfig = sioconfig;
|
|
|
|
// Generate RegExps for reserved names
|
|
var reserved = cfg["reserved-names"];
|
|
for (var key in reserved) {
|
|
if (reserved[key] && reserved[key].length > 0) {
|
|
reserved[key] = new RegExp(reserved[key].join("|"), "i");
|
|
} else {
|
|
reserved[key] = false;
|
|
}
|
|
}
|
|
|
|
/* Convert channel blacklist to a hashtable */
|
|
var tbl = {};
|
|
cfg["channel-blacklist"].forEach(function (c) {
|
|
tbl[c.toLowerCase()] = true;
|
|
});
|
|
cfg["channel-blacklist"] = tbl;
|
|
|
|
if (cfg["link-domain-blacklist"].length > 0) {
|
|
cfg["link-domain-blacklist-regex"] = new RegExp(
|
|
cfg["link-domain-blacklist"].join("|").replace(/\./g, "\\."), "gi");
|
|
} else {
|
|
// Match nothing
|
|
cfg["link-domain-blacklist-regex"] = new RegExp("$^", "gi");
|
|
}
|
|
|
|
if (cfg["youtube-v3-key"]) {
|
|
require("cytube-mediaquery/lib/provider/youtube").setApiKey(
|
|
cfg["youtube-v3-key"]);
|
|
} else {
|
|
LOGGER.warn("No YouTube v3 API key set. YouTube links will " +
|
|
"not work. See youtube-v3-key in config.template.yaml and " +
|
|
"https://developers.google.com/youtube/registering_an_application for " +
|
|
"information on registering an API key.");
|
|
}
|
|
|
|
if (cfg["twitch-client-id"]) {
|
|
require("cytube-mediaquery/lib/provider/twitch-vod").setClientID(
|
|
cfg["twitch-client-id"]);
|
|
require("cytube-mediaquery/lib/provider/twitch-clip").setClientID(
|
|
cfg["twitch-client-id"]);
|
|
} else {
|
|
LOGGER.warn("No Twitch Client ID set. Twitch VOD links will " +
|
|
"not work. See twitch-client-id in config.template.yaml and " +
|
|
"https://github.com/justintv/Twitch-API/blob/master/authentication.md#developer-setup" +
|
|
"for more information on registering a client ID");
|
|
}
|
|
|
|
// Remove calzoneman from contact config (old default)
|
|
cfg.contacts = cfg.contacts.filter(contact => {
|
|
return contact.name !== 'calzoneman';
|
|
});
|
|
|
|
return cfg;
|
|
}
|
|
|
|
/**
|
|
* Retrieves a configuration value with the given key
|
|
*
|
|
* Accepts a dot-separated key for nested values, e.g. "http.port"
|
|
* Throws an error if a nonexistant key is requested
|
|
*/
|
|
exports.get = function (key) {
|
|
var obj = cfg;
|
|
var keylist = key.split(".");
|
|
var current = keylist.shift();
|
|
var path = current;
|
|
while (keylist.length > 0) {
|
|
if (!(current in obj)) {
|
|
throw new Error("Nonexistant config key '" + path + "." + current + "'");
|
|
}
|
|
obj = obj[current];
|
|
current = keylist.shift();
|
|
path += "." + current;
|
|
}
|
|
|
|
return obj[current];
|
|
};
|
|
|
|
/**
|
|
* Sets a configuration value with the given key
|
|
*
|
|
* Accepts a dot-separated key for nested values, e.g. "http.port"
|
|
* Throws an error if a nonexistant key is requested
|
|
*/
|
|
exports.set = function (key, value) {
|
|
var obj = cfg;
|
|
var keylist = key.split(".");
|
|
var current = keylist.shift();
|
|
var path = current;
|
|
while (keylist.length > 0) {
|
|
if (!(current in obj)) {
|
|
throw new Error("Nonexistant config key '" + path + "." + current + "'");
|
|
}
|
|
obj = obj[current];
|
|
current = keylist.shift();
|
|
path += "." + current;
|
|
}
|
|
|
|
obj[current] = value;
|
|
};
|
|
|
|
exports.getCamoConfig = function getCamoConfig() {
|
|
return camoConfig;
|
|
};
|