summaryrefslogtreecommitdiff
path: root/beinc_generic_client.py
diff options
context:
space:
mode:
Diffstat (limited to 'beinc_generic_client.py')
-rwxr-xr-xbeinc_generic_client.py269
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
21import argparse 20import argparse
22import errno 21import errno
23import getpass 22import getpass
24import httplib # for Python < 2.7.9 23import io
24import json
25import os 25import os
26import socket 26import socket
27import ssl 27import ssl
28import sys 28import sys
29import xmlrpclib 29import urllib.parse
30import 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
37BEINC_SSL_METHODS = {'TLSv1': ssl.PROTOCOL_TLSv1} 38def eprint(*arg, **kwargs):
38try: 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})
41except:
42 pass
43 41
44 42
45class BEINCCustomHTTPSConnection(httplib.HTTPConnection): 43def 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): 71def 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
77class 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): 99def 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
105def 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
158def main(): 125def 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
275if __name__ == '__main__': 232if __name__ == '__main__':