#!/usr/bin/env python
+from __future__ import with_statement
"""
irkerd - a simple IRC multiplexer daemon
*not* have a leading '#' unless the channel name itself does.
Options: -d sets the debug-message level (probably only of interest to
-developers). The -V option prints the program version and exits.
+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/>.
HOST = "localhost"
PORT = 6659
-NAMESTYLE = "irker%03d" # IRC nick template - must contain '%d'
XMIT_TTL = (3 * 60 * 60) # Time to live, seconds from last transmit
PING_TTL = (15 * 60) # Time to live, seconds from last PING
HANDSHAKE_TTL = 60 # Time to live, seconds from nick transmit
# No user-serviceable parts below this line
-version = "1.14"
+version = "1.18"
-import sys, getopt, urlparse, time, random, socket, signal
+import sys, getopt, urlparse, time, random, socket, signal, re
import threading, Queue, SocketServer
import irc.client, logging
try:
"Return a name for the nth server connection."
if n is None:
n = self.nick_trial
- return (NAMESTYLE % n)
+ if fallback:
+ return (namestyle % n)
+ else:
+ return namestyle
def handle_ping(self):
"Register the fact that the server has pinged this connection."
self.last_ping = time.time()
"The server says we're OK, with a non-conflicting nick."
self.status = "ready"
self.irker.debug(1, "nick %s accepted" % self.nickname())
+ if password:
+ self.connection.privmsg("nickserv", "identify %s" % password)
def handle_badnick(self):
- "The server says our nick has a conflict."
+ "The server says our nick is ill-formed or has a conflict."
self.irker.debug(1, "nick %s rejected" % self.nickname())
- # Randomness prevents a malicious user or bot from antcipating the
- # next trial name in order to block us from completing the handshake.
- self.nick_trial += random.randint(1, 3)
- self.last_xmit = time.time()
- self.connection.nick(self.nickname())
+ if fallback:
+ # Randomness prevents a malicious user or bot from
+ # anticipating the next trial name in order to block us
+ # from completing the handshake.
+ self.nick_trial += random.randint(1, 3)
+ self.last_xmit = time.time()
+ self.connection.nick(self.nickname())
+ # Otherwise fall through, it might be possible to
+ # recover manually.
def handle_disconnect(self):
"Server disconnected us for flooding or some other reason."
self.connection = None
if channel not in self.channels_joined:
self.connection.join(channel)
self.irker.debug(1, "joining %s on %s." % (channel, self.servername))
- for segment in message.split("\n"):
- self.connection.privmsg(channel, segment)
- time.sleep(ANTI_FLOOD_DELAY)
+ # An empty message might be used as a keepalive or
+ # to join a channel for logging, so suppress the
+ # privmsg send unless there is actual traffic.
+ if message:
+ for segment in message.split("\n"):
+ # Truncate the message if it's too long,
+ # but we're working with characters here,
+ # not bytes, so we could be off.
+ # 500 = 512 - CRLF - 'PRIVMSG ' - ' :'
+ maxlength = 500 - len(channel)
+ if len(segment) > maxlength:
+ segment = segment[:maxlength]
+ try:
+ self.connection.privmsg(channel, segment)
+ except ValueError as err:
+ self.irker.debug(1, "irclib rejected a message to %s on %s because: %s" % (channel, self.servername, str(err)))
+ time.sleep(ANTI_FLOOD_DELAY)
self.last_xmit = self.channels_joined[channel] = time.time()
self.irker.debug(1, "XMIT_TTL bump (%s transmission) at %s" % (self.servername, time.asctime()))
self.queue.task_done()
(exc_type, _exc_value, exc_traceback) = sys.exc_info()
self.irker.logerr("exception %s in thread for %s" % \
(exc_type, self.servername))
+
+ # 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
- else:
- # Maybe this should have its own status?
- self.status = "expired"
+ finally:
+ try:
+ # Make sure we don't leave any zombies behind
+ self.connection.close()
+ except:
+ # Irclib has a habit of throwing fresh exceptions here. Ignore that
+ pass
def live(self):
"Should this connection not be scavenged?"
return self.status != "expired"
self.irc.add_global_handler("featurelist", self._handle_features)
self.irc.add_global_handler("disconnect", self._handle_disconnect)
self.irc.add_global_handler("kick", self._handle_kick)
+ self.irc.add_global_handler("all_raw_messages", self._handle_all_raw_messages)
thread = threading.Thread(target=self.irc.process_forever)
thread.setDaemon(True)
self.irc._thread = thread
arguments = arguments()
for lump in arguments:
if lump.startswith("DEAF="):
- connection.mode(cxt.nickname(), "+"+lump[5:])
+ if not logfile:
+ connection.mode(cxt.nickname(), "+"+lump[5:])
elif lump.startswith("MAXCHANNELS="):
m = int(lump[12:])
for pref in "#&+":
def _handle_kick(self, connection, event):
"Server hung up the connection."
target = event.target
- # irclib 5.0 compatibility, because the maintainer continues to be a
- # fool.
+ # irclib 5.0 compatibility, because the maintainer continues
+ # to be a fool.
if callable(target):
target = target()
self.debug(1, "irker has been kicked from %s on %s" % (target, connection.server))
if connection.context:
connection.context.handle_kick(target)
+ def _handle_all_raw_messages(self, _connection, event):
+ "Log all messages when in watcher mode."
+ if logfile:
+ with open(logfile, "a") as logfp:
+ logfp.write("%03f|%s|%s\n" % \
+ (time.time(), event.source, event.arguments[0]))
def handle(self, line):
"Perform a JSON relay request."
try:
if __name__ == '__main__':
debuglvl = 0
- (options, arguments) = getopt.getopt(sys.argv[1:], "d:V")
+ namestyle = "irker%03d"
+ password = None
+ logfile = None
+ (options, arguments) = getopt.getopt(sys.argv[1:], "d:l:n:p:V:")
for (opt, val) in options:
if opt == '-d': # Enable debug/progress messages
debuglvl = int(val)
if debuglvl > 1:
logging.basicConfig(level=logging.DEBUG)
+ 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)
+ fallback = re.search("%.*d", namestyle)
irker = Irker(debuglevel=debuglvl)
irker.debug(1, "irkerd version %s" % version)
try: