diff options
Diffstat (limited to 'beinc_generic_client.py')
| -rwxr-xr-x | beinc_generic_client.py | 269 |
1 files changed, 113 insertions, 156 deletions
diff --git a/beinc_generic_client.py b/beinc_generic_client.py index 4d8fc35..5514642 100755 --- a/beinc_generic_client.py +++ b/beinc_generic_client.py | |||
| @@ -1,8 +1,8 @@ | |||
| 1 | #!/usr/bin/env python | 1 | #!/usr/bin/env python |
| 2 | # -*- coding: utf-8 -*- | 2 | # -*- coding: utf-8 -*- |
| 3 | 3 | ||
| 4 | # Blackmore's Enhanced IRC-Notification Collection (BEINC) v2.0 | 4 | # Blackmore's Enhanced IRC-Notification Collection (BEINC) v4.0 |
| 5 | # Copyright (C) 2013-2018 Simeon Simeonov | 5 | # Copyright (C) 2013-2020 Simeon Simeonov |
| 6 | 6 | ||
| 7 | # This program is free software: you can redistribute it and/or modify | 7 | # This program is free software: you can redistribute it and/or modify |
| 8 | # it under the terms of the GNU General Public License as published by | 8 | # it under the terms of the GNU General Public License as published by |
| @@ -16,146 +16,114 @@ | |||
| 16 | 16 | ||
| 17 | # You should have received a copy of the GNU General Public License | 17 | # You should have received a copy of the GNU General Public License |
| 18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 19 | 19 | """A generic BEINC client that can be used for testing or as a template""" | |
| 20 | |||
| 21 | import argparse | 20 | import argparse |
| 22 | import errno | 21 | import errno |
| 23 | import getpass | 22 | import getpass |
| 24 | import httplib # for Python < 2.7.9 | 23 | import io |
| 24 | import json | ||
| 25 | import os | 25 | import os |
| 26 | import socket | 26 | import socket |
| 27 | import ssl | 27 | import ssl |
| 28 | import sys | 28 | import sys |
| 29 | import xmlrpclib | 29 | import urllib.parse |
| 30 | import urllib.request | ||
| 30 | 31 | ||
| 31 | 32 | ||
| 32 | __author__ = 'Simeon Simeonov' | 33 | __author__ = 'Simeon Simeonov' |
| 33 | __version__ = '3.0' | 34 | __version__ = '4.0' |
| 34 | __license__ = 'GPL3' | 35 | __license__ = 'GPL3' |
| 35 | 36 | ||
| 36 | 37 | ||
| 37 | BEINC_SSL_METHODS = {'TLSv1': ssl.PROTOCOL_TLSv1} | 38 | def eprint(*arg, **kwargs): |
| 38 | try: | 39 | """stdderr print wrapper""" |
| 39 | BEINC_SSL_METHODS.update({'TLSv1_1': ssl.PROTOCOL_TLSv1_1}) | 40 | print(*arg, file=sys.stderr, flush=True, **kwargs) |
| 40 | BEINC_SSL_METHODS.update({'TLSv1_2': ssl.PROTOCOL_TLSv1_2}) | ||
| 41 | except: | ||
| 42 | pass | ||
| 43 | 41 | ||
| 44 | 42 | ||
| 45 | class BEINCCustomHTTPSConnection(httplib.HTTPConnection): | 43 | def fetch_password(args_password): |
| 46 | """ | 44 | """ |
| 47 | This class allows communication via SSL. | 45 | Fetches the password from the provided `args_password` |
| 46 | |||
| 47 | :param args_password: The password coming from argparse | ||
| 48 | :type args_password: str | ||
| 48 | 49 | ||
| 49 | It is a reimplementation of httplib.HTTPSConnection and | 50 | :return: The password string |
| 50 | allows the server certificate to be validated against CA | 51 | :rtype: str |
| 51 | This functionality lacks in Python < 2.7.9 | ||
| 52 | """ | 52 | """ |
| 53 | default_port = httplib.HTTPS_PORT | 53 | if not args_password: |
| 54 | try: | ||
| 55 | return getpass.getpass() | ||
| 56 | except KeyboardInterrupt: | ||
| 57 | eprint(os.linesep + 'Prompt terminated') | ||
| 58 | sys.exit(errno.EACCES) | ||
| 59 | elif os.path.isfile(args_password): | ||
| 60 | try: | ||
| 61 | with io.open(args_password, 'r') as fp: | ||
| 62 | passwd = fp.readline() | ||
| 63 | if passwd.strip(): | ||
| 64 | return passwd.strip() | ||
| 65 | except Exception as e: | ||
| 66 | eprint(f'Unable to open password file: {e}') | ||
| 67 | sys.exit(1) | ||
| 68 | return args_password | ||
| 54 | 69 | ||
| 55 | def __init__(self, host, port=None, key_file=None, cert_file=None, | ||
| 56 | strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, | ||
| 57 | source_address=None, custom_ssl_options={}): | ||
| 58 | httplib.HTTPConnection.__init__(self, host, port, strict, timeout, | ||
| 59 | source_address) | ||
| 60 | self.key_file = key_file | ||
| 61 | self.cert_file = cert_file | ||
| 62 | self.custom_ssl_options = custom_ssl_options | ||
| 63 | 70 | ||
| 64 | def connect(self): | 71 | def pull_notifications(ssl_context, args): |
| 65 | "Connect to a host on a given (SSL) port." | 72 | """ |
| 66 | sock = socket.create_connection((self.host, self.port), | 73 | Pulls the notifications from the BEINC server. |
| 67 | self.timeout, self.source_address) | ||
| 68 | if self._tunnel_host: | ||
| 69 | self.sock = sock | ||
| 70 | self._tunnel() | ||
| 71 | self.sock = ssl.wrap_socket(sock, | ||
| 72 | self.key_file, | ||
| 73 | self.cert_file, | ||
| 74 | **self.custom_ssl_options) | ||
| 75 | 74 | ||
| 75 | :param ssl_context: The SSL context object | ||
| 76 | :type ssl_context: ssl.SSLContext | ||
| 76 | 77 | ||
| 77 | class BEINCCustomSafeTransport(xmlrpclib.Transport): | 78 | :param args: The arguments assigned from argparse |
| 78 | """ | 79 | :type args: argparse.Namespace |
| 79 | Provides an alternative HTTPS transport for clients running on Python<2.7.9 | 80 | |
| 81 | :return: The notifications (if any) | ||
| 82 | :rtype: list | ||
| 80 | """ | 83 | """ |
| 84 | response = urllib.request.urlopen( | ||
| 85 | args.url, | ||
| 86 | data=urllib.parse.urlencode( | ||
| 87 | ( | ||
| 88 | ('resource_name', args.rname), | ||
| 89 | ('password', args.password) | ||
| 90 | )).encode('utf-8'), | ||
| 91 | timeout=args.socket_timeout, | ||
| 92 | context=ssl_context) | ||
| 93 | response_dict = json.loads(response.read().decode('utf-8')) | ||
| 94 | if response.code != 200: | ||
| 95 | raise socket.error(response_dict.get('message', '')) | ||
| 96 | return response_dict['data']['messages'] | ||
| 81 | 97 | ||
| 82 | def __init__(self, use_datetime=0, custom_ssl_options={}): | ||
| 83 | xmlrpclib.Transport.__init__(self, use_datetime=use_datetime) | ||
| 84 | self.custom_ssl_options = custom_ssl_options | ||
| 85 | 98 | ||
| 86 | def make_connection(self, host): | 99 | def push_notification(ssl_context, args): |
| 87 | if self._connection and host == self._connection[0]: | 100 | """ |
| 88 | return self._connection[1] | 101 | Pushes a single notification to the BEINC server. |
| 89 | try: | ||
| 90 | HTTPS = BEINCCustomHTTPSConnection | ||
| 91 | except AttributeError: | ||
| 92 | raise NotImplementedError( | ||
| 93 | "your version of httplib doesn't support HTTPS" | ||
| 94 | ) | ||
| 95 | else: | ||
| 96 | chost, self._extra_headers, x509 = self.get_host_info(host) | ||
| 97 | self._connection = host, HTTPS( | ||
| 98 | chost, | ||
| 99 | None, | ||
| 100 | custom_ssl_options=self.custom_ssl_options, | ||
| 101 | **(x509 or {})) | ||
| 102 | return self._connection[1] | ||
| 103 | 102 | ||
| 103 | :param ssl_context: The SSL context object | ||
| 104 | :type ssl_context: ssl.SSLContext | ||
| 104 | 105 | ||
| 105 | def action_execute(args): | 106 | :param args: The arguments assigned from argparse |
| 106 | """ | 107 | :type args: argparse.Namespace |
| 107 | """ | 108 | """ |
| 108 | try: | 109 | response = urllib.request.urlopen( |
| 109 | ssl_version = BEINC_SSL_METHODS.get(args.ssl_version, | 110 | args.url, |
| 110 | ssl.PROTOCOL_SSLv23) | 111 | data=urllib.parse.urlencode( |
| 111 | if sys.hexversion >= 0x20709f0: | 112 | ( |
| 112 | # Python >= 2.7.9 | 113 | ('resource_name', args.rname), |
| 113 | context = ssl.SSLContext(ssl_version) | 114 | ('password', args.password), |
| 114 | context.verify_mode = ssl.CERT_NONE | 115 | ('title', args.title), |
| 115 | if args.cert: | 116 | ('message', args.message) |
| 116 | context.verify_mode = ssl.CERT_REQUIRED | 117 | )).encode('utf-8'), |
| 117 | context.load_verify_locations(os.path.expanduser(args.cert)) | 118 | timeout=args.socket_timeout, |
| 118 | context.check_hostname = bool(not args.disable_hostname_check) | 119 | context=ssl_context) |
| 119 | if args.ciphers: | 120 | response_dict = json.loads(response.read().decode('utf-8')) |
| 120 | context.set_ciphers(args.ciphers) | 121 | if response.code != 200: |
| 121 | transport = xmlrpclib.SafeTransport(context=context) | 122 | raise socket.error(response_dict.get('message', '')) |
| 122 | else: | ||
| 123 | # Python < 2.7.9 | ||
| 124 | ssl_options = {} | ||
| 125 | ssl_options['ssl_version'] = ssl_version | ||
| 126 | if args.cert: | ||
| 127 | ssl_options['ca_certs'] = os.path.expanduser(args.cert) | ||
| 128 | ssl_options['cert_reqs'] = ssl.CERT_REQUIRED | ||
| 129 | if args.ciphers: | ||
| 130 | ssl_options['ciphers'] = args.ciphers | ||
| 131 | transport = BEINCCustomSafeTransport( | ||
| 132 | custom_ssl_options=ssl_options) | ||
| 133 | server = xmlrpclib.ServerProxy(args.url, | ||
| 134 | transport=transport) | ||
| 135 | if args.pull: | ||
| 136 | print(server.pull(args.rname, args.password)) | ||
| 137 | else: | ||
| 138 | print(server.push(args.rname, | ||
| 139 | args.password, | ||
| 140 | args.title, | ||
| 141 | args.message)) | ||
| 142 | except xmlrpclib.Fault as fault: | ||
| 143 | sys.stderr.write( | ||
| 144 | 'BEINC server answered with errorCode={0}: {1}\n'.format( | ||
| 145 | fault.faultCode, | ||
| 146 | fault.faultString)) | ||
| 147 | except ssl.SSLError as e: | ||
| 148 | sys.stderr.write('BEINC SSL/TLS error: {0}\n'.format(e)) | ||
| 149 | sys.exit(errno.EPERM) | ||
| 150 | except socket.error as e: | ||
| 151 | sys.stderr.write('BEINC connection error: {0}\n'.format(e)) | ||
| 152 | sys.exit(errno.EPERM) | ||
| 153 | except Exception as e: | ||
| 154 | sys.stderr.write('BEINC generic client error: {0}\n'.format(e)) | ||
| 155 | sys.exit(errno.EPERM) | ||
| 156 | 123 | ||
| 157 | 124 | ||
| 158 | def main(): | 125 | def main(inargs=None): |
| 126 | """main entry""" | ||
| 159 | parser = argparse.ArgumentParser( | 127 | parser = argparse.ArgumentParser( |
| 160 | description='The following options are available') | 128 | description='The following options are available') |
| 161 | parser.add_argument( | 129 | parser.add_argument( |
| @@ -178,13 +146,12 @@ def main(): | |||
| 178 | dest='ciphers', | 146 | dest='ciphers', |
| 179 | default='', | 147 | default='', |
| 180 | help='Preferred ciphers list (default: auto)') | 148 | help='Preferred ciphers list (default: auto)') |
| 181 | if sys.hexversion >= 0x20709f0: | 149 | parser.add_argument( |
| 182 | parser.add_argument( | 150 | '--disable-hostname-check', |
| 183 | '--disable-hostname-check', | 151 | action='store_true', |
| 184 | action='store_true', | 152 | dest='disable_hostname_check', |
| 185 | dest='disable_hostname_check', | 153 | default=False, |
| 186 | default=False, | 154 | help='Do not check whether server cert matches server hostname') |
| 187 | help='Do not check whether server cert matches server hostname') | ||
| 188 | parser.add_argument( | 155 | parser.add_argument( |
| 189 | '-m', '--message', | 156 | '-m', '--message', |
| 190 | metavar='MESSAGE', | 157 | metavar='MESSAGE', |
| @@ -213,23 +180,6 @@ def main(): | |||
| 213 | dest='pull', | 180 | dest='pull', |
| 214 | default=False, | 181 | default=False, |
| 215 | help='Perform a pull operation (default: push)') | 182 | help='Perform a pull operation (default: push)') |
| 216 | if sys.hexversion >= 0x20709f0: | ||
| 217 | parser.add_argument( | ||
| 218 | '-s', '--ssl-version', | ||
| 219 | metavar='VERSION', | ||
| 220 | type=str, | ||
| 221 | dest='ssl_version', | ||
| 222 | default='auto', | ||
| 223 | help='Use SSL version: "auto" (default), ' | ||
| 224 | '"SSLv3", "TLSv1", "TLSv1_1", "TLSv1_2"') | ||
| 225 | else: | ||
| 226 | parser.add_argument( | ||
| 227 | '-s', '--ssl-version', | ||
| 228 | metavar='VERSION', | ||
| 229 | type=str, | ||
| 230 | dest='ssl_version', | ||
| 231 | default='auto', | ||
| 232 | help='Use SSL version: "auto" (default), "SSLv3", "TLSv1"') | ||
| 233 | parser.add_argument( | 183 | parser.add_argument( |
| 234 | '-T', '--socket-timeout', | 184 | '-T', '--socket-timeout', |
| 235 | metavar='SECONDS', | 185 | metavar='SECONDS', |
| @@ -247,29 +197,36 @@ def main(): | |||
| 247 | parser.add_argument( | 197 | parser.add_argument( |
| 248 | '-v', '--version', | 198 | '-v', '--version', |
| 249 | action='version', | 199 | action='version', |
| 250 | version='%(prog)s {0}'.format(__version__), | 200 | version=f'%(prog)s {__version__}', |
| 251 | help='display program-version and exit') | 201 | help='display program-version and exit') |
| 252 | args = parser.parse_args() | 202 | args = parser.parse_args(inargs) |
| 253 | |||
| 254 | if not args.password: | ||
| 255 | try: | ||
| 256 | args.password = getpass.getpass() | ||
| 257 | except Exception as e: | ||
| 258 | sys.stderr.write('Prompt terminated\n') | ||
| 259 | sys.exit(errno.EACCES) | ||
| 260 | elif os.path.isfile(args.password): | ||
| 261 | try: | ||
| 262 | with open(args.password, 'r') as fp: | ||
| 263 | passwd = fp.readline() | ||
| 264 | if passwd.strip(): | ||
| 265 | args.password = passwd.strip() | ||
| 266 | except Exception as e: | ||
| 267 | sys.stderr.write('Unable to open password file: {0}'.format(e)) | ||
| 268 | sys.exit(1) | ||
| 269 | if args.socket_timeout: | 203 | if args.socket_timeout: |
| 270 | socket.setdefaulttimeout(args.socket_timeout) | 204 | socket.setdefaulttimeout(args.socket_timeout) |
| 271 | action_execute(args) | 205 | args.password = fetch_password(args.password) |
| 272 | sys.exit(0) | 206 | try: |
| 207 | context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) | ||
| 208 | context.verify_mode = ssl.CERT_NONE | ||
| 209 | if args.cert: | ||
| 210 | context.verify_mode = ssl.CERT_REQUIRED | ||
| 211 | context.load_verify_locations(cafile=os.path.expanduser(args.cert)) | ||
| 212 | context.check_hostname = bool(not args.disable_hostname_check) | ||
| 213 | if args.ciphers: | ||
| 214 | context.set_ciphers(args.ciphers) | ||
| 215 | if args.pull: | ||
| 216 | print(json.dumps(pull_notifications(context, args), indent=4)) | ||
| 217 | else: | ||
| 218 | push_notification(context, args) | ||
| 219 | print('OK') | ||
| 220 | sys.exit(0) | ||
| 221 | except ssl.SSLError as e: | ||
| 222 | eprint(f'BEINC SSL/TLS error: {e}') | ||
| 223 | sys.exit(errno.EPERM) | ||
| 224 | except socket.error as e: | ||
| 225 | eprint(f'BEINC connection error: {e}') | ||
| 226 | sys.exit(errno.EPERM) | ||
| 227 | except Exception as e: | ||
| 228 | eprint(f'BEINC generic client error: {e}') | ||
| 229 | sys.exit(errno.EPERM) | ||
| 273 | 230 | ||
| 274 | 231 | ||
| 275 | if __name__ == '__main__': | 232 | if __name__ == '__main__': |
