Allow client side login and remote issuing of server side commands.

Disabled by default. Requires a password to be set for it to be enabled.
This commit is contained in:
CaitSith2 2020-09-21 22:11:19 -07:00
parent 823d8362ac
commit 119a5a2b66
2 changed files with 80 additions and 11 deletions

View File

@ -58,8 +58,8 @@ class Client(Endpoint):
class Context(Node):
def __init__(self, host: str, port: int, password: str, location_check_points: int, hint_cost: int,
item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
super(Context, self).__init__()
self.compatibility: int = compatibility
@ -74,6 +74,7 @@ class Context(Node):
self.locations = {}
self.host = host
self.port = port
self.server_password = server_password
self.password = password
self.server = None
self.countdown_timer = 0
@ -376,6 +377,8 @@ async def on_client_joined(ctx: Context, client: Client):
async def on_client_left(ctx: Context, client: Client):
ctx.notify_all("%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1))
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
if ctx.commandprocessor.client == Client:
ctx.commandprocessor.client = None
async def countdown(ctx: Context, timer):
@ -571,6 +574,7 @@ def mark_raw(function):
class CommandProcessor(metaclass=CommandMeta):
commands: typing.Dict[str, typing.Callable]
client = None
marker = "/"
def output(self, text: str):
@ -646,6 +650,7 @@ class CommonCommandProcessor(CommandProcessor):
simple_options = {"hint_cost": int,
"location_check_points": int,
"server_password": str,
"password": str,
"forfeit_mode": str,
"item_cheat": bool,
@ -665,6 +670,9 @@ class CommonCommandProcessor(CommandProcessor):
"""List all current options. Warning: lists password."""
self.output("Current options:")
for option in self.simple_options:
if option == "server_password" and self.marker == "!": #Do not display the server password to the client.
self.output(f"Option server_password is set to {('*' * random.randint(4,16))}")
else:
self.output(f"Option {option} is set to {getattr(self.ctx, option)}")
class ClientMessageProcessor(CommonCommandProcessor):
@ -674,12 +682,62 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.ctx = ctx
self.client = client
def __call__(self, raw: str) -> typing.Optional[bool]:
if not raw.startswith("!admin"):
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + raw)
return super(ClientMessageProcessor, self).__call__(raw)
def output(self, text):
self.ctx.notify_client(self.client, text)
def default(self, raw: str):
pass # default is client sending just text
def is_authenticated(self):
return self.ctx.commandprocessor.client == self.client
@mark_raw
def _cmd_admin(self, command: str = ""):
"""Allow remote administration of the multiworld server"""
output = f"!admin {command}"
if output.lower().startswith("!admin login"): # disallow others from seeing the supplied password, whether or not it is correct.
output = f"!admin login {('*' * random.randint(4, 16))}"
elif output.lower().startswith("!admin /option server_password"): # disallow others from knowing what the new remote administration password is.
output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + output) # Otherwise notify the others what is happening.
if not self.ctx.server_password:
self.output("Sorry, Remote administration is disabled")
return False
if not command:
if self.is_authenticated():
self.output("Usage: !admin [Server command].\nUse !admin /help for help.\nUse !admin logout to log out of the current session.")
else:
self.output("Usage: !admin login [password]")
return True
if command.startswith("login "):
if command == f"login {self.ctx.server_password}":
self.output("Login successful. You can now issue server side commands.")
self.ctx.commandprocessor.client = self.client
return True
else:
self.output("Password incorrect.")
return False
if not self.is_authenticated():
self.output("You must first login using !admin login [password]")
return False
if command == "logout":
self.output("Logout successful. You can no longer issue server side commands.")
self.ctx.commandprocessor.client = None
return True
return self.ctx.commandprocessor(command)
def _cmd_players(self) -> bool:
"""Get information about connected and missing players"""
if len(self.ctx.player_names) < 10:
@ -997,7 +1055,6 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
await ctx.send_msgs(client, [['InvalidArguments', 'Say']])
return
ctx.notify_all(ctx.get_aliased_name(client.team, client.slot) + ': ' + args)
client.messageprocessor(args)
@ -1006,6 +1063,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.ctx = ctx
super(ServerCommandProcessor, self).__init__()
def output(self, text: str):
if self.client:
self.ctx.notify_client(self.client, text)
super(ServerCommandProcessor, self).output(text)
def default(self, raw: str):
self.ctx.notify_all('[Server]: ' + raw)
@ -1016,6 +1078,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
if client.auth and client.name.lower() == player_name.lower() and client.socket and not client.socket.closed:
asyncio.create_task(client.socket.close())
self.output(f"Kicked {self.ctx.get_aliased_name(client.team, client.slot)}")
if self.ctx.commandprocessor.client == client:
self.ctx.commandprocessor.client = None
return True
self.output(f"Could not find player {player_name} to kick")
@ -1162,10 +1226,12 @@ class ServerCommandProcessor(CommonCommandProcessor):
if attrtype:
if attrtype == bool:
def attrtype(input_text: str):
if input_text.lower() in {"off", "0", "false", "none", "null", "no"}:
return False
else:
return True
return input_text.lower() not in {"off", "0", "false", "none", "null", "no"}
elif attrtype == str and option_name.endswith("password"):
def attrtype(input_text: str):
if input_text.lower() in {"null", "none", '""', "''"}:
return None
return input_text
setattr(self.ctx, option_name, attrtype(option))
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
return True
@ -1192,6 +1258,7 @@ def parse_args() -> argparse.Namespace:
defaults = Utils.get_options()["server_options"]
parser.add_argument('--host', default=defaults["host"])
parser.add_argument('--port', default=defaults["port"], type=int)
parser.add_argument('--server_password', default=defaults["server_password"])
parser.add_argument('--password', default=defaults["password"])
parser.add_argument('--multidata', default=defaults["multidata"])
parser.add_argument('--savefile', default=defaults["savefile"])
@ -1261,9 +1328,9 @@ async def auto_shutdown(ctx, to_cancel=None):
async def main(args: argparse.Namespace):
logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
ctx = Context(args.host, args.port, args.password, args.location_check_points, args.hint_cost,
not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode, args.auto_shutdown,
args.compatibility)
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
args.auto_shutdown, args.compatibility)
data_filename = args.multidata

View File

@ -20,6 +20,8 @@ server_options:
savefile: null
disable_save: false
loglevel: "info"
# Allows for clients to log on and manage the server. If this is null, no remote administration is possible.
server_password: null
# Automatically forward the port that is used, then close that port after 24 hours
port_forward: false
# Disallow !getitem. Old /getitem cannot be blocked this way