diff --git a/config.template.yaml b/config.template.yaml index 28fdd7c4..06499369 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -138,8 +138,6 @@ mail: # 5. Click "Server key" # 6. Under "APIs & auth" click "YouTube Data API" and then click "Enable API" youtube-v3-key: '' -# Minutes between saving channel state to disk -channel-save-interval: 5 # Limit for the number of channels a user can register max-channels-per-user: 5 # Limit for the number of accounts an IP address can register @@ -147,6 +145,24 @@ max-accounts-per-ip: 5 # Minimum number of seconds between guest logins from the same IP guest-login-delay: 60 +# Allows you to customize the path divider. The /r/ in http://localhost/r/yourchannel +# Acceptable characters are a-z A-Z 0-9 _ and - +channel-path: 'r' +# Allows you to blacklist certain channels. Users will be automatically kicked +# upon trying to join one. +channel-blacklist: [] +# Minutes between saving channel state to disk +channel-save-interval: 5 +# Determines channel data storage mechanism. +# Defaults to 'file', in which channel data is JSON stringified and saved to a file +# in the `chandump/` folder. This is the legacy behavior of CyTube. +# The other possible option is 'database', in which case each key-value pair of +# channel data is stored as a row in the `channel_data` database table. +# To migrate legacy chandump files to the database, shut down CyTube (to prevent +# concurrent updates), then run `node lib/channel-storage/migrate.js`. +channel-storage: + type: 'file' + # Configure statistics tracking stats: # Interval (in milliseconds) between data points - default 1h @@ -208,10 +224,6 @@ playlist: # The server must be invoked with node --expose-gc index.js for this to have any effect. aggressive-gc: false -# Allows you to blacklist certain channels. Users will be automatically kicked -# upon trying to join one. -channel-blacklist: [] - # If you have ffmpeg installed, you can query metadata from raw files, allowing # server-synched raw file playback. This requires the following: # * ffmpeg must be installed on the server @@ -231,16 +243,6 @@ setuid: # how long to wait in ms before changing uid/gid timeout: 15 -# Determines channel data storage mechanism. -# Defaults to 'file', in which channel data is JSON stringified and saved to a file -# in the `chandump/` folder. This is the legacy behavior of CyTube. -# The other possible option is 'database', in which case each key-value pair of -# channel data is stored as a row in the `channel_data` database table. -# To migrate legacy chandump files to the database, shut down CyTube (to prevent -# concurrent updates), then run `node lib/channel-storage/migrate.js`. -channel-storage: - type: 'file' - # Allows for external services to access the system commandline # Useful for setups where stdin isn't available such as when using PM2 service-socket: diff --git a/docs/gdrive-userscript-serveradmins.md b/docs/gdrive-userscript-serveradmins.md index 3f4b19a6..5f2c6ea7 100644 --- a/docs/gdrive-userscript-serveradmins.md +++ b/docs/gdrive-userscript-serveradmins.md @@ -21,4 +21,4 @@ example, for cytu.be I use: npm run generate-userscript CyTube http://cytu.be/r/* https://cytu.be/r/* ``` -This will generate `www/js/cytube-google-drive.user.js`. +This will generate `www/js/cytube-google-drive.user.js`. If you've changed the channel path, be sure to take that into account. diff --git a/src/bgtask.js b/src/bgtask.js index f1c63c9e..c54bbe19 100644 --- a/src/bgtask.js +++ b/src/bgtask.js @@ -61,6 +61,7 @@ function initPasswordResetCleanup(Server) { } function initChannelDumper(Server) { + const chanPath = Config.get('channel-path'); var CHANNEL_SAVE_INTERVAL = parseInt(Config.get("channel-save-interval")) * 60000; setInterval(function () { @@ -70,9 +71,9 @@ function initChannelDumper(Server) { return Promise.delay(wait).then(() => { if (!chan.dead && chan.users && chan.users.length > 0) { return chan.saveState().tap(() => { - LOGGER.info(`Saved /r/${chan.name}`); + LOGGER.info(`Saved /${chanPath}/${chan.name}`); }).catch(err => { - LOGGER.error(`Failed to save /r/${chan.name}: ${err.stack}`); + LOGGER.error(`Failed to save /${chanPath}/${chan.name}: ${err.stack}`); }); } }).catch(error => { diff --git a/src/channel-storage/migrator.js b/src/channel-storage/migrator.js index 7ddceba2..a182673e 100644 --- a/src/channel-storage/migrator.js +++ b/src/channel-storage/migrator.js @@ -114,6 +114,8 @@ function fixOldChandump(data) { } function migrate(src, dest, opts) { + const chanPath = Config.get('channel-path'); + return src.listChannels().then(names => { return Promise.reduce(names, (_, name) => { // A long time ago there was a bug where CyTube would save a different @@ -143,11 +145,11 @@ function migrate(src, dest, opts) { }); return dest.save(name, data); }).then(() => { - console.log(`Migrated /r/${name}`); + console.log(`Migrated /${chanPath}/${name}`); }).catch(ChannelNotFoundError, err => { - console.log(`Skipping /r/${name} (not present in the database)`); + console.log(`Skipping /${chanPath}/${name} (not present in the database)`); }).catch(err => { - console.error(`Failed to migrate /r/${name}: ${err.stack}`); + console.error(`Failed to migrate /${chanPath}/${name}: ${err.stack}`); }); }, 0); }); diff --git a/src/config.js b/src/config.js index 74c3d550..1d9839fe 100644 --- a/src/config.js +++ b/src/config.js @@ -70,7 +70,12 @@ var defaults = { "from-name": "CyTube Services" }, "youtube-v3-key": "", + "channel-blacklist": [], + "channel-path": "r", "channel-save-interval": 5, + "channel-storage": { + type: "file" + }, "max-channels-per-user": 5, "max-accounts-per-ip": 5, "guest-login-delay": 60, @@ -102,7 +107,6 @@ var defaults = { "max-items": 4000, "update-interval": 5 }, - "channel-blacklist": [], ffmpeg: { enabled: false, "ffprobe-exec": "ffprobe" @@ -114,9 +118,6 @@ var defaults = { "user": "nobody", "timeout": 15 }, - "channel-storage": { - type: "file" - }, "service-socket": { enabled: false, socket: "service.sock" @@ -390,6 +391,12 @@ function preprocessConfig(cfg) { }); cfg["channel-blacklist"] = tbl; + /* Check channel path */ + if(!/^[-\w]+$/.test(cfg["channel-blacklist"])){ + LOGGER.error("Channel paths may only use the same characters as usernames and channel names."); + process.exit(78); // sysexits.h for bad config + } + if (cfg["link-domain-blacklist"].length > 0) { cfg["link-domain-blacklist-regex"] = new RegExp( cfg["link-domain-blacklist"].join("|").replace(/\./g, "\\."), "gi"); diff --git a/src/server.js b/src/server.js index a9030d9d..57578a97 100644 --- a/src/server.js +++ b/src/server.js @@ -62,6 +62,7 @@ var Server = function () { self.announcement = null; self.infogetter = null; self.servers = {}; + self.chanPath = Config.get('channel-path'); // backend init var initModule; @@ -264,7 +265,7 @@ Server.prototype.unloadChannel = function (chan, options) { if (!options.skipSave) { chan.saveState().catch(error => { - LOGGER.error(`Failed to save /r/${chan.name} for unload: ${error.stack}`); + LOGGER.error(`Failed to save /${this.chanPath}/${chan.name} for unload: ${error.stack}`); }); } @@ -354,9 +355,9 @@ Server.prototype.shutdown = function () { Promise.map(this.channels, channel => { try { return channel.saveState().tap(() => { - LOGGER.info(`Saved /r/${channel.name}`); + LOGGER.info(`Saved /${this.chanPath}/${channel.name}`); }).catch(err => { - LOGGER.error(`Failed to save /r/${channel.name}: ${err.stack}`); + LOGGER.error(`Failed to save /${this.chanPath}/${channel.name}: ${err.stack}`); }); } catch (error) { LOGGER.error(`Failed to save channel: ${error.stack}`); @@ -391,7 +392,7 @@ Server.prototype.handlePartitionMapChange = function () { }); this.unloadChannel(channel, { skipSave: true }); }).catch(error => { - LOGGER.error(`Failed to unload /r/${channel.name} for ` + + LOGGER.error(`Failed to unload /${this.chanPath}/${channel.name} for ` + `partition map flip: ${error.stack}`); }); } diff --git a/src/web/pug.js b/src/web/pug.js index b944ed0b..204513e5 100644 --- a/src/web/pug.js +++ b/src/web/pug.js @@ -16,7 +16,8 @@ function merge(locals, res) { loginDomain: Config.get("https.enabled") ? Config.get("https.full-address") : Config.get("http.full-address"), csrfToken: typeof res.req.csrfToken === 'function' ? res.req.csrfToken() : '', - baseUrl: getBaseUrl(res) + baseUrl: getBaseUrl(res), + channelPath: Config.get("channel-path"), }; if (typeof locals !== "object") { return _locals; diff --git a/src/web/routes/channel.js b/src/web/routes/channel.js index 6be33232..e7113d84 100644 --- a/src/web/routes/channel.js +++ b/src/web/routes/channel.js @@ -4,8 +4,8 @@ import { sendPug } from '../pug'; import * as HTTPStatus from '../httpstatus'; import { HTTPError } from '../../errors'; -export default function initialize(app, ioConfig) { - app.get('/r/:channel', (req, res) => { +export default function initialize(app, ioConfig, chanPath) { + app.get(`/${chanPath}/:channel`, (req, res) => { if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) { throw new HTTPError(`"${sanitizeText(req.params.channel)}" is not a valid ` + 'channel name.', { status: HTTPStatus.NOT_FOUND }); diff --git a/src/web/webserver.js b/src/web/webserver.js index 4f3077a6..379d375c 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -132,6 +132,8 @@ module.exports = { * Initializes webserver callbacks */ init: function (app, webConfig, ioConfig, clusterClient, channelIndex, session) { + const chanPath = Config.get('channel-path'); + app.use((req, res, next) => { counters.add("http:request", 1); next(); @@ -147,7 +149,7 @@ module.exports = { } app.use(cookieParser(webConfig.getCookieSecret())); app.use(csrf.init(webConfig.getCookieDomain())); - app.use('/r/:channel', require('./middleware/ipsessioncookie').ipSessionCookieMiddleware); + app.use(`/${chanPath}/:channel`, require('./middleware/ipsessioncookie').ipSessionCookieMiddleware); initializeLog(app); require('./middleware/authorize')(app, session); @@ -176,7 +178,7 @@ module.exports = { LOGGER.info('Enabled express-minify for CSS and JS'); } - require('./routes/channel')(app, ioConfig); + require('./routes/channel')(app, ioConfig, chanPath); require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries()); app.get('/sioconfig(.json)?', handleLegacySocketConfig); require('./routes/socketconfig')(app, clusterClient); diff --git a/templates/account-channels.pug b/templates/account-channels.pug index f756b483..286090f5 100644 --- a/templates/account-channels.pug +++ b/templates/account-channels.pug @@ -44,7 +44,7 @@ html(lang="en") input(type="hidden", name="name", value=c.name) button.btn.btn-xs.btn-danger(type="submit") Delete span.glyphicon.glyphicon-trash - a(href="/r/"+c.name, style="margin-left: 5px")= c.name + a(href=`/${channelPath}/${c.name}`, style="margin-left: 5px")= c.name .col-lg-6.col-md-6 h3 Register a new channel if newChannelError @@ -57,7 +57,7 @@ html(lang="en") .form-group label.control-label(for="channelname") Channel URL .input-group - span.input-group-addon #{baseUrl}/r/ + span.input-group-addon #{baseUrl}/#{channelPath}/ input#channelname.form-control(type="text", name="name", maxlength="30", onkeyup="checkChannel()") p#validate_channel.text-danger.pull-right button#register.btn.btn-primary.btn-block(type="submit") Register diff --git a/templates/channel.pug b/templates/channel.pug index 9fb6e24e..1452fc2a 100644 --- a/templates/channel.pug +++ b/templates/channel.pug @@ -11,7 +11,7 @@ html(lang="en") include nav +navheader() #nav-collapsible.collapse.navbar-collapse - - var cname = "/r/" + channelName + - var cname = `/${channelPath}/${channelName}` ul.nav.navbar-nav +navdefaultlinks(cname) li: a(href="javascript:void(0)", onclick="javascript:showUserOptions()") Options diff --git a/templates/channeloptions.pug b/templates/channeloptions.pug index e2c6f182..047f442d 100644 --- a/templates/channeloptions.pug +++ b/templates/channeloptions.pug @@ -86,7 +86,7 @@ mixin adminoptions #cs-adminoptions.tab-pane h4 Admin-Only Settings form.form-horizontal(action="javascript:void(0)") - - var defname = "CyTube - /r/" + channelName + - var defname = `CyTube - /${channelPath}/${channelName}` +textbox-auto("cs-pagetitle", "Page title", defname) +textbox-auto("cs-password", "Password", "leave blank to disable") +textbox-auto("cs-externalcss", "External CSS", "Stylesheet URL") diff --git a/templates/head.pug b/templates/head.pug index 57d047aa..ac944a24 100644 --- a/templates/head.pug +++ b/templates/head.pug @@ -11,8 +11,16 @@ mixin head() link(href="/css/cytube.css", rel="stylesheet") link(id="usertheme", href=DEFAULT_THEME, rel="stylesheet") - script(type="text/javascript"). - var DEFAULT_THEME = '#{DEFAULT_THEME}'; + if channelName + script(type="text/javascript"). + var DEFAULT_THEME = '#{DEFAULT_THEME}'; + var CHANNELPATH = '#{channelPath}'; + var CHANNELNAME = '#{channelName}'; + else + script(type="text/javascript"). + var DEFAULT_THEME = '#{DEFAULT_THEME}'; + var CHANNELPATH = '#{channelPath}'; + script(src="/js/theme.js") //[if lt IE 9] diff --git a/templates/index.pug b/templates/index.pug index 4ee05954..2b93f641 100644 --- a/templates/index.pug +++ b/templates/index.pug @@ -25,7 +25,7 @@ html(lang="en") tbody each chan in channels tr - td: a(href="/r/"+chan.name) #{chan.pagetitle} (#{chan.name}) + td: a(href=`/${channelPath}/${chan.name}`) #{chan.pagetitle} (#{chan.name}) td= chan.usercount td= chan.mediatitle .col-lg-3.col-md-3 @@ -37,6 +37,6 @@ html(lang="en") script(type="text/javascript"). $("#channelname").keydown(function (ev) { if (ev.keyCode === 13) { - location.href = "/r/" + $("#channelname").val(); + location.href = "/#{channelPath}/" + $("#channelname").val(); } }); diff --git a/www/js/acp.js b/www/js/acp.js index e9eef7f3..318ca849 100644 --- a/www/js/acp.js +++ b/www/js/acp.js @@ -453,8 +453,8 @@ console.log(channels[0]); channels.forEach(function (c) { var tr = $("