diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/HttpServer.hx | 106 | ||||
| -rw-r--r-- | src/server/Main.hx | 251 | ||||
| -rw-r--r-- | src/server/VideoTimer.hx | 54 |
3 files changed, 411 insertions, 0 deletions
diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx new file mode 100644 index 0000000..b49301b --- /dev/null +++ b/src/server/HttpServer.hx @@ -0,0 +1,106 @@ +package server; + +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; + +class HttpServer { + + static final mimeTypes = [ + "html" => "text/html", + "js" => "text/javascript", + "css" => "text/css", + "json" => "application/json", + "png" => "image/png", + "jpg" => "image/jpg", + "gif" => "image/gif", + "svg" => "image/svg+xml", + "ico" => "image/x-icon", + "wav" => "audio/wav", + "mp3" => "audio/mpeg", + "mp4" => "video/mp4", + "woff" => "application/font-woff", + "ttf" => "application/font-ttf", + "eot" => "application/vnd.ms-fontobject", + "otf" => "application/font-otf", + "wasm" => "application/wasm" + ]; + + static var dir:String; + + public static function init(directory:String):Void { + dir = directory; + } + + public static function serveFiles(req:IncomingMessage, res:ServerResponse):Void { + var filePath = dir + req.url; + if (req.url == "/") filePath = '$dir/index.html'; + + final extension = Path.extension(filePath).toLowerCase(); + final contentType = getMimeType(extension); + + if (!isChildOf(dir, filePath)) { + res.statusCode = 500; + var rel = JsPath.relative(dir, filePath); + res.end('Error getting the file: No access to $rel.'); + return; + } + + // load client code from build folder + if (filePath == '$dir/client.js') { + filePath = '$__dirname/client.js'; + } + + Fs.readFile(filePath, function(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.'); + } + return; + } + res.setHeader("Content-Type", contentType); + if (extension == "html") { + // replace ${textId} to localized strings + data = cast localizeHtml(data.toString(), req.headers["accept-language"]); + } + res.end(data); + }); + } + + static final matchLang = ~/^[A-z]+/; + + 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) -> { + final key = regExp.matched(1); + return Lang.get(lang, key); + }); + return data; + } + + 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); + } + + static function getMimeType(ext:String):String { + var contentType = mimeTypes[ext]; + if (contentType == null) contentType = "application/octet-stream"; + return contentType; + } + +} diff --git a/src/server/Main.hx b/src/server/Main.hx new file mode 100644 index 0000000..8ec7e87 --- /dev/null +++ b/src/server/Main.hx @@ -0,0 +1,251 @@ +package server; + +import haxe.Timer; +import Client.ClientData; +import haxe.Json; +import js.Node.process; +import js.Node.__dirname; +import js.npm.ws.Server as WSServer; +import js.npm.ws.WebSocket; +import js.node.Http; +import js.node.Dns; +import Types; +using ClientTools; +using Lambda; + +class Main { + + final wss:WSServer; + final clients:Array<Client> = []; + final videoList:Array<VideoItem> = []; + final videoTimer = new VideoTimer(); + + static function main():Void new Main(); + + public function new(port = 4200, wsPort = 4201) { + wss = new WSServer({port: wsPort}); + wss.on("connection", onConnect); + function exit() { + process.exit(); + } + process.on("exit", exit); + process.on("SIGINT", exit); // ctrl+c + process.on("uncaughtException", (log) -> { + trace(log); + }); + process.on("unhandledRejection", (reason, promise) -> { + trace("Unhandled Rejection at:", reason); + }); + + getPublicIp(ip -> { + trace('Local: http://127.0.0.1:$port'); + trace('Global: http://$ip:$port'); + }); + + final dir = '$__dirname/../res'; + HttpServer.init(dir); + Lang.init('$dir/langs'); + + Http.createServer((req, res) -> { + HttpServer.serveFiles(req, res); + }).listen(port); + } + + function getPublicIp(callback:(ip:String)->Void):Void { + Dns.resolve("google.com", function(err, arr) { + if (err != null) { + callback("ERROR " + err.code); + return; + } + Http.get("http://myexternalip.com/raw", r -> { + r.setEncoding("utf8"); + r.on("data", callback); + }); + }); + } + + function onConnect(ws:WebSocket, req):Void { + final ip = req.connection.remoteAddress; + trace('Client connected ($ip)'); + final client = new Client(ws, "Unknown", false); + clients.push(client); + + send(client, { + type: Connected, + connected: { + isUnknownClient: true, + clientName: client.name, + clients: [ + for (client in clients) client.getData() + ], + videoList: videoList + } + }); + sendClientList(); + + ws.on("message", data -> { + onMessage(client, Json.parse(data)); + }); + ws.on("close", err -> { + trace('Client ${client.name} disconnected'); + clients.remove(client); + sendClientList(); + if (client.isLeader) { + if (videoTimer.isPaused()) videoTimer.play(); + } + }); + } + + function onMessage(client:Client, data:WsEvent):Void { + switch (data.type) { + case Connected: + case UpdateClients: + sendClientList(); + case Login: + final name = data.login.clientName; + if (name.length == 0 || name.length > 20 || clients.getByName(name) != null) { + send(client, {type: LoginError}); + return; + } + client.name = data.login.clientName; + send(client, { + type: data.type, + login: { + isUnknownClient: true, + clientName: client.name, + clients: clientList() + } + }); + sendClientList(); + case LoginError: + case Logout: + final oldName = client.name; + client.name = "Unknown"; + send(client, { + type: data.type, + logout: { + clientName: oldName, + clients: clientList() + } + }); + sendClientList(); + case Message: + // todo message log, max items + // todo message max length check + data.message.clientName = client.name; + broadcast(data); + case AddVideo: + videoList.push(data.addVideo.item); + broadcast(data); + if (videoList.length == 1) { + waitVideoStart = Timer.delay(startVideoPlayback, 3000); + } + case VideoLoaded: + prepareVideoPlayback(); + case RemoveVideo: + if (videoList.length == 0) return; + final url = data.removeVideo.url; + if (videoList[0].url == url) videoTimer.stop(); + videoList.remove( + videoList.find(item -> item.url == url) + ); + broadcast(data); + 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) { + videoTimer.stop(); + onMessage(client, { + type: RemoveVideo, + removeVideo: { + url: videoList[0].url + } + }); + return; + } + send(client, { + type: GetTime, getTime: { + 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 SetLeader: + clients.setLeader(data.setLeader.clientName); + sendClientList(); + if (videoList.length == 0) return; + if (!clients.hasLeader()) { + if (videoTimer.isPaused()) videoTimer.play(); + broadcast({ + type: Play, play: { + time: videoTimer.getTime() + } + }); + } + } + } + + function clientList():Array<ClientData> { + return [ + for (client in clients) client.getData() + ]; + } + + function sendClientList():Void { + broadcast({ + type: UpdateClients, + updateClients: { + clients: clientList() + } + }); + } + + function send(client:Client, data:WsEvent):Void { + client.ws.send(Json.stringify(data), null); + } + + function broadcast(data:WsEvent):Void { + final json = Json.stringify(data); + for (client in clients) client.ws.send(json, null); + } + + function broadcastExcept(skipped:Client, data:WsEvent):Void { + final json = Json.stringify(data); + for (client in clients) { + if (client == skipped) continue; + client.ws.send(json, null); + } + } + + var waitVideoStart:Timer; + var loadedClientsCount = 0; + + function prepareVideoPlayback():Void { + if (videoTimer.isStarted) return; + loadedClientsCount++; + if (loadedClientsCount == 1) { + waitVideoStart = Timer.delay(startVideoPlayback, 3000); + } + if (loadedClientsCount >= clients.length) startVideoPlayback(); + } + + function startVideoPlayback():Void { + if (waitVideoStart != null) waitVideoStart.stop(); + loadedClientsCount = 0; + broadcast({type: VideoLoaded}); + videoTimer.start(); + } + +} diff --git a/src/server/VideoTimer.hx b/src/server/VideoTimer.hx new file mode 100644 index 0000000..695ea5d --- /dev/null +++ b/src/server/VideoTimer.hx @@ -0,0 +1,54 @@ +package server; + +import haxe.Timer; + +class VideoTimer { + + public var isStarted(default, null) = false; + var startTime = 0.0; + var pauseStartTime = 0.0; + + public function new() {} + + public function start():Void { + isStarted = true; + startTime = Timer.stamp(); + pauseStartTime = 0; + } + + public function stop():Void { + isStarted = false; + startTime = 0.0; + pauseStartTime = 0.0; + } + + public function pause():Void { + pauseStartTime = Timer.stamp(); + } + + public function play():Void { + if (!isStarted) start(); + startTime += pauseTime(); + pauseStartTime = 0; + } + + public function getTime():Float { + if (startTime == 0) return 0; + return Timer.stamp() - startTime - pauseTime(); + } + + public function setTime(secs:Float):Void { + startTime = Timer.stamp() - secs; + if (isPaused()) pause(); + } + + public function isPaused():Bool { + return pauseStartTime != 0; + } + + function pauseTime():Float { + if (!isPaused()) return 0; + return Timer.stamp() - pauseStartTime; + } + +} |
