"""User objects for the MUFF Engine"""
-# Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# - Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# - Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following
-# disclaimer in the documentation and/or other materials provided
-# with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
+# Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
+# Licensed per terms in the LICENSE file distributed with this software.
+# user accounts are stored in ini-style files supported by ConfigParser
import ConfigParser
+
+# test for existence of the account dir with os.listdir and os.mkdir to make it
+import os
+
+# string.replace is used to perform substitutions for color codes and the like
import string
+# hack to load all modules in the muff package
import muff
for module in muff.__all__:
exec("import " + module)
class User:
+ """This is a connected user."""
+
def __init__(self):
+ """Default values for the in-memory user variables."""
+
+ # the account name
self.name = ""
+
+ # the password hash
self.passhash = ""
+
+ # the current client ip address
self.address = ""
+
+ # the previous client ip address
+ self.last_address = ""
+
+ # the current socket connection object
self.connection = None
+
+ # a flag to denote whether the user is authenticated
self.authenticated = False
+
+ # number of times password entry has failed during this session
self.password_tries = 1
+
+ # the current state of the user
self.state = "entering account name"
+
+ # flag to indicate whether a menu has been displayed
self.menu_seen = False
+
+ # current error condition, if any
self.error = ""
+
+ # fifo-style queue for lines of user input
self.input_queue = []
+
+ # fifo-style queue for blocks of user output
self.output_queue = []
+
+ # holding pen for unterminated user input
self.partial_input = ""
+
+ # flag to indicate the current echo status of the client
self.echoing = True
+
+ # an object containing persistent account data
self.record = ConfigParser.SafeConfigParser()
+
def load(self):
- filename = muffconf.config_data.get("general", "account_path") + "/" + self.name
+ """Retrieve account data from cold storage."""
+
+ # what the filename for the user account should be
+ filename = muffconf.config_data.get("files", "accounts") + "/" + self.name
+
+ # try to load the password hash and last connection ipa
try:
self.record.read(filename)
self.passhash = self.record.get("account", "passhash")
self.last_address = self.record.get("account", "last_address", self.address)
+
+ # if we can't, that's okay too
except:
pass
+
def get_passhash(self):
- filename = muffconf.config_data.get("general", "account_path") + "/" + self.proposed_name
+ """Retrieve the user's account password hash from storage."""
+
+ # what the filename for the user account could be
+ filename = muffconf.config_data.get("files", "accounts") + "/" + self.proposed_name
+
+ # create a temporary account record object
temporary_record = ConfigParser.SafeConfigParser()
+
+ # try to load the indicated account and get a password hash
try:
temporary_record.read(filename)
self.passhash = temporary_record.get("account", "passhash")
+
+ # otherwise, the password hash is empty
except:
self.passhash = ""
+
def save(self):
+ """Record account data to cold storage."""
+
+ # the user account must be authenticated to save
if self.authenticated:
- filename = muffconf.config_data.get("general", "account_path") + "/" + self.name.lower()
+
+ # create an account section if it doesn't exist
if not self.record.has_section("account"):
self.record.add_section("account")
+
+ # write some in-memory data to the record
self.record.set("account", "name", self.name)
self.record.set("account", "passhash", self.passhash)
self.record.set("account", "last_address", self.address)
+
+ # the account files live here
+ account_path = muffconf.config_data.get("files", "accounts")
+ # the filename to which we'll write
+ filename = account_path + "/" + self.name.lower()
+
+ # if the directory doesn't exist, create it
+ # TODO: create account_path with 0700 perms
+ try:
+ if os.listdir(account_path): pass
+ except:
+ os.mkdir(account_path, )
+
+ # open the user account file for writing
+ # TODO: create filename with 0600 perms
record_file = file(filename, "w")
+
+ # dump the account data to it
self.record.write(record_file)
+
+ # close the user account file
record_file.close()
+
def show_menu(self):
+ """Send the user their current menu."""
self.send(muffmenu.get_menu(self))
+
def remove(self):
+ """Remove a user from the list of connected users."""
muffvars.userlist.remove(self)
+
def send(self, output, eol="$(eol)"):
+ """Send arbitrary text to a connected user."""
+
+ # only when there is actual output
if output:
+
+ # start with a newline, append the message, then end
+ # with the optional eol string passed to this function
output = "$(eol)" + output + eol
+
+ # replace eol markers with a crlf
+ # TODO: search for markers and replace from a dict
output = string.replace(output, "$(eol)", "\r\n")
- output = string.replace(output, "$(div)", "\r\n\r\n")
+
+ # replace display markers with ansi escapse sequences
output = string.replace(output, "$(bld)", chr(27)+"[1m")
output = string.replace(output, "$(nrm)", chr(27)+"[0m")
output = string.replace(output, "$(blk)", chr(27)+"[30m")
output = string.replace(output, "$(grn)", chr(27)+"[32m")
output = string.replace(output, "$(red)", chr(27)+"[31m")
+
+ # the user's account name
output = string.replace(output, "$(account)", self.name)
+
+ # wrap the text at 80 characters
+ # TODO: prompt user for preferred wrap width
output = muffmisc.wrap_ansi_text(output, 80)
+
+ # drop the formatted output into the output queue
self.output_queue.append(output)
+
+ # try to send the last item in the queue, remove it and
+ # flag that menu display is not needed
try:
self.connection.send(self.output_queue[0])
self.output_queue.remove(self.output_queue[0])
self.menu_seen = False
+
+ # but if we can't, that's okay too
except:
pass