From 1d40f688e5bda258c72e3279b04f2d22a93b9f88 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 27 Jan 2012 23:53:03 +0100 Subject: [PATCH] Added the @ban and @unban commands to the default command sets, allowing an admin to filter access from users without outright deleting their accounts. The ban list is stored in the database, but the checking is not hard-coded in the server/portal, but done in the normal login command(s), meaning it can be customized easily. Also contrib/menu_login has been updated to check for banned connections. --- contrib/menu_login.py | 17 ++- src/commands/default/admin.py | 168 ++++++++++++++++++++++++- src/commands/default/cmdset_default.py | 2 + src/commands/default/unloggedin.py | 12 ++ src/server/session.py | 1 + src/utils/utils.py | 2 +- 6 files changed, 196 insertions(+), 6 deletions(-) diff --git a/contrib/menu_login.py b/contrib/menu_login.py index a52407a1bd..720faa6bf7 100644 --- a/contrib/menu_login.py +++ b/contrib/menu_login.py @@ -31,14 +31,11 @@ the initial splash screen. import re import traceback from django.conf import settings -from django.contrib.auth.models import User -from src.server import sessionhandler from src.players.models import PlayerDB -from src.objects.models import ObjectDB from src.server.models import ServerConfig from src.comms.models import Channel -from src.utils import create, logger, utils, ansi +from src.utils import create, logger, utils from src.commands.command import Command from src.commands.cmdset import CmdSet from src.commands.cmdhandler import CMD_LOGINSTART @@ -116,6 +113,18 @@ class CmdPasswordSelect(Command): self.menutree.goto("node1b") return + # before going on, check eventual bans + bans = ServerConfig.objects.conf("server_bans") + if bans and (any(tup[0]==player.name for tup in bans) + or + any(tup[2].match(player.sessions[0].address[0]) for tup in bans if tup[2])): + # this is a banned IP or name! + string = "{rYou have been banned and cannot continue from here." + string += "\nIf you feel this ban is in error, please email an admin.{x" + self.caller.msg(string) + self.caller.session_disconnect() + return + # we are ok, log us in. self.caller.msg("{gWelcome %s! Logging in ...{n" % player.key) self.caller.session_login(player) diff --git a/src/commands/default/admin.py b/src/commands/default/admin.py index dfb0900737..8ca4c62d23 100644 --- a/src/commands/default/admin.py +++ b/src/commands/default/admin.py @@ -4,10 +4,12 @@ Admin commands """ +import time, re from django.conf import settings from django.contrib.auth.models import User from src.players.models import PlayerDB from src.server.sessionhandler import SESSIONS +from src.server.models import ServerConfig from src.utils import utils from src.commands.default.muxcommand import MuxCommand @@ -93,6 +95,170 @@ class CmdBoot(MuxCommand): caller.msg("You booted %s." % name) +# regex matching IP addresses with wildcards, eg. 233.122.4.* +IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}") + +def list_bans(banlist): + """ + Helper function to display a list of active bans. Input argument + is the banlist read into the two commands @ban and @undban below. + """ + if not banlist: + return "No active bans were found." + + table = [["id"], ["name/ip"], ["date"], ["reason"]] + table[0].extend([str(i+1) for i in range(len(banlist))]) + for ban in banlist: + if ban[0]: + table[1].append(ban[0]) + else: + table[1].append(ban[1]) + table[2].extend([ban[3] for ban in banlist]) + table[3].extend([ban[4] for ban in banlist]) + ftable = utils.format_table(table, 4) + string = "{wActive bans:{x" + for irow, row in enumerate(ftable): + if irow == 0: + srow = "\n" + "".join(row) + srow = "{w%s{n" % srow.rstrip() + else: + srow = "\n" + "{w%s{n" % row[0] + "".join(row[1:]) + string += srow.rstrip() + return string + +class CmdBan(MuxCommand): + """ + ban a player from the server + + Usage: + @ban [ [: reason]] + + Without any arguments, shows numbered list of active bans. + + This command bans a user from accessing the game. Supply an + optional reason to be able to later remember why the ban was put in + place + + It is often to + prefer over deleting a player with @delplayer. If banned by name, + that player account can no longer be logged into. + + IP (Internet Protocol) address banning allows to block all access + from a specific address or subnet. Use the asterisk (*) as a + wildcard. + + Examples: + @ban thomas - ban account 'thomas' + @ban/ip 134.233.2.111 - ban specific ip address + @ban/ip 134.233.2.* - ban all in the 134.233.2 subnet + @ban/ip 134.233.*.* - ban all in the 134.233 subnet + + A single IP filter is easy to circumvent by changing the computer + (also, some ISPs assign only temporary IPs to their users in the + first placer. Widening the IP block filter with wildcards might be + tempting, but remember that blocking too much may accidentally + also block innocent users connecting from the same country and + region. + + """ + key = "@ban" + aliases = ["@bans"] + locks = "cmd:perm(ban) or perm(Immortals)" + help_category="Admin" + + def func(self): + """ + Bans are stored in a serverconf db object as a list of + dictionaries: + [ (name, ip, ipregex, date, reason), + (name, ip, ipregex, date, reason),... ] + where name and ip are set by the user and are shown in + lists. ipregex is a converted form of ip where the * is + replaced by an appropriate regex pattern for fast + matching. date is the time stamp the ban was instigated and + 'reason' is any optional info given to the command. Unset + values in each tuple is set to the empty string. + """ + banlist = ServerConfig.objects.conf('server_bans') + if not banlist: + banlist = [] + + if not self.args or (self.switches + and not any(switch in ('ip', 'name') for switch in self.switches)): + self.caller.msg(list_bans(banlist)) + return + + now = time.ctime() + reason = "" + if ':' in self.args: + ban, reason = self.args.rsplit(':',1) + else: + ban = self.args + ban = ban.lower() + ipban = IPREGEX.findall(ban) + if not ipban: + # store as name + typ = "Name" + bantup = (ban, "", "", now, reason) + else: + # an ip address. + typ = "IP" + ban = ipban[0] + # replace * with regex form and compile it + ipregex = ban.replace('.','\.') + ipregex = ipregex.replace('*', '[0-9]{1,3}') + print "regex:",ipregex + ipregex = re.compile(r"%s" % ipregex) + bantup = ("", ban, ipregex, now, reason) + # save updated banlist + banlist.append(bantup) + ServerConfig.objects.conf('server_bans', banlist) + self.caller.msg("%s-Ban {w%s{x was added." % (typ, ban)) + +class CmdUnban(MuxCommand): + """ + remove a ban + + Usage: + @unban + + This will clear a player name/ip ban previously set with the @ban + command. Use this command without an argument to view a numbered + list of bans. Use the numbers in this list to select which one to + unban. + + """ + key = "@unban" + locks = "cmd:perm(unban) or perm(Immortals)" + help_category="Admin" + + def func(self): + "Implement unbanning" + + banlist = ServerConfig.objects.conf('server_bans') + + if not self.args: + self.caller.msg(list_bans(banlist)) + return + + try: + num = int(self.args) + except Exception: + self.caller.msg("You must supply a valid ban id to clear.") + return + + if not banlist: + self.caller.msg("There are no bans to clear.") + elif not (0 < num < len(banlist) + 1): + self.caller.msg("Ban id {w%s{x was not found." % self.args) + else: + # all is ok, clear ban + ban = banlist[num-1] + del banlist[num-1] + ServerConfig.objects.conf('server_bans', banlist) + self.caller.msg("Cleared ban %s: %s" % (num, " ".join([s for s in ban[:2]]))) + + class CmdDelPlayer(MuxCommand): """ delplayer - delete player from server @@ -102,7 +268,7 @@ class CmdDelPlayer(MuxCommand): Switch: delobj - also delete the player's currently - assigned in-game object. + assigned in-game object. Completely deletes a user from the server database, making their nick and e-mail again available. diff --git a/src/commands/default/cmdset_default.py b/src/commands/default/cmdset_default.py index 39390d59bf..8f0ef062be 100644 --- a/src/commands/default/cmdset_default.py +++ b/src/commands/default/cmdset_default.py @@ -47,6 +47,8 @@ class DefaultCmdSet(CmdSet): # Admin commands self.add(admin.CmdBoot()) + self.add(admin.CmdBan()) + self.add(admin.CmdUnban()) self.add(admin.CmdDelPlayer()) self.add(admin.CmdEmit()) self.add(admin.CmdNewPassword()) diff --git a/src/commands/default/unloggedin.py b/src/commands/default/unloggedin.py index c89bc63d22..32444f5f3f 100644 --- a/src/commands/default/unloggedin.py +++ b/src/commands/default/unloggedin.py @@ -62,6 +62,18 @@ class CmdConnect(MuxCommand): session.msg("Incorrect password.") return + # Check IP and/or name bans + bans = ServerConfig.objects.conf("server_bans") + if bans and (any(tup[0]==player.name for tup in bans) + or + any(tup[2].match(session.address[0]) for tup in bans if tup[2])): + # this is a banned IP or name! + string = "{rYou have been banned and cannot continue from here." + string += "\nIf you feel this ban is in error, please email an admin.{x" + session.msg(string) + session.execute_cmd("quit") + return + # actually do the login. This will call all hooks. session.session_login(player) diff --git a/src/server/session.py b/src/server/session.py index a4b71ebdbc..4fd6ab86cc 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -50,6 +50,7 @@ class Session(object): self.protocol_key = protocol_key # Protocol address tied to this session self.address = address + # suid is used by some protocols, it's a hex key. self.suid = None diff --git a/src/utils/utils.py b/src/utils/utils.py index e1f65da48c..9d360ddbea 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -193,7 +193,7 @@ def time_format(seconds, style=0): def datetime_format(dtobj): """ Takes a datetime object instance (e.g. from django's DateTimeField) - and returns a string. + and returns a string describing how long ago that date was. """ year, month, day = dtobj.year, dtobj.month, dtobj.day