aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/Buttons.hx6
-rw-r--r--src/client/FileUploader.hx6
-rw-r--r--src/client/Main.hx20
-rw-r--r--src/client/Player.hx4
-rw-r--r--src/server/HttpServer.hx21
-rw-r--r--src/server/Main.hx113
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;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage