diff --git a/.gitignore b/.gitignore index fad7bdd..8c8153b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules site/live -config/local.toml -config/jwt.pem -config/generated.toml +config/**/* +!config/.gitkeep install/db_setup.sql build/** diff --git a/config/.gitkeep b/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config/default.toml b/config/default.toml deleted file mode 100644 index 21b7055..0000000 --- a/config/default.toml +++ /dev/null @@ -1,58 +0,0 @@ -#DO NOT EDIT THIS FILE -#ALL CHANGES SHOULD GO IN LOCAL.TOML -[bcrypt] -saltRounds = 12 - -[satyr] -name = '' -domain = '' -registration = false -restrictedNames = ['live'] -rootredirect = '/users/live' - -[ircd] -enable = false -port = 6667 -sid = '' -server = '' -pass = '' -vhost = 'web.satyr.net' - -[database] -host = 'localhost' -user = 'satyr' -password = '' -database = 'satyr_db' -connectionLimit = '50' -connectionTimeout = '1000' -insecureAuth = false -debug = false - -[server] -logs = 0 -api = false -api_user = false -api_pass = false - -[server.rtmp] -port = 1935 -chunk_size = 6000 -gop_cache = true -ping = 30 -ping_timeout = 60 - -[server.http] -hsts = false -directory = './site' -port = 8000 - -[media] -record = false -publicEndpoint = 'live' -privateEndpoint = 'stream' -ffmpeg = '' - -[transcode] -adapative = false -variants = 3 -format = 'dash' \ No newline at end of file diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f9bf420..43f4cfe 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,27 +1,48 @@ ## Configuring Satyr ### Config file -All changes to satyr's config will go in the config/local.toml file +All changes to satyr's config will go in the config/config.yml file Some values you might want to change are ``` -[satyr] -registration = true -#allow new users to register -rootRedirect = '/users/live' -#the page users are directed to when they visit your site root -[media] -record = true -#allow users to record VODs -[bcrypt] -saltRounds = 12 +satyr: + registration: true +# allow new users to register + rootRedirect: '/users/live' +# the page users are directed to when they visit your site root + +http: + hsts: true +# enable strict transport security + +media: + record: true +# allow users to record VODs + +transcode: + adapative: true +# enable adaptive livestreaming +# will help users with poor connections, but EXTREMELY cpu intensive +# even 3 variants will max out most budget VPSs with a single stream + variants: 3 +# the number of adaptive streaming variants to generate +# satyr will always copy the source stream +# and the remaining variants will lower the quality incrementally + +# So the default setting of 3 will copy the source stream once +# And generate two lower quality & bitrate variants + +crypto: + saltRounds: 12 #change the number of rounds of bcrypt to fit your hardware #if you don't understand the implications, don't change this -[ircd] -enable = true -#enable IRC peering -#unused for now + +irc: + port: 6667 +#irc settings +#currently unused ``` ### Web Frontend If you want to customize the front-end css, place a file with any changes you wish to make at site/local.css -You can change the logo by replacing site/logo.svg. \ No newline at end of file +You can change the logo by replacing site/logo.svg. +You should also consider editing templates/about.html and templates/tos.html \ No newline at end of file diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index abcf2eb..0058c81 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -2,8 +2,8 @@ A more detailed walkthrough. ### System Dependencies -Install ffmpeg and mysql through your distribution's package manager. -See [this page](https://nodejs.org/en/download/package-manager/) for instructions on install node. Compatible versions are >=10. Nightly builds may fail to compile some of the native addons. +Install ffmpeg(>= 4.2.1) and mysql through your distribution's package manager. +See [this page](https://nodejs.org/en/download/package-manager/) for instructions on installing node. Compatible versions are >=10. Nightly builds may fail to compile some of the native addons. ### Installing Satyr Clone the repository and change to the directory @@ -25,10 +25,9 @@ Run the setup script for the database. sudo mysql source install/db_setup.sql; ``` -Compile the code and start the server. +Then start the server. ```bash -npm run build -npm start +npm run start ``` It is reccomended that you run Satyr behind a TLS terminating reverse proxy, like nginx. @@ -40,10 +39,10 @@ Updating should be as simple as pulling the latest code and dependencies, then b ```bash git pull npm i -npm run build +npm update ``` Then restart the server. ## Migrating Satyr -To backup and restore, you will need to export the mysqlDB. Restore the new database from the backup, then copy the config/local.toml file and the site directory to the new install. \ No newline at end of file +To backup and restore, you will need to export the mysqlDB. Restore the new database from the backup, then copy config and site directories to the new location. \ No newline at end of file diff --git a/docs/USAGE.md b/docs/USAGE.md index 38014d9..d8a35a1 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -52,6 +52,9 @@ The following commands are available: `/nick kawen (password)` Password is only required if kawen is a registered user. `/join kawen` Join the chatroom for kawen's stream and leave the previous room. `/kick lain` Available only in your own room if you are a streamer. Forcefully disconnect the user. +`/ban lain (time)` Ban a user from your room. Bans are based on IP address. The optional time is in minutes. The default is 30. +`/banlist` List the IPs currently banned from your room. +`/unban (ip)` self explanatory #### Streaming Users should stream to rtmp://example.tld/stream/examplestreamkey diff --git a/install/config.example.yml b/install/config.example.yml new file mode 100644 index 0000000..4f4ee13 --- /dev/null +++ b/install/config.example.yml @@ -0,0 +1,24 @@ +satyr: + name: '<iname>' + domain: '<domain>' + email: '<email>' + registration: false + +media: + record: false + ffmpeg: '<ffmpeg>' + +http: + # uncomment to set HSTS when SSL is ready + #hsts: true + +database: + user: '<dbuser>' + password: '<dbpass>' + database: '<dbname>' + host: '<dbhost>' + +transcode: + adaptive: false + format: dash + variants: 3 \ No newline at end of file diff --git a/install/setup.sh b/install/setup.sh index 6741d48..e4a153a 100644 --- a/install/setup.sh +++ b/install/setup.sh @@ -38,8 +38,8 @@ dbclient="${dbclient:='*'}" else dbclient="localhost" fi -sed -e "s#<iname>#$name#g" -e "s#<domain>#$domain#g" -e "s#<ffmpeg>#$ffmpeg#g" -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<email>#$email#g" install/template.local.toml > config/generated.toml +sed -e "s#<iname>#$name#g" -e "s#<domain>#$domain#g" -e "s#<ffmpeg>#$ffmpeg#g" -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<email>#$email#g" install/config.example.yml > config/generated.yml sed -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<dbclient>#$dbclient#g" install/db_template.sql > install/db_setup.sql echo "A setup script for the database has been generated at install/db_setup.sql. Please run it by connecting to your database software and executing 'source install/db_setup.sql;''" -echo "A default configuration file has been generated at config/generated.toml" -echo "If everything looks fine, move it to config/local.toml and start your instance." \ No newline at end of file +echo "A default configuration file has been generated at config/generated.yml" +echo "If everything looks fine, move it to config/config.yml and start your instance." \ No newline at end of file diff --git a/install/template.local.toml b/install/template.local.toml deleted file mode 100644 index bb32b64..0000000 --- a/install/template.local.toml +++ /dev/null @@ -1,19 +0,0 @@ -[satyr] -name = '<iname>' -domain = '<domain>' -email = '<email>' -registration = false - -[media] -record = false -ffmpeg = '<ffmpeg>' - -[server.http] -# uncomment to set HSTS when SSL is enabled -# hsts = true - -[database] -user = '<dbuser>' -password = '<dbpass>' -database = '<dbname>' -host = '<dbhost>' diff --git a/package-lock.json b/package-lock.json index 9c53b90..b0e10eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "satyr", - "version": "0.4.4", + "version": "0.5.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -82,6 +82,11 @@ "readable-stream": "^2.0.6" } }, + "arg": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz", + "integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==" + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -342,6 +347,11 @@ } } }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -644,6 +654,11 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==" + }, "dirty": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz", @@ -1910,6 +1925,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==" + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -2043,6 +2063,11 @@ "minimist": "0.0.8" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2287,6 +2312,11 @@ "os-tmpdir": "^1.0.0" } }, + "parse-yaml": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/parse-yaml/-/parse-yaml-0.1.0.tgz", + "integrity": "sha512-tLfs2QiziUPFTA4nNrv2rrC0CnHDIF2o2m5TCgNss/E0asI0ltVjBcNKhcd/8vteZa8xKV5RGfD0ZFFlECMCqQ==" + }, "parseqs": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", @@ -2712,6 +2742,14 @@ } } }, + "socket-anti-spam": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/socket-anti-spam/-/socket-anti-spam-2.0.0.tgz", + "integrity": "sha512-glCDT8LrqwSY+tQJtvaz3YwTw1HL6bgWVvaQFumkClOcF+Jbg0NlAImqQabowNJcrCxr1dibKRoAvIfN98FKVw==", + "requires": { + "moment": "^2.21.0" + } + }, "socket.io": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", @@ -2855,6 +2893,22 @@ "urix": "^0.1.0" } }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", @@ -3016,6 +3070,18 @@ "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" }, + "ts-node": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz", + "integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==", + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + } + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3188,6 +3254,11 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" } } } diff --git a/package.json b/package.json index f98013f..a5012b2 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "license": "AGPL-3.0", "author": "knotteye", "scripts": { - "start": "tsc && node build/controller.js", - "user": "node build/cli.js", + "start": "ts-node src/index.ts", + "user": "ts-node src/cli.ts", "setup": "sh install/setup.sh" }, "repository": { @@ -16,7 +16,6 @@ "dependencies": { "bcrypt": "^3.0.6", "body-parser": "^1.19.0", - "config": "^3.2.2", "cookie-parser": "^1.4.4", "dirty": "^1.1.0", "express": "^4.17.1", @@ -25,10 +24,12 @@ "mysql": "^2.17.1", "node-media-server": ">=2.1.3 <3.0.0", "nunjucks": "^3.2.0", + "parse-yaml": "^0.1.0", "recursive-readdir": "^2.2.2", + "socket-anti-spam": "^2.0.0", "socket.io": "^2.3.0", "strftime": "^0.10.0", - "toml": "^3.0.0", + "ts-node": "^8.5.4", "typescript": "^3.6.3" }, "devDependencies": { diff --git a/src/api.ts b/src/api.ts index 7ff9944..bd9a853 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,17 +1,12 @@ import * as db from "./database" -import { unregisterUser } from "./irc"; - -var config: any; -function init(conf: object){ - config = conf; -} +import { config } from "./config"; async function register(name: string, password: string, confirm: string) { - if(!config.registration) return {"error":"registration disabled"}; + if(!config['satyr']['registration']) return {"error":"registration disabled"}; if(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"}; if(password !== confirm) return {"error":"mismatched passwords"}; - for(let i=0;i<config.restrictedNames.length;i++){ - if (name === config.restrictedNames[i]) return {"error":"restricted name"}; + for(let i=0;i<config['satyr']['restrictedNames'].length;i++){ + if (name === config['satyr']['restrictedNames'][i]) return {"error":"restricted name"}; } let r: boolean = await db.addUser(name, password); if(r) { @@ -61,4 +56,4 @@ async function login(name: string, password: string){ return false; } -export { init, register, update, changepwd, changesk, login }; \ No newline at end of file +export { register, update, changepwd, changesk, login }; \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 3ec41ac..110ecc6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,7 @@ import * as db from "./database" import * as flags from "flags"; -import * as config from "config" -db.init(config.database, config.bcrypt); +db.init(); flags.defineString('adduser', '', 'User to add'); flags.defineString('rmuser', '', 'User to remove'); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..f15fbc9 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,49 @@ +import {parseAsYaml as parse} from "parse-yaml"; +import {readFileSync as read} from "fs"; +var localconfig: Object = parse(read('config/config.yml')); +const config: Object = { + crypto: Object.assign({ + saltRounds: 12 + }, localconfig['crypto']), + satyr: Object.assign({ + name: '', + domain: '', + registration: false, + restrictedNames: [ 'live' ], + rootredirect: '/users/live', + version: process.env.npm_package_version, + }, localconfig['satyr']), + ircd: Object.assign({ + port: 6667, + }, localconfig['ircd']), + database: Object.assign({ + host: 'localhost', + user: 'satyr', + password: '', + database: 'satyr_db', + connectionLimit: '50', + connectionTimeout: '1000', + insecureAuth: false, + debug: false }, localconfig['database']), + rtmp: Object.assign({ + port: 1935, + chunk_size: 6000, + gop_cache: true, + ping: 30, + ping_timeout: 60 }, localconfig['rtmp']), + http: Object.assign({ + hsts: false, directory: './site', port: 8000 + }, localconfig['http']), + media: Object.assign({ + record: false, + publicEndpoint: 'live', + privateEndpoint: 'stream', + ffmpeg: '' + }, localconfig['media']), + transcode: Object.assign({ + adapative: false, + variants: 3, + format: 'dash' + }, localconfig['transcode']) +}; +export { config }; \ No newline at end of file diff --git a/src/controller.ts b/src/controller.ts deleted file mode 100644 index c87f0c9..0000000 --- a/src/controller.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as mediaserver from "./server"; -import * as db from "./database"; -import * as api from "./api"; -import * as http from "./http"; -import * as cleanup from "./cleanup"; -import * as config from "config"; - -async function run() { - const dbcfg: object = config.database; - const bcryptcfg: object = config.bcrypt; - const satyr: object = { - privateEndpoint: config.media.privateEndpoint, - publicEndpoint: config.media.publicEndpoint, - record: config.media.record, - registration: config.satyr.registration, - webFormat: config.satyr.webFormat, - restrictedNames: config.satyr.restrictedNames, - name: config.satyr.name, - domain: config.satyr.domain, - email: config.satyr.email, - rootredirect: config.satyr.rootredirect, - version: process.env.npm_package_version, - directory: config.server.http.directory, - ffmpeg: config.media.ffmpeg - }; - const nms: object = { - logType: config.server.logs, - rtmp: { - port: config.server.rtmp.port, - chunk_size: config.server.rtmp.chunk_size, - gop_cache: config.server.rtmp.gop_cache, - ping: config.server.rtmp.ping, - ping_timeout: config.server.rtmp.ping_timeout, - }, - /*http: { - port: config.server.http.port + 1, - mediaroot: config.server.http.directory, - allow_origin: config.server.http.allow_origin - }, - trans: { - ffmpeg: config.media.ffmpeg, - tasks: [ - { - app: config.media.publicEndpoint, - hls: config.transcode.hls, - hlsFlags: config.transcode.hlsFlags, - dash: config.transcode.dash, - dashFlags: config.transcode.dashFlags - } - ] - },*/ - auth: { - api: config.server.api, - api_user: config.server.api_user, - api_pass: config.server.api_pass - } - - }; - db.init(dbcfg, bcryptcfg); - await cleanup.init(); - api.init(satyr); - http.init(satyr, config.server.http, config.ircd); - mediaserver.init(nms, satyr); - console.log(`Satyr v${process.env.npm_package_version} ready`); -} -run(); -export { run }; \ No newline at end of file diff --git a/src/database.ts b/src/database.ts index df0f3d2..6e1032f 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,20 +1,21 @@ import * as mysql from "mysql"; import * as bcrypt from "bcrypt"; import * as crypto from "crypto"; +import { config } from "./config"; import { resolve } from "url"; -var raw: any; -var cryptoconfig: any; +var raw; +var cryptoconfig: Object; -function init (db: object, bcrypt: object){ - raw = mysql.createPool(db); - cryptoconfig = bcrypt; +function init (){ + raw = mysql.createPool(config['database']); + cryptoconfig = config['crypto']; } async function addUser(name: string, password: string){ //does not respect registration setting in config if(password === '') return false; let key: string = await genKey(); - let hash: string = await bcrypt.hash(password, cryptoconfig.saltRounds); + let hash: string = await bcrypt.hash(password, cryptoconfig['saltRounds']); let dupe = await query('select * from users where username='+raw.escape(name)); if(dupe[0]) return false; await query('INSERT INTO users (username, password_hash, stream_key, record_flag) VALUES ('+raw.escape(name)+', '+raw.escape(hash)+', '+raw.escape(key)+', 0)'); @@ -54,7 +55,7 @@ async function validatePassword(username: string, password: string){ } async function hash(pwd){ - return await bcrypt.hash(pwd, cryptoconfig.saltRounds); + return await bcrypt.hash(pwd, cryptoconfig['saltRounds']); } export { query, raw, init, addUser, rmUser, validatePassword, hash, genKey }; \ No newline at end of file diff --git a/src/http.ts b/src/http.ts index 47b00e6..c45fffa 100644 --- a/src/http.ts +++ b/src/http.ts @@ -5,9 +5,10 @@ import * as socketio from "socket.io"; import * as http from "http"; import * as cookies from "cookie-parser"; import * as dirty from "dirty"; +import * as socketSpam from "socket-anti-spam"; import * as api from "./api"; import * as db from "./database"; -import * as irc from "./irc"; +import { config } from "./config"; import { readdir, readFileSync, writeFileSync } from "fs"; import { JWT, JWK } from "jose"; import { strict } from "assert"; @@ -18,9 +19,11 @@ const app = express(); const server = http.createServer(app); const io = socketio(server); const store = dirty(); +var banlist; var jwkey; try{ jwkey = JWK.asKey(readFileSync('./config/jwt.pem')); + console.log('Found key for JWT signing.'); } catch (e) { console.log("No key found for JWT signing, generating one now."); jwkey = JWK.generateSync('RSA', 2048, { use: 'sig' }); @@ -28,23 +31,23 @@ try{ } var njkconf; -async function init(satyr: any, http: object, ircconf: any){ +async function init(){ njk.configure('templates', { autoescape : true, express : app, watch : false }); - njkconf ={ - sitename: satyr.name, - domain: satyr.domain, - email: satyr.email, - rootredirect: satyr.rootredirect, - version: satyr.version + njkconf = { + sitename: config['satyr']['name'], + domain: config['satyr']['domain'], + email: config['satyr']['email'], + rootredirect: config['satyr']['rootredirect'], + version: config['satyr']['version'] }; app.use(cookies()); app.use(bodyparser.json()); app.use(bodyparser.urlencoded({ extended: true })); - if(http['hsts']){ + if(config['http']['hsts']){ app.use((req, res, next) => { res.append('Strict-Transport-Security', 'max-age=5184000'); next(); @@ -52,11 +55,11 @@ async function init(satyr: any, http: object, ircconf: any){ } app.disable('x-powered-by'); //site handlers - await initSite(satyr.registration); + await initSite(config['satyr']['registration']); //api handlers await initAPI(); //static files if nothing else matches first - app.use(express.static(satyr.directory)); + app.use(express.static(config['http']['directory'])); //404 Handler app.use(function (req, res, next) { if(tryDecode(req.cookies.Authorization)) { @@ -65,19 +68,22 @@ async function init(satyr: any, http: object, ircconf: any){ else res.status(404).render('404.njk', njkconf); //res.status(404).render('404.njk', njkconf); }); - await initChat(ircconf); - server.listen(http['port']); + banlist = new dirty('./config/bans.db').on('load', () => {initChat()}); + server.listen(config['http']['port']); } -async function newNick(socket, skip?: boolean) { +async function newNick(socket, skip?: boolean, i?: number) { if(socket.handshake.headers['cookie'] && !skip){ let c = await parseCookie(socket.handshake.headers['cookie']); let t = await validToken(c['Authorization']); - if(t) return t['username']; + if(t) { + store.set(t['username'], [].concat(store.get(t['username']), socket.id).filter(item => item !== undefined)); + return t['username']; + } } - //i just realized how shitty of an idea this is - let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(1000)); - if(store.get(n)) return newNick(socket, true); + if(!i) i = 10; + let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(i)); + if(store.get(n)) return newNick(socket, true, Math.floor(i * 10)); else { store.set(n, socket.id); return n; @@ -89,8 +95,12 @@ async function chgNick(socket, nick, f?: boolean) { for(let i=1;i<rooms.length;i++){ io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick); } - if(store.get(socket.nick)) store.rm(socket.nick); - if (!f) store.set(nick, socket.id); + if(store.get(socket.nick)) { + if(Array.isArray(store.get(socket.nick))) store.set(socket.nick, store.get(socket.nick).filter(item => item !== socket.id)); + else store.rm(socket.nick); + } + if(f) store.set(nick, [].concat(store.get(nick), [socket.id]).filter(item => item !== undefined)); + else store.set(nick, socket.id); socket.nick = nick; } @@ -328,30 +338,26 @@ async function initSite(openReg) { }); } -async function initChat(ircconf: any) { - //irc peering - if(ircconf.enable){ - await irc.connect({ - port: ircconf.port, - sid: ircconf.sid, - pass: ircconf.pass, - server: ircconf.server, - vhost: ircconf.vhost - }); - irc.events.on('message', (nick, channel, msg) => { - io.to(channel).emit('MSG', {nick: nick, msg: msg}); - }); - } +async function initChat() { + //set a cookie to request same nick + //socket.io chat logic io.on('connection', async (socket) => { socket.nick = await newNick(socket); - if(ircconf.enable) irc.registerUser(socket.nick); socket.on('JOINROOM', async (data) => { let t: any = await db.query('select username from users where username='+db.raw.escape(data)); if(t[0]){ + if(banlist.get(data) && banlist.get(data)[socket['handshake']['address']]){ + if(Math.floor(banlist.get(data)[socket['handshake']['address']]['time'] + (banlist.get(data)[socket['handshake']['address']]['length'] * 60)) < Math.floor(Date.now() / 1000)){ + banlist.set(data, Object.assign({}, banlist.get(data), {[socket['handshake']['address']]: null})); + } + else { + socket.emit('ALERT', 'You are banned from that room'); + return; + } + } socket.join(data); io.to(data).emit('JOINED', {nick: socket.nick}); - if(ircconf.enable) irc.join(socket.nick, data); } else socket.emit('ALERT', 'Room does not exist'); }); @@ -372,24 +378,22 @@ async function initChat(ircconf: any) { }); socket.on('LEAVEROOM', (data) => { socket.leave(data); - if(ircconf.enable) irc.part(socket.nick, data); io.to(data).emit('LEFT', {nick: socket.nick}); }); socket.on('disconnecting', (reason) => { let rooms = Object.keys(socket.rooms); for(let i=1;i<rooms.length;i++){ - if(ircconf.enable) irc.part(socket.nick, rooms[i]); io.to(rooms[i]).emit('ALERT', socket.nick+' disconnected'); } - if(ircconf.enable) irc.unregisterUser(socket.nick); + if(Array.isArray(store.get(socket.nick))) { + store.set(socket.nick, store.get(socket.nick).filter(item => item !== socket.id)) + if(store.get(socket.nick) !== []) + return; + } store.rm(socket.nick); }); socket.on('NICK', async (data) => { data.nick = data.nick.replace(' ',''); - if(store.get(data.nick)){ - socket.emit('ALERT', 'Nickname is already in use'); - return false; - } let user = await db.query('select username from users where username='+db.raw.escape(data.nick)); if(user[0]){ if(!data.password){ @@ -402,27 +406,102 @@ async function initChat(ircconf: any) { else socket.emit('ALERT','Incorrect username or password'); } else { + if(store.get(data.nick)){ + socket.emit('ALERT', 'Nickname is already in use'); + return false; + } chgNick(socket, data.nick); } }); socket.on('MSG', (data) => { if(data.msg === "" || !data.msg.replace(/\s/g, '').length) return; - io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg}); - if(ircconf.enable) irc.send(socket.nick, data.room, data.msg); + if(socket.rooms[data['room']]) io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg}); }); socket.on('KICK', (data) => { if(socket.nick === data.room){ //find client with data.nick let id: string = store.get(data.nick); if(id){ + if(Array.isArray(id)) { + for(let i=0;i<id.length;i++){ + io.sockets.connected[id[i]].leave(data.room); + } + io.in(data.room).emit('ALERT', data.nick+' has been kicked.'); + return; + } let target = io.sockets.connected[id]; io.in(data.room).emit('ALERT', data.nick+' has been kicked.'); - target.disconnect(true); + target.leave(data.room); } else socket.emit('ALERT', 'No such user found.'); } else socket.emit('ALERT', 'Not authorized to do that.'); }); + socket.on('BAN', (data: Object) => { + if(socket.nick === data['room']){ + let id: string = store.get(data['nick']); + if(id){ + if(Array.isArray(id)) { + for(let i=0;i<id.length;i++){ + let target = io.sockets.connected[id[i]]; + if(typeof(data['time']) === 'number' && (data['time'] !== 0 && data['time'] !== NaN)) banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: data['time']}})); + else banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: 30}})); + target.leave(data['room']); + } + io.to(data['room']).emit('ALERT', data['nick']+' was banned.'); + return; + } + let target = io.sockets.connected[id]; + if(typeof(data['time']) === 'number' && (data['time'] !== 0 && data['time'] !== NaN)) banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: data['time']}})); + else banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: 30}})); + target.leave(data['room']); + io.to(data['room']).emit('ALERT', target.nick+' was banned.'); + } + else socket.emit('ALERT', 'No such user found.'); + } + else socket.emit('ALERT', 'Not authorized to do that.'); + }); + socket.on('UNBAN', (data: Object) => { + if(socket.nick === data['room']){ + if(banlist.get(data['room']) && banlist.get(data['room'])[data['ip']]){ + banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[data['ip']]: null})); + socket.emit('ALERT', data['ip']+' was unbanned.'); + } + else + socket.emit('ALERT', 'That IP is not banned.'); + } + else socket.emit('ALERT', 'Not authorized to do that.'); + }); + socket.on('LISTBAN', (data: Object) => { + if(socket.nick === data['room']){ + if(banlist.get(data['room'])) { + let bans = Object.keys(banlist.get(data['room'])); + let str = ''; + for(let i=0;i<bans.length;i++){ + str += bans[i]+', '; + } + socket.emit('ALERT', 'Banned IP adresses: '+str.substring(0, str.length - 2)); + return; + } + socket.emit('ALERT', 'No one is banned from this room'); + } + else socket.emit('ALERT', 'Not authorized to do that.'); + }); + }); + //socketio spam + const socketAS = new socketSpam({ + banTime: 20, + kickThreshold: 10, + kickTimesBeforeBan: 3, + banning: true, + io: io + }); + socketAS.event.on('ban', (socket) => { + let rooms = Object.keys(socket.rooms); + for(let i=1;i<rooms.length;i++){ + io.to(rooms[i]).emit('ALERT', socket.nick+' was banned.'); + } + store.rm(socket.nick); }); } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c384b81 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,15 @@ +import * as mediaserver from "./server"; +import * as db from "./database"; +import * as http from "./http"; +import * as cleanup from "./cleanup"; +import { config } from "./config"; + +async function run() { + await db.init(); + await cleanup.init(); + await http.init(); + await mediaserver.init(); + console.log(`Satyr v${config['satyr']['version']} ready`); +} +run(); +export { run }; \ No newline at end of file diff --git a/src/irc.js b/src/irc.js deleted file mode 100644 index 86359e7..0000000 --- a/src/irc.js +++ /dev/null @@ -1,212 +0,0 @@ -// written by crushv <nik@telekem.net> -// thanks nikki - -const net = require('net') -const EventEmitter = require('events') - -const socket = new net.Socket() -const emitter = new EventEmitter() - -socket.setEncoding('utf8') - -socket.on('error', console.error) - -function m (text) { - console.log('> ' + text) - socket.write(text + '\r\n') -} - -var config - -socket.once('connect', async () => { - console.log('Connected') - m(`PASS ${config.pass} TS 6 :${config.sid}`) - m('CAPAB QS ENCAP EX IE SAVE EUID') - m(`SERVER ${config.server} 1 satyr`) -}) - -function parseLine (l) { - const colIndex = l.lastIndexOf(':') - if (colIndex > -1) { - return { - params: l.substring(0, colIndex - 1).split(' '), - query: l.substring(colIndex + 1) - } - } else return { params: l.split(' ') } -} - -const servers = [] -const users = {} -const channels = {} - -const globalCommands = { - // PING :42X - // params: SID - PING: l => { - const { query } = parseLine(l) - m(`PONG :${query}`) - emitter.emit('ping') - }, - // PASS hunter2 TS 6 :42X - // params: password, 'TS', TS version, SID - PASS: l => { - const { query } = parseLine(l) - // adds a server - servers.push(query) - } -} - -const serverCommands = { - // EUID nik 1 1569146316 +i ~nik localhost6.attlocal.net 0::1 42XAAAAAB * * :nik - // params: nickname, hopcount, nickTS, umodes, username, visible hostname, IP address, UID, real hostname, account name, gecos - EUID: l => { - const { params } = parseLine(l) - const user = { - nick: params[0], - nickTS: params[2], - modes: params[3], - username: params[4], - vhost: params[5], - ip: params[6], - uid: params[7] - } - users[user.uid] = user - }, - // SJOIN 1569142987 #test +nt :42XAAAAAB - // params: channelTS, channel, simple modes, opt. mode parameters..., nicklist - SJOIN: l => { - const { params, query } = parseLine(l) - const channel = { - timestamp: params[0], - name: params[1], - modes: params.slice(2).join(' '), - nicklist: query.split(' ').map(uid => { - if (/[^0-9a-zA-Z]/.test(uid[0])) return { uid: uid.slice(1), mode: uid[0] } - else return { uid: uid, mode: '' } - }) - } - channels[channel.name] = channel - } -} - -const userCommands = { - // :42XAAAAAC PRIVMSG #test :asd - // params: target, msg - PRIVMSG: (l, source) => { - const { params, query } = parseLine(l) - emitter.emit('message', users[source].nick, params[0], query) - }, - // :42XAAAAAC JOIN 1569149395 #test + - JOIN: (l, source) => { - const { params } = parseLine(l) - channels[params[1]].nicklist.push({ - uid: source - }) - }, - // :42XAAAAAC PART #test :WeeChat 2.6 - PART: (l, source) => { - const { params } = parseLine(l) - for (let i = 0; i < channels[params[0]].nicklist.length; i++) { - if (channels[params[0]].nicklist[i].uid === source) { - channels[params[0]].nicklist.splice(i, 1) - return - } - } - }, - QUIT: (_l, source) => { - delete users[source] - } -} - -function parser (l) { - const split = l.split(' ') - const cmd = split[0] - const args = split.slice(1).join(' ') - if (globalCommands[cmd]) return globalCommands[cmd](args) - if (cmd[0] === ':') { - const source = cmd.slice(1) - const subcmd = split[1] - const subargs = split.slice(2).join(' ') - if (servers.indexOf(source) > -1 && serverCommands[subcmd]) serverCommands[subcmd](subargs) - if (users[source] && userCommands[subcmd]) userCommands[subcmd](subargs, source) - } -} - -socket.on('data', data => { - data.split('\r\n') - .filter(l => l !== '') - .forEach(l => { - console.log('< ' + l) - parser(l) - }) -}) - -module.exports.connect = conf => new Promise((resolve, reject) => { - emitter.once('ping', resolve) - config = conf - socket.connect(config.port) - process.on('SIGINT', () => { - socket.write('QUIT\r\n') - process.exit() - }) -}) -module.exports.events = emitter - -const genTS = () => Math.trunc((new Date()).getTime() / 1000) -const genUID = () => { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - var uid = '' - for (let i = 0; i < 6; i++) uid += chars.charAt(Math.floor(Math.random() * chars.length)) - if (users[uid]) return genUID() - return config.sid + uid -} -const getUID = nick => { - for (const key in users) if (users[key].nick === nick) return key -} - -module.exports.registerUser = nick => { - const user = { - nick: nick, - nickTS: genTS(), - modes: '+i', - username: '~' + nick, - vhost: config.vhost, - ip: '0::1', - uid: genUID() - } - users[user.uid] = user - m(`EUID ${user.nick} 1 ${user.nickTS} ${user.modes} ~${user.nick} ${user.vhost} 0::1 ${user.uid} * * :${user.nick}`) -} -module.exports.unregisterUser = nick => { - const uid = getUID(nick) - m(`:${uid} QUIT :Quit: satyr`) - delete users[uid] -} -module.exports.join = (nick, channelName) => { - const uid = getUID(nick) - if (!channels[channelName]) { - const channel = { - timestamp: genTS(), - name: channelName, - modes: '+nt', - nicklist: [{ uid: uid, mode: '' }] - } - channels[channel.name] = channel - } - m(`:${uid} JOIN ${channels[channelName].timestamp} ${channelName} +`) -} -module.exports.part = (nick, channelName) => { - const uid = getUID(nick) - m(`:${uid} PART ${channelName} :satyr`) - for (let i = 0; i < channels[channelName].nicklist.length; i++) { - if (channels[channelName].nicklist[i].uid === uid) { - channels[channelName].nicklist.splice(i, 1) - return - } - } -} -module.exports.send = (nick, channelName, message) => { - const uid = getUID(nick) - m(`:${uid} PRIVMSG ${channelName} :${message}`) - emitter.emit('message', nick, channelName, message) -} diff --git a/src/server.ts b/src/server.ts index 62d8a8d..054ec06 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,13 +3,14 @@ import * as dirty from "dirty"; import { mkdir, fstat, access } from "fs"; import * as strf from "strftime"; import * as db from "./database"; +import {config} from "./config"; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const { exec, execFile } = require('child_process'); const keystore = dirty(); -function init (mediaconfig: any, satyrconfig: any) { - const nms = new NodeMediaServer(mediaconfig); +function init () { + const nms = new NodeMediaServer({logType: 0,rtmp: config['rtmp']}); nms.run(); nms.on('postPublish', (id, StreamPath, args) => { @@ -23,7 +24,7 @@ function init (mediaconfig: any, satyrconfig: any) { session.reject(); return false; } - if(app !== satyrconfig.privateEndpoint){ + if(app !== config['media']['privateEndpoint']){ //app isn't at public endpoint if we've reached this point console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id); session.reject(); @@ -34,23 +35,23 @@ function init (mediaconfig: any, satyrconfig: any) { //otherwise kill the session db.query('select username,record_flag from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => { if(results[0]){ - //push to rtmp - //execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-analyzeduration', '0', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-crf', '18', '-f', 'flv', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username], {maxBuffer: Infinity}); - //push to mpd after making sure directory exists + //transcode to mpd after making sure directory exists keystore[results[0].username] = key; - mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, ()=>{;}); + mkdir(config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username, { recursive : true }, ()=>{;}); while(true){ if(session.audioCodec !== 0 && session.videoCodec !== 0){ - execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-map', '0:2', '-map', '0:2', '-map', '0:2', '-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy', '-c:v:1', 'libx264', '-c:v:2', 'libx264', '-crf:1', '33', '-crf:2', '40', '-b:v:1', '3000K', '-b:v:2', '1500K', '-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/index.mpd'], {maxBuffer: Infinity}); + transCommand(results[0].username, key).then((r) => { + execFile(config['media']['ffmpeg'], r, {maxBuffer: Infinity}); + }); break; } await sleep(300); } - if(results[0].record_flag && satyrconfig.record){ + if(results[0].record_flag && config['media']['record']){ console.log('[NodeMediaServer] Initiating recording for stream:',id); - mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => { + mkdir(config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username, { recursive : true }, (err) => { if (err) throw err; - execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.prviateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], { + execFile(config['media']['ffmpeg'], ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+config['media']['privateEndpoint']+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], { detached : true, stdio : 'inherit', maxBuffer: Infinity @@ -74,7 +75,7 @@ function init (mediaconfig: any, satyrconfig: any) { nms.on('donePublish', (id, StreamPath, args) => { let app: string = StreamPath.split("/")[1]; let key: string = StreamPath.split("/")[2]; - if(app === satyrconfig.privateEndpoint) { + if(app === config['media']['privateEndpoint']) { db.query('update user_meta,users set user_meta.live=false where users.stream_key='+db.raw.escape(key)); db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => { if(results[0]) keystore.rm(results[0].username); @@ -94,19 +95,48 @@ function init (mediaconfig: any, satyrconfig: any) { } //localhost can play from whatever endpoint //other clients must use private endpoint - if(app !== satyrconfig.publicEndpoint && !session.isLocal) { + if(app !== config['media']['publicEndpoint'] && !session.isLocal) { console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id); session.reject(); return false; } //rewrite playpath to private endpoint serverside //(hopefully) - if(app === satyrconfig.publicEndpoint) { + if(app === config['media']['publicEndpoint']) { if(keystore[key]){ - session.playStreamPath = '/'+satyrconfig.privateEndpoint+'/'+keystore[key]; + session.playStreamPath = '/'+config['media']['privateEndpoint']+'/'+keystore[key]; return true; } } }); } + +async function transCommand(user: string, key: string): Promise<string[]>{ + let args: string[] = ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+config['media']['privateEndpoint']+'/'+key]; + if(config['transcode']['adaptive']===true && config['transcode']['variants'] > 1) { + for(let i=0;i<config['transcode']['variants'];i++){ + args = args.concat(['-map', '0:2']); + } + args = args.concat(['-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy']); + for(let i=1;i<config['transcode']['variants'];i++){ + args = args.concat(['-c:v:'+i, 'libx264',]); + } + for(let i=1;i<config['transcode']['variants'];i++){ + let crf: number = Math.floor(18 + (i * 8)) > 51 ? 51 : Math.floor(18 + (i * 7)); + args = args.concat(['-crf:'+i, ''+crf]); + } + for(let i=1;i<config['transcode']['variants'];i++){ + let bv: number = Math.floor((5000 / config['transcode']['variants']) * (config['transcode']['variants'] - i)); + args = args.concat(['-b:v:'+i, ''+bv]); + } + } + else { + args = args.concat(['-c:a', 'copy', '-c:v', 'copy']); + } + if(config['transcode']['format'] === 'dash') + args = args.concat(['-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+user+'/index.mpd']); + else if(config['transcode']['format'] === 'hls') + args = args.concat(['-remove_at_exit', '1', '-hls_time', '1', '-hls_list_size', '30', '-f', 'hls', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+user+'/index.m3u8']); + return args; +} export { init }; \ No newline at end of file diff --git a/templates/base.njk b/templates/base.njk index 7f2a642..bc1f99a 100644 --- a/templates/base.njk +++ b/templates/base.njk @@ -4,6 +4,8 @@ <link rel="stylesheet" type="text/css" href="/local.css"> <link rel="icon" type="image/svg" href="/logo.svg"> <title>{{ sitename }}</title> + {% block head %} + {% endblock %} </head> <body> <div id="wrapper"> diff --git a/templates/chat.html b/templates/chat.html index a8b30ef..5f9af78 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -36,6 +36,24 @@ else if(m.startsWith('/list')){ socket.emit('LIST', {room: room}); } + else if(m.startsWith('/banlist')){ + socket.emit('LISTBAN', { + room: room, + }); + } + else if(m.startsWith('/ban')){ + socket.emit('BAN', { + room: room, + nick: m.split(' ')[1], + time: (1 * m.split(' ')[2]) + }); + } + else if(m.startsWith('/unban')){ + socket.emit('UNBAN', { + room: room, + ip: m.split(' ')[1] + }); + } else socket.emit('MSG', {room: room, msg: m}); document.getElementById('m').value = ''; } diff --git a/templates/help.njk b/templates/help.njk index 432eb09..fdfc607 100644 --- a/templates/help.njk +++ b/templates/help.njk @@ -4,9 +4,12 @@ <h4>Chatting</h4> The webclient for chat can be accessed on the streamer's page, or at <a href="https://{{ domain }}/chat">https://{{ domain }}/chat</a></br></br> The following commands are available:</br> -`/nick kawen (password)` Password is only required if kawen is a registered user.</br> -`/join kawen` Join the chatroom for kawen's stream and leave the previous room.</br> -`/kick cvcvcv` Available only in your own room if you are a streamer. Forcefully disconnect the user.</br> +<code><a>/nick kawen (password)</a></code> Password is only required if kawen is a registered user.</br> +<code><a>/join kawen</a></code> Join the chatroom for kawen's stream and leave the previous room.</br> +<code><a>/kick cvcvcv</a></code> Available only in your own room if you are a streamer. Forcefully disconnect the user.</br> +<code><a>/ban cvcvcv (time)</a></code> Ban a user from your room. Bans are based on IP address. The optional time is in minutes. The default is 30.</br> +<code><a>/banlist</a></code> List the IPs currently banned from your room.</br> +<code><a>/unban (ip)</a></code> self explanatory</br> <h4>Streaming</h4> Users should stream to <a>rtmp://{{ domain }}/stream/Stream-Key</a></br></br> diff --git a/templates/user.njk b/templates/user.njk index 7e86d4e..b25ebbe 100644 --- a/templates/user.njk +++ b/templates/user.njk @@ -1,4 +1,8 @@ {% extends "base.njk" %} +{% block head %} + <script src="/videojs/video.min.js"></script> + <link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css"> +{% endblock %} {% block content %} <script> function newPopup(url) { @@ -16,10 +20,11 @@ function newPopup(url) { <iframe src="/chat?room={{ username }}" frameborder="0" style="width: 100%;height: 100%; min-height: 500px;" allowfullscreen></iframe> </div> </div> - <script>window.HELP_IMPROVE_VIDEOJS = false;</script> - <script src="/videojs/video.min.js"></script> - <link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css"> + </br> + <noscript>The webclients for the stream and the chat require javascript, but feel free to use the direct links above!</br></br></noscript> + {{ about | escape }} <script> + window.HELP_IMPROVE_VIDEOJS = false; var player = videojs('live-video', { html: { nativeCaptions: false, @@ -34,7 +39,5 @@ function newPopup(url) { type: 'application/dash+xml' }); }) - </script></br> - <noscript>The webclients for the stream and the chat require javascript, but feel free to use the direct links above!</br></br></noscript> - {{ about | escape }} + </script> {% endblock %} \ No newline at end of file