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):
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue