X-Git-Url: https://mudpy.org/gitweb?p=mudpy.git;a=blobdiff_plain;f=mudpy%2Fmisc.py;h=074f53855494bfa952200c6c181236d630423e57;hp=50a94569fbad396bf2c84b577caeb0aba6c8f1e1;hb=8b0085791954dc96a909d49d3611c049a96b5c21;hpb=56f918a8f40771b5fd7db125d301b8e50e9a7a32 diff --git a/mudpy/misc.py b/mudpy/misc.py index 50a9456..074f538 100644 --- a/mudpy/misc.py +++ b/mudpy/misc.py @@ -1,6 +1,6 @@ """Miscellaneous functions for the mudpy engine.""" -# Copyright (c) 2004-2019 mudpy authors. Permission to use, copy, +# Copyright (c) 2004-2020 mudpy authors. Permission to use, copy, # modify, and distribute this software is granted under terms # provided in the LICENSE file distributed with this software. @@ -406,7 +406,7 @@ class Universe: """Create a new, empty Universe (the Big Bang).""" new_universe = Universe() for attribute in vars(self).keys(): - exec("new_universe." + attribute + " = self." + attribute) + setattr(new_universe, attribute, getattr(self, attribute)) new_universe.reload_flag = False del self return new_universe @@ -457,10 +457,8 @@ class Universe: self.listening_socket.listen(1) # note that we're now ready for user connections - log( - "Listening for Telnet connections on: " + - host + ":" + str(port) - ) + log("Listening for Telnet connections on %s port %s" % ( + host, str(port))) def get_time(self): """Convenience method to get the elapsed time counter.""" @@ -490,6 +488,7 @@ class User: self.address = "" self.authenticated = False self.avatar = None + self.choice = "" self.columns = 79 self.connection = None self.error = "" @@ -502,8 +501,10 @@ class User: self.output_queue = [] self.partial_input = b"" self.password_tries = 0 + self.rows = 23 self.state = "telopt_negotiation" self.telopts = {} + self.ttype = None self.universe = universe def quit(self): @@ -639,14 +640,13 @@ class User: """Flag the user as authenticated and disconnect duplicates.""" if self.state != "authenticated": self.authenticated = True + log("User %s authenticated for account %s." % ( + self, self.account.subkey), 2) if ("mudpy.limit" in universe.contents and self.account.subkey in universe.contents["mudpy.limit"].get("admins")): self.account.set("administrator", True) - log("Administrator %s authenticated." % - self.account.get("name"), 2) - else: - log("User %s authenticated for account %s." % ( - self, self.account.subkey), 2) + log("Account %s is an administrator." % ( + self.account.subkey), 2) def show_menu(self): """Send the user their current menu.""" @@ -807,6 +807,13 @@ class User: else: self.check_idle() + # ask the client for their current terminal type (RFC 1091); it's None + # if it's not been initialized, the empty string if it has but the + # output was indeterminate, "UNKNOWN" if the client specified it has no + # terminal types to supply + if self.ttype is None: + mudpy.telnet.request_ttype(self) + # if output is paused, decrement the counter if self.state == "telopt_negotiation": if self.negotiation_pause: @@ -852,7 +859,7 @@ class User: # check for some input try: raw_input = self.connection.recv(1024) - except (BlockingIOError, OSError): + except OSError: raw_input = b"" # we got something @@ -1220,7 +1227,9 @@ def weighted_choice(data): expanded.append(key) # return one at random - return random.choice(expanded) + # Whitelist the random.randrange() call in bandit since it's not used for + # security/cryptographic purposes + return random.choice(expanded) # nosec def random_name(): @@ -1267,7 +1276,9 @@ def random_name(): name = "" # create a name of random length from the syllables - for _syllable in range(random.randrange(2, 6)): + # Whitelist the random.randrange() call in bandit since it's not used for + # security/cryptographic purposes + for _syllable in range(random.randrange(2, 6)): # nosec name += weighted_choice(syllables) # strip any leading quotemark, capitalize and return the name @@ -1434,9 +1445,12 @@ def reload_data(): """Reload all relevant objects.""" universe.save() old_userlist = universe.userlist[:] + old_loglines = universe.loglines[:] for element in list(universe.contents.values()): element.destroy() universe.load() + new_loglines = universe.loglines[:] + universe.loglines = old_loglines + new_loglines for user in old_userlist: user.reload() @@ -1474,6 +1488,27 @@ def check_for_connection(listening_socket): return user +def find_command(command_name): + """Try to find a command by name or abbreviation.""" + + # lowercase the command + command_name = command_name.lower() + + command = None + if command_name in universe.groups["command"]: + # the command matches a command word for which we have data + command = universe.groups["command"][command_name] + else: + for candidate in sorted(universe.groups["command"]): + if candidate.startswith(command_name) and not universe.groups[ + "command"][candidate].get("administrative"): + # the command matches the start of a command word and is not + # restricted to administrators + command = universe.groups["command"][candidate] + break + return command + + def get_menu(state, error=None, choices=None): """Show the correct menu text to a user.""" @@ -1563,19 +1598,18 @@ def get_menu_prompt(state): def get_menu_choices(user): """Return a dict of choice:meaning.""" - menu = universe.groups["menu"][user.state] - create_choices = menu.get("create") + state = universe.groups["menu"][user.state] + create_choices = state.get("create") if create_choices: - choices = eval(create_choices) + choices = call_hook_function(create_choices, (user,)) else: choices = {} ignores = [] options = {} creates = {} - for facet in menu.facets(): - if facet.startswith("demand_") and not eval( - universe.groups["menu"][user.state].get(facet) - ): + for facet in state.facets(): + if facet.startswith("demand_") and not call_hook_function( + universe.groups["menu"][user.state].get(facet), (user,)): ignores.append(facet.split("_", 2)[1]) elif facet.startswith("create_"): creates[facet] = facet.split("_", 2)[1] @@ -1583,10 +1617,11 @@ def get_menu_choices(user): options[facet] = facet.split("_", 2)[1] for facet in creates.keys(): if not creates[facet] in ignores: - choices[creates[facet]] = eval(menu.get(facet)) + choices[creates[facet]] = call_hook_function( + state.get(facet), (user,)) for facet in options.keys(): if not options[facet] in ignores: - choices[options[facet]] = menu.get(facet) + choices[options[facet]] = state.get(facet) return choices @@ -1620,12 +1655,12 @@ def get_default_branch(state): return universe.groups["menu"][state].get("branch") -def get_choice_branch(user, choice): +def get_choice_branch(user): """Returns the new state matching the given choice.""" branches = get_menu_branches(user.state) - if choice in branches.keys(): - return branches[choice] - elif choice in user.menu_choices.keys(): + if user.choice in branches.keys(): + return branches[user.choice] + elif user.choice in user.menu_choices.keys(): return get_default_branch(user.state) else: return "" @@ -1647,17 +1682,39 @@ def get_default_action(state): return universe.groups["menu"][state].get("action") -def get_choice_action(user, choice): +def get_choice_action(user): """Run any indicated script for the given choice.""" actions = get_menu_actions(user.state) - if choice in actions.keys(): - return actions[choice] - elif choice in user.menu_choices.keys(): + if user.choice in actions.keys(): + return actions[user.choice] + elif user.choice in user.menu_choices.keys(): return get_default_action(user.state) else: return "" +def call_hook_function(fname, arglist): + """Safely execute named function with supplied arguments, return result.""" + + # all functions relative to mudpy package + function = mudpy + + for component in fname.split("."): + try: + function = getattr(function, component) + except AttributeError: + log('Could not find mudpy.%s() for arguments "%s"' + % (fname, arglist), 7) + function = None + break + if function: + try: + return function(*arglist) + except Exception: + log('Calling mudpy.%s(%s) raised an exception...\n%s' + % (fname, (*arglist,), traceback.format_exc()), 7) + + def handle_user_input(user): """The main handler, branches to a state-specific handler.""" @@ -1667,9 +1724,9 @@ def handle_user_input(user): user.send("", add_prompt=False, prepend_padding=False) # check to make sure the state is expected, then call that handler - if "handler_" + user.state in globals(): - exec("handler_" + user.state + "(user)") - else: + try: + globals()["handler_" + user.state](user) + except KeyError: generic_menu_handler(user) # since we got input, flag that the menu/prompt needs to be redisplayed @@ -1684,16 +1741,18 @@ def generic_menu_handler(user): # get a lower-case representation of the next line of input if user.input_queue: - choice = user.input_queue.pop(0) - if choice: - choice = choice.lower() + user.choice = user.input_queue.pop(0) + if user.choice: + user.choice = user.choice.lower() else: - choice = "" - if not choice: - choice = get_default_menu_choice(user.state) - if choice in user.menu_choices: - exec(get_choice_action(user, choice)) - new_state = get_choice_branch(user, choice) + user.choice = "" + if not user.choice: + user.choice = get_default_menu_choice(user.state) + if user.choice in user.menu_choices: + action = get_choice_action(user) + if action: + call_hook_function(action, (user,)) + new_state = get_choice_branch(user) if new_state: user.state = new_state else: @@ -1864,21 +1923,18 @@ def handler_active(user): else: command_name, parameters = first_word(input_data) - # lowercase the command - command_name = command_name.lower() - - # the command matches a command word for which we have data - if command_name in universe.groups["command"]: - command = universe.groups["command"][command_name] - else: - command = None + # expand to an actual command + command = find_command(command_name) # if it's allowed, do it + result = None if actor.can_run(command): - exec(command.get("action")) + action_fname = command.get("action", command.key) + if action_fname: + result = call_hook_function(action_fname, (actor, parameters)) - # otherwise, give an error - elif command_name: + # if the command was not run, give an error + if not result: mudpy.command.error(actor, input_data) # if no input, just idle back with a prompt @@ -2044,8 +2100,9 @@ def setup(): log("Import path: %s" % ", ".join(sys.path), 1) log("Installed dependencies: %s" % universe.versions.dependencies_text, 1) log("Other python packages: %s" % universe.versions.environment_text, 1) - log("Started %s with command line: %s" % ( - universe.versions.version, " ".join(sys.argv)), 1) + log("Running version: %s" % universe.versions.version, 1) + log("Initial directory: %s" % universe.startdir, 1) + log("Command line: %s" % " ".join(sys.argv), 1) # pass the initialized universe back return universe