Merge branch 'develop' into 'master'

Develop -> Master

See merge request knotteye/satyr!7
merge-requests/7/merge
knotteye 4 years ago
commit d09b40d69c
  1. 5
      .gitignore
  2. 0
      config/.gitkeep
  3. 58
      config/default.toml
  4. 53
      docs/CONFIGURATION.md
  5. 13
      docs/INSTALLATION.md
  6. 3
      docs/USAGE.md
  7. 24
      install/config.example.yml
  8. 6
      install/setup.sh
  9. 19
      install/template.local.toml
  10. 73
      package-lock.json
  11. 9
      package.json
  12. 15
      src/api.ts
  13. 3
      src/cli.ts
  14. 49
      src/config.ts
  15. 67
      src/controller.ts
  16. 15
      src/database.ts
  17. 171
      src/http.ts
  18. 15
      src/index.ts
  19. 212
      src/irc.js
  20. 60
      src/server.ts
  21. 2
      templates/base.njk
  22. 18
      templates/chat.html
  23. 9
      templates/help.njk
  24. 15
      templates/user.njk

5
.gitignore vendored

@ -1,7 +1,6 @@
node_modules node_modules
site/live site/live
config/local.toml config/**/*
config/jwt.pem !config/.gitkeep
config/generated.toml
install/db_setup.sql install/db_setup.sql
build/** build/**

@ -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'

@ -1,27 +1,48 @@
## Configuring Satyr ## Configuring Satyr
### Config file ### 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 Some values you might want to change are
``` ```
[satyr] satyr:
registration = true registration: true
#allow new users to register # allow new users to register
rootRedirect = '/users/live' rootRedirect: '/users/live'
#the page users are directed to when they visit your site root # the page users are directed to when they visit your site root
[media]
record = true http:
#allow users to record VODs hsts: true
[bcrypt] # enable strict transport security
saltRounds = 12
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 #change the number of rounds of bcrypt to fit your hardware
#if you don't understand the implications, don't change this #if you don't understand the implications, don't change this
[ircd]
enable = true irc:
#enable IRC peering port: 6667
#unused for now #irc settings
#currently unused
``` ```
### Web Frontend ### 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 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. You can change the logo by replacing site/logo.svg.
You should also consider editing templates/about.html and templates/tos.html

@ -2,8 +2,8 @@
A more detailed walkthrough. A more detailed walkthrough.
### System Dependencies ### System Dependencies
Install ffmpeg and mysql through your distribution's package manager. 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 install node. Compatible versions are >=10. Nightly builds may fail to compile some of the native addons. 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 ### Installing Satyr
Clone the repository and change to the directory Clone the repository and change to the directory
@ -25,10 +25,9 @@ Run the setup script for the database.
sudo mysql sudo mysql
source install/db_setup.sql; source install/db_setup.sql;
``` ```
Compile the code and start the server. Then start the server.
```bash ```bash
npm run build npm run start
npm start
``` ```
It is reccomended that you run Satyr behind a TLS terminating reverse proxy, like nginx. 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 ```bash
git pull git pull
npm i npm i
npm run build npm update
``` ```
Then restart the server. Then restart the server.
## Migrating Satyr ## 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. 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.

@ -52,6 +52,9 @@ The following commands are available:
`/nick kawen (password)` Password is only required if kawen is a registered user. `/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. `/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. `/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 #### Streaming
Users should stream to rtmp://example.tld/stream/examplestreamkey Users should stream to rtmp://example.tld/stream/examplestreamkey

@ -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

@ -38,8 +38,8 @@ dbclient="${dbclient:='*'}"
else else
dbclient="localhost" dbclient="localhost"
fi 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 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 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 "A default configuration file has been generated at config/generated.yml"
echo "If everything looks fine, move it to config/local.toml and start your instance." echo "If everything looks fine, move it to config/config.yml and start your instance."

@ -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>'

73
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "satyr", "name": "satyr",
"version": "0.4.4", "version": "0.5.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -82,6 +82,11 @@
"readable-stream": "^2.0.6" "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": { "arr-diff": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "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": { "bytes": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" "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": { "dirty": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" "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": { "map-cache": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@ -2043,6 +2063,11 @@
"minimist": "0.0.8" "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": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -2287,6 +2312,11 @@
"os-tmpdir": "^1.0.0" "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": { "parseqs": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", "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": { "socket.io": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz",
@ -2855,6 +2893,22 @@
"urix": "^0.1.0" "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": { "source-map-url": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "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", "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" "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": { "type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -3188,6 +3254,11 @@
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
} }
} }
} }

@ -5,8 +5,8 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"author": "knotteye", "author": "knotteye",
"scripts": { "scripts": {
"start": "tsc && node build/controller.js", "start": "ts-node src/index.ts",
"user": "node build/cli.js", "user": "ts-node src/cli.ts",
"setup": "sh install/setup.sh" "setup": "sh install/setup.sh"
}, },
"repository": { "repository": {
@ -16,7 +16,6 @@
"dependencies": { "dependencies": {
"bcrypt": "^3.0.6", "bcrypt": "^3.0.6",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"config": "^3.2.2",
"cookie-parser": "^1.4.4", "cookie-parser": "^1.4.4",
"dirty": "^1.1.0", "dirty": "^1.1.0",
"express": "^4.17.1", "express": "^4.17.1",
@ -25,10 +24,12 @@
"mysql": "^2.17.1", "mysql": "^2.17.1",
"node-media-server": ">=2.1.3 <3.0.0", "node-media-server": ">=2.1.3 <3.0.0",
"nunjucks": "^3.2.0", "nunjucks": "^3.2.0",
"parse-yaml": "^0.1.0",
"recursive-readdir": "^2.2.2", "recursive-readdir": "^2.2.2",
"socket-anti-spam": "^2.0.0",
"socket.io": "^2.3.0", "socket.io": "^2.3.0",
"strftime": "^0.10.0", "strftime": "^0.10.0",
"toml": "^3.0.0", "ts-node": "^8.5.4",
"typescript": "^3.6.3" "typescript": "^3.6.3"
}, },
"devDependencies": { "devDependencies": {

@ -1,17 +1,12 @@
import * as db from "./database" import * as db from "./database"
import { unregisterUser } from "./irc"; import { config } from "./config";
var config: any;
function init(conf: object){
config = conf;
}
async function register(name: string, password: string, confirm: string) { 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(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"};
if(password !== confirm) return {"error":"mismatched passwords"}; if(password !== confirm) return {"error":"mismatched passwords"};
for(let i=0;i<config.restrictedNames.length;i++){ for(let i=0;i<config['satyr']['restrictedNames'].length;i++){
if (name === config.restrictedNames[i]) return {"error":"restricted name"}; if (name === config['satyr']['restrictedNames'][i]) return {"error":"restricted name"};
} }
let r: boolean = await db.addUser(name, password); let r: boolean = await db.addUser(name, password);
if(r) { if(r) {
@ -61,4 +56,4 @@ async function login(name: string, password: string){
return false; return false;
} }
export { init, register, update, changepwd, changesk, login }; export { register, update, changepwd, changesk, login };

@ -1,8 +1,7 @@
import * as db from "./database" import * as db from "./database"
import * as flags from "flags"; 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('adduser', '', 'User to add');
flags.defineString('rmuser', '', 'User to remove'); flags.defineString('rmuser', '', 'User to remove');

@ -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 };

@ -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 };

@ -1,20 +1,21 @@
import * as mysql from "mysql"; import * as mysql from "mysql";
import * as bcrypt from "bcrypt"; import * as bcrypt from "bcrypt";
import * as crypto from "crypto"; import * as crypto from "crypto";
import { config } from "./config";
import { resolve } from "url"; import { resolve } from "url";
var raw: any; var raw;
var cryptoconfig: any; var cryptoconfig: Object;
function init (db: object, bcrypt: object){ function init (){
raw = mysql.createPool(db); raw = mysql.createPool(config['database']);
cryptoconfig = bcrypt; cryptoconfig = config['crypto'];
} }
async function addUser(name: string, password: string){ async function addUser(name: string, password: string){
//does not respect registration setting in config //does not respect registration setting in config
if(password === '') return false; if(password === '') return false;
let key: string = await genKey(); 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)); let dupe = await query('select * from users where username='+raw.escape(name));
if(dupe[0]) return false; 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)'); 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){ 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 }; export { query, raw, init, addUser, rmUser, validatePassword, hash, genKey };

@ -5,9 +5,10 @@ import * as socketio from "socket.io";
import * as http from "http"; import * as http from "http";
import * as cookies from "cookie-parser"; import * as cookies from "cookie-parser";
import * as dirty from "dirty"; import * as dirty from "dirty";
import * as socketSpam from "socket-anti-spam";
import * as api from "./api"; import * as api from "./api";
import * as db from "./database"; import * as db from "./database";
import * as irc from "./irc"; import { config } from "./config";
import { readdir, readFileSync, writeFileSync } from "fs"; import { readdir, readFileSync, writeFileSync } from "fs";
import { JWT, JWK } from "jose"; import { JWT, JWK } from "jose";
import { strict } from "assert"; import { strict } from "assert";
@ -18,9 +19,11 @@ const app = express();
const server = http.createServer(app); const server = http.createServer(app);
const io = socketio(server); const io = socketio(server);
const store = dirty(); const store = dirty();
var banlist;
var jwkey; var jwkey;
try{ try{
jwkey = JWK.asKey(readFileSync('./config/jwt.pem')); jwkey = JWK.asKey(readFileSync('./config/jwt.pem'));
console.log('Found key for JWT signing.');
} catch (e) { } catch (e) {
console.log("No key found for JWT signing, generating one now."); console.log("No key found for JWT signing, generating one now.");
jwkey = JWK.generateSync('RSA', 2048, { use: 'sig' }); jwkey = JWK.generateSync('RSA', 2048, { use: 'sig' });
@ -28,23 +31,23 @@ try{
} }
var njkconf; var njkconf;
async function init(satyr: any, http: object, ircconf: any){ async function init(){
njk.configure('templates', { njk.configure('templates', {
autoescape : true, autoescape : true,
express : app, express : app,
watch : false watch : false
}); });
njkconf ={ njkconf = {
sitename: satyr.name, sitename: config['satyr']['name'],
domain: satyr.domain, domain: config['satyr']['domain'],
email: satyr.email, email: config['satyr']['email'],
rootredirect: satyr.rootredirect, rootredirect: config['satyr']['rootredirect'],
version: satyr.version version: config['satyr']['version']
}; };
app.use(cookies()); app.use(cookies());
app.use(bodyparser.json()); app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: true })); app.use(bodyparser.urlencoded({ extended: true }));
if(http['hsts']){ if(config['http']['hsts']){
app.use((req, res, next) => { app.use((req, res, next) => {
res.append('Strict-Transport-Security', 'max-age=5184000'); res.append('Strict-Transport-Security', 'max-age=5184000');
next(); next();
@ -52,11 +55,11 @@ async function init(satyr: any, http: object, ircconf: any){
} }
app.disable('x-powered-by'); app.disable('x-powered-by');
//site handlers //site handlers
await initSite(satyr.registration); await initSite(config['satyr']['registration']);
//api handlers //api handlers
await initAPI(); await initAPI();
//static files if nothing else matches first //static files if nothing else matches first
app.use(express.static(satyr.directory)); app.use(express.static(config['http']['directory']));
//404 Handler //404 Handler
app.use(function (req, res, next) { app.use(function (req, res, next) {
if(tryDecode(req.cookies.Authorization)) { 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); else res.status(404).render('404.njk', njkconf);
//res.status(404).render('404.njk', njkconf); //res.status(404).render('404.njk', njkconf);
}); });
await initChat(ircconf); banlist = new dirty('./config/bans.db').on('load', () => {initChat()});
server.listen(http['port']); 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){ if(socket.handshake.headers['cookie'] && !skip){
let c = await parseCookie(socket.handshake.headers['cookie']); let c = await parseCookie(socket.handshake.headers['cookie']);
let t = await validToken(c['Authorization']); 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 if(!i) i = 10;
let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(1000)); let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(i));
if(store.get(n)) return newNick(socket, true); if(store.get(n)) return newNick(socket, true, Math.floor(i * 10));
else { else {
store.set(n, socket.id); store.set(n, socket.id);
return n; return n;
@ -89,8 +95,12 @@ async function chgNick(socket, nick, f?: boolean) {
for(let i=1;i<rooms.length;i++){ for(let i=1;i<rooms.length;i++){
io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick); io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick);
} }
if(store.get(socket.nick)) store.rm(socket.nick); if(store.get(socket.nick)) {
if (!f) store.set(nick, socket.id); 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; socket.nick = nick;
} }
@ -328,30 +338,26 @@ async function initSite(openReg) {
}); });
} }
async function initChat(ircconf: any) { async function initChat() {
//irc peering //set a cookie to request same nick
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});
});
}
//socket.io chat logic //socket.io chat logic
io.on('connection', async (socket) => { io.on('connection', async (socket) => {
socket.nick = await newNick(socket); socket.nick = await newNick(socket);
if(ircconf.enable) irc.registerUser(socket.nick);
socket.on('JOINROOM', async (data) => { socket.on('JOINROOM', async (data) => {
let t: any = await db.query('select username from users where username='+db.raw.escape(data)); let t: any = await db.query('select username from users where username='+db.raw.escape(data));
if(t[0]){ 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); socket.join(data);
io.to(data).emit('JOINED', {nick: socket.nick}); io.to(data).emit('JOINED', {nick: socket.nick});
if(ircconf.enable) irc.join(socket.nick, data);
} }
else socket.emit('ALERT', 'Room does not exist'); else socket.emit('ALERT', 'Room does not exist');
}); });
@ -372,24 +378,22 @@ async function initChat(ircconf: any) {
}); });
socket.on('LEAVEROOM', (data) => { socket.on('LEAVEROOM', (data) => {
socket.leave(data); socket.leave(data);
if(ircconf.enable) irc.part(socket.nick, data);
io.to(data).emit('LEFT', {nick: socket.nick}); io.to(data).emit('LEFT', {nick: socket.nick});
}); });
socket.on('disconnecting', (reason) => { socket.on('disconnecting', (reason) => {
let rooms = Object.keys(socket.rooms); let rooms = Object.keys(socket.rooms);
for(let i=1;i<rooms.length;i++){ 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'); 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); store.rm(socket.nick);
}); });
socket.on('NICK', async (data) => { socket.on('NICK', async (data) => {
data.nick = data.nick.replace(' ',''); 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)); let user = await db.query('select username from users where username='+db.raw.escape(data.nick));
if(user[0]){ if(user[0]){
if(!data.password){ if(!data.password){
@ -402,27 +406,102 @@ async function initChat(ircconf: any) {
else socket.emit('ALERT','Incorrect username or password'); else socket.emit('ALERT','Incorrect username or password');
} }
else { else {
if(store.get(data.nick)){
socket.emit('ALERT', 'Nickname is already in use');
return false;
}
chgNick(socket, data.nick); chgNick(socket, data.nick);
} }
}); });
socket.on('MSG', (data) => { socket.on('MSG', (data) => {
if(data.msg === "" || !data.msg.replace(/\s/g, '').length) return; if(data.msg === "" || !data.msg.replace(/\s/g, '').length) return;
io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg}); if(socket.rooms[data['room']]) io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg});
if(ircconf.enable) irc.send(socket.nick, data.room, data.msg);
}); });
socket.on('KICK', (data) => { socket.on('KICK', (data) => {
if(socket.nick === data.room){ if(socket.nick === data.room){
//find client with data.nick //find client with data.nick
let id: string = store.get(data.nick); let id: string = store.get(data.nick);
if(id){ 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]; let target = io.sockets.connected[id];
io.in(data.room).emit('ALERT', data.nick+' has been kicked.'); 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', 'No such user found.');
} }
else socket.emit('ALERT', 'Not authorized to do that.'); 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);
}); });
} }

@ -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 };

@ -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)
}

@ -3,13 +3,14 @@ import * as dirty from "dirty";
import { mkdir, fstat, access } from "fs"; import { mkdir, fstat, access } from "fs";
import * as strf from "strftime"; import * as strf from "strftime";
import * as db from "./database"; import * as db from "./database";
import {config} from "./config";
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const { exec, execFile } = require('child_process'); const { exec, execFile } = require('child_process');
const keystore = dirty(); const keystore = dirty();
function init (mediaconfig: any, satyrconfig: any) { function init () {
const nms = new NodeMediaServer(mediaconfig); const nms = new NodeMediaServer({logType: 0,rtmp: config['rtmp']});
nms.run(); nms.run();
nms.on('postPublish', (id, StreamPath, args) => { nms.on('postPublish', (id, StreamPath, args) => {
@ -23,7 +24,7 @@ function init (mediaconfig: any, satyrconfig: any) {
session.reject(); session.reject();
return false; return false;
} }
if(app !== satyrconfig.privateEndpoint){ if(app !== config['media']['privateEndpoint']){
//app isn't at public endpoint if we've reached this point //app isn't at public endpoint if we've reached this point
console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id); console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id);
session.reject(); session.reject();
@ -34,23 +35,23 @@ function init (mediaconfig: any, satyrconfig: any) {
//otherwise kill the session //otherwise kill the session
db.query('select username,record_flag from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => { db.query('select username,record_flag from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
if(results[0]){ if(results[0]){
//push to rtmp //transcode to mpd after making sure directory exists
//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
keystore[results[0].username] = key; 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){ while(true){
if(session.audioCodec !== 0 && session.videoCodec !== 0){ 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; break;
} }
await sleep(300); 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); 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; 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, detached : true,
stdio : 'inherit', stdio : 'inherit',
maxBuffer: Infinity maxBuffer: Infinity
@ -74,7 +75,7 @@ function init (mediaconfig: any, satyrconfig: any) {
nms.on('donePublish', (id, StreamPath, args) => { nms.on('donePublish', (id, StreamPath, args) => {
let app: string = StreamPath.split("/")[1]; let app: string = StreamPath.split("/")[1];
let key: string = StreamPath.split("/")[2]; 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('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) => { 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); if(results[0]) keystore.rm(results[0].username);
@ -94,19 +95,48 @@ function init (mediaconfig: any, satyrconfig: any) {
} }
//localhost can play from whatever endpoint //localhost can play from whatever endpoint
//other clients must use private 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); console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id);
session.reject(); session.reject();
return false; return false;
} }
//rewrite playpath to private endpoint serverside //rewrite playpath to private endpoint serverside
//(hopefully) //(hopefully)
if(app === satyrconfig.publicEndpoint) { if(app === config['media']['publicEndpoint']) {
if(keystore[key]){ if(keystore[key]){
session.playStreamPath = '/'+satyrconfig.privateEndpoint+'/'+keystore[key]; session.playStreamPath = '/'+config['media']['privateEndpoint']+'/'+keystore[key];
return true; 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 }; export { init };

@ -4,6 +4,8 @@
<link rel="stylesheet" type="text/css" href="/local.css"> <link rel="stylesheet" type="text/css" href="/local.css">
<link rel="icon" type="image/svg" href="/logo.svg"> <link rel="icon" type="image/svg" href="/logo.svg">
<title>{{ sitename }}</title> <title>{{ sitename }}</title>
{% block head %}
{% endblock %}
</head> </head>
<body> <body>
<div id="wrapper"> <div id="wrapper">

@ -36,6 +36,24 @@
else if(m.startsWith('/list')){ else if(m.startsWith('/list')){
socket.emit('LIST', {room: room}); 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}); else socket.emit('MSG', {room: room, msg: m});
document.getElementById('m').value = ''; document.getElementById('m').value = '';
} }

@ -4,9 +4,12 @@
<h4>Chatting</h4> <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 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> The following commands are available:</br>
`/nick kawen (password)` Password is only required if kawen is a registered user.</br> <code><a>/nick kawen (password)</a></code> 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> <code><a>/join kawen</a></code> 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>/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> <h4>Streaming</h4>
Users should stream to <a>rtmp://{{ domain }}/stream/Stream-Key</a></br></br> Users should stream to <a>rtmp://{{ domain }}/stream/Stream-Key</a></br></br>

@ -1,4 +1,8 @@
{% extends "base.njk" %} {% 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 %} {% block content %}
<script> <script>
function newPopup(url) { 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> <iframe src="/chat?room={{ username }}" frameborder="0" style="width: 100%;height: 100%; min-height: 500px;" allowfullscreen></iframe>
</div> </div>
</div> </div>
<script>window.HELP_IMPROVE_VIDEOJS = false;</script> </br>
<script src="/videojs/video.min.js"></script> <noscript>The webclients for the stream and the chat require javascript, but feel free to use the direct links above!</br></br></noscript>
<link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css"> {{ about | escape }}
<script> <script>
window.HELP_IMPROVE_VIDEOJS = false;
var player = videojs('live-video', { var player = videojs('live-video', {
html: { html: {
nativeCaptions: false, nativeCaptions: false,
@ -34,7 +39,5 @@ function newPopup(url) {
type: 'application/dash+xml' type: 'application/dash+xml'
}); });
}) })
</script></br> </script>
<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 }}
{% endblock %} {% endblock %}