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