Big Refactor

Stop using config and toml as dependencies
Stop passing around config variables through function calls
Add config.ts and pull the values you need directly in the files
Remove irc.js for incoming new IRC solution
Rename controller to index because that was stupid
Minor git bullshit with the config folder
Change to yaml as a config format
merge-requests/7/head
knotteye 4 years ago
parent 681802dbaf
commit f7733b9507
  1. 6
      .gitignore
  2. 0
      config/.gitkeep
  3. 58
      config/default.toml
  4. 24
      install/config.example.yml
  5. 6
      install/setup.sh
  6. 19
      install/template.local.toml
  7. 58
      package-lock.json
  8. 8
      package.json
  9. 15
      src/api.ts
  10. 3
      src/cli.ts
  11. 49
      src/config.ts
  12. 67
      src/controller.ts
  13. 15
      src/database.ts
  14. 56
      src/http.ts
  15. 15
      src/index.ts
  16. 212
      src/irc.js
  17. 35
      src/server.ts

6
.gitignore vendored

@ -1,8 +1,6 @@
node_modules
site/live
config/local.toml
config/jwt.pem
config/generated.toml
config/bans.db
config/**/*
!config/.gitkeep
install/db_setup.sql
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'

@ -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
dbclient="localhost"
fi
sed -e "s#<iname>#$name#g" -e "s#<domain>#$domain#g" -e "s#<ffmpeg>#$ffmpeg#g" -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<email>#$email#g" install/template.local.toml > config/generated.toml
sed -e "s#<iname>#$name#g" -e "s#<domain>#$domain#g" -e "s#<ffmpeg>#$ffmpeg#g" -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<email>#$email#g" install/config.example.yml > config/generated.yml
sed -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<dbclient>#$dbclient#g" install/db_template.sql > install/db_setup.sql
echo "A setup script for the database has been generated at install/db_setup.sql. Please run it by connecting to your database software and executing 'source install/db_setup.sql;''"
echo "A default configuration file has been generated at config/generated.toml"
echo "If everything looks fine, move it to config/local.toml and start your instance."
echo "A default configuration file has been generated at config/generated.yml"
echo "If everything looks fine, move it to config/config.yml and start your instance."

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

58
package-lock.json generated

@ -82,6 +82,11 @@
"readable-stream": "^2.0.6"
}
},
"arg": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz",
"integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg=="
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@ -342,6 +347,11 @@
}
}
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@ -644,6 +654,11 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"diff": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz",
"integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q=="
},
"dirty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz",
@ -1910,6 +1925,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"make-error": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
"integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g=="
},
"map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@ -2292,6 +2312,11 @@
"os-tmpdir": "^1.0.0"
}
},
"parse-yaml": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/parse-yaml/-/parse-yaml-0.1.0.tgz",
"integrity": "sha512-tLfs2QiziUPFTA4nNrv2rrC0CnHDIF2o2m5TCgNss/E0asI0ltVjBcNKhcd/8vteZa8xKV5RGfD0ZFFlECMCqQ=="
},
"parseqs": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
@ -2868,6 +2893,22 @@
"urix": "^0.1.0"
}
},
"source-map-support": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
"integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"source-map-url": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
@ -3029,6 +3070,18 @@
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
},
"ts-node": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz",
"integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==",
"requires": {
"arg": "^4.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"source-map-support": "^0.5.6",
"yn": "^3.0.0"
}
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -3201,6 +3254,11 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
}
}
}

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

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

@ -1,8 +1,7 @@
import * as db from "./database"
import * as flags from "flags";
import * as config from "config"
db.init(config.database, config.bcrypt);
db.init();
flags.defineString('adduser', '', 'User to add');
flags.defineString('rmuser', '', 'User to remove');

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

