Merge branch 'develop' into 'master'

Develop -> Master

See merge request knotteye/satyr!7
merge-requests/8/merge
knotteye 2019-12-21 23:34:59 +00:00
commit e5dfa446a2
24 changed files with 435 additions and 479 deletions

5
.gitignore vendored
View File

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

0
config/.gitkeep Normal file
View File

View File

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

View File

@ -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:
hsts: true
# enable strict transport security
media:
record: true
# allow users to record VODs # allow users to record VODs
[bcrypt]
saltRounds = 12 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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -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=="
} }
} }
} }

View File

@ -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": {

View File

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

View File

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

49
src/config.ts Normal file
View File

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

View File

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

View File

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

View File

@ -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 }
let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(1000)); if(!i) i = 10;
if(store.get(n)) return newNick(socket, true); let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(i));
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', 'No such user found.');
} }
else socket.emit('ALERT', 'Not authorized to do that.'); 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);
}); });
} }

15
src/index.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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