Clean up core and add support for TLS client certs

master
mutantmonkey 2017-04-09 22:50:00 -07:00
parent e4d28c0990
commit 538fec692d
3 changed files with 200 additions and 155 deletions

View File

@ -7,7 +7,12 @@ Licensed under the Eiffel Forum License 2.
http://inamidst.com/phenny/ http://inamidst.com/phenny/
""" """
import sys, os, time, threading, signal import os
import signal
import sys
import threading
import time
class Watcher(object): class Watcher(object):
# Cf. http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496735 # Cf. http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496735
@ -18,51 +23,65 @@ class Watcher(object):
self.watch() self.watch()
def watch(self): def watch(self):
try: os.wait() try:
os.wait()
except KeyboardInterrupt: except KeyboardInterrupt:
self.kill() self.kill()
sys.exit() sys.exit()
def kill(self): def kill(self):
try: os.kill(self.child, signal.SIGKILL) try:
except OSError: pass os.kill(self.child, signal.SIGKILL)
except OSError:
pass
def sig_term(self, signum, frame): def sig_term(self, signum, frame):
self.kill() self.kill()
sys.exit() sys.exit()
def run_phenny(config): def run_phenny(config):
if hasattr(config, 'delay'): if hasattr(config, 'delay'):
delay = config.delay delay = config.delay
else: delay = 20 else:
delay = 20
def connect(config): def connect(config):
import bot import bot
p = bot.Phenny(config) p = bot.Phenny(config)
p.run(config.host, config.port, config.ssl, config.ipv6,
config.ca_certs)
try: Watcher() ssl_context = p.get_ssl_context(config.ca_certs)
if config.ssl_cert and config.ssl_key:
ssl_context.load_cert_chain(config.ssl_cert, config.ssl_key)
p.run(config.host, config.port, config.ssl, config.ipv6, None,
ssl_context)
try:
Watcher()
except Exception as e: except Exception as e:
print('Warning:', e, '(in __init__.py)', file=sys.stderr) print('Warning:', e, '(in __init__.py)', file=sys.stderr)
while True: while True:
try: connect(config) try:
connect(config)
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit() sys.exit()
if not isinstance(delay, int): if not isinstance(delay, int):
break break
warning = 'Warning: Disconnected. Reconnecting in %s seconds...' % delay msg = "Warning: Disconnected. Reconnecting in {0} seconds..."
print(warning, file=sys.stderr) print(msg.format(delay), file=sys.stderr)
time.sleep(delay) time.sleep(delay)
def run(config): def run(config):
t = threading.Thread(target=run_phenny, args=(config,)) t = threading.Thread(target=run_phenny, args=(config,))
if hasattr(t, 'run'): if hasattr(t, 'run'):
t.run() t.run()
else: t.start() else:
t.start()
if __name__ == '__main__': if __name__ == '__main__':
print(__doc__) print(__doc__)

77
irc.py
View File

