diff options
| author | RblSb <msrblsb@gmail.com> | 2020-02-24 07:58:56 +0300 |
|---|---|---|
| committer | RblSb <msrblsb@gmail.com> | 2020-02-24 07:58:56 +0300 |
| commit | 9c6cd2c2310d2e3ce3d1a6e3350a97e7ba0ca657 (patch) | |
| tree | 3eb12f0eee7ba05b7e70c740561eff31e06b608a | |
| parent | c561fb9e2e42e4968f2b48cd535f208e90f8c12c (diff) | |
Start working on user folder
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | README.md | 10 | ||||
| -rw-r--r-- | build-client.hxml | 2 | ||||
| -rw-r--r-- | build/server.js | 170 | ||||
| -rw-r--r-- | res/client.js (renamed from build/client.js) | 29 | ||||
| -rw-r--r-- | res/css/custom.css | 4 | ||||
| -rw-r--r-- | src/Client.hx | 11 | ||||
| -rw-r--r-- | src/Types.hx | 3 | ||||
| -rw-r--r-- | src/client/Main.hx | 39 | ||||
| -rw-r--r-- | src/client/Player.hx | 1 | ||||
| -rw-r--r-- | src/server/HttpServer.hx | 79 | ||||
| -rw-r--r-- | src/server/Main.hx | 66 | ||||
| -rw-r--r-- | src/server/Utils.hx | 11 | ||||
| -rw-r--r-- | user/README.md | 18 |
14 files changed, 332 insertions, 117 deletions
@@ -1,3 +1,7 @@ /node_modules
/res/temp
-/config.json
+/user/config.json
+/user/state.json
+/user/res/
+/user/logs/
+/user/errors/
@@ -1,16 +1,15 @@ ## SyncTube
Synchronized video viewing with chat and other features.
-Based on CyTube, but with lightweight implementation and very easy way to run locally.
+Based on CyTube layout, but with lightweight implementation and very easy way to run locally.
### New features
Even if some original features are not implemented yet, there is some new things:
- Multi-Language
- Mobile view with page fullscreen
-- Updated Des theme
-
-TODO:
- Way to play local videos for network users (without NAT loopback feature)
- `/30`, `/-21`, etc to rewind video playback in seconds
+- Override every front-end file you want (`user/res` folder)
+- Updated Des theme
### Setup
- Open `4200` and `4201` ports in your router settings
@@ -18,6 +17,9 @@ TODO: - Run `node build/server.js`
- Open showed "Local" link for yourself and send "Global" link to friends
+### Configuration
+It's just works, but you can also check [user/ folder](/user/README.md) for server settings and additional customization.
+
### Development
- Install Haxe 4, VSCode and vshaxe extension.
- `haxelib install all` to install extern libs.
diff --git a/build-client.hxml b/build-client.hxml index d8a77e3..f4eca40 100644 --- a/build-client.hxml +++ b/build-client.hxml @@ -2,4 +2,4 @@ --main client.Main
-D analyzer-optimize
--dce full
---js build/client.js
+--js res/client.js
diff --git a/build/server.js b/build/server.js index 68ad69d..ce5bb7b 100644 --- a/build/server.js +++ b/build/server.js @@ -12,8 +12,9 @@ var ClientGroup = $hxEnums["ClientGroup"] = { __ename__ : true, __constructs__ : ,Leader: {_hx_index:1,__enum__:"ClientGroup",toString:$estr} ,Admin: {_hx_index:2,__enum__:"ClientGroup",toString:$estr} }; -var Client = function(ws,id,name,group) { +var Client = function(ws,req,id,name,group) { this.ws = ws; + this.req = req; this.id = id; this.name = name; var i = group; @@ -308,6 +309,9 @@ StringTools.startsWith = function(s,start) { return false; } }; +StringTools.replace = function(s,sub,by) { + return s.split(sub).join(by); +}; var haxe_Log = function() { }; haxe_Log.__name__ = true; haxe_Log.formatOutput = function(v,infos) { @@ -514,36 +518,49 @@ var js_node_Path = require("path"); var js_npm_ws_Server = require("ws").Server; var server_HttpServer = function() { }; server_HttpServer.__name__ = true; -server_HttpServer.init = function(directory) { - server_HttpServer.dir = directory; +server_HttpServer.init = function(dir,customDir) { + server_HttpServer.dir = dir; + if(customDir == null) { + return; + } + server_HttpServer.customDir = customDir; + server_HttpServer.hasCustomRes = sys_FileSystem.exists(customDir); }; server_HttpServer.serveFiles = function(req,res) { - var filePath = server_HttpServer.dir + req.url; - if(req.url == "/") { - filePath = "" + server_HttpServer.dir + "/index.html"; + var url = req.url; + if(url == "/") { + url = "/index.html"; } + var filePath = server_HttpServer.dir + url; var extension = haxe_io_Path.extension(filePath).toLowerCase(); var contentType = server_HttpServer.getMimeType(extension); + var tmp; + if(req.connection.remoteAddress != req.connection.localAddress) { + var _this = server_HttpServer.allowedLocalFiles; + tmp = __map_reserved[url] != null ? _this.getReserved(url) : _this.h[url]; + } else { + tmp = true; + } + if(tmp) { + if(server_HttpServer.serveLocalFile(res,url,extension,contentType)) { + return; + } + } if(!server_HttpServer.isChildOf(server_HttpServer.dir,filePath)) { res.statusCode = 500; - var tmp = "Error getting the file: No access to " + js_node_Path.relative(server_HttpServer.dir,filePath) + "."; - res.end(tmp); + var tmp1 = "Error getting the file: No access to " + js_node_Path.relative(server_HttpServer.dir,filePath) + "."; + res.end(tmp1); return; } - if(filePath == "" + server_HttpServer.dir + "/client.js") { - filePath = "" + __dirname + "/client.js"; + if(server_HttpServer.hasCustomRes) { + var path = server_HttpServer.customDir + url; + if(js_node_Fs.existsSync(path)) { + filePath = path; + } } js_node_Fs.readFile(filePath,function(err,data) { if(err != null) { - if(err.code == "ENOENT") { - res.statusCode = 404; - var tmp1 = "File " + js_node_Path.relative(server_HttpServer.dir,filePath) + " not found."; - res.end(tmp1); - } else { - res.statusCode = 500; - var tmp2 = "Error getting the file: " + Std.string(err) + "."; - res.end(tmp2); - } + server_HttpServer.readFileError(err,res,filePath); return; } res.setHeader("Content-Type",contentType); @@ -551,24 +568,58 @@ server_HttpServer.serveFiles = function(req,res) { data = server_HttpServer.localizeHtml(data.toString(),req.headers["accept-language"]); } res.end(data); + return; }); }; +server_HttpServer.readFileError = function(err,res,filePath) { + if(err.code == "ENOENT") { + res.statusCode = 404; + res.end("File " + js_node_Path.relative(server_HttpServer.dir,filePath) + " not found."); + } else { + res.statusCode = 500; + res.end("Error getting the file: " + Std.string(err) + "."); + } +}; +server_HttpServer.serveLocalFile = function(res,filePath,ext,contentType) { + if(ext != "mp4" && ext != "mp3" && ext != "wav") { + return false; + } + if(!js_node_Fs.existsSync(filePath)) { + return false; + } + var _this = server_HttpServer.allowedLocalFiles; + if(__map_reserved[filePath] != null) { + _this.setReserved(filePath,true); + } else { + _this.h[filePath] = true; + } + js_node_Fs.readFile(filePath,function(err,data) { + if(err != null) { + server_HttpServer.readFileError(err,res,filePath); + return; + } + res.setHeader("Content-Type",contentType); + res.end(data); + return; + }); + return true; +}; server_HttpServer.localizeHtml = function(data,lang) { if(lang != null && server_HttpServer.matchLang.match(lang)) { lang = server_HttpServer.matchLang.matched(0); } else { lang = "en"; } - data = new EReg("\\${([A-z_]+)}","g").map(data,function(regExp) { + data = server_HttpServer.matchVarString.map(data,function(regExp) { var key = regExp.matched(1); return Lang.get(lang,key); }); return data; }; server_HttpServer.isChildOf = function(parent,child) { - var relative = js_node_Path.relative(parent,child); - if(relative.length > 0 && !StringTools.startsWith(relative,"..")) { - return !js_node_Path.isAbsolute(relative); + var rel = js_node_Path.relative(parent,child); + if(rel.length > 0 && !StringTools.startsWith(rel,"..")) { + return !js_node_Path.isAbsolute(rel); } else { return false; } @@ -577,7 +628,7 @@ server_HttpServer.getMimeType = function(ext) { var _this = server_HttpServer.mimeTypes; var contentType = __map_reserved[ext] != null ? _this.getReserved(ext) : _this.h[ext]; if(contentType == null) { - contentType = "application/octet-stream"; + return "application/octet-stream"; } return contentType; }; @@ -595,6 +646,7 @@ var server_Main = function(port,wsPort) { this.freeIds = []; this.clients = []; this.rootDir = "" + __dirname + "/.."; + var _gthis = this; this.config = this.getUserConfig(); this.wss = new js_npm_ws_Server({ port : wsPort}); this.wss.on("connection",$bind(this,this.onConnect)); @@ -603,21 +655,27 @@ var server_Main = function(port,wsPort) { }; process.on("exit",exit); process.on("SIGINT",exit); + process.on("SIGUSR1",exit); + process.on("SIGUSR2",exit); process.on("uncaughtException",function(log) { - haxe_Log.trace(log,{ fileName : "src/server/Main.hx", lineNumber : 41, className : "server.Main", methodName : "new"}); + haxe_Log.trace(log,{ fileName : "src/server/Main.hx", lineNumber : 49, className : "server.Main", methodName : "new"}); return; }); process.on("unhandledRejection",function(reason,promise) { - haxe_Log.trace("Unhandled Rejection at:",{ fileName : "src/server/Main.hx", lineNumber : 44, className : "server.Main", methodName : "new", customParams : [reason]}); + haxe_Log.trace("Unhandled Rejection at:",{ fileName : "src/server/Main.hx", lineNumber : 52, className : "server.Main", methodName : "new", customParams : [reason]}); return; }); + this.localIp = server_Utils.getLocalIp(); + this.globalIp = this.localIp; + this.port = port; server_Utils.getGlobalIp(function(ip) { - haxe_Log.trace("Local: http://" + server_Utils.getLocalIp() + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 49, className : "server.Main", methodName : "new"}); - haxe_Log.trace("Global: http://" + ip + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 50, className : "server.Main", methodName : "new"}); + _gthis.globalIp = ip; + haxe_Log.trace("Local: http://" + _gthis.localIp + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 60, className : "server.Main", methodName : "new"}); + haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 61, className : "server.Main", methodName : "new"}); return; }); var dir = "" + this.rootDir + "/res"; - server_HttpServer.init(dir); + server_HttpServer.init(dir,"" + this.rootDir + "/user/res"); Lang.init("" + dir + "/langs"); js_node_Http.createServer(function(req,res) { server_HttpServer.serveFiles(req,res); @@ -631,7 +689,7 @@ server_Main.main = function() { server_Main.prototype = { getUserConfig: function() { var config = JSON.parse(js_node_Fs.readFileSync("" + this.rootDir + "/default-config.json",{ encoding : "utf8"})); - var customPath = "" + this.rootDir + "/config.json"; + var customPath = "" + this.rootDir + "/user/config.json"; if(!sys_FileSystem.exists(customPath)) { return config; } @@ -642,7 +700,7 @@ server_Main.prototype = { var field = _g1[_g]; ++_g; if(Reflect.field(config,field) == null) { - haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 68, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 79, className : "server.Main", methodName : "getUserConfig"}); } config[field] = Reflect.field(customConfig,field); } @@ -653,8 +711,8 @@ server_Main.prototype = { var ip = req.connection.remoteAddress; var id = this.freeIds.length > 0 ? this.freeIds.shift() : this.clients.length; var name = "Guest " + (id + 1); - haxe_Log.trace("" + name + " connected (" + ip + ")",{ fileName : "src/server/Main.hx", lineNumber : 78, className : "server.Main", methodName : "onConnect"}); - var client = new Client(ws,id,name,0); + haxe_Log.trace("" + name + " connected (" + ip + ")",{ fileName : "src/server/Main.hx", lineNumber : 89, className : "server.Main", methodName : "onConnect"}); + var client = new Client(ws,req,id,name,0); if(req.connection.localAddress == ip) { client.group |= 4; } @@ -671,7 +729,7 @@ server_Main.prototype = { var _g1 = 0; var _g2 = this.clients; while(_g1 < _g2.length) _g.push(_g2[_g1++].getData()); - this.send(client,{ type : "Connected", connected : { config : tmp, history : tmp1, isUnknownClient : true, clientName : client1, clients : _g, videoList : this.videoList}}); + this.send(client,{ type : "Connected", connected : { config : tmp, history : tmp1, isUnknownClient : true, clientName : client1, clients : _g, videoList : this.videoList, globalIp : this.globalIp}}); this.sendClientList(); ws.on("message",function(data) { var tmp2 = JSON.parse(data); @@ -679,8 +737,8 @@ server_Main.prototype = { return; }); ws.on("close",function(err) { - haxe_Log.trace("Client " + client.name + " disconnected",{ fileName : "src/server/Main.hx", lineNumber : 105, className : "server.Main", methodName : "onConnect"}); - _gthis.sortedPush(_gthis.freeIds,client.id); + haxe_Log.trace("Client " + client.name + " disconnected",{ fileName : "src/server/Main.hx", lineNumber : 117, className : "server.Main", methodName : "onConnect"}); + server_Utils.sortedPush(_gthis.freeIds,client.id); HxOverrides.remove(_gthis.clients,client); _gthis.sendClientList(); if((client.group & 2) != 0) { @@ -697,25 +755,18 @@ server_Main.prototype = { return; }); } - ,sortedPush: function(ids,id) { - var _g = 0; - var _g1 = ids.length; - while(_g < _g1) { - var i = _g++; - if(id < ids[i]) { - ids.splice(i,0,id); - return; - } - } - ids.push(id); - } ,onMessage: function(client,data) { switch(data.type) { case "AddVideo": + var item = data.addVideo.item; + var localOrigin = "" + this.localIp + ":" + this.port; + if(item.url.indexOf(localOrigin) != -1) { + item.url = StringTools.replace(item.url,localOrigin,"" + this.globalIp + ":" + this.port); + } if(data.addVideo.atEnd) { - this.videoList.push(data.addVideo.item); + this.videoList.push(item); } else { - this.videoList.splice(1,0,data.addVideo.item); + this.videoList.splice(1,0,item); } this.broadcast(data); if(this.videoList.length == 1) { @@ -808,8 +859,8 @@ server_Main.prototype = { if(this.videoList[0].url == url) { this.videoTimer.stop(); } - HxOverrides.remove(this.videoList,Lambda.find(this.videoList,function(item) { - return item.url == url; + HxOverrides.remove(this.videoList,Lambda.find(this.videoList,function(item1) { + return item1.url == url; })); this.broadcast(data); if(this.videoList.length > 0) { @@ -954,6 +1005,18 @@ server_Utils.getLocalIp = function() { } return "127.0.0.1"; }; +server_Utils.sortedPush = function(ids,id) { + var _g = 0; + var _g1 = ids.length; + while(_g < _g1) { + var i = _g++; + if(id < ids[i]) { + ids.splice(i,0,id); + return; + } + } + ids.push(id); +}; server_Utils.shuffle = function(arr) { var _g = 0; var _g1 = arr.length; @@ -1127,6 +1190,9 @@ server_HttpServer.mimeTypes = (function($this) { $r = _g; return $r; }(this)); +server_HttpServer.hasCustomRes = false; +server_HttpServer.allowedLocalFiles = new haxe_ds_StringMap(); server_HttpServer.matchLang = new EReg("^[A-z]+",""); +server_HttpServer.matchVarString = new EReg("\\${([A-z_]+)}","g"); server_Main.main(); })(typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this); diff --git a/build/client.js b/res/client.js index 4b9954b..d9cc67b 100644 --- a/build/client.js +++ b/res/client.js @@ -12,7 +12,7 @@ var ClientGroup = $hxEnums["ClientGroup"] = { __ename__ : true, __constructs__ : ,Leader: {_hx_index:1,__enum__:"ClientGroup",toString:$estr} ,Admin: {_hx_index:2,__enum__:"ClientGroup",toString:$estr} }; -var Client = function(ws,id,name,group) { +var Client = function(name,group) { this.name = name; var i = group; if(group == null) { @@ -22,7 +22,7 @@ var Client = function(ws,id,name,group) { }; Client.__name__ = true; Client.fromData = function(data) { - return new Client(null,null,data.name,data.group); + return new Client(data.name,data.group); }; Client.prototype = { setGroupFlag: function(type,flag) { @@ -465,8 +465,9 @@ var client_Main = function(host,port) { this.matchNumbers = new EReg("^-?[0-9]+$",""); this.onTimeGet = new haxe_Timer(2000); this.isConnected = false; - this.personal = new Client(null,null,"Unknown",0); + this.personal = new Client("Unknown",0); this.filters = []; + this.globalIp = ""; this.pageTitle = window.document.title; this.clients = []; var _gthis = this; @@ -477,8 +478,12 @@ var client_Main = function(host,port) { if(host == "") { host = "localhost"; } + this.host = host; this.initListeners(); this.onTimeGet.run = function() { + if(_gthis.player.isListEmpty()) { + return; + } _gthis.send({ type : "GetTime"}); return; }; @@ -570,8 +575,12 @@ client_Main.prototype = { } ,addVideo: function(url,atEnd,callback) { var _gthis = this; + var protocol = window.location.protocol; + if(StringTools.startsWith(url,"/")) { + url = "" + protocol + "//" + window.location.hostname + ":" + window.location.port + url; + } if(!StringTools.startsWith(url,"http")) { - url = "" + window.location.protocol + "//" + url; + url = "" + protocol + "//" + url; } var pos = url.lastIndexOf("/") + 1; var name = HxOverrides.substr(url,pos,null); @@ -597,6 +606,12 @@ client_Main.prototype = { while(_g1 < items.length) _g.push(items[_g1++].url); return _g; } + ,replaceLocalIp: function(url) { + if(this.host == this.globalIp) { + return url; + } + return StringTools.replace(url,this.globalIp,this.host); + } ,getRemoteVideoDuration: function(src,callback) { var player = window.document.querySelector("#ytapiplayer"); var video = window.document.createElement("video"); @@ -618,7 +633,7 @@ client_Main.prototype = { var data = JSON.parse(e.data); var t = data.type; var t1 = t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null); - haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 171, className : "client.Main", methodName : "onMessage", customParams : [data[t1]]}); + haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 188, className : "client.Main", methodName : "onMessage", customParams : [data[t1]]}); switch(data.type) { case "AddVideo": if(this.player.isListEmpty()) { @@ -668,7 +683,7 @@ client_Main.prototype = { break; case "Logout": this.updateClients(data.logout.clients); - this.personal = new Client(null,null,data.logout.clientName,0); + this.personal = new Client(data.logout.clientName,0); this.showGuestLoginPanel(); break; case "Message": @@ -730,6 +745,7 @@ client_Main.prototype = { } ,onConnected: function(data) { var connected = data.connected; + this.globalIp = connected.globalIp; this.setConfig(connected.config); if(connected.isUnknownClient) { this.updateClients(connected.clients); @@ -981,6 +997,7 @@ client_Player.prototype = { this.isLoaded = false; this.video = window.document.createElement("video"); this.video.id = "videoplayer"; + item.url = this.main.replaceLocalIp(item.url); this.video.src = item.url; this.video.controls = true; this.video.oncanplaythrough = function(e) { diff --git a/res/css/custom.css b/res/css/custom.css index e69de29..05b94fa 100644 --- a/res/css/custom.css +++ b/res/css/custom.css @@ -0,0 +1,4 @@ +/* + Create user/res/css/custom.css + in project folder to override this file. +*/ diff --git a/src/Client.hx b/src/Client.hx index 7aa14c5..4604ab7 100644 --- a/src/Client.hx +++ b/src/Client.hx @@ -1,9 +1,8 @@ package; #if nodejs +import js.node.http.IncomingMessage; import js.npm.ws.WebSocket; -#elseif js -import js.html.WebSocket; #end import haxe.EnumFlags; @@ -23,15 +22,21 @@ class Client { #if nodejs public final ws:WebSocket; public final id:Int; + public final req:IncomingMessage; #end public var name:String; public var group:EnumFlags<ClientGroup>; public var isLeader(get, set):Bool; public var isAdmin(get, set):Bool; - public function new(?ws:WebSocket, ?id:Int, name:String, group:Int) { + #if nodejs + public function new(?ws:WebSocket, ?req:IncomingMessage, ?id:Int, name:String, group:Int) { + #else + public function new(name:String, group:Int) { + #end #if nodejs this.ws = ws; + this.req = req; this.id = id; #end this.name = name; diff --git a/src/Types.hx b/src/Types.hx index f2b506f..5706111 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -46,7 +46,8 @@ typedef WsEvent = { clients:Array<ClientData>, isUnknownClient:Bool, clientName:String, - videoList:Array<VideoItem> + videoList:Array<VideoItem>, + globalIp:String }, ?login:{ clientName:String, diff --git a/src/client/Main.hx b/src/client/Main.hx index 96353f8..432a095 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -20,6 +20,8 @@ class Main { final clients:Array<Client> = []; var pageTitle = document.title; + final host:String; + var globalIp = ""; var config:Null<Config>; final filters:Array<{regex:EReg, replace:String}> = []; var personal = new Client("Unknown", 0); @@ -35,9 +37,13 @@ class Main { player = new Player(this); if (host == null) host = Browser.location.hostname; if (host == "") host = "localhost"; + this.host = host; initListeners(); - onTimeGet.run = () -> send({type: GetTime}); + onTimeGet.run = () -> { + if (player.isListEmpty()) return; + send({type: GetTime}); + } document.onvisibilitychange = () -> { if (!document.hidden && onBlinkTab != null) { document.title = getPageTitle(); @@ -117,7 +123,13 @@ class Main { } function addVideo(url:String, atEnd:Bool, callback:()->Void):Void { - if (!url.startsWith("http")) url = '${Browser.location.protocol}//$url'; + final protocol = Browser.location.protocol; + if (url.startsWith("/")) { + final host = Browser.location.hostname; + final port = Browser.location.port; + url = '$protocol//$host:$port$url'; + } + if (!url.startsWith("http")) url = '$protocol//$url'; var name = url.substr(url.lastIndexOf('/') + 1); final matchName = ~/^(.+)\./; if (matchName.match(name)) name = matchName.matched(1); @@ -149,6 +161,11 @@ class Main { ]; } + public function tryLocalIp(url:String):String { + if (host == globalIp) return url; + return url.replace(globalIp, host); + } + function getRemoteVideoDuration(src:String, callback:(duration:Float)->Void):Void { final player:Element = ge("#ytapiplayer"); final video = document.createVideoElement(); @@ -173,38 +190,49 @@ class Main { case Connected: onConnected(data); onTimeGet.run(); + case Login: onLogin(data.login.clients, data.login.clientName); + case LoginError: final text = Lang.get("usernameError") .replace("$MAX", '${config.maxLoginLength}'); serverMessage(4, text); + case Logout: updateClients(data.logout.clients); personal = new Client(data.logout.clientName, 0); showGuestLoginPanel(); + case UpdateClients: updateClients(data.updateClients.clients); personal = clients.getByName(personal.name, personal); + case Message: addMessage(data.message.clientName, data.message.text); + case AddVideo: if (player.isListEmpty()) player.setVideo(data.addVideo.item); player.addVideoItem(data.addVideo.item, data.addVideo.atEnd); + case VideoLoaded: player.setTime(0); player.play(); + case RemoveVideo: player.removeItem(data.removeVideo.url); if (player.isListEmpty()) player.pause(); + case Pause: if (isLeader()) return; player.pause(); player.setTime(data.pause.time); + case Play: if (isLeader()) return; player.setTime(data.play.time); player.play(); + case GetTime: final newTime = data.getTime.time; final time = player.getTime(); @@ -219,23 +247,29 @@ class Main { else player.pause(); if (Math.abs(time - newTime) < 2) return; player.setTime(newTime); + case SetTime: final newTime = data.setTime.time; final time = player.getTime(); if (Math.abs(time - newTime) < 2) return; player.setTime(newTime); + case Rewind: player.setTime(data.rewind.time); + case SetLeader: clients.setLeader(data.setLeader.clientName); updateUserList(); setLeaderButton(isLeader()); if (isLeader()) player.setTime(player.getTime(), false); + case ClearChat: ge("#messagebuffer").innerHTML = ""; + case ClearPlaylist: player.clearItems(); if (player.isListEmpty()) player.pause(); + case ShufflePlaylist: // server-only case UpdatePlaylist: player.setItems(data.updatePlaylist.videoList); @@ -244,6 +278,7 @@ class Main { function onConnected(data:WsEvent):Void { final connected = data.connected; + globalIp = connected.globalIp; setConfig(connected.config); if (connected.isUnknownClient) { updateClients(connected.clients); diff --git a/src/client/Player.hx b/src/client/Player.hx index 34ef716..ca3d78f 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -25,6 +25,7 @@ class Player { isLoaded = false; video = document.createVideoElement(); video.id = "videoplayer"; + item.url = main.tryLocalIp(item.url); video.src = item.url; video.controls = true; video.oncanplaythrough = e -> { diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx index b49301b..a5b752b 100644 --- a/src/server/HttpServer.hx +++ b/src/server/HttpServer.hx @@ -1,12 +1,11 @@ package server; +import sys.FileSystem; import js.node.Buffer; import haxe.io.Path; import js.node.Fs; -import sys.io.File; import js.node.http.IncomingMessage; import js.node.http.ServerResponse; -import js.Node.__dirname; import js.node.Path as JsPath; using StringTools; @@ -33,18 +32,31 @@ class HttpServer { ]; static var dir:String; + static var customDir:String; + static var hasCustomRes = false; + static var allowedLocalFiles:Map<String, Bool> = []; - public static function init(directory:String):Void { - dir = directory; + public static function init(dir:String, ?customDir:String):Void { + HttpServer.dir = dir; + if (customDir == null) return; + HttpServer.customDir = customDir; + hasCustomRes = FileSystem.exists(customDir); } public static function serveFiles(req:IncomingMessage, res:ServerResponse):Void { - var filePath = dir + req.url; - if (req.url == "/") filePath = '$dir/index.html'; + var url = req.url; + if (url == "/") url = "/index.html"; + var filePath = dir + url; final extension = Path.extension(filePath).toLowerCase(); final contentType = getMimeType(extension); + if (req.connection.remoteAddress == req.connection.localAddress + || allowedLocalFiles[url]) { + final isExists = serveLocalFile(res, url, extension, contentType); + if (isExists) return; + } + if (!isChildOf(dir, filePath)) { res.statusCode = 500; var rel = JsPath.relative(dir, filePath); @@ -52,21 +64,14 @@ class HttpServer { return; } - // load client code from build folder - if (filePath == '$dir/client.js') { - filePath = '$__dirname/client.js'; + if (hasCustomRes) { + final path = customDir + url; + if (Fs.existsSync(path)) filePath = path; } - Fs.readFile(filePath, function(err:Dynamic, data:Buffer) { + Fs.readFile(filePath, (err:Dynamic, data:Buffer) -> { if (err != null) { - if (err.code == "ENOENT") { - res.statusCode = 404; - var rel = JsPath.relative(dir, filePath); - res.end('File $rel not found.'); - } else { - res.statusCode = 500; - res.end('Error getting the file: $err.'); - } + readFileError(err, res, filePath); return; } res.setHeader("Content-Type", contentType); @@ -78,13 +83,40 @@ class HttpServer { }); } + static function readFileError(err:Dynamic, res:ServerResponse, filePath:String):Void { + if (err.code == "ENOENT") { + res.statusCode = 404; + var rel = JsPath.relative(dir, filePath); + res.end('File $rel not found.'); + } else { + res.statusCode = 500; + res.end('Error getting the file: $err.'); + } + } + + static function serveLocalFile(res:ServerResponse, filePath:String, ext:String, contentType:String):Bool { + if (ext != "mp4" && ext != "mp3" && ext != "wav") return false; + if (!Fs.existsSync(filePath)) return false; + allowedLocalFiles[filePath] = true; + Fs.readFile(filePath, (err:Dynamic, data:Buffer) -> { + if (err != null) { + readFileError(err, res, filePath); + return; + } + res.setHeader("Content-Type", contentType); + res.end(data); + }); + return true; + } + static final matchLang = ~/^[A-z]+/; + static final matchVarString = ~/\${([A-z_]+)}/g; static function localizeHtml(data:String, lang:String):String { if (lang != null && matchLang.match(lang)) { lang = matchLang.matched(0); } else lang = "en"; - data = ~/\${([A-z_]+)}/g.map(data, (regExp) -> { + data = matchVarString.map(data, (regExp) -> { final key = regExp.matched(1); return Lang.get(lang, key); }); @@ -92,14 +124,13 @@ class HttpServer { } static function isChildOf(parent:String, child:String):Bool { - final path = JsPath; - final relative = path.relative(parent, child); - return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative); + final rel = JsPath.relative(parent, child); + return rel.length > 0 && !rel.startsWith('..') && !JsPath.isAbsolute(rel); } static function getMimeType(ext:String):String { - var contentType = mimeTypes[ext]; - if (contentType == null) contentType = "application/octet-stream"; + final contentType = mimeTypes[ext]; + if (contentType == null) return "application/octet-stream"; return contentType; } diff --git a/src/server/Main.hx b/src/server/Main.hx index e2b9b18..442abc8 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -10,8 +10,10 @@ import js.Node.process; import js.Node.__dirname; import js.npm.ws.Server as WSServer; import js.npm.ws.WebSocket; +import js.node.http.IncomingMessage; import js.node.Http; import Types; +using StringTools; using ClientTools; using Lambda; @@ -19,6 +21,9 @@ class Main { final rootDir = '$__dirname/..'; final wss:WSServer; + final localIp:String; + var globalIp:String; + final port:Int; final config:Config; final clients:Array<Client> = []; final freeIds:Array<Int> = []; @@ -33,25 +38,31 @@ class Main { wss = new WSServer({port: wsPort}); wss.on("connection", onConnect); function exit() { + // TODO save state process.exit(); } process.on("exit", exit); process.on("SIGINT", exit); // ctrl+c + process.on("SIGUSR1", exit); // kill pid + process.on("SIGUSR2", exit); process.on("uncaughtException", (log) -> { trace(log); }); process.on("unhandledRejection", (reason, promise) -> { trace("Unhandled Rejection at:", reason); }); + localIp = Utils.getLocalIp(); + globalIp = localIp; + this.port = port; Utils.getGlobalIp(ip -> { - final local = Utils.getLocalIp(); - trace('Local: http://$local:$port'); - trace('Global: http://$ip:$port'); + globalIp = ip; + trace('Local: http://$localIp:$port'); + trace('Global: http://$globalIp:$port'); }); final dir = '$rootDir/res'; - HttpServer.init(dir); + HttpServer.init(dir, '$rootDir/user/res'); Lang.init('$dir/langs'); Http.createServer((req, res) -> { @@ -61,7 +72,7 @@ class Main { function getUserConfig():Config { final config:Config = Json.parse(File.getContent('$rootDir/default-config.json')); - final customPath = '$rootDir/config.json'; + final customPath = '$rootDir/user/config.json'; if (!FileSystem.exists(customPath)) return config; final customConfig:Config = Json.parse(File.getContent(customPath)); for (field in Reflect.fields(customConfig)) { @@ -71,13 +82,13 @@ class Main { return config; } - function onConnect(ws:WebSocket, req):Void { + function onConnect(ws:WebSocket, req:IncomingMessage):Void { final ip = req.connection.remoteAddress; final id = freeIds.length > 0 ? freeIds.shift() : clients.length; final name = 'Guest ${id + 1}'; trace('$name connected ($ip)'); final isAdmin = req.connection.localAddress == ip; - final client = new Client(ws, id, name, 0); + final client = new Client(ws, req, id, name, 0); if (isAdmin) client.group.set(Admin); clients.push(client); if (clients.length == 1 && videoList.length > 0) @@ -93,7 +104,8 @@ class Main { clients: [ for (client in clients) client.getData() ], - videoList: videoList + videoList: videoList, + globalIp: globalIp } }); sendClientList(); @@ -103,7 +115,7 @@ class Main { }); ws.on("close", err -> { trace('Client ${client.name} disconnected'); - sortedPush(freeIds, client.id); + Utils.sortedPush(freeIds, client.id); clients.remove(client); sendClientList(); if (client.isLeader) { @@ -116,17 +128,6 @@ class Main { }); } - function sortedPush(ids:Array<Int>, id:Int):Void { - for (i in 0...ids.length) { - final n = ids[i]; - if (id < n) { - ids.insert(i, id); - return; - } - } - ids.push(id); - } - function onMessage(client:Client, data:WsEvent):Void { switch (data.type) { case Connected: @@ -149,6 +150,7 @@ class Main { } }); sendClientList(); + case LoginError: case Logout: final oldName = client.name; @@ -163,6 +165,7 @@ class Main { } }); sendClientList(); + case Message: var text = data.message.text; if (text.length == 0) return; @@ -175,15 +178,23 @@ class Main { messages.push({text: text, name: client.name, time: time}); if (messages.length > config.serverChatHistory) messages.shift(); broadcast(data); + case AddVideo: - if (data.addVideo.atEnd) videoList.push(data.addVideo.item); - else videoList.insert(1, data.addVideo.item); + final item = data.addVideo.item; + final localOrigin = '$localIp:$port'; + if (item.url.indexOf(localOrigin) != -1) { + item.url = item.url.replace(localOrigin, '$globalIp:$port'); + } + if (data.addVideo.atEnd) videoList.push(item); + else videoList.insert(1, item); broadcast(data); // Initial timer start if VideoLoaded is not happen if (videoList.length == 1) restartWaitTimer(); + case VideoLoaded: // Called if client loads next video and can play it prepareVideoPlayback(); + case RemoveVideo: if (videoList.length == 0) return; final url = data.removeVideo.url; @@ -193,16 +204,19 @@ class Main { ); broadcast(data); if (videoList.length > 0) restartWaitTimer(); + case Pause: if (videoList.length == 0) return; if (!client.isLeader) return; videoTimer.pause(); broadcast(data); + case Play: if (videoList.length == 0) return; if (!client.isLeader) return; videoTimer.play(); broadcast(data); + case GetTime: if (videoList.length == 0) return; if (videoTimer.getTime() > videoList[0].duration) { @@ -220,11 +234,13 @@ class Main { time: videoTimer.getTime(), paused: videoTimer.isPaused() }}); + case SetTime: if (videoList.length == 0) return; if (!client.isLeader) return; videoTimer.setTime(data.setTime.time); broadcastExcept(client, data); + case Rewind: if (videoList.length == 0) return; // TODO permission @@ -232,6 +248,7 @@ class Main { if (data.rewind.time < 0) data.rewind.time = 0; videoTimer.setTime(data.rewind.time); broadcast(data); + case SetLeader: clients.setLeader(data.setLeader.clientName); broadcast({ @@ -248,12 +265,15 @@ class Main { } }); } + case ClearChat: if (client.isAdmin) broadcast(data); + case ClearPlaylist: videoTimer.stop(); videoList.resize(0); broadcast(data); + case ShufflePlaylist: if (videoList.length == 0) return; final first = videoList.shift(); @@ -262,7 +282,7 @@ class Main { broadcast({type: UpdatePlaylist, updatePlaylist: { videoList: videoList }}); - case UpdatePlaylist: + case UpdatePlaylist: // client-only } } diff --git a/src/server/Utils.hx b/src/server/Utils.hx index 2ecbd42..22ddc77 100644 --- a/src/server/Utils.hx +++ b/src/server/Utils.hx @@ -28,6 +28,17 @@ class Utils { return "127.0.0.1"; } + public static function sortedPush(ids:Array<Int>, id:Int):Void { + for (i in 0...ids.length) { + final n = ids[i]; + if (id < n) { + ids.insert(i, id); + return; + } + } + ids.push(id); + } + public static function shuffle<T>(arr:Array<T>):Void { for (i in 0...arr.length) { final n = Std.random(arr.length); diff --git a/user/README.md b/user/README.md new file mode 100644 index 0000000..f59e576 --- /dev/null +++ b/user/README.md @@ -0,0 +1,18 @@ +## User-specific config +You can create `config.json` file in this folder to override `default-config.json` options. +All root config fields are optional to override, so you need to create only what you want to change. +File example: +```json +{ + "channelName": "-=SuperChannel=-", + "videoLimit": 10, +} +``` +## User-specific resources +You can patch any file you want in project `res/` by creating `user/res/sameName` files. +For example `user/res/index.html` or `user/res/css/custom.css`. +You can also add any new files. +## Other files here +- `state.json` - saved state of latest server session (messages, videos, video time). +- `logs/` - latest 10 logs. You can change count in config. +- `crashes/` - folder with latest error logs, when the server had to restart itself. |
