1 """User objects for the MUFF Engine"""
3 # Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
4 # Licensed per terms in the LICENSE file distributed with this software.
6 # user accounts are stored in ini-style files supported by ConfigParser
9 # test for existence of the account dir with os.listdir and os.mkdir to make it
12 # string.replace is used to perform substitutions for color codes and the like
15 # hack to load all modules in the muff package
17 for module in muff.__all__:
18 exec("import " + module)
21 """This is a connected user."""
24 """Default values for the in-memory user variables."""
32 # the current client ip address
35 # the previous client ip address
36 self.last_address = ""
38 # the current socket connection object
39 self.connection = None
41 # a flag to denote whether the user is authenticated
42 self.authenticated = False
44 # number of times password entry has failed during this session
45 self.password_tries = 1
47 # the current state of the user
48 self.state = "entering account name"
50 # flag to indicate whether a menu has been displayed
51 self.menu_seen = False
53 # current error condition, if any
56 # fifo-style queue for lines of user input
59 # fifo-style queue for blocks of user output
60 self.output_queue = []
62 # holding pen for unterminated user input
63 self.partial_input = ""
65 # flag to indicate the current echo status of the client
68 # an object containing persistent account data
69 self.record = ConfigParser.SafeConfigParser()
72 """Log, save, close the connection and remove."""
73 if self.name: message = "User " + self.name
74 else: message = "An unnamed user"
75 message += " logged out."
78 self.connection.close()
82 """Save, load a new user and relocate the connection."""
84 # unauthenticated connections get the boot
85 if not self.authenticated:
86 muffmisc.log("An unauthenticated user was disconnected during reload.")
87 self.state = "disconnecting"
92 # save and get out of the list
96 # create a new user object
97 new_user = muffuser.User()
99 # give it the same name
100 new_user.name = self.name
105 # set everything else equivalent
106 new_user.address = self.address
107 new_user.last_address = self.last_address
108 new_user.connection = self.connection
109 new_user.authenticated = self.authenticated
110 new_user.password_tries = self.password_tries
111 new_user.state = self.state
112 new_user.menu_seen = self.menu_seen
113 new_user.error = self.error
114 new_user.input_queue = self.input_queue
115 new_user.output_queue = self.output_queue
116 new_user.partial_input = self.partial_input
117 new_user.echoing = self.echoing
120 muffvars.userlist.append(new_user)
122 # get rid of the old user object
125 def replace_old_connections(self):
126 """Disconnect active users with the same name."""
128 # the default return value
131 # iterate over each user in the list
132 for old_user in muffvars.userlist:
134 # the name is the same but it's not us
135 if old_user.name == self.name and old_user is not self:
138 muffmisc.log("User " + self.name + " reconnected--closing old connection to " + old_user.address + ".")
139 old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)")
140 self.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
142 # close the old connection
143 old_user.connection.close()
145 # replace the old connection with this one
146 old_user.connection = self.connection
147 old_user.last_address = old_user.address
148 old_user.address = self.address
149 old_user.echoing = self.echoing
151 # take this one out of the list and delete
157 # true if an old connection was replaced, false if not
160 def authenticate(self):
161 """Flag the user as authenticated and disconnect duplicates."""
162 if not self.state is "authenticated":
163 muffmisc.log("User " + self.name + " logged in.")
164 self.authenticated = True
167 """Retrieve account data from cold storage."""
169 # what the filename for the user account should be
170 filename = muffconf.get("files", "accounts") + "/" + self.name
172 # try to load the password hash and last connection ipa
174 self.record.read(filename)
175 self.passhash = self.record.get("account", "passhash")
176 self.last_address = self.record.get("account", "last_address", self.address)
178 # if we can't, that's okay too
182 def get_passhash(self):
183 """Retrieve the user's account password hash from storage."""
185 # what the filename for the user account could be
186 filename = muffconf.get("files", "accounts") + "/" + self.proposed_name
188 # create a temporary account record object
189 temporary_record = ConfigParser.SafeConfigParser()
191 # try to load the indicated account and get a password hash
193 temporary_record.read(filename)
194 self.passhash = temporary_record.get("account", "passhash")
197 # otherwise, the password hash is empty
203 """Record account data to cold storage."""
205 # the user account must be authenticated to save
206 if self.authenticated:
208 # create an account section if it doesn't exist
209 if not self.record.has_section("account"):
210 self.record.add_section("account")
212 # write some in-memory data to the record
213 self.record.set("account", "name", self.name)
214 self.record.set("account", "passhash", self.passhash)
215 self.record.set("account", "last_address", self.address)
217 # the account files live here
218 account_path = muffconf.get("files", "accounts")
219 # the filename to which we'll write
220 filename = account_path + "/" + self.name.lower()
222 # if the directory doesn't exist, create it
224 if os.listdir(account_path): pass
226 os.mkdir(account_path)
228 # open the user account file for writing
229 record_file = file(filename, "w")
231 # dump the account data to it
232 self.record.write(record_file)
234 # close the user account file
238 # set the permissions to 0600
239 os.chmod(filename, 0600)
242 """Send the user their current menu."""
243 self.send(muffmenu.get_menu(self))
246 """Remove a user from the list of connected users."""
247 muffvars.userlist.remove(self)
249 def send(self, output, eol="$(eol)"):
250 """Send arbitrary text to a connected user."""
252 # only when there is actual output
255 # start with a newline, append the message, then end
256 # with the optional eol string passed to this function
257 # and the ansi escape to return to normal text
258 output = "\r\n" + output + eol + chr(27) + "[0m"
260 # find and replace macros in the output
261 output = muffmisc.replace_macros(self, output)
263 # wrap the text at 80 characters
264 # TODO: prompt user for preferred wrap width
265 output = muffmisc.wrap_ansi_text(output, 80)
267 # drop the formatted output into the output queue
268 self.output_queue.append(output)
270 # try to send the last item in the queue, remove it and
271 # flag that menu display is not needed
273 self.connection.send(self.output_queue[0])
274 self.output_queue.remove(self.output_queue[0])
275 self.menu_seen = False
277 # but if we can't, that's okay too
282 """All the things to do to the user per increment."""
284 # if the world is terminating, disconnect
285 if muffvars.terminate_world:
286 self.state = "disconnecting"
287 self.menu_seen = False
289 # show the user a menu as needed
292 # disconnect users with the appropriate state
293 if self.state == "disconnecting":
296 # the user is unique and not flagged to disconnect
299 # check for input and add it to the queue
302 # there is input waiting in the queue
303 if self.input_queue: muffcmds.handle_user_input(self)
305 def enqueue_input(self):
306 """Process and enqueue any new input."""
308 # check for some input
310 input_data = self.connection.recv(1024)
317 # tack this on to any previous partial
318 self.partial_input += input_data
320 # separate multiple input lines
321 new_input_lines = self.partial_input.split("\n")
323 # if input doesn't end in a newline, replace the
324 # held partial input with the last line of it
325 if not self.partial_input.endswith("\n"):
326 self.partial_input = new_input_lines.pop()
328 # otherwise, chop off the extra null input and reset
329 # the held partial input
331 new_input_lines.pop()
332 self.partial_input = ""
334 # iterate over the remaining lines
335 for line in new_input_lines:
337 # filter out non-printables
338 line = filter(lambda x: x>=' ' and x<='~', line)
340 # strip off extra whitespace
343 # put on the end of the queue
344 self.input_queue.append(line)