@ -7,9 +7,13 @@ Licensed under the Eiffel Forum License 2.
http://inamidst.com/phenny/ http://inamidst.com/phenny/
""" """
import sys, re, time, traceback import asynchat
import socket, asyncore, asynchat import asyncore
import re
import socket
import ssl import ssl
import sys
import time
class Origin(object): class Origin(object):
@ -23,7 +27,8 @@ class Origin(object):
if len(args) > 1: if len(args) > 1:
target = args[1] target = args[1]
else: target = None else:
target = None
mappings = {bot.nick: self.nick, None: None} mappings = {bot.nick: self.nick, None: None}
self.sender = mappings.get(target, target) self.sender = mappings.get(target, target)
@ -82,14 +87,19 @@ class Bot(asynchat.async_chat):
self.__write(args, text) self.__write(args, text)
except Exception as e: except Exception as e:
raise raise
#pass
def run(self, host, port=6667, ssl=False, def run(self, host, port=6667, ssl=False, ipv6=False, ca_certs=None,
ipv6=False, ca_certs=None): ssl_context=None):
self.ca_certs = ca_certs if ssl_context is None:
self.initiate_connect(host, port, ssl, ipv6) ssl_context = self.get_ssl_context(ca_certs)
self.initiate_connect(host, port, ssl, ipv6, ssl_context)
def initiate_connect(self, host, port, use_ssl, ipv6): def get_ssl_context(self, ca_certs):
return ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH,
cafile=ca_certs)
def initiate_connect(self, host, port, use_ssl, ipv6, ssl_context):
if self.verbose: if self.verbose:
message = 'Connecting to %s:%s...' % (host, port) message = 'Connecting to %s:%s...' % (host, port)
print(message, end=' ', file=sys.stderr) print(message, end=' ', file=sys.stderr)
@ -97,30 +107,19 @@ class Bot(asynchat.async_chat):
af = socket.AF_INET6 af = socket.AF_INET6
else: else:
af = socket.AF_INET af = socket.AF_INET
self.create_socket(af, socket.SOCK_STREAM, use_ssl, host) self.create_socket(af, socket.SOCK_STREAM, use_ssl, host, ssl_context)
self.connect((host, port)) self.connect((host, port))
try: asyncore.loop() try:
asyncore.loop()
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit() sys.exit()
def create_socket(self, family, type, use_ssl=False, hostname=None): def create_socket(self, family, type, use_ssl=False, hostname=None,
ssl_context=None):
self.family_and_type = family, type self.family_and_type = family, type
sock = socket.socket(family, type) sock = socket.socket(family, type)
if use_ssl: if use_ssl:
# this stuff is all new in python 3.4, so fallback if needed sock = ssl_context.wrap_socket(sock, server_hostname=hostname)
try:
context = ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH,
cafile=self.ca_certs)
sock = context.wrap_socket(sock, server_hostname=hostname)
except:
if self.ca_certs is None:
# default to standard path on most non-EL distros
ca_certs = "/etc/ssl/certs/ca-certificates.crt"
else:
ca_certs = self.ca_certs
sock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_TLSv1,
cert_reqs=ssl.CERT_REQUIRED, ca_certs=ca_certs)
# FIXME: this doesn't work with SSL enabled # FIXME: this doesn't work with SSL enabled
#sock.setblocking(False) #sock.setblocking(False)
self.set_socket(sock) self.set_socket(sock)
@ -153,11 +152,13 @@ class Bot(asynchat.async_chat):
if line.startswith(':'): if line.startswith(':'):
source, line = line[1:].split(' ', 1) source, line = line[1:].split(' ', 1)
else: source = None else:
source = None
if ' :' in line: if ' :' in line:
argstr, text = line.split(' :', 1) argstr, text = line.split(' :', 1)
else: argstr, text = line, '' else:
argstr, text = line, ''
args = argstr.split() args = argstr.split()
origin = Origin(self, source, args) origin = Origin(self, source, args)
@ -174,11 +175,13 @@ class Bot(asynchat.async_chat):
# Cf. http://swhack.com/logs/2006-03-01#T19-43-25 # Cf. http://swhack.com/logs/2006-03-01#T19-43-25
if isinstance(text, str): if isinstance(text, str):
try: text = text.encode('utf-8') try:
text = text.encode('utf-8')
except UnicodeEncodeError as e: except UnicodeEncodeError as e:
text = e.__class__ + ': ' + str(e) text = e.__class__ + ': ' + str(e)
if isinstance(recipient, str): if isinstance(recipient, str):
try: recipient = recipient.encode('utf-8') try:
recipient = recipient.encode('utf-8')
except UnicodeEncodeError as e: except UnicodeEncodeError as e:
return return
@ -244,20 +247,23 @@ class Bot(asynchat.async_chat):
if line.startswith('File "/'): if line.startswith('File "/'):
report.append(line[0].lower() + line[1:]) report.append(line[0].lower() + line[1:])
break break
else: report.append('source unknown') else:
report.append('source unknown')
self.msg(origin.sender, report[0] + ' (' + report[1] + ')') self.msg(origin.sender, report[0] + ' (' + report[1] + ')')
except: self.msg(origin.sender, "Got an error.") except:
self.msg(origin.sender, "Got an error.")
class TestBot(Bot): class TestBot(Bot):
def f_ping(self, origin, match, args): def f_ping(self, origin, match, args):
delay = m.group(1) delay = match.group(1)
if delay is not None: if delay is not None:
import time import time
time.sleep(int(delay)) time.sleep(int(delay))
self.msg(origin.sender, 'pong (%s)' % delay) self.msg(origin.sender, 'pong (%s)' % delay)
else: self.msg(origin.sender, 'pong') else:
self.msg(origin.sender, 'pong')
f_ping.rule = r'^\.ping(?:[ \t]+(\d+))?$' f_ping.rule = r'^\.ping(?:[ \t]+(\d+))?$'
@ -266,5 +272,6 @@ def main():
bot.run('irc.freenode.net') bot.run('irc.freenode.net')
print(__doc__) print(__doc__)
if __name__=="__main__":
if __name__ == "__main__":
main() main()

