Esempio n. 1
0
class SockSignal:
    """
    Used to interrupt a select() call.

    Include SockSignal.signal in the list passed as the first parameter of select().

    Call SockSignal.set() to interrupt the select() call. SockSignal.signal will
    be in the list of readable fds returned by select().
    """
    log = get_logger()

    def __init__(self):
        self.signal, self._write = socket.socketpair()
        self.signal.setblocking(False)

        # Prevent blocking writes, otherwise set() tends to block if
        # reset() was never called.
        self._write.setblocking(False)

    def set(self):
        self.log.debug("Setting SockSignal")
        self._write.send(b'\0')

    def reset(self):
        try:
            self.signal.recv(1)
        except socket.error as e:
            if e.errno != errno.EAGAIN:
                self.log.exception(
                    "Socket error in SockSignal: This shouldn't happen!", e)
            pass

    def __del__(self):
        self._write.close()
Esempio n. 2
0
class ThreadPool(object):
    "Generic threadpool implementation."
    # We don't want to rely on the presence of concurrent.futures
    # so we implement our own ThreadPool (not difficult to do)
    log = get_logger()

    def __init__(self, threads=4, basename="PoolWorker"):
        self.workers = []
        self.queue = Queue()

        # Initialize worker threads
        for tid in range(threads):
            worker = ThreadPoolWorker(self.queue, "%s%d" % (basename, tid))
            worker.start()
            self.workers.append(worker)

    def stop(self):
        """
        Shuts down the threadpool by killing all workers.
        """
        for tid in range(len(self.workers)):
            # Kill workers by making them raise SystemExit via sys.exit()
            # We simply run as many sys.exit tasks as there are workers
            self.queue.put(SystemExit)

    def invoke(self, target):
        """
        Invoke a target callable with no arguments or callback.
        
        To provide arguments and/or request a callback, use get_proxy to wrap your target
        in a proxy method, then call the proxy. See documentation for get_proxy.
        """
        if callable(target):
            # add a task to the queue with no args or kwargs
            self.queue.put(ThreadPoolTask(target))
        else:
            raise ValueError("First argument must be callable")

    def Proxy(self, target, callback=None):
        """
        Factory function, returns a proxy callable for a target callable.

        When the proxy is called, it will return immediately. The target function
        will then be called in turn by a worker thread.

        This can be done in one line, for example: Proxy(target)(arg1, arg2)
        You can keep the proxy object and call it multiple times.
        
        The callback function, if provided, must take a single argument and will
        be called (on the worker thread) with the return value of the target passed
        as its sole argument.
        """
        def proxy(*args, **kwargs):
            task = ThreadPoolTask(target, args, kwargs, callback)
            self.queue.put(task)
            return task

        return proxy
Esempio n. 3
0
class ThreadPoolWorker(threading.Thread):
    """
    Simple ThreadPool worker thread.

    It waits on a Queue maintained by the ThreadPool for work. Work is either
    a ThreadPoolTask instance, or SystemExit which is used as a special value
    to indicate that this thread should exit.
    """
    log = get_logger()

    def __init__(self, queue, name):
        super(ThreadPoolWorker, self).__init__(name=name)
        self.daemon = True  # die upon interpreter exit
        self.queue = queue

    @set_thread_name
    @catch_all(retry=True)
    def run(self):
        self.log.debug('%s now waiting for work' % self.name)
        while True:
            # Block on queue.get() until work arrives, then execute work
            task = self.queue.get()
            if task is SystemExit: raise SystemExit()
            if task: task.execute()
