import socket import time import re import os import base64 import traceback import logging import datetime from message import Message, PEST_VERSION from broadcast import Broadcast from direct import Direct from station import VERSION from funcs import * from commands import BROADCAST, DIRECT class Client(object): __linesep_regexp = re.compile(r"\r?\n") # The RFC limit for nicknames is 9 characters, but what the heck. __valid_nickname_regexp = re.compile( r"^[][\`_^{|}A-Za-z][][\`_^{|}A-Za-z0-9-]{0,50}$") __valid_channelname_regexp = re.compile( r"^[&#+!][^\x00\x07\x0a\x0d ,:]{0,50}$") def __init__(self, server, socket): self.server = server self.state = None self.socket = socket self.channels = {} # irc_lower(Channel name) --> Channel self.nickname = None self.user = None self.realname = None (self.host, self.port) = socket.getpeername() self.__timestamp = time.time() self.__readbuffer = "" self.__writebuffer = "" self.__sent_ping = False if self.server.password: self.__handle_command = self.__pass_handler else: self.__handle_command = self.__registration_handler def message_from_station(self, msg): targetname = self.server.channel_name if msg.command == BROADCAST else self.nickname pest_prefix = msg.prefix if msg.prefix else msg.speaker formatted_message = ":%s PRIVMSG %s :%s" % ( pest_prefix, targetname, msg.body ) self.__writebuffer += formatted_message + "\r\n" def get_prefix(self): return "%s" % (self.nickname) prefix = property(get_prefix) def check_aliveness(self): now = time.time() if self.__timestamp + 180 < now: self.disconnect("ping timeout") return if not self.__sent_ping and self.__timestamp + 90 < now: if self.__handle_command == self.__command_handler: # Registered. self.message("PING :%s" % self.server.name) self.__sent_ping = True else: # Not registered. self.disconnect("ping timeout") def write_queue_size(self): return len(self.__writebuffer) def __parse_read_buffer(self): lines = self.__linesep_regexp.split(self.__readbuffer) self.__readbuffer = lines[-1] lines = lines[:-1] for line in lines: if not line: # Empty line. Ignore. continue command, arguments = self.__parse_command_arguments(line) self.__handle_command(command, arguments) def __parse_command_arguments(self, line): x = line.split(" ", 1) command = x[0].upper() if len(x) == 1: arguments = [] else: if len(x[1]) > 0 and x[1][0] == ":": arguments = [x[1][1:]] else: y = string.split(x[1], " :", 1) arguments = string.split(y[0]) if len(y) == 2: arguments.append(y[1]) return command, arguments def __pass_handler(self, command, arguments): server = self.server if command == "PASS": if len(arguments) == 0: self.reply_461("PASS") else: if arguments[0].lower() == server.password: self.__handle_command = self.__registration_handler else: self.reply("464 :Password incorrect") elif command == "QUIT": self.disconnect("Client quit") return def __registration_handler(self, command, arguments): server = self.server if command == "NICK": if len(arguments) < 1: self.reply("431 :No nickname given") return nick = arguments[0] if server.get_client(nick): self.reply("433 * %s :Nickname is already in use" % nick) elif not self.__valid_nickname_regexp.match(nick): self.reply("432 * %s :Erroneous nickname" % nick) else: self.nickname = nick self.state.set_knob("nick", nick) server.client_changed_nickname(self, None) elif command == "USER": if len(arguments) < 4: self.reply_461("USER") return self.user = arguments[0] self.realname = arguments[3] elif command == "QUIT": self.disconnect("Client quit") return if self.nickname and self.user: self.reply("001 %s :Hi, welcome to PestNet" % self.nickname) self.reply("002 %s :Your host is %s, running Blatta %d and Pest 0x%X" % (self.nickname, server.name, VERSION, PEST_VERSION)) self.reply("003 %s :This server was created %s" % (self.nickname, datetime.datetime.now())) self.reply("004 %s :%s blatta-%s o o" % (self.nickname, server.name, VERSION)) self.send_motd() self.__handle_command = self.__command_handler def __command_handler(self, command, arguments): def away_handler(): pass def ison_handler(): if len(arguments) < 1: self.reply_461("ISON") return nicks = arguments online = [n for n in nicks if server.get_client(n)] self.reply("303 %s :%s" % (self.nickname, " ".join(online))) def join_handler(): if len(arguments) < 1: self.reply_461("JOIN") return if arguments[0] == "0": for (channelname, channel) in self.channels.items(): self.message_channel(channel, "PART", channelname, True) server.remove_member_from_channel(self, channelname) self.channels = {} return channelnames = arguments[0].split(",") for channelname in channelnames: if irc_lower(channelname) in self.channels: continue if not valid_channel_re.match(channelname): self.reply_403(channelname) continue channel = server.get_channel(channelname) channel.add_member(self) self.channels[irc_lower(channelname)] = channel self.message_channel(channel, "JOIN", channelname, True) self.reply("366 %s %s :End of NAMES list" % (self.nickname, channelname)) def list_handler(): pass def lusers_handler(): pass def mode_handler(): pass def motd_handler(): self.send_motd() def nick_handler(): if len(arguments) < 1: self.reply("431 :No nickname given") return newnick = arguments[0] client = server.get_client(newnick) if newnick == self.nickname: pass elif client and client is not self: self.reply("433 %s %s :Nickname is already in use" % (self.nickname, newnick)) elif not self.__valid_nickname_regexp.match(newnick): self.reply("432 %s %s :Erroneous Nickname" % (self.nickname, newnick)) else: oldnickname = self.nickname self.nickname = newnick server.client_changed_nickname(self, oldnickname) self.message_related( ":%s!%s@%s NICK %s" % (oldnickname, self.user, self.host, self.nickname), True) self.state.set_knob('nick', self.nickname) def notice_and_privmsg_handler(): if len(arguments) == 0: self.reply("411 %s :No recipient given (%s)" % (self.nickname, command)) return if len(arguments) == 1: self.reply("412 %s :No text to send" % self.nickname) return targetname = arguments[0] message = arguments[1] # check for pest commands before handling this as a message if message[0] is "%": pest_command, pest_arguments = self.__parse_command_arguments(message[1:]) self.__handle_command(pest_command, pest_arguments) return if server.has_channel(targetname): channel = server.get_channel(targetname) self.message_channel( channel, command, "%s :%s" % (channel.name, message)) # send the channel message to peers as well Broadcast( { "speaker": self.nickname, "body": message, "long_buffer": self.server.station.long_buffer }, self.state).send() else: Direct({ "speaker": self.nickname, "handle": targetname, "body": message, "long_buffer": self.server.station.long_buffer }, self.state).send() def part_handler(): if len(arguments) < 1: self.reply_461("PART") return if len(arguments) > 1: partmsg = arguments[1] else: partmsg = self.nickname for channelname in arguments[0].split(","): if not valid_channel_re.match(channelname): self.reply_403(channelname) elif not irc_lower(channelname) in self.channels: self.reply("442 %s %s :You're not on that channel" % (self.nickname, channelname)) else: channel = self.channels[irc_lower(channelname)] self.message_channel( channel, "PART", "%s :%s" % (channelname, partmsg), True) del self.channels[irc_lower(channelname)] server.remove_member_from_channel(self, channelname) def ping_handler(): if len(arguments) < 1: self.reply("409 %s :No origin specified" % self.nickname) return self.reply("PONG %s :%s" % (server.name, arguments[0])) def pong_handler(): pass def quit_handler(): if len(arguments) < 1: quitmsg = self.nickname else: quitmsg = arguments[0] self.disconnect(quitmsg) def topic_handler(): pass def wallops_handler(): pass def who_handler(): pass def whois_handler(): pass def wot_handler(): if len(arguments) < 1: # Display the current WOT peers = self.state.get_peers() if len(peers) > 0: for peer in peers: if peer.address and peer.port: address = "%s:%s" % (peer.address, peer.port) else: address = "
" self.pest_reply("%s %s" % (string.join(peer.handles, ","), address)) else: self.pest_reply("WOT is empty") elif len(arguments) == 1: # Display all WOT data concerning the peer identified by HANDLE, # including all known keys, starting with the most recently used, for that peer. handle = arguments[0] peer = self.state.get_peer_by_handle(handle) if peer: self.pest_reply("keys:") for key in peer.keys: self.pest_reply("'%s'" % key) else: self.pest_reply("unknown peer: %s" % handle) else: pass def peer_handler(): if len(arguments) == 1: try: self.state.add_peer(arguments[0]) self.pest_reply("added new peer %s" % arguments[0]) self.message(":%s JOIN %s" % (arguments[0], self.server.channel_name)) except Exception, ex: self.pest_reply("error attempting to add peer %s" % arguments[0]) stack = traceback.format_exc() logging.debug(ex) logging.debug(stack) else: self.pest_reply("Usage: PEER ") def unpeer_handler(): if len(arguments) == 1: try: self.state.remove_peer(arguments[0]) self.pest_reply("removed peer %s" % arguments[0]) self.message(":%s PART %s" % (arguments[0], self.server.channel_name)) except Exception, ex: self.pest_reply("Error attempting to remove peer: %s" % ex) stack = traceback.format_exc() logging.debug(stack) else: self.pest_reply("Usage: UNPEER ") def genkey_handler(): self.pest_reply(base64.b64encode(os.urandom(64))) def key_handler(): if len(arguments) != 2: self.pest_reply("Usage: KEY ") else: handle = arguments[0] key = arguments[1] try: self.state.add_key(handle, key) self.pest_reply("added key: %s" % key) except: self.pest_reply("Error attempting to add key") def unkey_handler(): if len(arguments) != 1: self.pest_reply("Usage: UNKEY ") else: try: self.state.remove_key(arguments[0]) self.pest_reply("removed key: %s" % arguments[0]) except Exception, e: self.pest_reply("Error attempting to remove key") logging.debug(e) def at_handler(): if len(arguments) == 0: at = self.state.get_at() elif len(arguments) == 1: handle = arguments[0] at = self.state.get_at(handle) elif len(arguments) == 2: try: handle, address = arguments address_ip, port = string.split(address, ":") self.state.update_at( {"handle": handle, "address": address_ip, "port": int(port)}, False) self.pest_reply("updated address table: %s %s" % (handle, address)) except Exception as ex: self.pest_reply("Error attempting to update address table: %s" % ex) stack = traceback.format_exc() logging.debug(stack) return elif len(arguments) > 2: self.pest_reply("Usage: AT [] [
]") return if len(at) > 0: for address in at: self.pest_reply("%s %s %s" % (address["handle"], address["address"], address["active_at"])) else: self.pest_reply("no results") def knob_handler(): if len(arguments) == 0: knobs = self.state.get_knobs() if len(knobs) > 0: for key in knobs.keys(): self.pest_reply("%s %s" % (key, knobs[key])) else: self.pest_reply("no knobs configured") elif len(arguments) == 1: knob_value = self.state.get_knob(arguments[0]) if knob_value: self.pest_reply("%s %s" % (arguments[0], knob_value)) else: self.pest_reply("no such knob") elif len(arguments) == 2: self.state.set_knob(arguments[0], arguments[1]) self.pest_reply("set %s to %s" % (arguments[0], arguments[1])) else: self.pest_reply("Usage: KNOB [] []") def resolve_handler(): if len(arguments) == 1: handle = arguments[0] peer = self.state.get_peer_by_handle(handle) if peer: self.state.resolve(handle) self.pest_reply("resolved %s" % handle) else: self.pest_reply("peer with handle %s not found" % handle) else: self.pest_reply("Usage: RESOLVE ") handler_table = { "AWAY": away_handler, "AT": at_handler, "GENKEY": genkey_handler, "ISON": ison_handler, "JOIN": join_handler, "KEY": key_handler, "KNOB": knob_handler, "LIST": list_handler, "LUSERS": lusers_handler, "MODE": mode_handler, "MOTD": motd_handler, "NICK": nick_handler, "NOTICE": notice_and_privmsg_handler, "PART": part_handler, "PEER": peer_handler, "PING": ping_handler, "PONG": pong_handler, "PRIVMSG": notice_and_privmsg_handler, "QUIT": quit_handler, "RESOLVE": resolve_handler, "TOPIC": topic_handler, "UNKEY": unkey_handler, "UNPEER": unpeer_handler, "WALLOPS": wallops_handler, "WHO": who_handler, "WHOIS": whois_handler, "WOT": wot_handler, } server = self.server valid_channel_re = self.__valid_channelname_regexp try: handler_table[command]() except KeyError: self.reply("421 %s %s :Unknown command" % (self.nickname, command)) stack = traceback.format_exc() logging.debug(stack) def socket_readable_notification(self): try: data = self.socket.recv(2 ** 10) if os.environ.get("LOG_CLIENT_MESSAGES"): logging.debug( "[%s:%d] -> %r" % (self.host, self.port, data)) quitmsg = "EOT" except socket.error as x: data = "" quitmsg = x if data: self.__readbuffer += data self.__parse_read_buffer() self.__timestamp = time.time() self.__sent_ping = False else: self.disconnect(quitmsg) def socket_writable_notification(self): try: sent = self.socket.send(self.__writebuffer) if os.environ.get("LOG_CLIENT_MESSAGES"): logging.debug( "[%s:%d] <- %r" % ( self.host, self.port, self.__writebuffer[:sent])) self.__writebuffer = self.__writebuffer[sent:] except socket.error as x: self.disconnect(x) def disconnect(self, quitmsg): self.message("ERROR :%s" % quitmsg) logging.info( "Disconnected connection from %s:%s (%s)." % ( self.host, self.port, quitmsg)) self.socket.close() self.server.remove_client(self, quitmsg) def message(self, msg): self.__writebuffer += msg + "\r\n" def reply(self, msg): self.message(":%s %s" % (self.server.name, msg)) def pest_reply(self, msg): self.message(":Pest NOTICE %s :%s" % (self.server.channel_name, msg)) def pest_dm_reply(self, speaker, msg): self.message(":%s NOTICE %s :%s" % (speaker, self.nickname, msg)) def send_join(self, handle): self.message(":%s JOIN %s" % (handle, self.server.channel_name)) def send_part(self, handle): self.message(":%s PART %s" % (handle, self.server.channel_name)) def send_away(self, handle): self.message(":%s AWAY :No recent messages" % (handle)) def send_back(self, handle): self.message(":%s AWAY :" % (handle)) def reply_403(self, channeyl): self.reply("403 %s %s :No such channel" % (self.nickname, channel)) def reply_461(self, command): nickname = self.nickname or "*" self.reply("461 %s %s :Not enough parameters" % (nickname, command)) def message_channel(self, channel, command, message, include_self=False): line = ":%s %s %s" % (self.prefix, command, message) if include_self: self.message(line) def message_related(self, msg, include_self=False): clients = set() if include_self: clients.add(self) for channel in self.channels.values(): clients |= channel.members if not include_self: clients.discard(self) for client in clients: client.message(msg) def send_motd(self): server = self.server motdlines = server.get_motd_lines() if motdlines: self.reply("375 %s :- %s Message of the day -" % (self.nickname, server.name)) for line in motdlines: self.reply("372 %s :- %s" % (self.nickname, line.rstrip())) self.reply("376 %s :End of /MOTD command" % self.nickname) else: self.reply("422 %s :MOTD File is missing" % self.nickname)