From 0ce648a0fd2cc55c18ca9f20b8f67bbb932d3293 Mon Sep 17 00:00:00 2001 From: Simeon Simeonov Date: Wed, 15 Apr 2015 22:34:26 +0200 Subject: beinc_poller.py v.2.0 + few bugfixes --- beinc_poller.py | 221 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 155 insertions(+), 66 deletions(-) (limited to 'beinc_poller.py') diff --git a/beinc_poller.py b/beinc_poller.py index 2bdfe29..b5701ce 100755 --- a/beinc_poller.py +++ b/beinc_poller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Blackmore's Enhanced IRC-Notification Collection (BEINC) v1.0 +# Blackmore's Enhanced IRC-Notification Collection (BEINC) v2.0 # Copyright (C) 2013-2015 Simeon Simeonov # This program is free software: you can redistribute it and/or modify @@ -21,16 +21,13 @@ import argparse import errno import getpass -import httplib -import json +import httplib # for Python < 2.7.9 import os -import sched -import socket +import socket # for Python < 2.7.9 import ssl import sys import time -import urllib -import urllib2 +import xmlrpclib try: import pynotify @@ -50,38 +47,74 @@ except ImportError as e: __author__ = 'Simeon Simeonov' -__version__ = '1.0' +__version__ = '2.0' __license__ = 'GPL3' -class ValidHTTPSConnection(httplib.HTTPConnection): - """ - Implements a simple CERT verification functionality +BEINC_SSL_METHODS = {'SSLv3': ssl.PROTOCOL_SSLv3, + 'TLSv1': ssl.PROTOCOL_TLSv1} +try: + BEINC_SSL_METHODS.update({'TLSv1_1': ssl.PROTOCOL_TLSv1_1}) + BEINC_SSL_METHODS.update({'TLSv1_2': ssl.PROTOCOL_TLSv1_2}) +except: + pass + + +class BEINCCustomHTTPSConnection(httplib.HTTPConnection): """ + This class allows communication via SSL. + It is a reimplementation of httplib.HTTPSConnection and + allows the server certificate to be validated against CA + This functionality lacks in Python < 2.7.9 + """ default_port = httplib.HTTPS_PORT - def __init__(self, *args, **kwargs): - httplib.HTTPConnection.__init__(self, *args, **kwargs) + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, custom_ssl_options={}): + httplib.HTTPConnection.__init__(self, host, port, strict, timeout, + source_address) + self.key_file = key_file + self.cert_file = cert_file + self.custom_ssl_options = custom_ssl_options def connect(self): + "Connect to a host on a given (SSL) port." sock = socket.create_connection((self.host, self.port), self.timeout, self.source_address) if self._tunnel_host: self.sock = sock self._tunnel() self.sock = ssl.wrap_socket(sock, - ca_certs=global_beinc_cert_file, - cert_reqs=ssl.CERT_REQUIRED) + self.key_file, + self.cert_file, + **self.custom_ssl_options) -class ValidHTTPSHandler(urllib2.HTTPSHandler): - """ - Implements a simple CERT verification functionality - """ +class BEINCCustomSafeTransport(xmlrpclib.Transport): + + def __init__(self, use_datetime=0, custom_ssl_options={}): + xmlrpclib.Transport.__init__(self, use_datetime=use_datetime) + self.custom_ssl_options = custom_ssl_options - def https_open(self, req): - return self.do_open(ValidHTTPSConnection, req) + def make_connection(self, host): + if self._connection and host == self._connection[0]: + return self._connection[1] + try: + HTTPS = BEINCCustomHTTPSConnection + except AttributeError: + raise NotImplementedError( + "your version of httplib doesn't support HTTPS" + ) + else: + chost, self._extra_headers, x509 = self.get_host_info(host) + self._connection = host, HTTPS( + chost, + None, + custom_ssl_options=self.custom_ssl_options, + **(x509 or {})) + return self._connection[1] def display_notification(args, title, message): @@ -141,23 +174,35 @@ def poll_notifications(scheduler, args): args: the argparse processed command-line arguments """ try: - post_values = {'password': args.password} - data = urllib.urlencode(post_values) - req = urllib2.Request(args.url, data) - if args.cert: # check for cert validity - global global_beinc_cert_file # ugly hack - global_beinc_cert_file = args.cert - opener = urllib2.build_opener(ValidHTTPSHandler) - response = opener.open(req) - else: # ... or don't - response = urllib2.urlopen(req) - res_code = response.code - res_str = response.read() - if res_code == 200 and args.debug: - print('Server responded: OK') - print('Body:\n{0}'.format(res_str)) - response.close() - res_list = json.loads(res_str) + ssl_version = BEINC_SSL_METHODS.get(args.ssl_version, + ssl.PROTOCOL_SSLv23) + if sys.hexversion >= 0x20709f0: + # Python >= 2.7.9 + context = ssl.SSLContext(ssl_version) + context.verify_mode = ssl.CERT_REQUIRED + if args.no_cert_validate: + context.verify_mode = ssl.CERT_NONE + context.check_hostname = bool(not args.disable_hostname_check) + if args.cert and not args.no_cert_validate: + context.load_verify_locations(os.path.expanduser(args.cert)) + if args.ciphers: + context.set_ciphers(args.ciphers) + transport = xmlrpclib.SafeTransport(context=context) + else: + # Python < 2.7.9 + ssl_options = {} + ssl_options['ssl_version'] = ssl_version + if args.cert and not args.no_cert_validate: + ssl_options['ca_certs'] = os.path.expanduser(args.cert) + if not args.no_cert_validate: + ssl_options['cert_reqs'] = ssl.CERT_REQUIRED + if args.ciphers: + ssl_options['ciphers'] = args.ciphers + transport = BEINCCustomSafeTransport( + custom_ssl_options=ssl_options) + server = xmlrpclib.ServerProxy(args.url, + transport=transport) + res_list = server.pull(args.rname, args.password) for entry in res_list: title = entry.get('title', '') message = entry.get('message', '') @@ -166,11 +211,16 @@ def poll_notifications(scheduler, args): 1, poll_notifications, (scheduler, args)) - except urllib2.HTTPError as e: - sys.stderr.write('BEINC-server error ({0} - {1})\n'.format(e.code, - e.reason)) + except xmlrpclib.Fault as fault: + sys.stderr.write( + 'BEINC server answered with errorCode={0}: {1}\n'.format( + fault.faultCode, + fault.faultString)) + except ssl.SSLError as e: + sys.stderr.write('BEINC SSL/TLS error: {0}\n'.format(e)) + sys.exit(errno.EPERM) except Exception as e: - sys.stderr.write('BEINC-poller error: {0}\nTerminating...'.format(e)) + sys.stderr.write('BEINC generic client error: {0}\n'.format(e)) sys.exit(errno.EPERM) @@ -181,21 +231,14 @@ def main(): 'url', metavar='URL', type=str, - help='BEINC destination URL') + help='BEINC server destination URL') parser.add_argument( '-a', '--align', metavar='ALIGNMENT', type=str, dest='alignment', default='left', - help='Alignment for "pyosd" (default: "left")') - parser.add_argument( - '-c', '--cert-file', - metavar='FILE', - type=str, - dest='cert', - default='', - help='BEINC CA-cert to check the server-cert against (default: None)') + help='Alignment for "pyosd": "left" (default), "center", "right"') parser.add_argument( '-C', '--color', metavar='COLOR', @@ -204,18 +247,27 @@ def main(): default='blue', help='Color for "pyosd" (default: "blue")') parser.add_argument( - '-d', '--debug', - action='store_true', - dest='debug', - default=False, - help='Run the poller-process in debug-mode') + '-c', '--cert-file', + metavar='FILE', + type=str, + dest='cert', + default='', + help='BEINC CA-cert to check the server-cert against ' + '(default: Check disabled)') parser.add_argument( - '-f', '--frequency', - metavar='SECONDS', - type=int, - dest='frequency', - default=10, - help='Polling frequency in seconds (default: 10)') + '--ciphers', + metavar='CIPHERS', + type=str, + dest='ciphers', + default='', + help='Preferred ciphers list (default: auto)') + if sys.hexversion >= 0x20709f0: + parser.add_argument( + '--disable-hostname-check', + action='store_true', + dest='disable_hostname_check', + default=False, + help='Do not check whether server cert matches server hostname') parser.add_argument( '--font', metavar='FONT', @@ -223,6 +275,13 @@ def main(): dest='font', default=None, help='Custom font for "pyosd" (default: Default font)') + parser.add_argument( + '-f', '--frequency', + metavar='SECONDS', + type=int, + dest='frequency', + default=10, + help='Polling frequency in seconds (default: 10)') parser.add_argument( '--h-offset', metavar='OFFSET', @@ -230,15 +289,28 @@ def main(): dest='hoffset', default=30, help='Horizontal offset for "pyosd" (default: 30)') + parser.add_argument( + '-n', '--resource-name', + metavar='NAME', + type=str, + dest='rname', + required=True, + help='The name of the BEINC-resource on the remote server') + parser.add_argument( + '--no-cert-validate', + action='store_true', + dest='no_cert_validate', + default=False, + help='Do not validate server certificate') parser.add_argument( '-o', '--osd-system', metavar='SYSTEM', type=str, dest='osd_sys', default='pynotify', - help='BEINC osd-system ("pynotify" or "pyosd") (default: pynotify)') + help='BEINC osd-system: "pynotify" (default), "pyosd"') parser.add_argument( - '-P', '--password', + '-p', '--password', metavar='PASSWORD[FILE]', type=str, dest='password', @@ -246,12 +318,29 @@ def main(): help='BEINC taget-password / text-file containing the target password' ' (default & recommended: prompt for passwd)') parser.add_argument( - '-p', '--position', + '-P', '--position', metavar='POSITION', type=str, dest='position', default='bottom', - help='Position for "pyosd" (default: "bottom")') + help='Position for "pyosd": "top", "middle", "bottom" (default)') + if sys.hexversion >= 0x20709f0: + parser.add_argument( + '-s', '--ssl-version', + metavar='VERSION', + type=str, + dest='ssl_version', + default='auto', + help='Use SSL version: "auto" (default), ' + '"SSLv3", "TLSv1", "TLSv1_1", "TLSv1_2"') + else: + parser.add_argument( + '-s', '--ssl-version', + metavar='VERSION', + type=str, + dest='ssl_version', + default='auto', + help='Use SSL version: "auto" (default), "SSLv3", "TLSv1"') parser.add_argument( '-t', '--osd-timeout', metavar='SECONDS', -- cgit v1.3