Esempio n. 4
0
and notifying the relevant IRCConnection when data is available.
"""

# Python imports
from time import time
from threading import Thread, RLock
import socket
import logging
import select
import fcntl
import errno
import os

# Set up logging
from ircstack.util import get_logger, catch_all, hrepr, set_thread_name
log = get_logger()


class SockSignal:
    """
    Used to interrupt a select() call.

    Include SockSignal.signal in the list passed as the first parameter of select().

    Call SockSignal.set() to interrupt the select() call. SockSignal.signal will
    be in the list of readable fds returned by select().
    """
    log = get_logger()

    def __init__(self):
        self.signal, self._write = socket.socketpair()
Esempio n. 5
0
"""
This module implements the plugin loading and unloading functionality of DemiBot.
"""

# Python imports
import os, sys, traceback, imp, weakref
import time

# Set up logging
from ircstack.util import get_logger
log = get_logger(__name__)

# ircstack imports
from ircstack.dispatch.async import Dispatcher, Async
from ircstack.dispatch.events import PluginUnloadEvent, event_handler

#: A dict of currently loaded Plugin instances, keyed by plugin name.
plugins = {}

#: A list of weak references. Currently unused.
weakrefs = []

class PluginUnloadMonitor(object):
    """
    This class is used to monitor for a plugin being unloaded, which it does
    by executing code in its __del__ method. Currently it only prints a log message.
    """
    def __init__(self, name, when):
        self.name = name
        self.when = when
    def __del__(self):
Esempio n. 6
0
"""
This module implements the plugin loading and unloading functionality of DemiBot.
"""

# Python imports
import os, sys, traceback, imp, weakref
import time

# Set up logging
from ircstack.util import get_logger
log = get_logger(__name__)

# ircstack imports
from ircstack.dispatch. async import Dispatcher, Async
from ircstack.dispatch.events import PluginUnloadEvent, event_handler

#: A dict of currently loaded Plugin instances, keyed by plugin name.
plugins = {}

#: A list of weak references. Currently unused.
weakrefs = []


class PluginUnloadMonitor(object):
    """
    This class is used to monitor for a plugin being unloaded, which it does
    by executing code in its __del__ method. Currently it only prints a log message.
    """
    def __init__(self, name, when):
        self.name = name
        self.when = when
Esempio n. 7
0
"""
IRCStack/Protocol/IRC
High-level IRC protocol handling.
"""

# Python imports
try:
    import urlparse
except ImportError:
    import urllib.parse as urlparse # Python 3
import itertools
import socket

# Set up logging
from ircstack.util import get_logger, hrepr
log = get_logger()

# ircstack imports
import ircstack.network
from ircstack.dispatch import Dispatcher
from ircstack.dispatch.events import EventListener, event_handler
from ircstack.dispatch.async import SyncDelayed, AsyncDelayed
import ircstack.dispatch.events as events
from ircstack.protocol import isupport


#dispatch = Dispatcher.dispatch

handlers = {}

# Constants
Esempio n. 8
0
class IRCServerList(object):
    """
    Manages a list of IRC servers, monitors connections, and manages automatic reconnection.

    (At the moment the IRCConnection class takes a list of servers to connect to and tries to
            establish a connection to each. This needs to be dumbed down; an IRCConnection
            should only represent a connection to a server specified by a single IP or DNS name
            plus port number.)

    **WIP**

    IRCServerList handles:
        - Server prioritization
        - IPv4/IPv6 preference
        - Connection retries
        - DNS lookups
        - Maintianing a blacklist of known-bad servers to avoid connecting to
    """
    #TODO: Implement this
    log = get_logger()

    def __init__(self, serverlist, encoder=None):
        #: A list of servers (IRCServer instances) to use.
        self.serverlist = serverlist

        #: The IRCEncoder to provide to the IRCConnections. If not supplied, a sane default is used.
        self.encoder = ircstack.network.IRCEncoder(
            'iso-8859-1') if encoder is None else encoder

        #: A generator that provides the next configured server.
        self.servers = itertools.cycle(serverlist)
        #: A generator that provides the next address in the current server.
        self.addresses = (x for x in ())

        #: The current IRCConnection.
        self.connection = None

        # TODO: Configurable option to select IPv6 only / IPv4 only / IPv6 preferred / IPv4 preferred
        self.prefer_ipv4 = False  #: Sets whether IPv4 should be preferred instead of IPv6.
        self.only_preferred = False  #: Sets whether the protocol selected by "prefer_ipv4" should be forced.

    def get_connection(self, force_ipv4=False):
        """
        Gets an IRCConnection for the next available IRC server.
        """
        while True:
            self.log.debug("Fetching next address")
            try:
                address = self.addresses.__next__() if hasattr(
                    self.addresses, '__next__') else self.addresses.next()
                break
            except StopIteration:
                # We've run out of addresses to connect to for the current server
                # Try the next server in the list
                self.log.debug("No addresses left, fetching next server")
                try:
                    self.current_server = self.servers.__next__() if hasattr(
                        self.servers, '__next__') else self.servers.next()
                except StopIteration:
                    # No servers are left
                    self.log.error(
                        'Connection request failed: No more servers to connect to'
                    )
                    self.connection = None
                    raise NoValidAddressError('No valid addresses supplied')
                else:
                    # Server acquired, is it a DNS name, or a bare IP address?
                    try:
                        # Is this server already an IPv4 address?
                        socket.inet_pton(socket.AF_INET,
                                         self.current_server.host)
                        self.addresses = (
                            x for x in [self.current_server.address])
                        continue
                    except socket.error:
                        # it isn't IPv4. It might be IPv6
                        if not force_ipv4:
                            try:
                                # Check if it's IPv6
                                socket.inet_pton(socket.AF_INET6,
                                                 self.current_server.host)
                                self.addresses = (
                                    x for x in [self.current_server.address])
                                continue
                            except socket.error:
                                pass
                        self.log.debug("DNS lookup required")
                        # It's not an IP address, do a DNS lookup
                        ip4list, ip6list = self.dns_lookup(self.current_server)
                        if force_ipv4:
                            self.addresses = (x for x in ip4list)
                        elif self.only_preferred:
                            self.addresses = (
                                x for x in ip4list) if self.prefer_ipv4 else (
                                    x for x in ip6list)
                        else:
                            self.addresses = (x for x in list(ip4list)+list(ip6list)) if self.prefer_ipv4 \
                                else (x for x in list(ip6list)+list(ip4list))
                        # jump to top to fetch first address
                        continue
        # attempt successful connection
        return ircstack.network.IRCConnection(address, self.current_server.ssl,
                                              self.encoder)

    def dns_lookup(self, server):
        ip4list, ip6list = [], []
        self.log.info('Resolving address {0.host}:{0.port}'.format(server))
        try:
            addrinfo = socket.getaddrinfo(*server.address)
        except socket.error as e:
            if e[0] == errno.ENOENT:
                # Hostname does not exist
                self.log.warn('Host not found resolving {0}'.format(
                    server.host))
            else:
                self.log.error(
                    'Unknown error {0[0]} resolving {1}: {0[1]}'.format(
                        e, server.host))
        else:
            # Python 2.6 doesn't support dict comprehensions
            #ip4list += {tuple(x[4]) for x in addrinfo if x[0]==socket.AF_INET}
            #ip6list += {tuple(x[4][:2]) for x in addrinfo if x[0]==socket.AF_INET6}
            ip4list += [
                tuple(x[4]) for x in addrinfo if x[0] == socket.AF_INET
            ]
            ip6list += [
                tuple(x[4][:2]) for x in addrinfo if x[0] == socket.AF_INET6
            ]

        # Dedup lists
        ip4list, ip6list = dict(ip4list).items(), dict(ip6list).items()
        #log.debug("IPv4 addresses: "+repr(ip4list))
        #log.debug("IPv6 addresses: "+repr(ip6list))

        if not ip6list and not ip4list:
            self.log.warn('DNS lookup for {0} returned no addresses'.format(
                server.host))

        return ip4list, ip6list
Esempio n. 9
0
class IRCNetwork(EventListener):
    """
    An IRCNetwork stores the state of a connection to an IRC network, and acts as an object-oriented
    interface for performing standard IRC commands and actions.
    
    It handles incoming IRCMessages and parses them to handle various commands and replies appropriately.
    It tracks important data such as the channels joined, current nickname, modes, etc. It parses the
    initial ISUPPORT numeric from the server to learn about the specific quirks of the network, in order
    to act accordingly.

    Most importantly, the IRCNetwork creates and fires appropriate events that can be subscribed to by
    plugins. The IRCNetwork does not take any actions itself beyond connecting and joining certain channels
    as defined in the network's config. After that point, all actions are carried out by plugins, with the
    IRCNetwork serving mainly as a framework to facilitate them.
    """
    log = get_logger()

    def __init__(self, config, autoconnect=False):
        """
        Creates a new IRCNetwork instance based on the given config.
        """
        # TODO: don't store config, make IRCNetwork completely ignorant of demibot code
        # Probably add a ton of constructor arguments here, and make the config class
        # return an IRCNetwork instance from a factory function

        # Store config
        self.conf = config

        # Compile server list from config
        serverlist = [IRCServer(uri) for uri in config.servers]

        # DEBUG: Using a sane encoder
        self.encoder = ircstack.network.IRCEncoder('iso-8859-1')
        self.serverlist = IRCServerList(serverlist, self.encoder)
        self._new_conn()

        # Important settings - use RFC1459-specified defaults in case server doesn't send ISUPPORT
        self.isupport = isupport.ISupport()

        # Informational stuff
        self.name = self.conf.name
        self.nick = None

        self.enabled_plugins = {}

        # SyncDelayed task to abort STARTTLS
        self._starttls_task = None

        # Subscribe to events (via EventListener superclass)
        # XXX: What events do we subscribe to, now? Do we still need to be an EventListener?
        super(IRCNetwork, self).__init__()

        # Load/enable plugins
        for k in config.plugins.keys():
            try:
                #TODO: Move all config/plugin stuff out to demibot
                from demibot import PluginLoader
                PluginLoader.get_plugin(k).enable(self)
                self.log.debug("Loaded plugin %s in network %s" %
                               (k, repr(self)))
            except:
                self.log.exception("Failed to load plugin %s in network %s" %
                                   (k, repr(self)))

        self._nickindex = 0

    def __repr__(self):
        return hrepr(self)

    def Message(self, command, *params):
        """
        Factory function for an IRCMessage suitable for sending to the network.
        """
        # Silly plugin writers are probably going to feed us byte strings
        # If we get one, assume it's UTF-8 and convert it
        if isinstance(command, str):
            command = command.decode('utf-8', 'replace')
        params = [
            param.decode('utf-8', 'replace')
            if isinstance(param, str) else param for param in params
        ]

        return ircstack.network.IRCMessage(self.encoder, None, command, params)

    def _ctcp(self, cmd, type_, target, params):
        return self.Message(
            cmd, target, u"\u0001%s%s\u0001" %
            (type_.upper(), u' ' + ' '.join(params) if params else u''))

    def CTCPRequest(self, type_, target, *params):
        """Factory function to generate an IRCMessage consisting of a single CTCP request."""
        return self._ctcp(u'PRIVMSG', type_, target, params)

    def CTCPReply(self, type_, target, *params):
        """Factory function to generate an IRCMessage consisting of a single CTCP reply."""
        return self._ctcp(u'NOTICE', type_, target, params)

    def _new_conn(self):
        conn = self.serverlist.get_connection()

        conn.hooks.connected += self.on_connected
        conn.hooks.connect_failed += self.on_connect_failed
        conn.hooks.received += self.on_received
        conn.hooks.hangup += self.on_disconnected
        conn.hooks.error += self.on_disconnected

        self._conn = conn

    def connect(self):
        # Connect to our config-defined IRC servers
        self._conn.connect()

    def send_raw(self, ircmessage, priority=NORMAL):
        "Send a raw IRCMessage to the server."
        self._conn.send(ircmessage, priority)

    ### IRC Commands

    def privmsg(self, target, message):
        self.send_raw(self.Message(u'PRIVMSG', target, message))

    def lazy_privmsg(self, target, message):
        #TODO: Better name
        self.send_raw(self.Message(u'PRIVMSG', target, message), LAZY)

    def quit(self, message):
        # Quit from IRC
        self.send_raw(self.Message(u'QUIT', message), URGENT)

    def join(self, channel, key=None):
        """Join an individual IRC channel."""
        self.send_raw(self.Message(u'JOIN', channel))

    def kick(self, channel, nick):
        self.send_raw(self.Message(u'KICK', nick), URGENT)

    def mode(self, target, mode):
        self.send_raw(self.Message(u'MODE', target, mode), NORMAL)

    def next_nick(self):
        "Uses the next nickname defined in config."
        # Get next nickname
        if self._nickindex < len(self.conf.nick):
            self._nickindex += 1
            self.nick = self.conf.nick[self._nickindex]
        else:
            self.nick = u"%s_" % self.nick
        self.send_raw(self.Message(u'NICK', self.nick))

    def join_all(self, channels):
        """
        Joins many channels in a single command.
        Expects a dict with channel names as keys.
        Values are the channel key (password), or none.
        """
        c = channels.items()
        self.send_raw(
            self.Message(u'JOIN', ','.join(i[0] for i in c),
                         ','.join('' if i[1] is None else i[1] for i in c)))

    ### Event Callbacks

    def on_connected(self):
        """
        Called when we are successfully connected to the IRC server,
        before any commands are sent or received.
        """
        # Send IRC login information

        self._nickindex = 0
        nickname, ident, realname = (self.conf.nick[0], self.conf.ident,
                                     self.conf.realname)

        # These are ignored for a client connection.
        hostname, servername = (u'*', u'*')

        # XXX: EXPERIMENTAL
        #self.send_raw(self.Message(u'MODE', u'IRCv3'))
        self._conn.starttls_begin()
        # after 5 seconds, assume STARTTLS was ignored
        self._starttls_task = SyncDelayed(self._conn.starttls_abort, 5.0)()

        # TODO: Send PASS here
        self.send_raw(self.Message(u'NICK', nickname), IMMEDIATE)
        self.send_raw(
            self.Message(u'USER', ident, hostname, servername, realname),
            IMMEDIATE)

        self.nick = nickname

    def on_connect_failed(self, conn):
        log.debug('Connection failed - Network on_connect_failed was called')
        self._new_conn()
        AsyncDelayed(self.connect, 10)()

    def on_disconnected(self):
        """
        Called when the IRC network disconnects us.
        """
        # TODO: Replace with an Event ACTUALLY MAYBE DON'T DO THAT need to think about it
        pass

    #@event_handler(events.MessageReceivedEvent)
    def on_received(self, msg):
        """
        Called when an IRCMessage is received by our current IRCConnection.

        This method looks for an appropriate internal handler method (as registered using
        the @handler decorator) for the particular type of message received from the IRC
        server, and calls it if it finds one. That handler will be responsible for modifying
        the IRCNetwork's state as required, and generating the appropriate IRCEvent subclass
        for use by plugins.
        """

        cmd = msg.command.upper()
        # Call the registered handler for this message, if there is one
        if msg.command in handlers:
            handlers[msg.command.upper()](self, msg)
            # Also fire an IRCEvent for anyone interested in the raw message
            # (NOTE: IRCEvent is fired AFTER internal message handling is complete)
            events.IRCEvent(self, msg).dispatch()
        else:
            if len(cmd) == 3 and cmd.isdigit():
                self.log.warning("Received unknown %s numeric: %s" %
                                 (cmd, msg))
            else:
                self.log.warning("Received unknown %s command: %s" %
                                 (cmd, msg))
            self.on_unhandled(events.IRCEvent(self, msg))


##### IRCNetwork        ###############
##### Internal Handlers ###############

# Handlers below are divided into three sections:

# RFC 1459 Commands:
# Contains handlers for all the standard IRC commands as defined in RFC 1459.

# RFC 1459 Numerics:
# Contains handlers for all of the numeric reply codes as defined in RFC 1459.

# RFC 2812:
# Contains handlers for IRC commands and numeric replies defined in RFC 2812.

# Miscellaneous:
# Contains handlers for non-standard IRC commands and reply codes that are not
# defined in either RFC 1459 or RFC 2812, i.e. for proprietary IRCd features.

# Resource: https://www.alien.net.au/irc/irc2numerics.html

    def on_unhandled(self, evt):
        """
        Called when there is no handler available for a particular message type.
        """
        events.UnknownMessageEvent(self, evt.ircmessage).dispatch()
        pass

    ############
    # RFC 1459 #
    # Commands #
    ############

    @handles('NICK')
    def handle_nick(self, msg):
        "Handles nickname change messages."
        event = events.NickEvent(self, msg)
        if event.sender.nick is self.nick:
            # our nickname has changed
            self.log.info("Our nickname on %s changed to %s" %
                          (self.name, msg.params[1]))
            self.nick = msg.params[1]
        event.dispatch()

    @handles('PRIVMSG')
    def handle_privmsg(self, msg):
        events.PrivmsgEvent(self, msg)
        if '#' in msg.params[0]:
            self.log.info(
                u'(%d) [%s] <%s> %s' %
                (msg.seq, msg.params[0], events.Sender(msg).nick, msg.message))
            events.ChannelMessageEvent(self, msg).dispatch()
        else:
            self.log.info(u'(%d) <%s> %s' %
                          (msg.seq, events.Sender(msg).nick, msg.message))
            events.PrivateMessageEvent(self, msg).dispatch()

        if msg.message[0] == self.conf.prefix_char:
            events.CommandEvent(self, msg).dispatch()

    @handles('NOTICE')
    def handle_notice(self, msg):
        self.log.info(u"NOTICE from %s: %s" % (msg.prefix, msg.message))
        events.NoticeEvent(self, msg).dispatch()

    @handles('QUIT')
    def handle_join(self, msg):
        self.log.info(u"%s has quit: %s" % (msg.prefix, msg.message))
        events.QuitEvent(self, msg).dispatch()

    @handles('JOIN')
    def handle_join(self, msg):
        self.log.info(u"%s has joined %s" % (msg.prefix, msg.message))
        events.JoinEvent(self, msg).dispatch()

    @handles('PART')
    def handle_part(self, msg):
        if len(msg.params) > 1:
            self.log.info(u"%s has left %s (%s)" %
                          (msg.prefix, msg.params[0], msg.message))
        else:
            self.log.info(u"%s has left %s" % (msg.prefix, msg.params[0]))
        # TODO: Remove user from relevant IRCChannels
        events.PartEvent(self, msg).dispatch()

    @handles('MODE')
    def handle_mode(self, msg):
        target, params = msg.params[0], msg.params[1:]

        # TODO: NYI
        return

        # Is target a user (usually us), or a channel?
        if target[0] in self.isupport.chantypes:
            events.ChannelModeEvent(self, msg).dispatch()
        else:
            events.UserModeEvent(self, msg).dispatch()
        events.ModeEvent(self, msg).dispatch()

    ############
    # RFC 1459 #
    # Numerics #
    ############

    @handles(1)
    def handle_welcome(self, msg):
        self.log.info(msg.message)
        log.debug(events.WelcomeEvent.subscribers.items())
        events.WelcomeEvent(self, msg).dispatch()

    @handles(2)
    def handle_yourhost(self, msg):
        "Your host is X, running version Y"
        self.log.info(msg.message)

    @handles(3)
    def handle_created(self, msg):
        "This server was created..."
        self.log.info(msg.message)

    @handles(4)
    def handle_myinfo(self, msg):
        server_name, version, user_modes, chan_modes = msg.params[:4]
        # Do something with these

    @handles(5)
    def handle_isupport(self, msg):
        # TODO: Proper parsing of RPL_ISUPPORT

        # NOTE that while RFC2812 defines 005 as RPL_BOUNCE, the only IRCd known
        # to implement RPL_BOUNCE has changed it to 010 instead.

        # This handler implements the RPL_ISUPPORT draft laid out at:
        # http://tools.ietf.org/html/draft-brocklesby-irc-isupport-03
        # It is, however, backwards compatible with previous versions of the draft.

        # Reference: http://www.irc.org/tech_docs/005.html

        # Strip initial nick and trailing text
        params = msg.params[1:-1]

        for param in params:
            self.isupport.from_param(*param.split('=', 1))

    @handles(372)  # MOTD message
    def handle_motd(self, msg):
        self.log.debug(msg.message)

    @handles(375)  # Start of MOTD message
    def handle_startofmotd(self, msg):
        pass

    @handles(376)  # End of MOTD message
    def handle_endofmotd(self, msg):

        # Automatically join configured channels
        def autojoin():
            chans = {}
            for chan in self.conf.channels:
                if self.conf.channels[chan].autojoin is not False:
                    chans[chan] = self.conf.channels[chan].key
            if chans: self.join_all(chans)

        # Execute delayed autojoin task
        SyncDelayed(autojoin, 2.0)()
        events.EndOfMOTDEvent(self, msg).dispatch()

    @handles(437)
    def handle_unavailresource(self, msg):
        """
        RPL_UNAVAILRESOURCE: Defined in RFC2812.

        * Returned by a server to a user trying to join a channel
          currently blocked by the channel delay mechanism.

        * Returned by a server to a user trying to change nickname
          when the desired nickname is blocked by the nick delay
          mechanism.
        """

        if msg.params[1] == self.nick:
            # The nickname we selected is temporarily unavailable -
            # it is probably being held by services
            self.log.info(
                '%s: Nickname "%s" appears to be locked down by Services' %
                (self.name, self.nick))
            self.next_nick()
        else:
            self.log.debug(msg)

    @handles(451)
    def handle_notregistered(self, msg):
        """
        Handles the ERR_NOTREGISTERED numeric, received when the server
        rejects a command because we haven't registered with USER yet.
        """
        if self._starttls_task is not None:
            # Abort STARTTLS if server rejects us
            self._conn.starttls_abort()
            self._starttls_task.cancel()
            self._starttls_task = None
        self.log.info(msg)

    @handles(670)
    def handle_starttls(self, msg):
        """
        This reply is sent in response to a STARTTLS command
        to indicate to us that it is safe to switch the socket over to SSL mode.
        """
        self.log.info("Activating Transport Layer Security")
        self._conn.starttls_complete()

    @handles(691)
    def handle_starttls_failed(self, msg):
        self.log.warn("Server reported an error activating TLS")
        self._conn.starttls_abort()
Esempio n. 10
0
class IRCBot(Singleton, EventListener):
    """
    The DemiBot application.

    This class implements the Demibot IRC bot. Call the run() method to execute it.
    
    Note that IRCBot runs as an interactive frontend, presenting a readline-enabled command
    line interface, allowing it to be controlled using commands. For this reason, IRCBot is
    a singleton. Attempting to instantiate it more than once will return the existing instance
    instead of a new one.
    """
    log = get_logger()

    def __init__(self):

        self.conf = load_config()
        self.networks = {}

        super(IRCBot, self).__init__()

    def run(self):
        ### INITIALIZE SERVICES ###
        # Initialize the interactive console.
        # TODO: Use ncurses
        console.setup()
        try:

            # Start the IRCSocketManager. This needs to be started before any networks can be connected
            IRCSocketManager.start()
            # Start the Dispatcher main loop. This needs to be started early, because it is responsible
            # for executing delayed tasks.
            Dispatcher.start()

            ### INITIALIZE NETWORKS ###

            # TODO: Network manager? Is it required?
            for nconf in self.conf.networks:
                try:
                    net = IRCNetwork(nconf)
                except NoValidAddressError as e:
                    self.log.warning("Failed to connect to %s: %s" %
                                     (nconf['name'], e.message))
                else:
                    self.networks[net.name] = net

            # Connect all networks
            ccount = 0
            for network in self.networks.values():
                if network.conf.enabled:
                    try:
                        network.connect()
                    except NoValidAddressError as e:
                        self.log.warning(
                            "Failed to connect to %s: Could not connect to any of the provided addresses."
                            % network.name)
                    ccount += 1
                else:
                    self.log.info("Ignoring disabled network %s" %
                                  network.name)
            self.log.info("%d networks connected" % ccount)

            ### DEBUGGING ###

            def test(msg):
                self.log.info(msg)

            #DelayedTask(test, 8.0)("This is the 8 second debug timer..")

            #plugin = PluginLoader.get_plugin('pingpong')
            #for net in self.networks: net.enabled_plugins.append('pingpong')
            #DelayedTask(plugin.unload, 20.0)()

            # Here is where we would register any built-in events
            #ChannelMessageEvent.subscribe(rocket)

            ### END DEBUGGING ###

            # Infinite loop waiting for ^D
            console.run()

            # If we get here, there was a keyboard interrupt (or other exception) and we should shut down
            self.shutdown()
        finally:
            console.teardown()
            pass

    @event_handler(events.WelcomeEvent)
    def on_welcome(self, event):
        self.log.info('Setting modes for %s...' % event.network.name)
        event.network.mode(event.network.nick, '+B-x')

    def quit_networks(self, reason='Terminated'):
        self.log.info("Sending quit command to IRC networks...")
        for network in self.networks.values():
            if network.conf.enabled:
                try:
                    network.quit(reason)
                except RuntimeError:
                    # Socket not connected
                    pass

    def handle_console(self, command):
        """Handles a console command."""
        self.log.debug("Console: %s" % command)
        params = command.split()
        if params[0].lower() in ('stop', 'quit', 'exit', 'shutdown'):
            exit('Shutdown by console command')
        elif params[0].lower() == 'plugin':
            if len(params) < 2:
                self.log.error('Not enough parameters for "plugin" command')
                return
            if params[1].lower() == 'load':
                if len(params) < 3:
                    self.log.error('Usage: "plugin load <plugin>"')
                else:
                    try:
                        PluginLoader.get_plugin(params[2])
                    except:
                        self.log.exception("Unable to load plugin")
            elif params[1].lower() == 'enable':
                if len(params) < 4:
                    self.log.error('Usage: "plugin enable <plugin> <network>"')
                else:
                    if params[3] not in self.networks:
                        self.log.error('No such network')
                        return
                    try:
                        PluginLoader.get_plugin(params[2]).enable(
                            self.networks[params[3]])
                    except:
                        self.log.exception("Unable to load plugin")
            elif params[1].lower() == 'disable':
                if len(params) < 4:
                    self.log.error(
                        'Usage: "plugin disable <plugin> <network>"')
                else:
                    if params[3] not in self.networks:
                        self.log.error('No such network')
                        return
                    try:
                        PluginLoader.get_plugin(params[2]).disable(
                            self.networks[params[3]])
                    except:
                        self.log.exception("Unable to find plugin")
            elif params[1].lower() == 'unload':
                if len(params) < 3:
                    self.log.error('Usage: "plugin unload <plugin>"')
                else:
                    if params[2] in PluginLoader.plugins:
                        PluginLoader.plugins[params[2]].unload()
                    else:
                        self.log.error("That plugin is not loaded.")
            elif params[1].lower() == 'reload':
                if len(params) < 3:
                    self.log.error('Usage: "plugin reload <plugin>"')
                else:
                    if params[2] in PluginLoader.plugins:
                        PluginLoader.plugins[params[2]].reload()
                    else:
                        self.log.error("That plugin is not loaded.")
            else:
                self.log.error('Unknown subcommand for "plugin" command')
        else:
            self.log.debug("Dispatching command: %s" % ' '.join(params))
            events.ConsoleCommandEvent(params[0], params[1:]).dispatch()

    def shutdown(self):
        self.log.info('Shutting down IRCBot...')
        IRCSocketManager.shutdown()
        Dispatcher.shutdown()
        self.log.info('Shutdown complete.')
Esempio n. 11
0
class SendThrottledSocket(LineBufferedSocket):
    """
    An extension of LineBufferedSocket that throttles outgoing messages.

    Detects flooding and queues outbound messages to avoid flooding the IRC server.
    """
    log = get_logger()

    def __init__(self,
                 family=socket.AF_INET,
                 parent=None,
                 use_ssl=False,
                 rate=10.0,
                 per=6.0):

        # These two parameters are important (and for now, are hardcoded).
        # At its most basic: Allows "rate" messages every "per" seconds.

        # rate: Controls how many messages may be sent before flood protection kicks in.
        #       This value determines the max "allowance" for sending.
        self.rate = rate

        # per:  Controls how many seconds it takes for the "allowance" to reach full value
        #       again, thus how long until another "rate" messages may be sent unhindered.
        self.per = per

        self.allowance = self.rate
        self.last_check = time()

        self.sendq = PriorityQueue()
        super(SendThrottledSocket, self).__init__(family, parent, use_ssl)

    def _update_allowance(self):
        """
        Update (increase) the send queue allowance depending on time.

        This system works by giving the send queue an "allowance" which increases
        over time (capped at a certain value). Value is removed from this
        "allowance" to "pay" for sending a line. If there is not enough to "pay"
        for the line, it is queued until sufficient "funds" accumulate to send it.
        """
        current = time()
        time_passed = current - self.last_check
        self.last_check = current

        # Work out how much the allowance has increased since the last time this was called.
        self.allowance += time_passed * (self.rate / self.per)
        if self.allowance > self.rate: self.allowance = self.rate

    def send_now(self, message):
        """
        Sends a message, bypassing the send queue, however this still consumes
        send queue allowance.

        Use this only for replying to a PING, or for sending a PING when we
        are measuring lag (and don't want it delayed). Use sparingly.
        """
        if super(SendThrottledSocket, self).send(message + '\r\n'):
            self.allowance -= 1.0

    def _get_delay(self):
        """
        Returns the minimum time until a single line may next be sent to the server.

        This works by calculating how long it will take for the "allowance" to exceed the
        "cost" of sending a line, i.e. 1. It returns zero if there is no delay and a line
        would be sent instantly.
        """

        # Time until allowance recharges enough to send a single message
        delay = self.per * (1 - self.allowance) / self.rate
        return delay if delay > 0 else 0

    def _get_queue_delay(self):
        """Returns the minimum time until all messages currently in the send queue would be sent."""
        # Plus the minimum time needed to send the rest of the queue
        return self._get_delay() + len(self.sendq) * self.per / self.rate

    def sendline(self, message, priority=1):
        """
        Sends a message to the socket, delaying it if neccessary.
        Takes an optional priority value, which defaults to 1.
        """

        # TODO: remove debug message
        self.log.debug(repr(message))

        self._update_allowance()
        if self.allowance < 1.0 or self.sendq.has_items():
            # Exceeded rate limit, queue the message for later. OR
            # Messages are currently queued, queue this one too so we don't send out of order.

            delay = self._get_queue_delay(
            )  # Estimate required delay before adding message to queue.
            # Add message to queue.
            self.sendq.put(message + '\r\n', priority)
            # Request a callback to process the queued message
            AsyncDelayed(self.tick, delay)()
            self.log.debug(
                "Throttling engaged: len(sendq)=%d, allowance=%.2f, delay=%.2f"
                % (len(self.sendq), self.allowance, delay))
        else:
            # No queue. Send the data immediately.
            if super(SendThrottledSocket, self).send(message + '\r\n'):
                self.allowance -= 1.0

    def tick(self):
        """
        Callback to process the send queue.

        It doesn't hurt to call this manually if for some reason you need to
        check if outgoing queued messages are ready to be sent.
        """
        # Ignore tick if this socket is dead! Don't schedule another
        if isinstance(self._sock, socket._closedsocket): return

        # Update send allowance
        self._update_allowance()
        self.log.debug("Got tick: len(sendq)=%d, allowance=%.2f" %
                       (len(self.sendq), self.allowance))

        # Pop and send only if there is data to send and allowance to send it with
        if self.sendq.has_items():
            if self.allowance >= 1.0:
                self.send_now(self.sendq.get())
            else:
                delay = self._get_delay()
                self.log.debug(
                    'Ticked, but not enough allowance! allowance=%f len(sendq)=%d\n'
                    'Rescheduling tick in %.2f seconds.' %
                    (self.allowance, len(self.sendq), delay))
                AsyncDelayed(self.tick, delay)()
        else:
            self.log.debug('Ticked, but the queue is empty! Nothing to send!')
Esempio n. 12
0
class LineBufferedSocket(socket.socket):
    """
    An extension of a socket, which buffers input until a complete line is received.
    This implementation is also SSL-capable (using the ssl module).

    Keeps a record of the last line fragment received by the socket, which is usually not a complete line.
    It is prepended onto the next block of data to make a complete line.
    """

    # Inheritance notes: Under normal circumstances, we will actually be inheriting
    # from socket._socketobject. In practice this doesn't matter.

    log = get_logger()
    netlog = get_logger('network')

    def __init__(self, family=socket.AF_INET, parent=None, use_ssl=False):
        """
        Constructs a new LineBufferedSocket that will connect to the specified address.
        """
        self._recvbuf = ''
        self._sendbuf = ''
        self._lines = []
        self._suspended = False

        #: Hooks for events that may occur. Available hooks are:
        #: dead
        self.hooks = Hooks(['dead'])

        #: Stores the address that this socket is connected to.
        self.address = None
        #: Stores the last socket error to occur.
        self.error = None

        self.use_ssl = use_ssl

        #: In order to be helpful to higher-level code, store a reference to
        #: some optional "parent" object. We don't use it or care what it is.
        self.parent = parent

        # Use a pair of re-entrant locks to prevent threading issues.
        self._readlock = threading.RLock()
        self._writelock = threading.RLock()

        if use_ssl:
            # Wrap a new, basic socket in an SSL layer
            wrapper = ssl.wrap_socket(socket.socket(family=family,
                                                    type=socket.SOCK_STREAM),
                                      ssl_version=ssl.PROTOCOL_TLSv1)

            # Workaround for unimplemented connect_ex function in 2.6
            if not hasattr(ssl.SSLSocket, '_real_connect'):
                setattr(
                    wrapper, '_sslobj',
                    ssl._ssl.sslwrap(wrapper._sock, False, None, None,
                                     ssl.CERT_NONE, ssl.PROTOCOL_TLSv1, None))

        super(LineBufferedSocket, self).__init__(
            family=family,
            type=socket.SOCK_STREAM,
            # If using SSL, tell base class to use the wrapped socket internally.
            # The SSL layer should be almost completely transparent to us.
            _sock=wrapper if use_ssl else None)

        # After this point the following methods will directly reference
        # the methods of the underlying socket implementation:
        #     recv,  recvfrom,  recv_into,  recvfrom_into,   send,  sendto

        # If we wish to wrap any of these we must now redefine them.
        # Override send() with our _send() implementation.
        # To actually send data we'll use self._sock.send() where _sock
        # is the native socket implementation.
        setattr(self, 'send', self._send)

        # Enable TCP keepalives and make self non-blocking
        self.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True)
        self.setblocking(False)

    #def close(self):
    #    super(LineBufferedSocket, self).close()

    def connect(self, address):
        with self._readlock:
            with self._writelock:
                self.address = address
                self.log.info('Opening connection to %s:%d' % address)
                # Call base class connect
                result = super(LineBufferedSocket, self).connect_ex(address)
                if result == errno.EINPROGRESS:
                    # This is normal (because we're nonblocking)
                    return True
                elif result != 0:
                    self.error = socket.error(result, os.strerror(result))
                    self.log.error('Error trying to connect: %s' %
                                   str(self.error))
                    # Take appropriate action?
                    return False
                else:
                    # we might end up here if the socket connected fast enough
                    self.log.debug("Connected immediately %s" % self)
                    return True

    def connect_ex(self, address):
        """See socket.connect_ex"""
        with self._readlock:
            with self._writelock:
                self.address = address
                return super(LineBufferedSocket, self).connect_ex(address)

    def __repr__(self):
        return hrepr(self)

    def disconnect(self):
        """
        A higher-level method for disconnecting the socket cleanly.
        This does not close the socket; you still need to manually call close().
        """
        with self._readlock:
            with self._writelock:
                try:
                    self.shutdown(
                        2
                    )  # SHUT_RDWR, using constant in case of calling from destructor
                    self._cleanup()
                except socket.error as msg:
                    if msg[0] == errno.ENOTCONN:
                        self.log.debug(
                            "%s was already disconnected in disconnect()" %
                            repr(self))

    def _recv(self):
        """
        Internal function: Receives new data from the socket and splits it into lines.
        Last (incomplete) line is kept for buffer purposes.
        Returns nothing; received data is stored internally. Call readlines() to retrieve it.
        """
        with self._readlock:
            data = ''
            try:
                data = self._recvbuf + self._sock.recv(4096)
                #self.netlog.info('%s >>> %s' % (self.address, repr(data)))
            except ssl.SSLError as e:
                if e[0] == ssl.SSL_ERROR_WANT_READ:
                    # The SSL layer needs us to call read() again before it can return
                    # actual data to us, because it needs to do SSL stuff (i.e. handshaking)
                    # behind the scenes.
                    # We simply return, and the SocketManager will call us again.
                    return
                elif e[0] == ssl.SSL_ERROR_WANT_WRITE:
                    # The SSL layer needs us to perform a write operation so that it can do SSL
                    # stuff before it is able to return data to us.
                    # We attempt to send an empty string (zero bytes).
                    self._send('')
                    return
            except socket.error as msg:
                if msg[0] in [errno.EAGAIN, errno.EWOULDBLOCK]:
                    # No data to read, nothing to do here.
                    return
                elif msg[0] == errno.ENOTCONN:
                    # Transport endpoint is not connected. This shouldn't happen!
                    self.log.debug('%s is not connected!' % repr(self))
                    return
                elif msg[0] in (errno.ECONNRESET, errno.ETIMEDOUT,
                                errno.EPIPE):
                    # A network error occurred.
                    self._cleanup(msg)
                    return
                # Unknown error, re-raise
                raise
            except socket.timeout:
                # Only raised when a blocking socket read times out.
                # Our socket is nonblocking so we should never end up here
                return

            if data == '':
                # Empty data on read means the other side has hung up on us.
                self.log.info('Remote host has disconnected cleanly')
                self._cleanup()

            self._lines += data.split("\r\n")
            self._recvbuf = self._lines[-1]
            self._lines = self._lines[:-1]

    def _cleanup(self, error=None):
        """Internal: Called immediately when the connection dies"""
        # Don't bother locking here, as socket is already dead
        self.error = error
        # Call on_dead handler
        self.hooks.dead(self)

    def is_ready(self):
        """
        Reads the socket and returns True if there are complete lines waiting to be retrieved.
        """
        with self._readlock:
            self._recv()
            return len(self._lines) > 0

    def readlines(self):
        """
        Reads from the socket and returns all complete lines that were retrieved.
        Can return an empty list if there has been no complete line received.
        """
        with self._readlock:
            self._recv()
            ret = self._lines
            self._lines = []
            return ret

    def readline(self):
        """
        Returns the next available line from the network, or None if no complete line has been received.
        It is recommended that you call readlines() instead of this method.
        """
        with self._readlock:
            if len(self._lines) == 0:
                self._recv()
            return self._lines.pop(0) if len(self._lines) else None

    def _send(self, data):
        # Acquire write lock
        with self._writelock:
            if self._suspended:
                self.log.debug(
                    "Attempt to send data while suspended, buffering: %s" %
                    repr(data))
                self._sendbuf += data
                return
            #log.debug(data)
            self._sendbuf, data = '', self._sendbuf + str(data)
            if not data: return True
            #self.log.debug("_send() %s %s" % (repr(self), repr(data)))
            try:
                l = self._sock.send(data)
                #self.netlog.info('%s <<< %s' % (self.address, repr(data[:l]))) # log it
                if l < len(data):
                    self.log.warn("Not all data could be sent, trying again")
                    self._sendbuf = data[l:]
                    AsyncDelayed(self._send, 0.1)(
                        '')  # is this really the best way to do this?
                    # TODO: find a way to get the SocketManager to do the retry instead?
            except socket.error as e:
                if e[0] == errno.EAGAIN:
                    self.log.warn("Error when sending some data, trying again")
                    self._sendbuf = data
                    AsyncDelayed(self._send, 0.1)('')
                elif e[0] in (errno.ECONNRESET, errno.ETIMEDOUT, errno.EPIPE):
                    # A network error occurred.
                    self._cleanup(e)
                return False
            return True

    def suspend(self, data=None):
        """
        (Method used for STARTTLS)
        Suspends writes, after first sending the provided data (if any) in an
        atomic operation, guaranteeing that data is the final thing sent.
        Further attempts to write will be buffered until resume() is called.
        """
        with self._writelock:
            if data: self._send(data)
            self._suspended = True

    def resume(self):
        """
        (Method used for STARTTLS)
        Resumes writes after a call to suspend(), and also immediately sends
        any data that was buffered while suspended.
        """
        self._suspended = False
        self._send('')

    def starttls(self):
        """
        Initiates a secure connection over the existing socket.
        If the socket is already using SSL, does nothing (no onion layering!)
        """
        # Should we raise a RuntimeError or just silently ignore?
        if isinstance(self._sock, ssl.SSLSocket): return

        # lock while doing this
        with self._readlock:
            with self._writelock:
                self._wrap_ssl()
                self.resume()

    def _wrap_ssl(self):
        # Wrap our existing socket in SSL
        self.setblocking(True)
        self._sock = ssl.wrap_socket(self.dup())
        self.setblocking(False)

        # Reassign methods (except send())
        for method in ssl._delegate_methods:
            if method != 'send':  # already ours
                setattr(self, method, getattr(self._sock, method))
Esempio n. 13
0
class IRCConnection(object):
    """
    Represents and handles a connection to an IRC server.

    THE FOLLOWING DESCRIPTION IS OUT OF DATE

    It is responsible for creating a socket and connecting it to an IRC server on
    the network. It is responsible for retrying the connection until it succeeds.
    It can accept a list of different server addresses to try, and will cycle
    through them until it finds a working one.

    If disconnected, the IRCConnection will attempt to reconnect automatically,
    unless it has received an "ERROR :Closing Link" message from the server, in
    which case it will cowardly refuse to reconnect. This ERROR response is
    generally sent by the server to notify the client that the connection is about
    to be deliberately closed.

    It is not responsible for actually reading the socket - instead it hands the
    socket to the IRCSocketManager, which handles reading raw data from the socket,
    and buffering input and output. If the socket disconnects or errors, the
    IRCSocketManager will notify this IRCConnection and then discard both the
    socket and its reference to this instance.

    It is responsible for certain very basic IRC operations which are listed in
    their completeness below:

    * sending the initial commands necessary to connect to the IRC server
    * transparently responding to PING messages as they arrive
    * Translating incoming messages to/from the correct character encoding

    It is responsible for low-level parsing of the IRC protocol at a basic level.
    It is completely agnostic about higher-level IRC protocol; it should happily
    deal with anything that looks like valid IRC syntax and throw it at the
    Dispatcher to deal with, allowing it to handle network-specific extensions to
    the IRC protocol such as WATCH, or the IRC profile of SASL.

    Most importantly, IRCConnection instances do not hold any high-level state
    regarding the IRC session. The only state it is concerned about is whether the
    connection is alive or not.

    * Parsing generates a tuple of (type, sender, params) which (excluding PINGs)
      are submitted to the Dispatcher.
    * PING messages are immediately processed without delay within the current thread.
    """

    log = get_logger()

    def __init__(self, address, ssl=False, encoder=None):
        """
        Creates an IRCConnection.
        
        network: The IRCNetwork that owns this connection.
        address: The server to connect to, as a (host, port) tuple.
        ssl: Whether to use SSL on this connection.
        encoding: Optional; sets the encoder to use for encoding messages.
        """
        self.log.debug("Created connection %s to %s" %
                       (repr(self), repr(address)))

        #: Hooks for events that may occur. Available hooks are:
        #: received, connected, connect_failed, error, hangup
        self.hooks = Hooks(
            ['received', 'connected', 'connect_failed', 'error', 'hangup'])

        #: Stores the address (host and port) to connect to.
        self.address = address
        #: Stores the socket that this IRCConnection uses to communicate with an IRC server.
        self.socket = None

        #: Uses the Dispatcher to route messages to our parent network.
        self.dispatch = Sync(self.hooks.received)

        #: Indicates whether the connection is SSL secured.
        self.ssl = ssl
        # Use the provided encoder, or create a sane one if it is omitted
        self.encoder = encoder if encoder else IRCEncoder('iso-8859-1')

        self.sequence = 0  # sequence numbers

        self.ping = None  # Stores the ping value sent to the server.
        self.ping_task = None  # Stores the ping timeout task so it can be cancelled
        self.random = random.Random()  # Generates random values for pings

        self.conn_task = None

    def get_random(self):
        """
        Gets a random hexadecimal string suitable for sending in a PING message.
        """
        return u"%08X" % self.random.getrandbits(32)

    def __repr__(self):
        #mod = self.__class__.__module__
        #cls = self.__class__.__name__
        #return '<%s.%s object "%s">' % (mod, cls, humanid(self))
        return hrepr(self)

    def send_ping(self):
        if self.socket is not None:
            # Check results of previous ping (if any)
            if self.ping is not None:
                self.socket.send_now(
                    IRCMessage(self.encoder, None, u'QUIT',
                               ("No ping response for 2 minutes", )))
                self.reconnect()
                return
            # Now send the next ping
            self.ping = self.get_random()
            self.socket.send_now(
                IRCMessage(self.encoder, None, u'PING', (self.ping, )))
        elif self.ping_task:
            # If there is no connection, cancel ping task
            self.ping_task.cancel()

    def connect(self, force_ipv4=False, depth=0):
        """
        Returns immediately. Will attempt to initiate a connection to the IRC server,
        retrying until successful or a certain number of tries has been exceeded.
        Will try multiple addresses if provided. Returns after successfully
        initiating connection without waiting for the connection to actually
        complete successfully.

        Internally, this function works as follows:

        A nonblocking socket is opened to the server's IP address and port (DNS
        lookups are performed separately). connect() is called, and then the
        IRCConnection passes itself to the IRCSocketManager and returns.

        The IRCSocketManager adds the socket to a list of connecting sockets, and
        passes the socket in to the write and error lists of select(). If the
        socket comes out in write, the IRCConnection's on_connected() is called;
        if the socket comes out in error, on_error() is called and the
        IRCConnection proceeds with automatic reconnection.
        """

        # Create new BufferedSocket (wraps real socket)
        self.log.debug("Address acquired, creating socket: " +
                       repr(self.address))
        self.socket = SendThrottledSocket(
            socket.AF_INET6 if ':' in self.address[0] else socket.AF_INET,
            parent=self,
            use_ssl=self.ssl)

        if not self.socket.connect(self.address):
            log.info("Error connecting to %s: %s" %
                     (self.address, self.socket.error))
            self.on_connect_failed()
            self.socket = None
            return
        else:
            # Register our connected socket with the socket manager
            IRCSocketManager.register(self.socket)
            self.log.debug("%s successfully began connection to %s" %
                           (repr(self), self.address))

        # Start task to ping server every 3 minutes
        self.ping_task = SyncRepeating(self.send_ping, 180)()

        #assert self.socket is not None

    def disconnect(self):
        """
        Closes the socket without notification to the server. Will not reconnect.
        
        This should only be used when the IRC server is not responding; the proper
        way to disconnect is to send a QUIT command and wait for the server to
        close the connection for us.
        """
        if self.ping_task: self.ping_task.cancel()
        if self.conn_task: self.conn_task.cancel()
        if self.socket:
            self.socket.disconnect()
            self.socket = None
            self.log.debug("Successfully disconnected from %s" %
                           repr(self.address))

    def reconnect(self):
        """
        Convenience method. Calls disconnect() followed by connect().
        """
        self.disconnect()
        self.connect()

    def starttls_begin(self):
        # Send STARTTLS command to server and suspend further writes
        self.socket.suspend('STARTTLS\r\n')

    def starttls_abort(self):
        # Resume writes
        self.socket.resume()

    def starttls_complete(self):
        # initiate SSL handshake and switch socket to SSL mode
        self.socket.starttls()
        self.ssl = True

    def on_connected(self):
        """
        Called by the IRCSocketManager when the socket has successfully connected,
        as indicated by select().
        """
        self.log.debug('Got connection callback for %s' % self.socket)

        self.hooks.connected()

    def on_connect_failed(self):
        """
        Called by the IRCSocketManager if the socket failed to connect.
        """
        self.log.info(
            'Failed to connect to %s with error %s, will retry in 10 seconds' %
            (self.address, self.socket.error))
        # Retry with a new address after 10 seconds
        #AsyncDelayed(self.connect, 10)()
        self.hooks.connect_failed(self)

    def on_error(self):
        """
        Called by the IRCSocketManager when the socket is in error. The socket will
        be closed and a new connection attempt made.
        """
        self.log.info('Network error: disconnected from %s' % (self.address, ))
        # Inform upstream Network of error
        self.hooks.error()
        self.socket = None
        #AsyncDelayed(self.connect, 10)()

    def on_hangup(self):
        """
        Called by the IRCSocketManager after the socket was closed cleanly by the server.
        The server should have sent an ERROR reply with the reason for disconnecting;
        we will decide whether or not to reconnect based on the reason.
        """
        self.log.info('%s has disconnected us.' % (self.address[0], ))
        self.hooks.hangup()
        self.socket = None
        #AsyncDelayed(self.connect, 10)()

    def on_received(self):
        """
        Called by the IRCSocketManager when any data is received on the socket.
        """
        if self.socket:
            for line in self.socket.readlines():
                # Obtain an IRCMessage representation of this line
                msg = self.process(line)

                # Send this message to our parent IRCNetwork (via the Dispatcher) for further processing
                # This moves processing to another thread ASAP so the SocketManager is not interrupted
                if msg:
                    # Get rid of the MessageReceivedEvent and all its IRCNetwork special-casing
                    # in favor of the much simpler Sync method:
                    self.dispatch(msg)
                    #events.MessageReceivedEvent(self.network, msg).dispatch()
        else:
            self.log.debug(
                "WARNING: on_received() called when socket was None!")

    def process(self, line):
        """
        Processes a single line received from the IRC server.
        """

        # From RFC2812 [page 5]:
        #>  The prefix, command, and all parameters are separated
        #>  by one ASCII space character (0x20) each.
        # So we split on single spaces. Multiple spaces will end up delimiting empty parameters.
        # Also note that RFC2812 specifies a maximum of 15 parameters.
        def ssplit(text):
            return filter(lambda x: x, text.split(u' ', 15))

        # Split up into sender, prefix and params
        match = reline.match(line)
        if not match:
            self.log.debug("Received garbage string: %s" % repr(line))
            return
        sender, msgtype, params = match.groups()

        # Respond immediately to a PING message and don't notify downstream
        if msgtype == 'PING':
            self.socket.send_now('PONG %s' % params)
            return
        # Filter out PONG messages that are in response to our own PINGs and don't notify downstream
        elif msgtype == 'PONG' and self.ping in params:
            # we aren't too picky about which param contains the ping token as long as it's there
            self.ping = None
            return

        # Further split parameters and decode character encodings
        if params:
            # Split at : delimiter
            params = params.split(' :',
                                  1) if params[0] != ':' else ('', params[1:])
            # Decode initial parameters with specified server encoding, and split params on spaces
            paramlist = self.encoder.decode_server(params[0]).split(u' ', 15)
            if len(params) > 1:
                # Attempt to decode final "message" parameter (if given) as utf-8 or fallback encoding
                paramlist.append(self.encoder.decode_utf8(params[1]))

        # NOTE that, aside from the potential character encoding, there is nothing special
        # about the final parameter. From RFC2812 [page 7]:
        #>   1) After extracting the parameter list, all parameters are equal
        #>      whether matched by <middle> or <trailing>. <trailing> is just a
        #>      syntactic trick to allow SPACE within the parameter.
        #
        # Therefore, according to the RFC, higher-level code does not need to know or care
        # whether the final parameter was prefixed by ':' or not. Different networks may choose
        # to include or omit this character in cases where the final parameter has no spaces;
        # as an example, here is a raw JOIN message as received from two different networks:
        #
        # A network running UnrealIRCd (a popular IRCd)
        #>          :nick!ident@host JOIN :#channelname
        # Freenode, running ircd-seven
        #>          :nick!ident@host JOIN #channelname
        #
        # So it's important not to treat text after the ':' specially, as some IRC libraries do.
        # If we separated out text after the ':' into a separate paremeter called "message",
        # we would have to process JOIN messages from these two networks differently.
        # Instead, we can simply refer to the final parameter as "message", whether or not
        # it was delimited by a ':' character.

        return IRCMessage(
            self.encoder,
            self.encoder.decode_server(sender) if sender else None,
            self.encoder.decode_server(msgtype),
            paramlist,
            seq=self.sequence)
        # NOTE: From this point on all string handling must be in unicode
        self.sequence += 1

        #self.log.debug(repr(msg))

    def send(self, message, prio=1):
        """
        Sends an IRCMessage (or a string) to the IRC server with the given priority.

        Normal priority should be used for commands/messages sent in response to
        user input, in order to ensure a timely response to the user in the case
        where background messages are queued for delivery.
        """
        if self.socket is None:
            self.log.warn("Attempted to send to a disconnected IRCConnection")
            return

        if isinstance(message, unicode):
            # Encode appropriately
            message = self.encoder.encode_server(message)
        # If message is not a unicode, just assume it can be converted to str.
        # IRCMessage has defined __str__, so it will be fine.
        self.socket.sendline(message, prio)

    def send_lazy(self, message):
        """
        Sends an IRCMessage (or a string) to the IRC server with low priority.

        If messages are being queued, then this command will be queued behind all
        commands of a higher priority.

        This priority should be used for any commands/messages that are sent
        automatically in the background to gather data; i.e. commands where the result
        will be saved for future use, so immediate response is not required. Commands
        sent on-demand in response to user input should use normal priority instead.
        """
        self.send(message, 0)

    def send_urgent(self, message):
        """
        Sends an IRCMessage (or a string) to the IRC server with high priority.

        If send throttling is in effect, this command will be queued ahead of all
        lower-priority commands.
        
        This priority should be used for any vital commands, such as QUIT and
        KICK, that need to be executed ASAP no matter how full the send queue is.
        Use sparingly to ensure urgent commands will not be queued behind earlier
        high-priority commands.
        """
        self.send(message, 2)