@ -8,7 +8,7 @@ import * as dirty from "dirty";
import * as socketSpam from "socket-anti-spam";
import * as api from "./api";
import * as db from "./database";
import * as irc from "./irc";
import { config } from "./config";
import { readdir, readFileSync, writeFileSync } from "fs";
import { JWT, JWK } from "jose";
import { strict } from "assert";
@ -20,10 +20,10 @@ const server = http.createServer(app);
const io = socketio(server);
const store = dirty();
var banlist;
var ircconf;
var jwkey;
try{
jwkey = JWK.asKey(readFileSync('./config/jwt.pem'));
console.log('Found key for JWT signing.');
} catch (e) {
console.log("No key found for JWT signing, generating one now.");
jwkey = JWK.generateSync('RSA', 2048, { use: 'sig' });
@ -31,24 +31,23 @@ try{
}
var njkconf;
async function init(satyr: any, http: object, irc: any){
ircconf = irc;
async function init(){
njk.configure('templates', {
autoescape : true,
express : app,
watch : false
});
njkconf ={
sitename: satyr.name,
domain: satyr.domain,
email: satyr.email,
rootredirect: satyr.rootredirect,
version: satyr.version
njkconf = {
sitename: config['satyr']['name'],
domain: config['satyr']['domain'],
email: config['satyr']['email'],
rootredirect: config['satyr']['rootredirect'],
version: config['satyr']['version']
};
app.use(cookies());
app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: true }));
if(http['hsts']){
if(config['http']['hsts']){
app.use((req, res, next) => {
res.append('Strict-Transport-Security', 'max-age=5184000');
next();
@ -56,11 +55,11 @@ async function init(satyr: any, http: object, irc: any){
}
app.disable('x-powered-by');
//site handlers
await initSite(satyr.registration);
await initSite(config['satyr']['registration']);
//api handlers
await initAPI();
//static files if nothing else matches first
app.use(express.static(satyr.directory));
app.use(express.static(config['http']['directory']));
//404 Handler
app.use(function (req, res, next) {
if(tryDecode(req.cookies.Authorization)) {
@ -70,7 +69,7 @@ async function init(satyr: any, http: object, irc: any){
//res.status(404).render('404.njk', njkconf);
});
banlist = new dirty('./config/bans.db').on('load', () => {initChat()});
server.listen(http['port']);
server.listen(config['http']['port']);
}
async function newNick(socket, skip?: boolean, i?: number) {
@ -336,23 +335,17 @@ async function initSite(openReg) {
}
async function initChat() {
//irc peering
if(ircconf.enable){
await irc.connect({
port: ircconf.port,
sid: ircconf.sid,
pass: ircconf.pass,
server: ircconf.server,
vhost: ircconf.vhost
});
irc.events.on('message', (nick, channel, msg) => {
io.to(channel).emit('MSG', {nick: nick, msg: msg});
});
}
//set a cookie to request same nick
//apply same nick if the request comes from the same ip
//structure of entry in store should be:
// knotteye: {
// ip: "127.0.0.1",
// id: ["aklsjdnaksj", "asjdnaksjnd", "aksjdnkajs"]
//}
//socket.io chat logic
io.on('connection', async (socket) => {
socket.nick = await newNick(socket);
if(ircconf.enable) irc.registerUser(socket.nick);
socket.on('JOINROOM', async (data) => {
let t: any = await db.query('select username from users where username='+db.raw.escape(data));
if(t[0]){
@ -367,7 +360,6 @@ async function initChat() {
}
socket.join(data);
io.to(data).emit('JOINED', {nick: socket.nick});
if(ircconf.enable) irc.join(socket.nick, data);
}
else socket.emit('ALERT', 'Room does not exist');
});
@ -388,16 +380,13 @@ async function initChat() {
});
socket.on('LEAVEROOM', (data) => {
socket.leave(data);
if(ircconf.enable) irc.part(socket.nick, data);
io.to(data).emit('LEFT', {nick: socket.nick});
});
socket.on('disconnecting', (reason) => {
let rooms = Object.keys(socket.rooms);
for(let i=1;i<rooms.length;i++){
if(ircconf.enable) irc.part(socket.nick, rooms[i]);
io.to(rooms[i]).emit('ALERT', socket.nick+' disconnected');
}
if(ircconf.enable) irc.unregisterUser(socket.nick);
store.rm(socket.nick);
});
socket.on('NICK', async (data) => {
@ -424,7 +413,6 @@ async function initChat() {
socket.on('MSG', (data) => {
if(data.msg === "" || !data.msg.replace(/\s/g, '').length) return;
io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg});
if(ircconf.enable) irc.send(socket.nick, data.room, data.msg);
});
socket.on('KICK', (data) => {
if(socket.nick === data.room){
@ -491,10 +479,8 @@ async function initChat() {
socketAS.event.on('ban', (socket) => {
let rooms = Object.keys(socket.rooms);
for(let i=1;i<rooms.length;i++){
if(ircconf.enable) irc.part(socket.nick, rooms[i]);
io.to(rooms[i]).emit('ALERT', socket.nick+' was banned.');
}
if(ircconf.enable) irc.unregisterUser(socket.nick);
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 * as strf from "strftime";
import * as db from "./database";
import {config} from "./config";
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const { exec, execFile } = require('child_process');
const keystore = dirty();
function init (mediaconfig: any, satyrconfig: any) {
const nms = new NodeMediaServer(mediaconfig);
function init () {
const nms = new NodeMediaServer({logType: 0,rtmp: config['rtmp']});
nms.run();
nms.on('postPublish', (id, StreamPath, args) => {
@ -23,7 +24,7 @@ function init (mediaconfig: any, satyrconfig: any) {
session.reject();
return false;
}
if(app !== satyrconfig.privateEndpoint){
if(app !== config['media']['privateEndpoint']){
//app isn't at public endpoint if we've reached this point
console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id);
session.reject();
@ -36,21 +37,21 @@ function init (mediaconfig: any, satyrconfig: any) {
if(results[0]){
//transcode to mpd after making sure directory exists
keystore[results[0].username] = key;
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, ()=>{;});
mkdir(config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username, { recursive : true }, ()=>{;});
while(true){
if(session.audioCodec !== 0 && session.videoCodec !== 0){
transCommand(mediaconfig, satyrconfig, results[0].username, key).then((r) => {
execFile(satyrconfig.ffmpeg, r, {maxBuffer: Infinity});
transCommand(results[0].username, key).then((r) => {
execFile(config['media']['ffmpeg'], r, {maxBuffer: Infinity});
});
break;
}
await sleep(300);
}
if(results[0].record_flag && satyrconfig.record){
if(results[0].record_flag && config['media']['record']){
console.log('[NodeMediaServer] Initiating recording for stream:',id);
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {
mkdir(config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username, { recursive : true }, (err) => {
if (err) throw err;
execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.prviateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], {
execFile(config['media']['ffmpeg'], ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+config['media']['privateEndpoint']+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], {
detached : true,
stdio : 'inherit',
maxBuffer: Infinity
@ -74,7 +75,7 @@ function init (mediaconfig: any, satyrconfig: any) {
nms.on('donePublish', (id, StreamPath, args) => {
let app: string = StreamPath.split("/")[1];
let key: string = StreamPath.split("/")[2];
if(app === satyrconfig.privateEndpoint) {
if(app === config['media']['privateEndpoint']) {
db.query('update user_meta,users set user_meta.live=false where users.stream_key='+db.raw.escape(key));
db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
if(results[0]) keystore.rm(results[0].username);
@ -94,24 +95,24 @@ function init (mediaconfig: any, satyrconfig: any) {
}
//localhost can play from whatever endpoint
//other clients must use private endpoint
if(app !== satyrconfig.publicEndpoint && !session.isLocal) {
if(app !== config['media']['publicEndpoint'] && !session.isLocal) {
console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id);
session.reject();
return false;
}
//rewrite playpath to private endpoint serverside
//(hopefully)
if(app === satyrconfig.publicEndpoint) {
if(app === config['media']['publicEndpoint']) {
if(keystore[key]){
session.playStreamPath = '/'+satyrconfig.privateEndpoint+'/'+keystore[key];
session.playStreamPath = '/'+config['media']['privateEndpoint']+'/'+keystore[key];
return true;
}
}
});
}
async function transCommand(config: object, satyrconfig: object, user: string, key: string): Promise<string[]>{
let args: string[] = ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+satyrconfig['privateEndpoint']+'/'+key];
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']);
@ -133,9 +134,9 @@ async function transCommand(config: object, satyrconfig: object, user: string, k
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', satyrconfig['directory']+'/'+satyrconfig['publicEndpoint']+'/'+user+'/index.mpd']);
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', satyrconfig['directory']+'/'+satyrconfig['publicEndpoint']+'/'+user+'/index.m3u8']);
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 };