aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorRblSb <msrblsb@gmail.com>2021-08-10 03:22:27 +0300
committerRblSb <msrblsb@gmail.com>2021-08-10 07:56:57 +0300
commitf972d4b7b1c6a69ab20cd2aee5a43df702796075 (patch)
tree608f8031ef7e0885143eaf8670275171112c4e9a /src
parent96e10fe71d6428eed4bb2f120bc4b3a2801ff4be (diff)
Ban/unban commands
/ban Name 3d10h20m59s /ban Name 2h30m /unban Name /removeBan Name see #26
Diffstat (limited to 'src')
-rw-r--r--src/Client.hx12
-rw-r--r--src/ClientTools.hx1
-rw-r--r--src/Types.hx13
-rw-r--r--src/client/Buttons.hx1
-rw-r--r--src/client/Main.hx85
-rw-r--r--src/client/Utils.hx8
-rw-r--r--src/server/ConsoleInput.hx23
-rw-r--r--src/server/HttpServer.hx3
-rw-r--r--src/server/Main.hx120
9 files changed, 231 insertions, 35 deletions
diff --git a/src/Client.hx b/src/Client.hx
index 254e294..ae35ef4 100644
--- a/src/Client.hx
+++ b/src/Client.hx
@@ -7,6 +7,7 @@ import js.npm.ws.WebSocket;
import haxe.EnumFlags;
enum ClientGroup {
+ Banned;
User;
Leader;
Admin;
@@ -20,12 +21,13 @@ typedef ClientData = {
class Client {
#if nodejs
public final ws:WebSocket;
- public final id:Int;
public final req:IncomingMessage;
+ public final id:Int;
public var isAlive = true;
#end
public var name:String;
public var group:EnumFlags<ClientGroup>;
+ public var isBanned(get, set):Bool;
public var isUser(get, set):Bool;
public var isLeader(get, set):Bool;
public var isAdmin(get, set):Bool;
@@ -45,6 +47,14 @@ class Client {
}
#end
+ inline function get_isBanned():Bool {
+ return group.has(Banned);
+ }
+
+ inline function set_isBanned(flag:Bool):Bool {
+ return setGroupFlag(Banned, flag);
+ }
+
inline function get_isUser():Bool {
return group.has(User);
}
diff --git a/src/ClientTools.hx b/src/ClientTools.hx
index b4ac9cb..c5053f1 100644
--- a/src/ClientTools.hx
+++ b/src/ClientTools.hx
@@ -29,6 +29,7 @@ class ClientTools {
public static function hasPermission(client:Client, permission:Permission, permissions:Permissions):Bool {
final p = permissions;
+ if (client.isBanned) return p.banned.contains(permission);
if (client.isAdmin) return p.admin.contains(permission);
if (client.isLeader) return p.leader.contains(permission);
if (client.isUser) return p.user.contains(permission);
diff --git a/src/Types.hx b/src/Types.hx
index 519ac76..c2136b4 100644
--- a/src/Types.hx
+++ b/src/Types.hx
@@ -36,6 +36,7 @@ typedef Config = {
}
typedef Permissions = {
+ banned:Array<Permission>,
guest:Array<Permission>,
user:Array<Permission>,
leader:Array<Permission>,
@@ -56,10 +57,12 @@ enum abstract Permission(String) {
var SetLeaderPerm = "setLeader";
var ChangeOrderPerm = "changeOrder";
var LockPlaylistPerm = "lockPlaylist";
+ var BanClientPerm = "banClient";
}
typedef UserList = {
admins:Array<UserField>,
+ bans:Array<BanField>,
?salt:String
}
@@ -68,6 +71,11 @@ typedef UserField = {
hash:String
}
+typedef BanField = {
+ ip:String,
+ toDate:Date
+}
+
typedef Emote = {
name:String,
image:String
@@ -130,6 +138,10 @@ typedef WsEvent = {
?updateClients:{
clients:Array<ClientData>,
},
+ ?banClient:{
+ name:String,
+ time:Float
+ },
?addVideo:{
item:VideoItem,
atEnd:Bool
@@ -192,6 +204,7 @@ enum abstract WsEventType(String) {
var UpdateClients;
// var AddClient;
// var RemoveClient;
+ var BanClient;
var AddVideo;
var RemoveVideo;
var SkipVideo;
diff --git a/src/client/Buttons.hx b/src/client/Buttons.hx
index 86a7d82..aba629e 100644
--- a/src/client/Buttons.hx
+++ b/src/client/Buttons.hx
@@ -351,6 +351,7 @@ class Buttons {
if (Utils.isTouch()) main.scrollChatToEnd();
}
new InputWithHistory(chatline, 50, value -> {
+ if (main.handleCommands(value)) return true;
main.send({
type: Message,
message: {
diff --git a/src/client/Main.hx b/src/client/Main.hx
index c5dca67..57a2d0f 100644
--- a/src/client/Main.hx
+++ b/src/client/Main.hx
@@ -382,8 +382,8 @@ class Main {
case Connected:
onConnected(data);
onTimeGet.run();
- case Disconnected: // server-only
+ case Disconnected: // server-only
case Login:
onLogin(data.login.clients, data.login.clientName);
@@ -411,6 +411,7 @@ class Main {
personal = clients.getByName(personal.name, personal);
if (personal.group.toInt() != oldGroup) onUserGroupChanged();
+ case BanClient: // server-only
case Message:
addMessage(data.message.clientName, data.message.text);
@@ -728,7 +729,8 @@ class Main {
for (client in clients) {
list.add('<div class="userlist_item">');
if (client.isLeader) list.add('<ion-icon name="play"></ion-icon>');
- final klass = client.isAdmin ? "userlist_owner" : "";
+ var klass = client.isBanned ? "userlist_banned" : "";
+ if (client.isAdmin) klass += " userlist_owner";
list.add('<span class="$klass">${client.name}</span></div>');
}
final userlist = ge("#userlist");
@@ -764,12 +766,8 @@ class Main {
textDiv.className = "text";
text = text.htmlEscape();
- if (text.startsWith("/")) {
- if (name == personal.name) handleCommands(text.substr(1));
- } else {
- for (filter in filters) {
- text = filter.regex.replace(text, filter.replace);
- }
+ for (filter in filters) {
+ text = filter.regex.replace(text, filter.replace);
}
textDiv.innerHTML = text;
final isInChatEnd = msgBuf.scrollTop + msgBuf.clientHeight >= msgBuf.scrollHeight - 1;
@@ -828,21 +826,80 @@ class Main {
msgBuf.scrollTop = msgBuf.scrollHeight;
}
- final matchNumbers = ~/^-?[0-9]+$/;
+ /* Returns `true` if text should not be sent to chat */
+ public function handleCommands(command:String):Bool {
+ if (!command.startsWith("/")) return false;
+ final args = command.trim().split(" ");
+ command = args.shift().substr(1);
- function handleCommands(text:String):Void {
- switch (text) {
+ switch (command) {
+ case "ban":
+ final name = args[0];
+ final time = parseSimpleDate(args[1]);
+ if (time < 0) return true;
+ send({
+ type: BanClient,
+ banClient: {
+ name: name,
+ time: time
+ }
+ });
+ return true;
+ case "unban", "removeBan":
+ final name = args[0];
+ send({
+ type: BanClient,
+ banClient: {
+ name: name,
+ time: 0
+ }
+ });
+ return true;
case "clear":
- if (isAdmin()) send({type: ClearChat});
+ send({type: ClearChat});
+ return true;
}
- if (matchNumbers.match(text)) {
+ if (matchSimpleDate.match(command)) {
send({
type: Rewind,
rewind: {
- time: Std.parseInt(text)
+ time: parseSimpleDate(command)
}
});
+ return false;
+ }
+ return false;
+ }
+
+ final matchSimpleDate = ~/^-?([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s?)?$/;
+
+ function parseSimpleDate(text:Null<String>):Int {
+ if (text == null) return 0;
+ if (!matchSimpleDate.match(text)) return 0;
+ final matches:Array<String> = [];
+ final length = Utils.matchedNum(matchSimpleDate);
+ for (i in 1...length) {
+ final group = matchSimpleDate.matched(i);
+ if (group == null) continue;
+ matches.push(group);
+ }
+ var seconds = 0;
+ for (block in matches) {
+ seconds += parseSimpleDateBlock(block);
+ }
+ if (text.startsWith("-")) seconds = -seconds;
+ return seconds;
+ }
+
+ function parseSimpleDateBlock(block:String):Int {
+ inline function time():Int {
+ return Std.parseInt(block.substr(0, block.length - 1));
}
+ if (block.endsWith("s")) return time();
+ else if (block.endsWith("m")) return time() * 60;
+ else if (block.endsWith("h")) return time() * 60 * 60;
+ else if (block.endsWith("d")) return time() * 60 * 60 * 24;
+ return Std.parseInt(block);
}
public function blinkTabWithTitle(title:String):Void {
diff --git a/src/client/Utils.hx b/src/client/Utils.hx
index 232ca02..f23365e 100644
--- a/src/client/Utils.hx
+++ b/src/client/Utils.hx
@@ -89,6 +89,14 @@ class Utils {
}
}
+ public static function matchedNum(ereg:EReg):Int {
+ #if js
+ return (ereg : Dynamic).r.m.length;
+ #else
+ #error "not implemented"
+ #end
+ }
+
public static function browseFileUrl(
onFileLoad:(url:String, name:String) -> Void,
isBinary = true,
diff --git a/src/server/ConsoleInput.hx b/src/server/ConsoleInput.hx
index 9b8faf3..119d87e 100644
--- a/src/server/ConsoleInput.hx
+++ b/src/server/ConsoleInput.hx
@@ -18,6 +18,7 @@ private typedef CommandData = {
private enum abstract Command(String) from String {
var AddAdmin = "addAdmin";
+ var RemoveAdmin = "removeAdmin";
var Replay = "replay";
var LogList = "logList";
var Exit = "exit";
@@ -30,6 +31,10 @@ class ConsoleInput {
args: ["name", "password"],
desc: "Adds channel admin"
},
+ RemoveAdmin => {
+ args: ["name"],
+ desc: "Removes channel admin"
+ },
Replay => {
args: ["name"],
desc: "Replay log file on server from user/logs/"
@@ -54,10 +59,10 @@ class ConsoleInput {
output: process.stdout,
completer: onCompletion
});
- haxe.Log.trace = (msg, ?pos) -> {
+ haxe.Log.trace = (msg:Dynamic, ?infos:haxe.PosInfos) -> {
Readline.clearLine(process.stdout, 0);
Readline.cursorTo(process.stdout, 0, null);
- Console.log(msg);
+ Console.log(formatOutput(msg, infos));
rl.prompt(true);
};
rl.prompt();
@@ -68,6 +73,16 @@ class ConsoleInput {
// rl.on("close", exit);
}
+ function formatOutput(v:Dynamic, infos:haxe.PosInfos):String {
+ var str = Std.string(v);
+ if (infos == null) return str;
+ if (infos.customParams != null) {
+ for (v in infos.customParams)
+ str += ", " + Std.string(v);
+ }
+ return str;
+ }
+
function onCompletion(line:String):Array<Or<Array<String>, String>> {
final commands:Array<String> = [
for (item in commands.keys()) '/$item '
@@ -102,6 +117,10 @@ class ConsoleInput {
}
main.addAdmin(name, password);
+ case RemoveAdmin:
+ final name = args[0];
+ main.removeAdmin(name);
+
case Replay:
Utils.ensureDir(main.logsDir);
final name = args[0];
diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx
index 44e4f36..2076336 100644
--- a/src/server/HttpServer.hx
+++ b/src/server/HttpServer.hx
@@ -58,8 +58,7 @@ class HttpServer {
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("Content-Type", getMimeType(ext));
- if (allowLocalRequests
- && req.connection.remoteAddress == req.connection.localAddress
+ if (allowLocalRequests && req.socket.remoteAddress == req.socket.localAddress
|| allowedLocalFiles[url]) {
if (isMediaExtension(ext)) {
allowedLocalFiles[url] = true;
diff --git a/src/server/Main.hx b/src/server/Main.hx
index ea1e40b..2f3a8a4 100644
--- a/src/server/Main.hx
+++ b/src/server/Main.hx
@@ -196,16 +196,32 @@ class Main {
function loadUsers():UserList {
final customPath = '$rootDir/user/users.json';
if (!FileSystem.exists(customPath)) return {
- admins: []
+ admins: [],
+ bans: []
};
- return Json.parse(File.getContent(customPath));
+ final users:UserList = Json.parse(File.getContent(customPath));
+ if (users.admins == null) users.admins = [];
+ if (users.bans == null) users.bans = [];
+ for (field in users.bans) {
+ field.toDate = Date.fromString(cast field.toDate);
+ }
+ return users;
}
function writeUsers(users:UserList):Void {
final folder = '$rootDir/user';
Utils.ensureDir(folder);
- final data = Json.stringify(users, "\t");
- File.saveContent('$folder/users.json', data);
+ final data:UserList = {
+ admins: users.admins,
+ bans: [
+ for (field in users.bans) {
+ ip: field.ip,
+ toDate: cast field.toDate.toString()
+ }
+ ],
+ salt: users.salt
+ }
+ File.saveContent('$folder/users.json', Json.stringify(data, "\t"));
}
function saveState():Void {
@@ -222,6 +238,7 @@ class Main {
}
final json = Json.stringify(data, "\t");
File.saveContent(statePath, json);
+ writeUsers(userList);
}
function loadState():Void {
@@ -251,10 +268,12 @@ class Main {
File.saveContent('$crashesFolder/$name.json', Json.stringify(data, "\t"));
}
+ var isHeroku = false;
+
function initIntergationHandlers():Void {
+ isHeroku = process.env["_"] != null && process.env["_"].contains("heroku");
// Prevent heroku idle when clients online (needs APP_URL env var)
- if (process.env["_"] != null && process.env["_"].contains("heroku")
- && process.env["APP_URL"] != null) {
+ if (isHeroku && process.env["APP_URL"] != null) {
var url = process.env["APP_URL"];
if (!url.startsWith("http")) url = 'http://$url';
new Timer(10 * 60 * 1000).run = function() {
@@ -265,18 +284,34 @@ class Main {
}
}
+ function clientIp(req:IncomingMessage):String {
+ // Heroku uses internal proxy, so header cannot be spoofed
+ if (isHeroku) {
+ var forwarded:String = req.headers["x-forwarded-for"];
+ forwarded = forwarded.split(",")[0].trim();
+ if (forwarded == null || forwarded.length == 0) return req.socket.remoteAddress;
+ return forwarded;
+ }
+ return req.socket.remoteAddress;
+ }
+
public function addAdmin(name:String, password:String):Void {
password += config.salt;
final hash = Sha256.encode(password);
- if (userList.admins == null) userList.admins = [];
userList.admins.push({
name: name,
hash: hash
});
- writeUsers(userList);
trace('Admin $name added.');
}
+ public function removeAdmin(name:String):Void {
+ userList.admins.remove(
+ userList.admins.find(item -> item.name == name)
+ );
+ trace('Admin $name removed.');
+ }
+
public function replayLog(events:Array<ServerEvent>):Void {
final timer = new Timer(1000);
timer.run = () -> {
@@ -309,11 +344,11 @@ class Main {
}
function onConnect(ws:WebSocket, req:IncomingMessage):Void {
- final ip = req.connection.remoteAddress;
+ final ip = clientIp(req);
final id = freeIds.length > 0 ? freeIds.shift() : clients.length;
final name = 'Guest ${id + 1}';
trace('$name connected ($ip)');
- final isAdmin = config.localAdmins && req.connection.localAddress == ip;
+ final isAdmin = config.localAdmins && req.socket.localAddress == ip;
final client = new Client(ws, req, id, name, 0);
client.isAdmin = isAdmin;
clients.push(client);
@@ -366,6 +401,7 @@ class Main {
if (videoTimer.isPaused()) videoTimer.play();
}
+ checkBan(client);
send(client, {
type: Connected,
connected: {
@@ -407,6 +443,34 @@ class Main {
case UpdateClients:
sendClientList();
+
+ case BanClient:
+ if (!checkPermission(client, BanClientPerm)) return;
+ final name = data.banClient.name;
+ final bannedClient = clients.getByName(name);
+ if (bannedClient == null) return;
+ if (client.name == name || bannedClient.isAdmin) {
+ serverMessage(client, "adminsCannotBeBannedError");
+ return;
+ }
+ final ip = clientIp(bannedClient.req);
+ userList.bans.remove(userList.bans.find(item -> item.ip == ip));
+ if (data.banClient.time == 0) {
+ bannedClient.isBanned = false;
+ sendClientList();
+ return;
+ }
+ final currentTime = Date.now().getTime();
+ final time = currentTime + data.banClient.time * 1000;
+ if (time < currentTime) return;
+ userList.bans.push({
+ ip: ip,
+ toDate: Date.fromTime(time)
+ });
+ checkBan(bannedClient);
+ serverMessage(client, '${bannedClient.name} ($ip) has been banned.');
+ sendClientList();
+
case Login:
final name = data.login.clientName.trim();
final lcName = name.toLowerCase();
@@ -434,6 +498,7 @@ class Main {
}
client.name = name;
client.isUser = true;
+ checkBan(client);
send(client, {
type: data.type,
login: {
@@ -750,16 +815,39 @@ class Main {
}
function checkPermission(client:Client, perm:Permission):Bool {
+ if (client.isBanned) checkBan(client);
final state = client.hasPermission(perm, config.permissions);
- if (!state) send(client, {
- type: ServerMessage,
- serverMessage: {
- textId: "accessError"
- }
- });
+ if (!state) {
+ send(client, {
+ type: ServerMessage,
+ serverMessage: {
+ textId: "accessError"
+ }
+ });
+ }
return state;
}
+ function checkBan(client:Client):Void {
+ if (client.isAdmin) {
+ client.isBanned = false;
+ return;
+ }
+ final ip = clientIp(client.req);
+ final currentTime = Date.now().getTime();
+ for (ban in userList.bans) {
+ if (ban.ip != ip) continue;
+ final isOutdated = ban.toDate.getTime() < currentTime;
+ client.isBanned = !isOutdated;
+ if (isOutdated) {
+ userList.bans.remove(ban);
+ trace('${client.name} ban removed');
+ sendClientList();
+ }
+ break;
+ }
+ }
+
final matchHtmlChars = ~/[&^<>'"]/;
final matchGuestName = ~/guest [0-9]+/;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage