all listed channels. Note that the channel portion of the URL need
*not* have a leading '#' unless the channel name itself does.
-Options: -d sets the debug-message level (probably only of interest to
-developers). -l sets a logfile to capture message traffic from
-channels. -n sets the nick and -p the nickserv password. The -V
-option prints the program version and exits.
-
Design and code by Eric S. Raymond <esr@thyrsus.com>. See the project
resource page at <http://www.catb.org/~esr/irker/>.
-Requires Python 2.6 or 2.5 with the simplejson library installed.
+Requires Python 2.7, or:
+* 2.6 with the argparse package installed.
"""
+from __future__ import unicode_literals
from __future__ import with_statement
# These things might need tuning
# No user-serviceable parts below this line
-version = "2.5"
+version = "2.9"
+
+import argparse
+import logging
+import logging.handlers
+import json
+import os
+try: # Python 3
+ import queue
+except ImportError: # Python 2
+ import Queue as queue
+import random
+import re
+import select
+import signal
+import socket
+try: # Python 3
+ import socketserver
+except ImportError: # Python 2
+ import SocketServer as socketserver
+import ssl
+import sys
+import threading
+import time
+import traceback
+try: # Python 3
+ import urllib.parse as urllib_parse
+except ImportError: # Python 2
+ import urlparse as urllib_parse
+
+
+LOG = logging.getLogger(__name__)
+LOG.setLevel(logging.ERROR)
+LOG_LEVELS = ['critical', 'error', 'warning', 'info', 'debug']
+
+try: # Python 2
+ UNICODE_TYPE = unicode
+except NameError: # Python 3
+ UNICODE_TYPE = str
-import sys, getopt, urlparse, time, random, socket, signal, re
-import threading, Queue, SocketServer, select
-try:
- import simplejson as json # Faster, also makes us Python-2.4-compatible
-except ImportError:
- import json
# Sketch of implementation:
#
# same problem - there is little point in reliable delivery to a relay
# that is down or unreliable.
#
-# This code uses only NICK, JOIN, PART, MODE, PRIVMSG, USER, and QUIT.
+# This code uses only NICK, JOIN, PART, MODE, PRIVMSG, USER, and QUIT.
# It is strictly compliant to RFC1459, except for the interpretation and
# use of the DEAF and CHANLIMIT and (obsolete) MAXCHANNELS features.
#
"An IRC exception"
pass
+
+class InvalidRequest(ValueError):
+ "An invalid JSON request"
+ pass
+
+
class IRCClient():
"An IRC client session to one or more servers."
- def __init__(self, debuglevel):
+ def __init__(self):
self.mutex = threading.RLock()
self.server_connections = []
self.event_handlers = {}
self.add_event_handler("ping",
lambda c, e: c.ship("PONG %s" % e.target))
- self.debuglevel = debuglevel
def newserver(self):
"Initialize a new server-connection object."
with self.mutex:
self.server_connections.remove(connection)
- def debug(self, level, errmsg):
- "Debugging information."
- if self.debuglevel >= level:
- sys.stderr.write("irkerd: %s\n" % errmsg)
class LineBufferedStream():
"Line-buffer a read stream."
- crlf_re = re.compile(b'\r?\n')
+ _crlf_re = re.compile(b'\r?\n')
def __init__(self):
- self.buffer = ''
+ self.buffer = b''
def append(self, newbytes):
self.buffer += newbytes
def lines(self):
"Iterate over lines in the buffer."
- lines = LineBufferedStream.crlf_re.split(self.buffer)
+ lines = self._crlf_re.split(self.buffer)
self.buffer = lines.pop()
return iter(lines)
self.master = master
self.socket = None
- def connect(self, server, port, nickname,
- password=None, username=None, ircname=None):
- self.master.debug(2, "connect(server=%r, port=%r, nickname=%r, ...)" %
- (server, port, nickname))
+ def _wrap_socket(self, socket, target, certfile=None, cafile=None,
+ protocol=ssl.PROTOCOL_TLSv1):
+ try: # Python 3.2 and greater
+ ssl_context = ssl.SSLContext(protocol)
+ except AttributeError: # Python < 3.2
+ self.socket = ssl.wrap_socket(
+ socket, certfile=certfile, cert_reqs=ssl.CERT_REQUIRED,
+ ssl_version=protocol, ca_certs=cafile)
+ else:
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
+ if cafile:
+ ssl_context.load_verify_locations(cafile=cafile)
+ else:
+ ssl_context.set_default_verify_paths()
+ kwargs = {}
+ if ssl.HAS_SNI:
+ kwargs['server_hostname'] = target.servername
+ self.socket = ssl_context.wrap_socket(socket, **kwargs)
+ return self.socket
+
+ def _check_hostname(self, target):
+ if hasattr(ssl, 'match_hostname'): # Python >= 3.2
+ cert = self.socket.getpeercert()
+ try:
+ ssl.match_hostname(cert, target.servername)
+ except ssl.CertificateError as e:
+ raise IRCServerConnectionError(
+ 'Invalid SSL/TLS certificate: %s' % e)
+ else: # Python < 3.2
+ LOG.warning(
+ 'cannot check SSL/TLS hostname with Python %s' % sys.version)
+
+ def connect(self, target, nickname, username=None, realname=None,
+ **kwargs):
+ LOG.debug("connect(server=%r, port=%r, nickname=%r, ...)" % (
+ target.servername, target.port, nickname))
if self.socket is not None:
self.disconnect("Changing servers")
self.buffer = LineBufferedStream()
self.event_handlers = {}
self.real_server_name = ""
- self.server = server
- self.port = port
- self.server_address = (server, port)
+ self.target = target
self.nickname = nickname
- self.username = username or nickname
- self.ircname = ircname or nickname
- self.password = password
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ if target.ssl:
+ self.socket = self._wrap_socket(
+ socket=self.socket, target=target, **kwargs)
self.socket.bind(('', 0))
- self.socket.connect(self.server_address)
+ self.socket.connect((target.servername, target.port))
except socket.error as err:
raise IRCServerConnectionError("Couldn't connect to socket: %s" % err)
- if self.password:
- self.ship("PASS " + self.password)
+ if target.ssl:
+ self._check_hostname(target=target)
+ if target.password:
+ self.ship("PASS " + target.password)
self.nick(self.nickname)
- self.user(self.username, self.ircname)
+ self.user(
+ username=target.username or username or 'irker',
+ realname=realname or 'irker relaying client')
return self
def close(self):
self.buffer.append(incoming)
for line in self.buffer:
- self.master.debug(2, "FROM: %s" % line)
+ if not isinstance(line, UNICODE_TYPE):
+ line = UNICODE_TYPE(line, 'utf-8')
+ LOG.debug("FROM: %s" % line)
if not line:
continue
target = arguments[0]
arguments = arguments[1:]
- self.master.debug(2,
- "command: %s, source: %s, target: %s, arguments: %s" % (command, prefix, target, arguments))
+ LOG.debug("command: %s, source: %s, target: %s, arguments: %s" % (
+ command, prefix, target, arguments))
self.handle_event(Event(command, prefix, target, arguments))
def handle_event(self, event):
pass
del self.socket
self.socket = None
- self.handle_event(Event("disconnect", self.server, "", [message]))
+ self.handle_event(
+ Event("disconnect", self.target.server, "", [message]))
def join(self, channel, key=""):
self.ship("JOIN %s%s" % (channel, (key and (" " + key))))
"Ship a command to the server, appending CR/LF"
try:
self.socket.send(string.encode('utf-8') + b'\r\n')
- self.master.debug(2, "TO: %s" % string)
+ LOG.debug("TO: %s" % string)
except socket.error:
self.disconnect("Connection reset by peer.")
return string and string[0] in "#&+!"
class Connection:
- def __init__(self, irkerd, servername, port):
- self.irker = irkerd
- self.servername = servername
- self.port = port
+ def __init__(self, irker, target, nick_template, nick_needs_number=False,
+ password=None, **kwargs):
+ self.irker = irker
+ self.target = target
+ self.nick_template = nick_template
+ self.nick_needs_number = nick_needs_number
+ self.password = password
+ self.kwargs = kwargs
self.nick_trial = None
self.connection = None
self.status = None
self.channels_joined = {}
self.channel_limits = {}
# The consumer thread
- self.queue = Queue.Queue()
+ self.queue = queue.Queue()
self.thread = None
def nickname(self, n=None):
"Return a name for the nth server connection."
if n is None:
n = self.nick_trial
- if fallback:
- return (namestyle % n)
+ if self.nick_needs_number:
+ return self.nick_template % n
else:
- return namestyle
+ return self.nick_template
def handle_ping(self):
"Register the fact that the server has pinged this connection."
self.last_ping = time.time()
def handle_welcome(self):
"The server says we're OK, with a non-conflicting nick."
self.status = "ready"
- self.irker.irc.debug(1, "nick %s accepted" % self.nickname())
- if password:
- self.connection.privmsg("nickserv", "identify %s" % password)
+ LOG.info("nick %s accepted" % self.nickname())
+ if self.password:
+ self.connection.privmsg("nickserv", "identify %s" % self.password)
def handle_badnick(self):
"The server says our nick is ill-formed or has a conflict."
- self.irker.irc.debug(1, "nick %s rejected" % self.nickname())
- if fallback:
+ LOG.info("nick %s rejected" % self.nickname())
+ if self.nick_needs_number:
# Randomness prevents a malicious user or bot from
# anticipating the next trial name in order to block us
# from completing the handshake.
try:
del self.channels_joined[outof]
except KeyError:
- self.irker.logerr("kicked by %s from %s that's not joined"
- % (self.servername, outof))
+ LOG.error("kicked by %s from %s that's not joined" % (
+ self.target, outof))
qcopy = []
while not self.queue.empty():
(channel, message, key) = self.queue.get()
self.status = "expired"
break
elif xmit_timeout or ping_timeout:
- self.irker.irc.debug(1, "timing out connection to %s at %s (ping_timeout=%s, xmit_timeout=%s)" % (self.servername, time.asctime(), ping_timeout, xmit_timeout))
+ LOG.info((
+ "timing out connection to %s at %s "
+ "(ping_timeout=%s, xmit_timeout=%s)") % (
+ self.target, time.asctime(), ping_timeout,
+ xmit_timeout))
with self.irker.irc.mutex:
self.connection.context = None
self.connection.quit("transmission timeout")
try:
# This will throw
# IRCServerConnectionError on failure
- self.connection.connect(self.servername,
- self.port,
- nickname=self.nickname(),
- username="irker",
- ircname="irker relaying client")
+ self.connection.connect(
+ target=self.target,
+ nickname=self.nickname(),
+ **self.kwargs)
self.status = "handshaking"
- self.irker.irc.debug(1, "XMIT_TTL bump (%s connection) at %s" % (self.servername, time.asctime()))
+ LOG.info("XMIT_TTL bump (%s connection) at %s" % (
+ self.target, time.asctime()))
self.last_xmit = time.time()
self.last_ping = time.time()
- except IRCServerConnectionError:
+ except IRCServerConnectionError as e:
+ LOG.error(e)
self.status = "expired"
+ break
elif self.status == "handshaking":
if time.time() > self.last_xmit + HANDSHAKE_TTL:
self.status = "expired"
(channel, message, key) = self.queue.get()
if channel not in self.channels_joined:
self.connection.join(channel, key=key)
- self.irker.irc.debug(1, "joining %s on %s." % (channel, self.servername))
+ LOG.info("joining %s on %s." % (channel, self.target))
# None is magic - it's a request to quit the server
if message is None:
self.connection.quit()
try:
self.connection.privmsg(channel, segment)
except ValueError as err:
- self.irker.irc.debug(1, "irclib rejected a message to %s on %s because: %s" % (channel, self.servername, str(err)))
- self.irker.irc.debug(50, err.format_exc())
+ LOG.warning((
+ "irclib rejected a message to %s on %s "
+ "because: %s") % (
+ channel, self.target, UNICODE_TYPE(err)))
+ LOG.debug(traceback.format_exc())
time.sleep(ANTI_FLOOD_DELAY)
self.last_xmit = self.channels_joined[channel] = time.time()
- self.irker.irc.debug(1, "XMIT_TTL bump (%s transmission) at %s" % (self.servername, time.asctime()))
+ LOG.info("XMIT_TTL bump (%s transmission) at %s" % (
+ self.target, time.asctime()))
self.queue.task_done()
- except:
- (exc_type, _exc_value, exc_traceback) = sys.exc_info()
- self.irker.logerr("exception %s in thread for %s" % \
- (exc_type, self.servername))
-
+ elif self.status == "expired":
+ LOG.error(
+ "We're expired but still running! This is a bug.")
+ break
+ except Exception as e:
+ LOG.error("exception %s in thread for %s" % (e, self.target))
# Maybe this should have its own status?
self.status = "expired"
-
- # This is so we can see tracebacks for errors inside the thread
- # when we need to be able to for debugging purposes.
- if debuglvl > 0:
- raise exc_type, _exc_value, exc_traceback
+ LOG.debug(traceback.format_exc())
finally:
try:
# Make sure we don't leave any zombies behind
class Target():
"Represent a transmission target."
def __init__(self, url):
- # Pre-2.6 Pythons don't recognize irc: as a valid URL prefix.
- url = url.replace("irc://", "http://")
- parsed = urlparse.urlparse(url)
- irchost, _, ircport = parsed.netloc.partition(':')
- if not ircport:
- ircport = 6667
- self.servername = irchost
+ self.url = url
+ parsed = urllib_parse.urlparse(url)
+ self.ssl = parsed.scheme == 'ircs'
+ if self.ssl:
+ default_ircport = 6697
+ else:
+ default_ircport = 6667
+ self.username = parsed.username
+ self.password = parsed.password
+ self.servername = parsed.hostname
+ self.port = parsed.port or default_ircport
# IRC channel names are case-insensitive. If we don't smash
# case here we may run into problems later. There was a bug
# observed on irc.rizon.net where an irkerd user specified #Channel,
self.key = ""
if parsed.query:
self.key = re.sub("^key=", "", parsed.query)
- self.port = int(ircport)
- def valid(self):
- "Both components must be present for a valid target."
- return self.servername and self.channel
+
+ def __str__(self):
+ "Represent this instance as a string"
+ return self.servername or self.url or repr(self)
+
+ def validate(self):
+ "Raise InvalidRequest if the URL is missing a critical component"
+ if not self.servername:
+ raise InvalidRequest(
+ 'target URL missing a servername: %r' % self.url)
+ if not self.channel:
+ raise InvalidRequest(
+ 'target URL missing a channel: %r' % self.url)
def server(self):
"Return a hashable tuple representing the destination server."
return (self.servername, self.port)
class Dispatcher:
"Manage connections to a particular server-port combination."
- def __init__(self, irkerd, servername, port):
- self.irker = irkerd
- self.servername = servername
- self.port = port
+ def __init__(self, irker, **kwargs):
+ self.irker = irker
+ self.kwargs = kwargs
self.connections = []
def dispatch(self, channel, message, key, quit_after=False):
"Dispatch messages for our server-port combination."
if age < time.time() - CHANNEL_TTL:
ancients.append((connection, chan, age))
if ancients:
- ancients.sort(key=lambda x: x[2])
+ ancients.sort(key=lambda x: x[2])
(found_connection, drop_channel, _drop_age) = ancients[0]
found_connection.part(drop_channel, "scavenged by irkerd")
del found_connection.channels_joined[drop_channel]
#time.sleep(ANTI_FLOOD_DELAY)
found_connection.enqueue(channel, message, key, quit_after)
return
- # Didn't find any channels with no recent activity
- newconn = Connection(self.irker,
- self.servername,
- self.port)
+ # All existing channels had recent activity
+ newconn = Connection(self.irker, **self.kwargs)
self.connections.append(newconn)
newconn.enqueue(channel, message, key, quit_after)
def live(self):
class Irker:
"Persistent IRC multiplexer."
- def __init__(self, debuglevel=0):
- self.debuglevel = debuglevel
- self.irc = IRCClient(self.debuglevel)
+ def __init__(self, logfile=None, **kwargs):
+ self.logfile = logfile
+ self.kwargs = kwargs
+ self.irc = IRCClient()
self.irc.add_event_handler("ping", self._handle_ping)
self.irc.add_event_handler("welcome", self._handle_welcome)
self.irc.add_event_handler("erroneusnickname", self._handle_badnick)
thread.setDaemon(True)
self.irc._thread = thread
thread.start()
- def logerr(self, errmsg):
- "Log a processing error."
- sys.stderr.write("irkerd: " + errmsg + "\n")
def _handle_ping(self, connection, _event):
"PING arrived, bump the last-received time for the connection."
if connection.context:
arguments = event.arguments
for lump in arguments:
if lump.startswith("DEAF="):
- if not logfile:
+ if not self.logfile:
connection.mode(cxt.nickname(), "+"+lump[5:])
elif lump.startswith("MAXCHANNELS="):
m = int(lump[12:])
for pref in "#&+":
cxt.channel_limits[pref] = m
- self.irc.debug(1, "%s maxchannels is %d"
- % (connection.server, m))
+ LOG.info("%s maxchannels is %d" % (connection.target, m))
elif lump.startswith("CHANLIMIT=#:"):
limits = lump[10:].split(",")
try:
limit = int(limit)
for c in prefixes:
cxt.channel_limits[c] = limit
- self.irc.debug(1, "%s channel limit map is %s"
- % (connection.server, cxt.channel_limits))
+ LOG.info("%s channel limit map is %s" % (
+ connection.target, cxt.channel_limits))
except ValueError:
- self.logerr("ill-formed CHANLIMIT property")
+ LOG.error("ill-formed CHANLIMIT property")
def _handle_disconnect(self, connection, _event):
"Server hung up the connection."
- self.irc.debug(1, "server %s disconnected" % connection.server)
+ LOG.info("server %s disconnected" % connection.target)
connection.close()
if connection.context:
connection.context.handle_disconnect()
def _handle_kick(self, connection, event):
"Server hung up the connection."
target = event.target
- self.irc.debug(1, "irker has been kicked from %s on %s" % (target, connection.server))
+ LOG.info("irker has been kicked from %s on %s" % (
+ target, connection.target))
if connection.context:
connection.context.handle_kick(target)
def _handle_every_raw_message(self, _connection, event):
"Log all messages when in watcher mode."
- if logfile:
- with open(logfile, "a") as logfp:
+ if self.logfile:
+ with open(self.logfile, "a") as logfp:
logfp.write("%03f|%s|%s\n" % \
(time.time(), event.source, event.arguments[0]))
def pending(self):
"Do we have any pending message traffic?"
return [k for (k, v) in self.servers.items() if v.pending()]
+
+ def _parse_request(self, line):
+ "Request-parsing helper for the handle() method"
+ request = json.loads(line.strip())
+ if not isinstance(request, dict):
+ raise InvalidRequest(
+ "request is not a JSON dictionary: %r" % request)
+ if "to" not in request or "privmsg" not in request:
+ raise InvalidRequest(
+ "malformed request - 'to' or 'privmsg' missing: %r" % request)
+ channels = request['to']
+ message = request['privmsg']
+ if not isinstance(channels, (list, UNICODE_TYPE)):
+ raise InvalidRequest(
+ "malformed request - unexpected channel type: %r" % channels)
+ if not isinstance(message, UNICODE_TYPE):
+ raise InvalidRequest(
+ "malformed request - unexpected message type: %r" % message)
+ if not isinstance(channels, list):
+ channels = [channels]
+ targets = []
+ for url in channels:
+ try:
+ if not isinstance(url, UNICODE_TYPE):
+ raise InvalidRequest(
+ "malformed request - URL has unexpected type: %r" %
+ url)
+ target = Target(url)
+ target.validate()
+ except InvalidRequest as e:
+ LOG.error(UNICODE_TYPE(e))
+ else:
+ targets.append(target)
+ return (targets, message)
+
def handle(self, line, quit_after=False):
"Perform a JSON relay request."
try:
- request = json.loads(line.strip())
- if not isinstance(request, dict):
- self.logerr("request is not a JSON dictionary: %r" % request)
- elif "to" not in request or "privmsg" not in request:
- self.logerr("malformed request - 'to' or 'privmsg' missing: %r" % request)
- else:
- channels = request['to']
- message = request['privmsg']
- if not isinstance(channels, (list, basestring)):
- self.logerr("malformed request - unexpected channel type: %r" % channels)
- if not isinstance(message, basestring):
- self.logerr("malformed request - unexpected message type: %r" % message)
- else:
- if not isinstance(channels, list):
- channels = [channels]
- for url in channels:
- if not isinstance(url, basestring):
- self.logerr("malformed request - URL has unexpected type: %r" % url)
- else:
- target = Target(url)
- if not target.valid():
- return
- if target.server() not in self.servers:
- self.servers[target.server()] = Dispatcher(self, target.servername, target.port)
- self.servers[target.server()].dispatch(target.channel, message, target.key, quit_after=quit_after)
- # GC dispatchers with no active connections
- servernames = self.servers.keys()
- for servername in servernames:
- if not self.servers[servername].live():
- del self.servers[servername]
- # If we might be pushing a resource limit
- # even after garbage collection, remove a
- # session. The goal here is to head off
- # DoS attacks that aim at exhausting
- # thread space or file descriptors. The
- # cost is that attempts to DoS this
- # service will cause lots of join/leave
- # spam as we scavenge old channels after
- # connecting to new ones. The particular
- # method used for selecting a session to
- # be terminated doesn't matter much; we
- # choose the one longest idle on the
- # assumption that message activity is likely
- # to be clumpy.
- if len(self.servers) >= CONNECTION_MAX:
- oldest = min(self.servers.keys(), key=lambda name: self.servers[name].last_xmit())
- del self.servers[oldest]
+ targets, message = self._parse_request(line=line)
+ for target in targets:
+ if target.server() not in self.servers:
+ self.servers[target.server()] = Dispatcher(
+ self, target=target, **self.kwargs)
+ self.servers[target.server()].dispatch(
+ target.channel, message, target.key, quit_after=quit_after)
+ # GC dispatchers with no active connections
+ servernames = self.servers.keys()
+ for servername in servernames:
+ if not self.servers[servername].live():
+ del self.servers[servername]
+ # If we might be pushing a resource limit even
+ # after garbage collection, remove a session. The
+ # goal here is to head off DoS attacks that aim at
+ # exhausting thread space or file descriptors.
+ # The cost is that attempts to DoS this service
+ # will cause lots of join/leave spam as we
+ # scavenge old channels after connecting to new
+ # ones. The particular method used for selecting a
+ # session to be terminated doesn't matter much; we
+ # choose the one longest idle on the assumption
+ # that message activity is likely to be clumpy.
+ if len(self.servers) >= CONNECTION_MAX:
+ oldest = min(
+ self.servers.keys(),
+ key=lambda name: self.servers[name].last_xmit())
+ del self.servers[oldest]
+ except InvalidRequest as e:
+ LOG.error(UNICODE_TYPE(e))
except ValueError:
self.logerr("can't recognize JSON on input: %r" % line)
except RuntimeError:
self.logerr("wildly malformed JSON blew the parser stack.")
-class IrkerTCPHandler(SocketServer.StreamRequestHandler):
+class IrkerTCPHandler(socketserver.StreamRequestHandler):
def handle(self):
while True:
line = self.rfile.readline()
if not line:
break
- irker.handle(line.strip())
+ if not isinstance(line, UNICODE_TYPE):
+ line = UNICODE_TYPE(line, 'utf-8')
+ irker.handle(line=line.strip())
-class IrkerUDPHandler(SocketServer.BaseRequestHandler):
+class IrkerUDPHandler(socketserver.BaseRequestHandler):
def handle(self):
- data = self.request[0].strip()
+ line = self.request[0].strip()
#socket = self.request[1]
- irker.handle(data)
-
-def usage():
- sys.stdout.write("""
-Usage:
- irkerd [-d debuglevel] [-l logfile] [-n nick] [-p password] [-i channel message] [-V] [-h]
-
-Options
- -d set debug level
- -l set logfile
- -n set nick-style
- -p set nickserv password
- -i immediate mode
- -V return irkerd version
- -h print this help dialog
-""")
+ if not isinstance(line, UNICODE_TYPE):
+ line = UNICODE_TYPE(line, 'utf-8')
+ irker.handle(line=line.strip())
-if __name__ == '__main__':
- debuglvl = 0
- immediate = None
- namestyle = "irker%03d"
- password = None
- logfile = None
+def in_background():
+ "Is this process running in background?"
try:
- (options, arguments) = getopt.getopt(sys.argv[1:], "d:i:l:n:p:Vh")
- except getopt.GetoptError as e:
- sys.stderr.write("%s" % e)
- usage()
- sys.exit(1)
- for (opt, val) in options:
- if opt == '-d': # Enable debug/progress messages
- debuglvl = int(val)
- elif opt == '-i': # Immediate mode - send one message, then exit.
- immediate = val
- elif opt == '-l': # Logfile mode - report traffic read in
- logfile = val
- elif opt == '-n': # Force the nick
- namestyle = val
- elif opt == '-p': # Set a nickserv password
- password = val
- elif opt == '-V': # Emit version and exit
- sys.stdout.write("irkerd version %s\n" % version)
- sys.exit(0)
- elif opt == '-h':
- usage()
- sys.exit(0)
- fallback = re.search("%.*d", namestyle)
- irker = Irker(debuglevel=debuglvl)
- irker.irc.debug(1, "irkerd version %s" % version)
- if immediate:
+ return os.getpgrp() != os.tcgetpgrp(1)
+ except OSError:
+ return True
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(
+ description=__doc__.strip().splitlines()[0])
+ parser.add_argument(
+ '-c', '--ca-file', metavar='PATH',
+ help='file of trusted certificates for SSL/TLS')
+ parser.add_argument(
+ '-e', '--cert-file', metavar='PATH',
+ help='pem file used to authenticate to the server')
+ parser.add_argument(
+ '-d', '--log-level', metavar='LEVEL', choices=LOG_LEVELS,
+ help='how much to log to the log file (one of %(choices)s)')
+ parser.add_argument(
+ '-H', '--host', metavar='ADDRESS', default=HOST,
+ help='IP address to listen on')
+ parser.add_argument(
+ '-l', '--log-file', metavar='PATH',
+ help='file for saving captured message traffic')
+ parser.add_argument(
+ '-n', '--nick', metavar='NAME', default='irker%03d',
+ help="nickname (optionally with a '%%.*d' server connection marker)")
+ parser.add_argument(
+ '-p', '--password', metavar='PASSWORD',
+ help='NickServ password')
+ parser.add_argument(
+ '-i', '--immediate', metavar='IRC-URL',
+ help=(
+ 'send a single message to IRC-URL and exit. The message is the '
+ 'first positional argument.'))
+ parser.add_argument(
+ '-V', '--version', action='version',
+ version='%(prog)s {0}'.format(version))
+ parser.add_argument(
+ 'message', metavar='MESSAGE', nargs='?',
+ help='message for --immediate mode')
+ args = parser.parse_args()
+
+ if not args.log_file and in_background():
+ handler = logging.handlers.SysLogHandler(address='/dev/log',
+ facility='daemon')
+ else:
+ handler = logging.StreamHandler()
+
+ LOG.addHandler(handler)
+ if args.log_level:
+ log_level = getattr(logging, args.log_level.upper())
+ LOG.setLevel(log_level)
+
+ irker = Irker(
+ logfile=args.log_file,
+ nick_template=args.nick,
+ nick_needs_number=re.search('%.*d', args.nick),
+ password=args.password,
+ cafile=args.ca_file,
+ certfile=args.cert_file,
+ )
+ LOG.info("irkerd version %s" % version)
+ if args.immediate:
+ if not args.message:
+ LOG.error(
+ '--immediate set (%r), but message argument not given' % (
+ args.immediate))
+ raise SystemExit(1)
irker.irc.add_event_handler("quit", lambda _c, _e: sys.exit(0))
- irker.handle('{"to":"%s","privmsg":"%s"}' % (immediate, arguments[0]), quit_after=True)
+ irker.handle('{"to":"%s","privmsg":"%s"}' % (
+ args.immediate, args.message), quit_after=True)
irker.irc.spin()
else:
+ if args.message:
+ LOG.error(
+ 'message argument given (%r), but --immediate not set' % (
+ args.message))
+ raise SystemExit(1)
irker.thread_launch()
try:
- tcpserver = SocketServer.TCPServer((HOST, PORT), IrkerTCPHandler)
- udpserver = SocketServer.UDPServer((HOST, PORT), IrkerUDPHandler)
+ tcpserver = socketserver.TCPServer((args.host, PORT), IrkerTCPHandler)
+ udpserver = socketserver.UDPServer((args.host, PORT), IrkerUDPHandler)
for server in [tcpserver, udpserver]:
server = threading.Thread(target=server.serve_forever)
server.setDaemon(True)
signal.pause()
except KeyboardInterrupt:
raise SystemExit(1)
- except socket.error, e:
- sys.stderr.write("irkerd: server launch failed: %r\n" % e)
+ except socket.error as e:
+ LOG.error("server launch failed: %r\n" % e)
# end