aboutsummaryrefslogtreecommitdiffstats
path: root/src/server
diff options
context:
space:
mode:
authorRblSb <msrblsb@gmail.com>2020-02-13 16:28:18 +0300
committerRblSb <msrblsb@gmail.com>2020-02-15 19:45:40 +0300
commit07d1955cefc093ffb12002902ed45e963030746e (patch)
tree8833eca2dc2ef07891aa8eb66daf7ad90f2ab0ce /src/server
Initial commit
Diffstat (limited to 'src/server')
-rw-r--r--src/server/HttpServer.hx106
-rw-r--r--src/server/Main.hx251
-rw-r--r--src/server/VideoTimer.hx54
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;
+ }
+
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage