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 +++++++++++++++++++-------- beinc_poller_xmlrpc.py | 248 ------------------------------ beinc_server.py | 398 +++++++++++++++++++++++++++++++++++++++++++++++++ beinc_server_.py | 398 ------------------------------------------------- 4 files changed, 553 insertions(+), 712 deletions(-) delete mode 100755 beinc_poller_xmlrpc.py create mode 100755 beinc_server.py delete mode 100755 beinc_server_.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', diff --git a/beinc_poller_xmlrpc.py b/beinc_poller_xmlrpc.py deleted file mode 100755 index 2efd7df..0000000 --- a/beinc_poller_xmlrpc.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# 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 -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -import argparse -import errno -import getpass -import httplib # for Python < 2.7.9 -import os -import socket # for Python < 2.7.9 -import ssl -import sys -import xmlrpclib - - -__author__ = 'Simeon Simeonov' -__version__ = '2.0' -__license__ = 'GPL3' - - -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, 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, - self.key_file, - self.cert_file, - **self.custom_ssl_options) - - -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 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 action_execute(args): - """ - """ - try: - 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) - if args.pull: - print(server.pull(args.rname, args.password)) - else: - print(server.push(args.rname, - args.password, - args.title, - args.message)) - 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 generic client error: {0}\n'.format(e)) - sys.exit(errno.EPERM) - - -def main(): - parser = argparse.ArgumentParser( - description='The following options are available') - parser.add_argument('url', - metavar='URL', - type=str, - help='Destination URL') - parser.add_argument('-c', '--cert-file', - metavar='FILE', - type=str, - dest='cert', - default='', - help='BEINC CA-cert to check the server-cert against') - parser.add_argument('-C', '--ciphers', - metavar='CIPHERS', - type=str, - dest='ciphers', - default='', - help='Preferred ciphers list (default: auto)') - parser.add_argument('-m', '--message', - metavar='MESSAGE', - type=str, - dest='message', - default='BEINC message', - help='BEINC message') - 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('-p', '--password', - metavar='PASSWORD', - type=str, - dest='password', - default='', - help='Password') - 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', '--title', - metavar='TITLE', - type=str, - dest='title', - default='BEINC title', - help='BEINC title') - parser.add_argument('-v', '--version', - action='version', - version='%(prog)s {0}'.format(__version__), - help='display program-version and exit') - 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('--no-cert-validate', - action='store_true', - dest='no_cert_validate', - default=False, - help='Do not validate server certificate') - parser.add_argument('--pull', - action='store_true', - dest='pull', - default=False, - help='Perform a pull operation (default: push)') - args = parser.parse_args() - if not args.password: - try: - args.password = getpass.getpass() - except Exception as e: - sys.stderr.write('Prompt terminated\n') - sys.exit(errno.EACCES) - action_execute(args) - sys.exit(0) - - -if __name__ == '__main__': - main() diff --git a/beinc_server.py b/beinc_server.py new file mode 100755 index 0000000..cf4f7c7 --- /dev/null +++ b/beinc_server.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# 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 +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import argparse +import errno +import getpass +import json +import os +import sys + +import OpenSSL + +from functools import wraps + +from twisted.web import xmlrpc, server +from twisted.internet import protocol, reactor, ssl +from twisted.python import filepath, log + +try: + import pynotify +except ImportError as e: + pynotify = None + +try: + import pyosd + pyosd_positions = {'top': pyosd.POS_TOP, + 'middle': pyosd.POS_MID, + 'bottom': pyosd.POS_BOT} + pyosd_alignments = {'left': pyosd.ALIGN_LEFT, + 'center': pyosd.ALIGN_CENTER, + 'right': pyosd.ALIGN_RIGHT} +except ImportError as e: + pyosd = None + + +__author__ = 'Simeon Simeonov' +__version__ = '2.0' +__license__ = 'GPL3' + + +BEINC_OSD_TYPE_NONE = 0 +BEINC_OSD_TYPE_PYNOTIFY = 1 +BEINC_OSD_TYPE_PYOSD = 2 + +BEINC_SSL_METHODS = {'SSLv3': OpenSSL.SSL.SSLv3_METHOD, + 'TLSv1': OpenSSL.SSL.TLSv1_METHOD} +try: + errstr = ("Warning: Current Twisted / " + "OpenSSL version doesn't support TLSv1.1") + BEINC_SSL_METHODS.update({'TLSv1_1': OpenSSL.SSL.TLSv1_1_METHOD}) + errstr = ("Warning: Current Twisted / " + + "OpenSSL version doesn't support TLSv1.2") + BEINC_SSL_METHODS.update({'TLSv1_2': OpenSSL.SSL.TLSv1_2_METHOD}) +except: + sys.stderr.write(errstr + '\n') + + +class BEINCInstance(object): + """ + Represents a single server-instance + """ + + def __init__(self, instance_dict): + """ + instance_dict: the config-dictionary node that represents this instance + """ + self.__message_queue = list() + self.__osd_type = BEINC_OSD_TYPE_NONE + self.__osd_notification = None + + self.__name = instance_dict.get('name') + self.__password = instance_dict.get('password', '') + self.__queue_size = int(instance_dict.get('queue_size', 3)) + if instance_dict['osd_system'].lower() == 'pynotify': + self.__queue_size = 0 # disable queueing + if not pynotify: + sys.stderr.write( + 'This server does not possess pynotify capability\n') + sys.stderr.write( + 'Remove the instance {0}'.format(self.__name)) + sys.stderr.write( + 'or define it with "osd_system": "none" ' + 'or other available backend\n') + sys.exit(errno.EPERM) + try: + self.__osd_notification = pynotify.Notification(' ') + self.__osd_notification.set_timeout( + 1000 * int(instance_dict.get('osd_timeout', 5))) + self.__osd_notification.set_property( + 'app_name', + '{0} {1}'.format(sys.argv[0], __version__)) + except Exception as e: + sys.stderr.write( + 'Unable to set up a ' + 'pynotify notification object for "{0}" ({1})\n'.format( + self.__name, + e)) + sys.exit(errno.EPERM) + self.__osd_type = BEINC_OSD_TYPE_PYNOTIFY + elif instance_dict['osd_system'].lower() == 'pyosd': + self.__queue_size = 0 # disable queueing + if not pyosd: + sys.stderr.write( + 'This server does not possess pyosd capability\n') + sys.stderr.write( + 'Remove the instance {0}'.format(self.__name)) + sys.stderr.write( + 'or define it with "osd_system": "none" ' + 'or other available backend\n') + sys.exit(errno.EPERM) + try: + self.__osd_notification = pyosd.osd() + self.__osd_notification.set_timeout( + int(instance_dict.get('osd_timeout', 5))) + pyosd_font = instance_dict.get('pyosd_font') + if pyosd_font: + self.__osd_notification.set_font(pyosd_font) + self.__osd_notification.set_vertical_offset( + instance_dict.get('pyosd_vertical_offset', 120)) + self.__osd_notification.set_horizontal_offset( + instance_dict.get('pyosd_horizontal_offset', 30)) + align_str = instance_dict.get('pyosd_align', 'left') + self.__osd_notification.set_align( + pyosd_alignments.get(align_str, pyosd.ALIGN_LEFT)) + position_str = instance_dict.get('pyosd_position', 'bottom') + self.__osd_notification.set_pos( + pyosd_positions.get(position_str, pyosd.POS_BOT)) + self.__osd_notification.set_colour( + instance_dict.get('pyosd_color', 'blue')) + except Exception as e: + sys.stderr.write( + 'Unable to set up a pyosd ' + 'notification object for "{0}" ({1})\n'.format( + self.__name, + e)) + sys.exit(errno.EPERM) + self.__osd_type = BEINC_OSD_TYPE_PYOSD + + @property + def name(self): + """ + name-property for the server instance (read-only) + """ + return self.__name + + @property + def queueable(self): + """ + True if this instance has a queueing capability (read-only) + """ + return bool(self.__queue_size) + + def password_match(self, password): + """ + Returns True if 'passowrd' matches the instance-password, + otherwise - False + """ + return True if self.__password == password else False + + def send_message(self, title, message): + """ + Displays or enqueues the message, + depending on the instance's type in regard to the osd_system + """ + if self.__osd_type == BEINC_OSD_TYPE_PYNOTIFY: + self.__send_pynotify_messaage(title, message) + elif self.__osd_type == BEINC_OSD_TYPE_PYOSD: + self.__send_pyosd_message(title, message) + else: + self.__send_message_to_queue(title, message) + + def get_queue(self): + """ + Reruens a json representation of the message queue + """ + r_value = self.__message_queue + self.__message_queue = list() + return r_value + + def __send_pynotify_messaage(self, title, message): + """ + Displays pynotify message + """ + self.__osd_notification.set_properties(summary=title, body=message) + self.__osd_notification.show() + + def __send_pyosd_message(self, title, message): + """ + Displays pyosd message + """ + self.__osd_notification.display(title, line=0) + self.__osd_notification.display(message, line=1) + + def __send_message_to_queue(self, title, message): + """ + Enqueues the message + """ + if len(self.__message_queue) >= self.__queue_size: + self.__message_queue.pop(0) + self.__message_queue.append({'title': title, 'message': message}) + + +def beinc_login_required(method): + """ + Decorator for checking login credentials + """ + + @wraps(method) + def wrapper(self, resource_name, password, *args, **kwargs): + try: + instance = self.instances[resource_name] + except Exception as e: + raise xmlrpc.Fault(401, + 'Wrong instance or password') + if not instance.password_match(password): + raise xmlrpc.Fault(401, + 'Wrong instance or password') + return method(self, resource_name, password, *args, **kwargs) + return wrapper + + +class XMLRPCNotifyServer(xmlrpc.XMLRPC): + """ + A class representing the entire server + """ + + def __init__(self, config): + """ + """ + xmlrpc.XMLRPC.__init__(self) + self.__config = config + self.__instances = dict() + # initialize pynotify if the module exists and if needed + if pynotify: + for instance in self.__config['server']['instances']: + # check if we have at least one instance that uses pynotify + # before initializing it + if instance.get('osd_system', '').lower() == 'pynotify': + if not pynotify.init('BEINC Notify'): + sys.stderr.write('pynotify.init failed! Exiting...\n') + sys.exit(1) + break + try: + for instance in self.__config['server']['instances']: + self.__instances[instance['name']] = BEINCInstance(instance) + print('Instance "{0}" added'.format(instance['name'])) + except Exception as e: + sys.stderr.write('Unable to create instance "{0}": {1}\n'.format( + instance['name'], + e)) + sys.exit(1) + + @property + def instances(self): + """ + a property that returns the instance list (read-only) + """ + return self.__instances + + @beinc_login_required + def xmlrpc_push(self, resource_name, password, title, message): + """ + Return all passed args. + """ + instance = self.__instances[resource_name] + try: + instance.send_message(title, message) + return 'OK' + except Exception as e: + sys.stderr.write( + 'Unable to handle message in {0}: ({1})\n'.format( + instance.name, + e)) + raise xmlrpc.Fault(500, 'Unable to send message') + + @beinc_login_required + def xmlrpc_pull(self, resource_name, password): + """ + Return sum of arguments. + """ + instance = self.__instances[resource_name] + if not instance.queueable: + raise xmlrpc.Fault( + 405, + 'BEINC instance "{0}" does not support queuing'.format( + instance.name)) + return instance.get_queue() + + +def main(): + """ + """ + parser = argparse.ArgumentParser( + description='The following options are available') + parser.add_argument( + '-d', + action='store_true', + dest='daemonize', + default=False, + help='Run the BEINC-server in the background') + parser.add_argument( + '-H', '--hostname', + metavar='HOSTNAME', + type=str, + dest='hostname', + default='127.0.0.1', + help='BEINC server IP / hostname (default: 127.0.0.1)') + parser.add_argument( + '-p', '--port', + metavar='PORT', + type=int, + dest='port', + default=9998, + help='BEINC server port (default: 9998)') + parser.add_argument( + '-f', '--config-file', + metavar='FILE', + type=str, + default=os.path.expanduser('~/.beinc_server.json'), + dest='config_file', + help='BEINC config file (default: ~/.beinc_server.json)') + parser.add_argument( + '-v', '--version', + action='version', + version='%(prog)s {0}'.format(__version__), + help='Display program-version and exit') + args = parser.parse_args() + log.startLogging(sys.stdout) + try: + with open(args.config_file, 'r') as fp: + config_dict = json.load(fp) + except Exception as e: + sys.stderr.write('Unable to parse {0}: {1}\n'.format(args.config_file, + e)) + sys.exit(errno.EIO) + try: + if config_dict.get('config_version') != 2: + sys.stderr.write( + 'Incompatible or missing config-file version for {0}\n'.format( + args.config_file)) + sys.exit(1) + ssl_certificate = config_dict['server']['general'].get( + 'ssl_certificate') + ssl_private_key = config_dict['server']['general'].get( + 'ssl_private_key') + ssl_method_str = config_dict['server']['general'].get( + 'ssl_method', 'auto') + ssl_acceptable_ciphers_str = config_dict['server']['general'].get( + 'ssl_acceptable_ciphers', 'auto') + beinc_server = XMLRPCNotifyServer(config_dict) + if ssl_certificate and ssl_private_key: + # SSL connection + cert_path = filepath.FilePath(ssl_certificate) + key_path = filepath.FilePath(ssl_private_key) + private_certificate = ssl.PrivateCertificate.loadPEM( + key_path.getContent() + cert_path.getContent()) + options = private_certificate.options() + ssl_method = BEINC_SSL_METHODS.get(ssl_method_str) + if ssl_method: + options.method = ssl_method + if ssl_acceptable_ciphers_str.lower() != 'auto': + options.acceptableCiphers = ( + ssl.AcceptableCiphers.fromOpenSSLCipherString( + ssl_acceptable_ciphers_str)) + reactor.listenSSL(args.port, + server.Site(beinc_server), + options, + interface=args.hostname) + else: + reactor.listenTCP(args.port, + server.Site(beinc_server), + interface=args.hostname) + reactor.run() + except Exception as e: + sys.stderr.write('WebServer error: {0}\n'.format(e)) + sys.exit(1) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/beinc_server_.py b/beinc_server_.py deleted file mode 100755 index cf4f7c7..0000000 --- a/beinc_server_.py +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# 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 -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -import argparse -import errno -import getpass -import json -import os -import sys - -import OpenSSL - -from functools import wraps - -from twisted.web import xmlrpc, server -from twisted.internet import protocol, reactor, ssl -from twisted.python import filepath, log - -try: - import pynotify -except ImportError as e: - pynotify = None - -try: - import pyosd - pyosd_positions = {'top': pyosd.POS_TOP, - 'middle': pyosd.POS_MID, - 'bottom': pyosd.POS_BOT} - pyosd_alignments = {'left': pyosd.ALIGN_LEFT, - 'center': pyosd.ALIGN_CENTER, - 'right': pyosd.ALIGN_RIGHT} -except ImportError as e: - pyosd = None - - -__author__ = 'Simeon Simeonov' -__version__ = '2.0' -__license__ = 'GPL3' - - -BEINC_OSD_TYPE_NONE = 0 -BEINC_OSD_TYPE_PYNOTIFY = 1 -BEINC_OSD_TYPE_PYOSD = 2 - -BEINC_SSL_METHODS = {'SSLv3': OpenSSL.SSL.SSLv3_METHOD, - 'TLSv1': OpenSSL.SSL.TLSv1_METHOD} -try: - errstr = ("Warning: Current Twisted / " - "OpenSSL version doesn't support TLSv1.1") - BEINC_SSL_METHODS.update({'TLSv1_1': OpenSSL.SSL.TLSv1_1_METHOD}) - errstr = ("Warning: Current Twisted / " + - "OpenSSL version doesn't support TLSv1.2") - BEINC_SSL_METHODS.update({'TLSv1_2': OpenSSL.SSL.TLSv1_2_METHOD}) -except: - sys.stderr.write(errstr + '\n') - - -class BEINCInstance(object): - """ - Represents a single server-instance - """ - - def __init__(self, instance_dict): - """ - instance_dict: the config-dictionary node that represents this instance - """ - self.__message_queue = list() - self.__osd_type = BEINC_OSD_TYPE_NONE - self.__osd_notification = None - - self.__name = instance_dict.get('name') - self.__password = instance_dict.get('password', '') - self.__queue_size = int(instance_dict.get('queue_size', 3)) - if instance_dict['osd_system'].lower() == 'pynotify': - self.__queue_size = 0 # disable queueing - if not pynotify: - sys.stderr.write( - 'This server does not possess pynotify capability\n') - sys.stderr.write( - 'Remove the instance {0}'.format(self.__name)) - sys.stderr.write( - 'or define it with "osd_system": "none" ' - 'or other available backend\n') - sys.exit(errno.EPERM) - try: - self.__osd_notification = pynotify.Notification(' ') - self.__osd_notification.set_timeout( - 1000 * int(instance_dict.get('osd_timeout', 5))) - self.__osd_notification.set_property( - 'app_name', - '{0} {1}'.format(sys.argv[0], __version__)) - except Exception as e: - sys.stderr.write( - 'Unable to set up a ' - 'pynotify notification object for "{0}" ({1})\n'.format( - self.__name, - e)) - sys.exit(errno.EPERM) - self.__osd_type = BEINC_OSD_TYPE_PYNOTIFY - elif instance_dict['osd_system'].lower() == 'pyosd': - self.__queue_size = 0 # disable queueing - if not pyosd: - sys.stderr.write( - 'This server does not possess pyosd capability\n') - sys.stderr.write( - 'Remove the instance {0}'.format(self.__name)) - sys.stderr.write( - 'or define it with "osd_system": "none" ' - 'or other available backend\n') - sys.exit(errno.EPERM) - try: - self.__osd_notification = pyosd.osd() - self.__osd_notification.set_timeout( - int(instance_dict.get('osd_timeout', 5))) - pyosd_font = instance_dict.get('pyosd_font') - if pyosd_font: - self.__osd_notification.set_font(pyosd_font) - self.__osd_notification.set_vertical_offset( - instance_dict.get('pyosd_vertical_offset', 120)) - self.__osd_notification.set_horizontal_offset( - instance_dict.get('pyosd_horizontal_offset', 30)) - align_str = instance_dict.get('pyosd_align', 'left') - self.__osd_notification.set_align( - pyosd_alignments.get(align_str, pyosd.ALIGN_LEFT)) - position_str = instance_dict.get('pyosd_position', 'bottom') - self.__osd_notification.set_pos( - pyosd_positions.get(position_str, pyosd.POS_BOT)) - self.__osd_notification.set_colour( - instance_dict.get('pyosd_color', 'blue')) - except Exception as e: - sys.stderr.write( - 'Unable to set up a pyosd ' - 'notification object for "{0}" ({1})\n'.format( - self.__name, - e)) - sys.exit(errno.EPERM) - self.__osd_type = BEINC_OSD_TYPE_PYOSD - - @property - def name(self): - """ - name-property for the server instance (read-only) - """ - return self.__name - - @property - def queueable(self): - """ - True if this instance has a queueing capability (read-only) - """ - return bool(self.__queue_size) - - def password_match(self, password): - """ - Returns True if 'passowrd' matches the instance-password, - otherwise - False - """ - return True if self.__password == password else False - - def send_message(self, title, message): - """ - Displays or enqueues the message, - depending on the instance's type in regard to the osd_system - """ - if self.__osd_type == BEINC_OSD_TYPE_PYNOTIFY: - self.__send_pynotify_messaage(title, message) - elif self.__osd_type == BEINC_OSD_TYPE_PYOSD: - self.__send_pyosd_message(title, message) - else: - self.__send_message_to_queue(title, message) - - def get_queue(self): - """ - Reruens a json representation of the message queue - """ - r_value = self.__message_queue - self.__message_queue = list() - return r_value - - def __send_pynotify_messaage(self, title, message): - """ - Displays pynotify message - """ - self.__osd_notification.set_properties(summary=title, body=message) - self.__osd_notification.show() - - def __send_pyosd_message(self, title, message): - """ - Displays pyosd message - """ - self.__osd_notification.display(title, line=0) - self.__osd_notification.display(message, line=1) - - def __send_message_to_queue(self, title, message): - """ - Enqueues the message - """ - if len(self.__message_queue) >= self.__queue_size: - self.__message_queue.pop(0) - self.__message_queue.append({'title': title, 'message': message}) - - -def beinc_login_required(method): - """ - Decorator for checking login credentials - """ - - @wraps(method) - def wrapper(self, resource_name, password, *args, **kwargs): - try: - instance = self.instances[resource_name] - except Exception as e: - raise xmlrpc.Fault(401, - 'Wrong instance or password') - if not instance.password_match(password): - raise xmlrpc.Fault(401, - 'Wrong instance or password') - return method(self, resource_name, password, *args, **kwargs) - return wrapper - - -class XMLRPCNotifyServer(xmlrpc.XMLRPC): - """ - A class representing the entire server - """ - - def __init__(self, config): - """ - """ - xmlrpc.XMLRPC.__init__(self) - self.__config = config - self.__instances = dict() - # initialize pynotify if the module exists and if needed - if pynotify: - for instance in self.__config['server']['instances']: - # check if we have at least one instance that uses pynotify - # before initializing it - if instance.get('osd_system', '').lower() == 'pynotify': - if not pynotify.init('BEINC Notify'): - sys.stderr.write('pynotify.init failed! Exiting...\n') - sys.exit(1) - break - try: - for instance in self.__config['server']['instances']: - self.__instances[instance['name']] = BEINCInstance(instance) - print('Instance "{0}" added'.format(instance['name'])) - except Exception as e: - sys.stderr.write('Unable to create instance "{0}": {1}\n'.format( - instance['name'], - e)) - sys.exit(1) - - @property - def instances(self): - """ - a property that returns the instance list (read-only) - """ - return self.__instances - - @beinc_login_required - def xmlrpc_push(self, resource_name, password, title, message): - """ - Return all passed args. - """ - instance = self.__instances[resource_name] - try: - instance.send_message(title, message) - return 'OK' - except Exception as e: - sys.stderr.write( - 'Unable to handle message in {0}: ({1})\n'.format( - instance.name, - e)) - raise xmlrpc.Fault(500, 'Unable to send message') - - @beinc_login_required - def xmlrpc_pull(self, resource_name, password): - """ - Return sum of arguments. - """ - instance = self.__instances[resource_name] - if not instance.queueable: - raise xmlrpc.Fault( - 405, - 'BEINC instance "{0}" does not support queuing'.format( - instance.name)) - return instance.get_queue() - - -def main(): - """ - """ - parser = argparse.ArgumentParser( - description='The following options are available') - parser.add_argument( - '-d', - action='store_true', - dest='daemonize', - default=False, - help='Run the BEINC-server in the background') - parser.add_argument( - '-H', '--hostname', - metavar='HOSTNAME', - type=str, - dest='hostname', - default='127.0.0.1', - help='BEINC server IP / hostname (default: 127.0.0.1)') - parser.add_argument( - '-p', '--port', - metavar='PORT', - type=int, - dest='port', - default=9998, - help='BEINC server port (default: 9998)') - parser.add_argument( - '-f', '--config-file', - metavar='FILE', - type=str, - default=os.path.expanduser('~/.beinc_server.json'), - dest='config_file', - help='BEINC config file (default: ~/.beinc_server.json)') - parser.add_argument( - '-v', '--version', - action='version', - version='%(prog)s {0}'.format(__version__), - help='Display program-version and exit') - args = parser.parse_args() - log.startLogging(sys.stdout) - try: - with open(args.config_file, 'r') as fp: - config_dict = json.load(fp) - except Exception as e: - sys.stderr.write('Unable to parse {0}: {1}\n'.format(args.config_file, - e)) - sys.exit(errno.EIO) - try: - if config_dict.get('config_version') != 2: - sys.stderr.write( - 'Incompatible or missing config-file version for {0}\n'.format( - args.config_file)) - sys.exit(1) - ssl_certificate = config_dict['server']['general'].get( - 'ssl_certificate') - ssl_private_key = config_dict['server']['general'].get( - 'ssl_private_key') - ssl_method_str = config_dict['server']['general'].get( - 'ssl_method', 'auto') - ssl_acceptable_ciphers_str = config_dict['server']['general'].get( - 'ssl_acceptable_ciphers', 'auto') - beinc_server = XMLRPCNotifyServer(config_dict) - if ssl_certificate and ssl_private_key: - # SSL connection - cert_path = filepath.FilePath(ssl_certificate) - key_path = filepath.FilePath(ssl_private_key) - private_certificate = ssl.PrivateCertificate.loadPEM( - key_path.getContent() + cert_path.getContent()) - options = private_certificate.options() - ssl_method = BEINC_SSL_METHODS.get(ssl_method_str) - if ssl_method: - options.method = ssl_method - if ssl_acceptable_ciphers_str.lower() != 'auto': - options.acceptableCiphers = ( - ssl.AcceptableCiphers.fromOpenSSLCipherString( - ssl_acceptable_ciphers_str)) - reactor.listenSSL(args.port, - server.Site(beinc_server), - options, - interface=args.hostname) - else: - reactor.listenTCP(args.port, - server.Site(beinc_server), - interface=args.hostname) - reactor.run() - except Exception as e: - sys.stderr.write('WebServer error: {0}\n'.format(e)) - sys.exit(1) - sys.exit(0) - - -if __name__ == "__main__": - main() -- cgit v1.3