diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/Buttons.hx | 6 | ||||
| -rw-r--r-- | src/client/FileUploader.hx | 6 | ||||
| -rw-r--r-- | src/client/Main.hx | 20 | ||||
| -rw-r--r-- | src/client/Player.hx | 4 | ||||
| -rw-r--r-- | src/server/HttpServer.hx | 21 | ||||
| -rw-r--r-- | src/server/Main.hx | 113 |
6 files changed, 162 insertions, 8 deletions
diff --git a/src/client/Buttons.hx b/src/client/Buttons.hx index 56f28d5..f3a2474 100644 --- a/src/client/Buttons.hx +++ b/src/client/Buttons.hx @@ -228,7 +228,7 @@ class Buttons { getEl("#voiceoverblock").style.display = (url.length > 0 && isSingle) ? "" : "none"; final isExternal = main.isExternalVideoUrl(url); - final showCache = isSingle && isExternal + final showCache = main.isAdmin() && isSingle && isExternal && main.playersCacheSupport.contains(playerType); checkboxCache.parentElement.style.display = showCache ? "" : "none"; checkboxCache.checked = settings.checkedCache.contains(playerType); @@ -258,6 +258,10 @@ class Buttons { } getEl("#mediaurl-upload").onclick = e -> { + if (!main.isAdmin()) { + main.serverMessage("Only admins may upload files.", true, false); + return; + } Utils.browseJsFile(file -> { final uploader = new FileUploader(main); uploader.uploadFile(file); diff --git a/src/client/FileUploader.hx b/src/client/FileUploader.hx index 60cc1d2..888dda6 100644 --- a/src/client/FileUploader.hx +++ b/src/client/FileUploader.hx @@ -18,6 +18,10 @@ class FileUploader { } public function uploadFile(file:File):Void { + if (!main.isAdmin()) { + main.serverMessage("Only admins may upload files.", true, false); + return; + } var name = ~/[?#%\/\\]/g.replace(file.name, "").trim(); if (name.length == 0) name = "video"; name = (window : Dynamic).encodeURIComponent(name); @@ -39,6 +43,7 @@ class FileUploader { final request = new XMLHttpRequest(); request.open("POST", "/upload", true); request.setRequestHeader("content-name", name); + request.setRequestHeader("x-client-uuid", main.settings.uuid ?? ""); request.upload.onprogress = (event:ProgressEvent) -> { var ratio = 0.0; @@ -81,6 +86,7 @@ class FileUploader { method: "POST", headers: { "content-name": name, + "x-client-uuid": main.settings.uuid ?? "", }, body: lastChunk, }); diff --git a/src/client/Main.hx b/src/client/Main.hx index 42aec3e..bea6b6b 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -356,7 +356,8 @@ class Main { final checkboxTemp:InputElement = getEl("#addfromurl .add-temp"); final isTemp = checkboxTemp.checked; final checkboxCache:InputElement = getEl("#cache-on-server"); - final doCache = checkboxCache.checked + final doCache = isAdmin() + && checkboxCache.checked && checkboxCache.parentElement.style.display != "none"; final url = mediaUrl.value; final subs = subsUrl.value; @@ -905,6 +906,13 @@ class Main { final adminMenu = getEl("#adminMenu"); if (isAdmin()) adminMenu.style.display = ""; else adminMenu.style.display = "none"; + + final uploadBtn = getEl("#mediaurl-upload"); + uploadBtn.style.display = isAdmin() ? "" : "none"; + + final checkboxCache:InputElement = getEl("#cache-on-server"); + checkboxCache.checked = isAdmin() && checkboxCache.checked; + checkboxCache.parentElement.style.display = isAdmin() ? "" : "none"; } public function guestLogin(name:String):Void { @@ -1377,6 +1385,16 @@ class Main { case "clear": send({type: ClearChat}); return true; + case "giveup": + mergeRedundantArgs(args, 0, 1); + final name = args[0] ?? ""; + send({ + type: SetLeader, + setLeader: { + clientName: name + } + }); + return true; case "flashback", "fb": send({type: Flashback}); return false; diff --git a/src/client/Player.hx b/src/client/Player.hx index 10c4351..68b1a4a 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -415,11 +415,13 @@ class Player { public function addVideoItem(item:VideoItem, atEnd:Bool):Void { final url = item.url.htmlEscape(true); final duration = item.playerType == IframeType ? "" : duration(item.duration); + final author = item.author.htmlEscape(); final itemEl = Utils.nodeFromString( - '<li class="queue_entry info" title="${Lang.get("addedBy")}: ${item.author}"> + '<li class="queue_entry info" title="${Lang.get("addedBy")}: $author"> <header> <span class="qe_time">$duration</span> <h4><a class="qe_title" href="$url" target="_blank">${item.title.htmlEscape()}</a></h4> + <small class="qe_author">${Lang.get("addedBy")}: $author</small> </header> <span class="controls"> <button class="qbtn-play" title="${Lang.get("play")}"><ion-icon name="play"></ion-icon></button> diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx index f0f565b..bcbf9aa 100644 --- a/src/server/HttpServer.hx +++ b/src/server/HttpServer.hx @@ -194,7 +194,21 @@ class HttpServer { }); } + function hasUploadAccess(req:IncomingMessage):Bool { + final uuid:Null<String> = cast req.headers["x-client-uuid"]; + if (uuid == null || uuid.length == 0) return false; + final client = main.clients.find(client -> client.uuid == uuid); + return client != null && client.isAdmin; + } + function uploadFileLastChunk(req:IncomingMessage, res:ServerResponse) { + if (!hasUploadAccess(req)) { + res.status(403).json({ + info: "Only admins may upload files.", + errorId: "accessError" + }); + return; + } var fileName = try decodeURIComponent(req.headers["content-name"]) catch (e) ""; if (fileName.trim().length == 0) fileName = null; final name = cache.getFreeFileName(fileName); @@ -213,6 +227,13 @@ class HttpServer { } function uploadFile(req:IncomingMessage, res:ServerResponse) { + if (!hasUploadAccess(req)) { + res.status(403).json({ + info: "Only admins may upload files.", + errorId: "accessError" + }); + return; + } var fileName = try decodeURIComponent(req.headers["content-name"]) catch (e) ""; if (fileName.trim().length == 0) fileName = null; final name = cache.getFreeFileName(fileName); diff --git a/src/server/Main.hx b/src/server/Main.hx index 8b9ade1..12a3bfa 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -82,6 +82,8 @@ class Main { **/ var emptyRoomCallbackTimer:Null<Timer>; var isServerPause = false; + var activeSkipVoteUrl:Null<String>; + final skipVotes:Map<Int, Bool> = []; static function main():Void { new Main({ @@ -426,6 +428,11 @@ class Main { return req.socket.remoteAddress; } + function isLoopbackIp(ip:Null<String>):Bool { + if (ip == null) return false; + return ip == "127.0.0.1" || ip == "::1" || ip == "::ffff:127.0.0.1"; + } + public function addAdmin(name:String, password:String):Void { password += config.salt; final hash = Sha256.encode(password); @@ -504,7 +511,8 @@ class Main { final id = freeIds.length > 0 ? freeIds.shift() : clients.length; final name = 'Guest ${id + 1}'; trace(Date.now().toString(), '$name connected ($ip)'); - final isAdmin = config.localAdmins && req.socket.localAddress == ip; + final remoteIp = req.socket.remoteAddress; + final isAdmin = config.localAdmins && isLoopbackIp(remoteIp); final client = new Client(ws, req, id, name, 0); client.uuid = uuid; client.isAdmin = isAdmin; @@ -760,6 +768,10 @@ class Main { // Initial timer start if VideoLoaded is not happen if (videoList.length == 1) restartWaitTimer(); } + if (item.doCache && !client.isAdmin) { + item.doCache = false; + serverMessage(client, "Only admins may cache videos."); + } if (!item.doCache) { addVideo(); } else { @@ -789,6 +801,7 @@ class Main { case RemoveVideo: if (videoList.length == 0) return; + if (activeSkipVoteUrl == data.removeVideo.url) resetSkipVote(); if (isPlaylistLockedFor(client)) return; if (!checkPermission(client, RemoveVideoPerm)) return; final url = data.removeVideo.url; @@ -805,7 +818,7 @@ class Main { case SkipVideo: if (!checkPermission(client, RemoveVideoPerm)) return; - skipVideo(data); + handleSkipVote(client, data); case Pause: if (videoList.length == 0) return; @@ -910,11 +923,26 @@ class Main { }); case SetLeader: - final clientName = data.setLeader.clientName; + var clientName = data.setLeader.clientName; if (client.name == clientName) { if (!checkPermission(client, RequestLeaderPerm)) return; - } else if (!client.isLeader && clientName != "") { - if (!checkPermission(client, SetLeaderPerm)) return; + final currentLeader = clients.find(c -> c.isLeader); + final hasOtherLeader = currentLeader != null && currentLeader != client; + if (!client.isAdmin && hasOtherLeader) { + serverMessage(client, + 'Leader request sent to ${currentLeader.name}. Waiting for /giveup ${client.name}.'); + serverMessage(currentLeader, + '${client.name} requested leader. Use /giveup ${client.name} to transfer leadership.'); + return; + } + } else if (clientName == "") { + if (!client.isLeader && !checkPermission(client, SetLeaderPerm)) return; + } else if (!client.isLeader && !checkPermission(client, SetLeaderPerm)) { + return; + } + if (clientName != "" && clients.getByName(clientName) == null) { + serverMessage(client, 'Client "$clientName" not found.'); + return; } isServerPause = false; clients.setLeader(clientName); @@ -938,6 +966,7 @@ class Main { case PlayItem: if (!checkPermission(client, ChangeOrderPerm)) return; + resetSkipVote(); final pos = data.playItem.pos; if (!videoList.hasItem(pos)) return; if (videoTimer.getTime() > FLASHBACK_DIST) { @@ -972,6 +1001,7 @@ class Main { case ClearPlaylist: if (isPlaylistLockedFor(client)) return; + resetSkipVote(); if (!checkPermission(client, RemoveVideoPerm)) return; if (videoList.length != 0) { if (videoTimer.getTime() > FLASHBACK_DIST) { @@ -1113,8 +1143,81 @@ class Main { return value; } + function resetSkipVote():Void { + activeSkipVoteUrl = null; + skipVotes.clear(); + } + + function getSkipVoteEligibleClients():Array<Client> { + return [ + for (c in clients) + if (!c.isBanned && c.hasPermission(RemoveVideoPerm, config.permissions)) c + ]; + } + + function getSkipVoteCount():Int { + var count = 0; + for (c in getSkipVoteEligibleClients()) { + if (skipVotes[c.id]) count++; + } + return count; + } + + function getSkipVoteRequiredCount():Int { + final total = getSkipVoteEligibleClients().length; + if (total <= 1) return 1; + return Std.int(Math.floor(total / 2)) + 1; + } + + function handleSkipVote(client:Client, data:WsEvent):Void { + if (videoList.length == 0) return; + final item = videoList.currentItem; + if (item.url != data.skipVideo.url) return; + + final isOwnItemAuthor = item.author == client.name; + if (client.isAdmin || isOwnItemAuthor) { + if (activeSkipVoteUrl != null) { + final reason = client.isAdmin ? "forced skip" : "skipped own queued video"; + broadcast({ + type: ServerMessage, + serverMessage: { + textId: '${client.name} ${reason}.' + } + }); + } + resetSkipVote(); + skipVideo(data); + return; + } + + if (activeSkipVoteUrl != item.url) { + activeSkipVoteUrl = item.url; + skipVotes.clear(); + } + if (skipVotes[client.id]) { + serverMessage(client, "You already voted to skip this video."); + return; + } + skipVotes[client.id] = true; + + final votes = getSkipVoteCount(); + final required = getSkipVoteRequiredCount(); + broadcast({ + type: ServerMessage, + serverMessage: { + textId: '${client.name} voted to skip (${votes}/${required}).' + } + }); + + if (votes >= required) { + resetSkipVote(); + skipVideo(data); + } + } + function skipVideo(data:WsEvent):Void { if (videoList.length == 0) return; + resetSkipVote(); final item = videoList.currentItem; if (item.url != data.skipVideo.url) return; final dur = videoList.currentItem.duration; |