33
phenny
View File

@ -11,18 +11,22 @@ Run ./phenny, then edit ~/.phenny/default.py
Then run ./phenny again Then run ./phenny again
""" """
import sys, os, imp
import argparse import argparse
import imp
import os
import sys
from textwrap import dedent as trim from textwrap import dedent as trim
dotdir = os.path.expanduser('~/.phenny') dotdir = os.path.expanduser('~/.phenny')
def check_python_version(): def check_python_version():
if sys.version_info < (3, 0): if sys.version_info < (3, 4):
error = 'Error: Requires Python 3.0 or later, from www.python.org' error = 'Error: Requires Python 3.4 or later, from www.python.org'
print(error, file=sys.stderr) print(error, file=sys.stderr)
sys.exit(1) sys.exit(1)
def create_default_config(fn): def create_default_config(fn):
f = open(fn, 'w') f = open(fn, 'w')
print(trim("""\ print(trim("""\
@ -69,6 +73,7 @@ def create_default_config(fn):
"""), file=f) """), file=f)
f.close() f.close()
def create_default_config_file(dotdir): def create_default_config_file(dotdir):
print('Creating a default config file at ~/.phenny/default.py...') print('Creating a default config file at ~/.phenny/default.py...')
default = os.path.join(dotdir, 'default.py') default = os.path.join(dotdir, 'default.py')
@ -77,9 +82,11 @@ def create_default_config_file(dotdir):
print('Done; now you can edit default.py, and run phenny! Enjoy.') print('Done; now you can edit default.py, and run phenny! Enjoy.')
sys.exit(0) sys.exit(0)
def create_dotdir(dotdir): def create_dotdir(dotdir):
print('Creating a config directory at ~/.phenny...') print('Creating a config directory at ~/.phenny...')
try: os.mkdir(dotdir) try:
os.mkdir(dotdir)
except Exception as e: except Exception as e:
print('There was a problem creating %s:' % dotdir, file=sys.stderr) print('There was a problem creating %s:' % dotdir, file=sys.stderr)
print(e.__class__, str(e), file=sys.stderr) print(e.__class__, str(e), file=sys.stderr)
@ -88,6 +95,7 @@ def create_dotdir(dotdir):
create_default_config_file(dotdir) create_default_config_file(dotdir)
def check_dotdir(): def check_dotdir():
default = os.path.join(dotdir, 'default.py') default = os.path.join(dotdir, 'default.py')
@ -96,6 +104,7 @@ def check_dotdir():
elif not os.path.isfile(default): elif not os.path.isfile(default):
create_default_config_file(dotdir) create_default_config_file(dotdir)
def config_names(config): def config_names(config):
config = config or 'default' config = config or 'default'
@ -123,6 +132,7 @@ def config_names(config):
print('What happened to ~/.phenny/default.py?', file=sys.stderr) print('What happened to ~/.phenny/default.py?', file=sys.stderr)
sys.exit(1) sys.exit(1)
def main(argv=None): def main(argv=None):
# Step One: Parse The Command Line # Step One: Parse The Command Line
@ -133,7 +143,7 @@ def main(argv=None):
# Step Two: Check Dependencies # Step Two: Check Dependencies
check_python_version() # require python2.4 or later check_python_version()
if not args.config: if not args.config:
check_dotdir() # require ~/.phenny, or make it and exit check_dotdir() # require ~/.phenny, or make it and exit
@ -160,6 +170,12 @@ def main(argv=None):
if not hasattr(module, 'ca_certs'): if not hasattr(module, 'ca_certs'):
module.ca_certs = None module.ca_certs = None
if not hasattr(module, 'ssl_cert'):
module.ssl_cert = None
if not hasattr(module, 'ssl_key'):
module.ssl_key = None
if not hasattr(module, 'ipv6'): if not hasattr(module, 'ipv6'):
module.ipv6 = False module.ipv6 = False
@ -176,9 +192,11 @@ def main(argv=None):
# Step Four: Load Phenny # Step Four: Load Phenny
try: from __init__ import run try:
from __init__ import run
except ImportError: except ImportError:
try: from phenny import run try:
from phenny import run
except ImportError: except ImportError:
print("Error: Couldn't find phenny to import", file=sys.stderr) print("Error: Couldn't find phenny to import", file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -189,5 +207,6 @@ def main(argv=None):
for config_module in config_modules: for config_module in config_modules:
run(config_module) # @@ thread this run(config_module) # @@ thread this
if __name__ == '__main__': if __name__ == '__main__':
main() main()