#!/usr/bin/env python
#
# $Id: wimd_server.py,v 1.4 2001/11/03 11:05:22 doughellmann Exp $
#
# Copyright 2001 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""Server and connection objects for WIMD protocol.
"""
__rcs_info__ = {
#
# Creation Information
#
'module_name' : '$RCSfile: wimd_server.py,v $',
'rcs_id' : '$Id: wimd_server.py,v 1.4 2001/11/03 11:05:22 doughellmann Exp $',
'creator' : 'Doug Hellmann <doug@hellfly.net>',
'project' : 'PmwContribD',
'created' : 'Sun, 01-Apr-2001 13:29:10 EDT',
#
# Current Information
#
'author' : '$Author: doughellmann $',
'version' : '$Revision: 1.4 $',
'date' : '$Date: 2001/11/03 11:05:22 $',
}
#
# Import system modules
#
import asyncore, asynchat
import socket
import string
import crypt
#
# Import Local modules
#
from wimd_defaults import *
from wimd_pwfile import wimd_pwfile
#
# Module
#
class WimdServerError(StandardError):
"""
All server related exceptions will be WimdServerError
objects.
"""
pass
class wimd_channel(asynchat.async_chat):
"""
This handler class processes WIMD protocol messages
comming over a socket.
"""
EOLN=str(chr(0x03))
VALID_DISPOSITIONS=( 'ONLINE',
'AWAY',
'NA',
'OCCUPIED',
'DND',
'PRIVACY',
)
def __init__(self, server, conn, addr):
self.server = server
self.conn = conn
self.addr = addr
self.disposition = 'ONLINE'
self.accumulator_buffer = ''
self.user = 'Not logged in'
asynchat.async_chat.__init__(self, conn)
#
# Set the terminator character for transmissions
#
self.set_terminator(self.EOLN)
#
# Start the login process
#
self.login_start()
return
def __repr__(self):
return '%s user:%s>' % (asynchat.async_chat.__repr__(self)[:-1],
self.user)
def log(self, message):
#asynchat.async_chat.log(self, '%s: %s' % (self.user, message))
return
#
# Handle the login sequence
#
def login_start(self):
self.send('USER>')
self.next_state_handler = self.login_receive_user
return
def login_receive_user(self, user):
#self.log('User: %s' % user)
self.send(self.EOLN)
try:
pwentry = self.server.user_info[user]
except KeyError:
# no such user
self.err_invalidUser()
self.next_state_handler = None
else:
self.user = user
self.send('PASS>')
self.next_state_handler = self.login_receive_pass
return
def login_receive_pass(self, password):
#self.log('Password: %s' % password)
self.send(self.EOLN)
self.password = password
#
# Verify that password is correct
#
if not self.validate_password(password):
self.err_invalidPassword()
self.close()
return
#
# Accept the connection for this user
#
try:
self.server.add_user_connection(self.user, self)
except WimdServerError:
# Duplicate!
self.log('Got duplicate login for %s' % self.user)
self.err_invalidLogin()
self.close()
return
#
# Get user info so it is easier to
# access
#
user_info = self.server.user_info[self.user]
self.nickname = user_info[0]
self.group = user_info[3]
self.unknown = user_info[4]
self.email = user_info[5]
self.next_state_handler = None
return
#
# Close the connection
#
def close(self):
# parent class close
asynchat.async_chat.close(self)
# tell the server to drop this connection
self.server.drop_user_connection(self.user)
return
def del_channel(self):
self.log('closing channel')
self.server.drop_user_connection(self.user)
asynchat.async_chat.del_channel(self)
return
#
# Error handlers
#
def err_invalidUser(self):
self.sendmsg('-ERR Invalid User')
return
def err_invalidPassword(self):
self.sendmsg('-ERR Invalid Password')
return
def err_invalidLogin(self):
self.sendmsg('-ERR Invalid Login')
return
def err_unknownCommand(self):
self.sendmsg('-ERR Unknown Command')
return
#
# Protocol Handlers for WIMD protocol messages
#
def ph_QUIT(self, command):
self.log('QUIT (%s)' % command)
self.server.wimd_UPDT('USER', self.server.user_info.get_moniker(self.user),
self.user, 'OFFLINE')
self.close()
return
def ph_STAT(self, command):
self.log('STAT (%s)' % command)
msg = '+STAT '
for uid, conn in self.server.user_connections.items():
nick = self.server.user_info.get_moniker(uid)
disp = conn.disposition
msg = msg + '%s:%s:%s,' % (nick, uid, disp)
msg = string.rstrip(msg)
self.sendmsg(msg)
return
def ph_MESG(self, command):
self.log('MESG (%s)' % command)
command = command[5:]
uid_end = string.find(command, ' ')
uid = command[:uid_end]
msg = command[uid_end+1:]
self.log('MESG uid="%s" msg="%s"' % (uid, msg))
if uid == '0':
# Broadcast message, always succeeds
self.sendmsg('+MESG')
else:
# User-to-user message, requires valid user
try:
nick = self.server.user_info.get_moniker(uid)
except KeyError:
self.sendmsg('-MESG Unknown user.')
return
else:
self.sendmsg('+MESG')
self.server.wimd_MESG(self.user, self.nickname, uid, msg)
return
def ph_INFO(self, command):
self.log('INFO (%s)' % command)
uid = string.strip(command[5:])
try:
nick, ignore, fileuid, group, unknown, email = \
self.server.user_info[uid]
disp = 'ONLINE'
except KeyError:
self.err_invalidUser()
else:
self.sendmsg('+INFO %s %s %s %s %s' \
% (uid, nick, email, group, disp))
return
def ph_MAIL(self, command):
self.log('MAIL (%s)' % command)
email = string.strip(command[5:])
self.log('\tatttempting to change the email of %s to "%s"' \
% (self.user, email))
try:
self.server.user_info.set_email(self.user, email)
self.server.user_info.store()
except KeyError:
self.err_invalidUser()
else:
self.sendmsg('+MAIL')
return
def ph_NICK(self, command):
self.log('NICK (%s)' % command)
nick = string.strip(command[5:])
self.log('\tattempting to change the nick of %s to "%s"' \
% (self.user, nick))
try:
self.server.user_info.set_moniker(self.user, nick)
self.server.user_info.store()
except KeyError:
self.err_invalidUser()
else:
self.server.wimd_UPDT('NICK', nick, self.user)
self.sendmsg('+NICK')
return
def ph_DISP(self, command):
self.log('DISP (%s)' % command)
disposition = command[5:]
if disposition in self.VALID_DISPOSITIONS:
self.disposition = disposition
self.server.wimd_UPDT('DISP', self.server.user_info.get_moniker(self.user),
self.user, disposition)
self.sendmsg('+DISP')
else:
self.sendmsg('-ERR Malformed Command')
return
def ph_PASS(self, command):
self.log('PASS (%s)' % command)
command = string.strip(command)
parts = string.split(command, ' ')
# Verify format of command
if len(parts) != 3:
self.sendmsg('-ERR Malformed Command')
return
# Parse command
old_password = parts[1]
new_password = parts[2]
# Verify their notion of the old password
if not self.validate_password(old_password):
self.sendmsg('-PASS Invalid Password')
return
# Change the password
self.server.user_info.set_password(self.user, new_password)
self.server.user_info.store()
self.sendmsg('+PASS')
return
#
# asynchat methods
#
def collect_incoming_data(self, more_data):
#self.log('collect_incoming_data(%s)' % str(more_data))
self.accumulator_buffer = self.accumulator_buffer + more_data
return
def found_terminator(self):
#self.log('found_terminator')
self.log('INCOMING "%s"' % self.accumulator_buffer)
if self.next_state_handler:
self.next_state_handler(self.accumulator_buffer)
else:
command_end = string.find(self.accumulator_buffer, ' ')
if command_end < 0:
# no space, one word command?
command = self.accumulator_buffer
else:
command = self.accumulator_buffer[:command_end]
try:
handler = getattr(self, 'ph_%s' % command)
except AttributeError:
self.err_unknownCommand()
self.log('discarding unhandled message:%s' %
self.accumulator_buffer)
else:
handler(self.accumulator_buffer)
self.accumulator_buffer = ''
return
#
# Convenience methods
#
def validate_password(self, password):
user_info = self.server.user_info[self.user]
required_password = user_info[1]
salt = required_password[:2]
encrypted_password = crypt.crypt(password, salt)
#print 'comparing input "%s" with required "%s"' \
# % (encrypted_password, required_password)
if encrypted_password == required_password:
return 1
else:
return 0
def sendmsg(self, msg):
self.log('OUTGOING: "%s"' % msg)
try:
while not self.send('%s%s' % (msg, self.EOLN)):
pass
except socket.error:
self.log('ERROR: Could not send message')
return
class wimd_server(asyncore.dispatcher):
"""
This server class uses asyncore to accept and dispatch
requests.
"""
channel_class = wimd_channel
def __init__(self, sock, conf_filename):
self.conf_filename = conf_filename
self.read_conf_files()
self.user_connections = {}
asyncore.dispatcher.__init__(self, sock)
return
###
### User connection methods
###
def add_user_connection(self, uid, connection):
"""
Associated a connection with a user id so that
when a message needs to be sent to a particular
user it is easy to look up the value.
"""
if (self.user_connections.has_key(uid)
and not self.boot_duplicate_users):
# Duplicates not allowed
raise WimdServerError('Duplicate user')
elif self.user_connections.has_key(uid):
# Kick the other connection off
old_conn = self.user_connections[uid]
try:
old_conn.sendmsg('*UPDT SERV KICK')
old_conn.close()
except socket.error:
pass
self.log('Storing connection to %s via %s' % (uid, connection))
self.user_connections[uid] = connection
self.wimd_UPDT('USER', self.user_info.get_moniker(uid), uid, 'ONLINE')
self.send_motd(connection)
return
def get_user_connection(self, uid):
"""
Return the connection for the user specified by
uid, unless that user is not connected. If the
user is not connected, raises a KeyError.
"""
return self.user_connections[uid]
def drop_user_connection(self, uid):
"""
Forget about the connection to a particular
user.
"""
self.wimd_UPDT('USER', self.user_info.get_moniker(uid), uid, 'OFFLINE')
try:
del self.user_connections[uid]
except KeyError:
pass
return
###
### Outgoing messages
###
def send_motd(self, conn):
"""
Given a connection, send the message of the day.
"""
for line in self.motd:
conn.sendmsg('*MOTD %s' % line)
return
def wimd_MESG(self, fromuid, nick, touid, msg):
"""
Given a UID and a message, send the message to that
user.
"""
self.log('MESG from %s (%s) to %s : "%s"' \
% (fromuid, nick, touid, msg))
if touid == '0':
msg_type = 'CAST'
conns = self.user_connections.items()
else:
msg_type = 'MESG'
conns = ( (touid, self.user_connections[touid]), )
for real_uid, conn in conns:
try:
conn.sendmsg('*%s FROM USER %s:%s "%s"' \
% (msg_type, nick, fromuid, msg))
except socket.error:
# transmission problem, close the
# connection
conn.close()
#print 'conn="%s"' % str(conn)
return
def wimd_UPDT(self, message_type, nick, uid, message=''):
if message:
space=' '
else:
space=''
msg_string = '*UPDT %(message_type)s %(nick)s:%(uid)s%(space)s%(message)s' \
% locals()
self.log('UPDT: "%s"' % msg_string)
for real_uid, conn in self.user_connections.items():
conn.sendmsg(msg_string)
return
###
### Startup methods
###
def read_conf_files(self):
"""
Read the configuration files which control
the server behavior.
"""
# First the wimd.conf file
try:
print 'Reading server configuration from %s' % self.conf_filename
conf_file = open(self.conf_filename, 'rt')
except IOError, msg:
raise WimdServerError(
'Could not open configuration file.',
self.conf_filename,
str(msg.args[1]),
)
useful_lines = filter(lambda x: (x[0] != '#' and string.strip(x)),
conf_file.readlines())
conf_vars = {}
for line in useful_lines:
var, val = map(string.strip, string.split(string.strip(line), '='))
conf_vars[var] = val
conf_file.close()
# Extract known variables from the file
for attr, var, convert in (
('server_port', 'PORT', int),
('motd_filename', 'MOTDPATH', str),
('pass_filename', 'PWPATH', str),
('boot_duplicate_users', 'KICKUSERS', int),
):
try:
val = convert(conf_vars[var])
print '\t%s=%s' % (var, val)
setattr(self, attr, val)
except KeyError:
print '\tdid not find %s' % var
setattr(self, attr, None)
self.user_info = wimd_pwfile(self.pass_filename)
self.motd = filter(lambda x:(x and x[0] != '#'),
map(string.strip,
open(self.motd_filename, 'rt').readlines())
)
self.motd = DEFAULT_MOTD_LINES + self.motd
print 'SERVER MOTD'
for line in self.motd:
print line
print
return
###
### asyncore/asynchat interface methods
###
def log(self, message):
#asyncore.dispatcher.log(self, message)
return
def handle_expt (self):
self.log ('unhandled exception')
return
def handle_read (self):
return
def handle_write (self):
self.log ('unhandled write event')
return
def handle_connect (self):
self.log ('unhandled connect event')
return
def handle_oob (self):
self.log ('unhandled out-of-band event')
return
def handle_accept (self):
try:
conn, addr = self.accept()
except socket.error:
# linux: on rare occasions we get a bogus socket back from
# accept. socketmodule.c:makesockaddr complains that the
# address family is unknown. We don't want the whole server
# to shut down because of this.
sys.stderr.write ('warning: server accept() threw an exception\n')
return
self.log('conn=%s, addr=%s' % (conn, addr))
self.channel_class(self, conn, addr)
return
def handle_close (self):
self.log ('unhandled close event')
self.close()
return
def loop(self):
asyncore.loop()
return
|