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:
parent
823d8362ac
commit
119a5a2b66
|
@ -58,8 +58,8 @@ class Client(Endpoint):
|
||||||
|
|
||||||
|
|
||||||
class Context(Node):
|
class Context(Node):
|
||||||
def __init__(self, host: str, port: int, password: str, location_check_points: int, hint_cost: int,
|
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||||
item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
|
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
|
||||||
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
|
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
|
||||||
super(Context, self).__init__()
|
super(Context, self).__init__()
|
||||||
self.compatibility: int = compatibility
|
self.compatibility: int = compatibility
|
||||||
|
@ -74,6 +74,7 @@ class Context(Node):
|
||||||
self.locations = {}
|
self.locations = {}
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.server_password = server_password
|
||||||
self.password = password
|
self.password = password
|
||||||
self.server = None
|
self.server = None
|
||||||
self.countdown_timer = 0
|
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):
|
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.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)
|
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):
|
async def countdown(ctx: Context, timer):
|
||||||
|
@ -571,6 +574,7 @@ def mark_raw(function):
|
||||||
|
|
||||||
class CommandProcessor(metaclass=CommandMeta):
|
class CommandProcessor(metaclass=CommandMeta):
|
||||||
commands: typing.Dict[str, typing.Callable]
|
commands: typing.Dict[str, typing.Callable]
|
||||||
|
client = None
|
||||||
marker = "/"
|
marker = "/"
|
||||||
|
|
||||||
def output(self, text: str):
|
def output(self, text: str):
|
||||||
|
@ -646,6 +650,7 @@ class CommonCommandProcessor(CommandProcessor):
|
||||||
|
|
||||||
simple_options = {"hint_cost": int,
|
simple_options = {"hint_cost": int,
|
||||||
"location_check_points": int,
|
"location_check_points": int,
|
||||||
|
"server_password": str,
|
||||||
"password": str,
|
"password": str,
|
||||||
"forfeit_mode": str,
|
"forfeit_mode": str,
|
||||||
"item_cheat": bool,
|
"item_cheat": bool,
|
||||||
|
@ -665,7 +670,10 @@ class CommonCommandProcessor(CommandProcessor):
|
||||||
"""List all current options. Warning: lists password."""
|
"""List all current options. Warning: lists password."""
|
||||||
self.output("Current options:")
|
self.output("Current options:")
|
||||||
for option in self.simple_options:
|
for option in self.simple_options:
|
||||||
self.output(f"Option {option} is set to {getattr(self.ctx, option)}")
|
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):
|
class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
marker = "!"
|
marker = "!"
|
||||||
|
@ -674,12 +682,62 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.client = client
|
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):
|
def output(self, text):
|
||||||
self.ctx.notify_client(self.client, text)
|
self.ctx.notify_client(self.client, text)
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
pass # default is client sending just text
|
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:
|
def _cmd_players(self) -> bool:
|
||||||
"""Get information about connected and missing players"""
|
"""Get information about connected and missing players"""
|
||||||
if len(self.ctx.player_names) < 10:
|
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']])
|
await ctx.send_msgs(client, [['InvalidArguments', 'Say']])
|
||||||
return
|
return
|
||||||
|
|
||||||
ctx.notify_all(ctx.get_aliased_name(client.team, client.slot) + ': ' + args)
|
|
||||||
client.messageprocessor(args)
|
client.messageprocessor(args)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1006,6 +1063,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
super(ServerCommandProcessor, self).__init__()
|
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):
|
def default(self, raw: str):
|
||||||
self.ctx.notify_all('[Server]: ' + raw)
|
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:
|
if client.auth and client.name.lower() == player_name.lower() and client.socket and not client.socket.closed:
|
||||||
asyncio.create_task(client.socket.close())
|
asyncio.create_task(client.socket.close())
|
||||||
self.output(f"Kicked {self.ctx.get_aliased_name(client.team, client.slot)}")
|
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
|
return True
|
||||||
|
|
||||||
self.output(f"Could not find player {player_name} to kick")
|
self.output(f"Could not find player {player_name} to kick")
|
||||||
|
@ -1162,10 +1226,12 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
if attrtype:
|
if attrtype:
|
||||||
if attrtype == bool:
|
if attrtype == bool:
|
||||||
def attrtype(input_text: str):
|
def attrtype(input_text: str):
|
||||||
if input_text.lower() in {"off", "0", "false", "none", "null", "no"}:
|
return input_text.lower() not in {"off", "0", "false", "none", "null", "no"}
|
||||||
return False
|
elif attrtype == str and option_name.endswith("password"):
|
||||||
else:
|
def attrtype(input_text: str):
|
||||||
return True
|
if input_text.lower() in {"null", "none", '""', "''"}:
|
||||||
|
return None
|
||||||
|
return input_text
|
||||||
setattr(self.ctx, option_name, attrtype(option))
|
setattr(self.ctx, option_name, attrtype(option))
|
||||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||||
return True
|
return True
|
||||||
|
@ -1192,6 +1258,7 @@ def parse_args() -> argparse.Namespace:
|
||||||
defaults = Utils.get_options()["server_options"]
|
defaults = Utils.get_options()["server_options"]
|
||||||
parser.add_argument('--host', default=defaults["host"])
|
parser.add_argument('--host', default=defaults["host"])
|
||||||
parser.add_argument('--port', default=defaults["port"], type=int)
|
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('--password', default=defaults["password"])
|
||||||
parser.add_argument('--multidata', default=defaults["multidata"])
|
parser.add_argument('--multidata', default=defaults["multidata"])
|
||||||
parser.add_argument('--savefile', default=defaults["savefile"])
|
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):
|
async def main(args: argparse.Namespace):
|
||||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
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,
|
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||||
not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode, args.auto_shutdown,
|
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
|
||||||
args.compatibility)
|
args.auto_shutdown, args.compatibility)
|
||||||
|
|
||||||
data_filename = args.multidata
|
data_filename = args.multidata
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ server_options:
|
||||||
savefile: null
|
savefile: null
|
||||||
disable_save: false
|
disable_save: false
|
||||||
loglevel: "info"
|
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
|
# Automatically forward the port that is used, then close that port after 24 hours
|
||||||
port_forward: false
|
port_forward: false
|
||||||
# Disallow !getitem. Old /getitem cannot be blocked this way
|
# Disallow !getitem. Old /getitem cannot be blocked this way
|
||||||
|
|
Loading…
Reference in New Issue