* 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.6"
+version = "2.9"
import argparse
import logging
+import logging.handlers
import json
+import os
try: # Python 3
import queue
except ImportError: # Python 2
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
LOG.setLevel(logging.ERROR)
LOG_LEVELS = ['critical', 'error', 'warning', 'info', 'debug']
+try: # Python 2
+ UNICODE_TYPE = unicode
+except NameError: # Python 3
+ UNICODE_TYPE = str
+
# 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.
#
pass
-class InvalidRequest (ValueError):
+class InvalidRequest(ValueError):
"An invalid JSON request"
pass
_crlf_re = re.compile(b'\r?\n')
def __init__(self):
- self.buffer = ''
+ self.buffer = b''
def append(self, newbytes):
self.buffer += newbytes
self.master = master
self.socket = None
- def connect(self, target, nickname,
- password=None, username=None, ircname=None):
+ 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.nickname = nickname
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((target.servername, target.port))
except socket.error as err:
raise IRCServerConnectionError("Couldn't connect to socket: %s" % err)
- if password:
- self.ship("PASS " + password)
+ if target.ssl:
+ self._check_hostname(target=target)
+ if target.password:
+ self.ship("PASS " + target.password)
self.nick(self.nickname)
- self.user(username=username or ircname, realname=ircname or nickname)
+ 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:
+ if not isinstance(line, UNICODE_TYPE):
+ line = UNICODE_TYPE(line, 'utf-8')
LOG.debug("FROM: %s" % line)
if not line:
if n is None:
n = self.nick_trial
if self.nick_needs_number:
- return (self.nick_template % n)
+ return self.nick_template % n
else:
return self.nick_template
def handle_ping(self):
self.connection.connect(
target=self.target,
nickname=self.nickname(),
- username="irker",
- ircname="irker relaying client",
**self.kwargs)
self.status = "handshaking"
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":
LOG.warning((
"irclib rejected a message to %s on %s "
"because: %s") % (
- channel, self.target, str(err)))
- LOG.debug(err.format_exc())
+ 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()
LOG.info("XMIT_TTL bump (%s transmission) at %s" % (
LOG.error("exception %s in thread for %s" % (e, self.target))
# Maybe this should have its own status?
self.status = "expired"
- LOG.debug(e.format_exc())
+ LOG.debug(traceback.format_exc())
finally:
try:
# Make sure we don't leave any zombies behind
def __init__(self, url):
self.url = url
parsed = urllib_parse.urlparse(url)
- irchost, _, ircport = parsed.netloc.partition(':')
- if not ircport:
- ircport = 6667
- self.servername = irchost
+ 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 __str__(self):
"Represent this instance as a string"
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]
m = int(lump[12:])
for pref in "#&+":
cxt.channel_limits[pref] = m
- LOG.info("%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:
"malformed request - 'to' or 'privmsg' missing: %r" % request)
channels = request['to']
message = request['privmsg']
- if not isinstance(channels, (list, basestring)):
+ if not isinstance(channels, (list, UNICODE_TYPE)):
raise InvalidRequest(
"malformed request - unexpected channel type: %r" % channels)
- if not isinstance(message, basestring):
+ if not isinstance(message, UNICODE_TYPE):
raise InvalidRequest(
"malformed request - unexpected message type: %r" % message)
if not isinstance(channels, list):
targets = []
for url in channels:
try:
- if not isinstance(url, basestring):
+ 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(str(e))
+ LOG.error(UNICODE_TYPE(e))
else:
targets.append(target)
return (targets, message)
key=lambda name: self.servers[name].last_xmit())
del self.servers[oldest]
except InvalidRequest as e:
- LOG.error(str(e))
+ LOG.error(UNICODE_TYPE(e))
except ValueError:
self.logerr("can't recognize JSON on input: %r" % line)
except RuntimeError:
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):
def handle(self):
- data = self.request[0].strip()
+ line = self.request[0].strip()
#socket = self.request[1]
- irker.handle(data)
+ if not isinstance(line, UNICODE_TYPE):
+ line = UNICODE_TYPE(line, 'utf-8')
+ irker.handle(line=line.strip())
+def in_background():
+ "Is this process running in background?"
+ try:
+ return os.getpgrp() != os.tcgetpgrp(1)
+ except OSError:
+ return True
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__doc__.strip().splitlines()[0])
parser.add_argument(
- '-d', '--log-level', metavar='LEVEL', choices=LOG_LEVELS,
+ '-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')
'-p', '--password', metavar='PASSWORD',
help='NickServ password')
parser.add_argument(
- '-i', '--immediate', action='store_const', const=True,
- help='disconnect after sending each message')
+ '-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()
- handler = logging.StreamHandler()
+ 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())
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)