diff options
Diffstat (limited to 'beinc_server.py')
| -rwxr-xr-x | beinc_server.py | 156 |
1 files changed, 95 insertions, 61 deletions
diff --git a/beinc_server.py b/beinc_server.py index 8957e73..77aa35e 100755 --- a/beinc_server.py +++ b/beinc_server.py | |||
| @@ -2,7 +2,7 @@ | |||
| 2 | # -*- coding: utf-8 -*- | 2 | # -*- coding: utf-8 -*- |
| 3 | 3 | ||
| 4 | # Blackmore's Enhanced IRC-Notification Collection (BEINC) v4.0 | 4 | # Blackmore's Enhanced IRC-Notification Collection (BEINC) v4.0 |
| 5 | # Copyright (C) 2013-2020 Simeon Simeonov | 5 | # Copyright (C) 2013-2022 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 |
| @@ -21,15 +21,15 @@ | |||
| 21 | import argparse | 21 | import argparse |
| 22 | import cgi | 22 | import cgi |
| 23 | import errno | 23 | import errno |
| 24 | import io | ||
| 24 | import json | 25 | import json |
| 25 | import logging | 26 | import logging |
| 26 | import os | 27 | import os |
| 27 | import ssl | 28 | import ssl |
| 28 | import sys | 29 | import sys |
| 29 | |||
| 30 | from functools import wraps | 30 | from functools import wraps |
| 31 | from logging.config import fileConfig | ||
| 32 | from http.server import BaseHTTPRequestHandler, HTTPServer | 31 | from http.server import BaseHTTPRequestHandler, HTTPServer |
| 32 | from logging.config import fileConfig | ||
| 33 | 33 | ||
| 34 | try: | 34 | try: |
| 35 | import notify2 as pynotify | 35 | import notify2 as pynotify |
| @@ -38,7 +38,7 @@ except ImportError: | |||
| 38 | 38 | ||
| 39 | 39 | ||
| 40 | __author__ = 'Simeon Simeonov' | 40 | __author__ = 'Simeon Simeonov' |
| 41 | __version__ = '4.1' | 41 | __version__ = '4.2' |
| 42 | __license__ = 'GPL3' | 42 | __license__ = 'GPL3' |
| 43 | 43 | ||
| 44 | 44 | ||
| @@ -71,6 +71,7 @@ def eprint(*arg, **kwargs): | |||
| 71 | 71 | ||
| 72 | def beinc_login_required(method): | 72 | def beinc_login_required(method): |
| 73 | """Decorator for checking login credentials""" | 73 | """Decorator for checking login credentials""" |
| 74 | |||
| 74 | @wraps(method) | 75 | @wraps(method) |
| 75 | def wrapper(self, data, *arg, **kwargs): | 76 | def wrapper(self, data, *arg, **kwargs): |
| 76 | if data.get('resource_name') is None: | 77 | if data.get('resource_name') is None: |
| @@ -84,6 +85,7 @@ def beinc_login_required(method): | |||
| 84 | if not instance.password_match(data.get('password')): | 85 | if not instance.password_match(data.get('password')): |
| 85 | raise BEINCError401('Wrong instance or password') | 86 | raise BEINCError401('Wrong instance or password') |
| 86 | return method(self, data, *arg, **kwargs) | 87 | return method(self, data, *arg, **kwargs) |
| 88 | |||
| 87 | return wrapper | 89 | return wrapper |
| 88 | 90 | ||
| 89 | 91 | ||
| @@ -105,19 +107,24 @@ class BEINCInstance: | |||
| 105 | self._queue_size = 0 # disable queueing | 107 | self._queue_size = 0 # disable queueing |
| 106 | if pynotify is None: | 108 | if pynotify is None: |
| 107 | eprint('This server does not possess pynotify capability') | 109 | eprint('This server does not possess pynotify capability') |
| 108 | eprint(f'Remove the instance {self._name} or define it with ' | 110 | eprint( |
| 109 | f'"osd_system": "none" or other ' | 111 | f'Remove the instance {self._name} or define it with ' |
| 110 | f'available backend') | 112 | f'"osd_system": "none" or other ' |
| 113 | f'available backend' | ||
| 114 | ) | ||
| 111 | sys.exit(errno.EPERM) | 115 | sys.exit(errno.EPERM) |
| 112 | try: | 116 | try: |
| 113 | self._osd_notification = pynotify.Notification(' ') | 117 | self._osd_notification = pynotify.Notification(' ') |
| 114 | self._osd_notification.timeout = 1000 * int( | 118 | self._osd_notification.timeout = 1000 * int( |
| 115 | instance_dict.get('osd_timeout', 5)) | 119 | instance_dict.get('osd_timeout', 5) |
| 120 | ) | ||
| 116 | self._osd_notification.set_category('im.received') | 121 | self._osd_notification.set_category('im.received') |
| 117 | self._osd_type = BEINC_OSD_TYPE_PYNOTIFY | 122 | self._osd_type = BEINC_OSD_TYPE_PYNOTIFY |
| 118 | except Exception as e: | 123 | except Exception as e: |
| 119 | eprint(f'Unable to set up a pynotify notification object ' | 124 | eprint( |
| 120 | f'for "{self._name}" ({e})') | 125 | f'Unable to set up a pynotify notification object ' |
| 126 | f'for "{self._name}" ({e})' | ||
| 127 | ) | ||
| 121 | sys.exit(errno.EPERM) | 128 | sys.exit(errno.EPERM) |
| 122 | 129 | ||
| 123 | @property | 130 | @property |
| @@ -195,6 +202,7 @@ class BEINCInstance: | |||
| 195 | 202 | ||
| 196 | class BEINCCustomHandler(BaseHTTPRequestHandler): | 203 | class BEINCCustomHandler(BaseHTTPRequestHandler): |
| 197 | """Custom handler""" | 204 | """Custom handler""" |
| 205 | |||
| 198 | def do_POST(self): | 206 | def do_POST(self): |
| 199 | """Handle POST requests""" | 207 | """Handle POST requests""" |
| 200 | if self.path.strip('/') not in ('beinc/push', 'beinc/pull'): | 208 | if self.path.strip('/') not in ('beinc/push', 'beinc/pull'): |
| @@ -203,14 +211,18 @@ class BEINCCustomHandler(BaseHTTPRequestHandler): | |||
| 203 | form = cgi.FieldStorage( | 211 | form = cgi.FieldStorage( |
| 204 | fp=self.rfile, | 212 | fp=self.rfile, |
| 205 | headers=self.headers, | 213 | headers=self.headers, |
| 206 | environ={'REQUEST_METHOD': 'POST', | 214 | environ={ |
| 207 | 'CONTENT_TYPE': self.headers['Content-Type']}) | 215 | 'REQUEST_METHOD': 'POST', |
| 216 | 'CONTENT_TYPE': self.headers['Content-Type'], | ||
| 217 | }, | ||
| 218 | ) | ||
| 208 | # extract all known fields | 219 | # extract all known fields |
| 209 | POST_data = dict( | 220 | POST_data = dict( |
| 210 | resource_name=form.getvalue('resource_name'), | 221 | resource_name=form.getvalue('resource_name'), |
| 211 | password=form.getvalue('password'), | 222 | password=form.getvalue('password'), |
| 212 | title=form.getvalue('title', ''), | 223 | title=form.getvalue('title', ''), |
| 213 | message=form.getvalue('message', '')) | 224 | message=form.getvalue('message', ''), |
| 225 | ) | ||
| 214 | try: | 226 | try: |
| 215 | result = {} | 227 | result = {} |
| 216 | if self.path.strip('/') == 'beinc/push': | 228 | if self.path.strip('/') == 'beinc/push': |
| @@ -249,10 +261,11 @@ class BEINCCustomHandler(BaseHTTPRequestHandler): | |||
| 249 | instance = self.server.instances[data.get('resource_name')] | 261 | instance = self.server.instances[data.get('resource_name')] |
| 250 | try: | 262 | try: |
| 251 | if not instance.queueable: | 263 | if not instance.queueable: |
| 252 | raise BEINCError405( | 264 | raise BEINCError405('This instance does not support queuing') |
| 253 | 'This instance does not support queuing') | 265 | return { |
| 254 | return {'message': 'OK. Fetched.', | 266 | 'message': 'OK. Fetched.', |
| 255 | 'data': {'messages': instance.get_queue()}} | 267 | 'data': {'messages': instance.get_queue()}, |
| 268 | } | ||
| 256 | except Exception as e: | 269 | except Exception as e: |
| 257 | self._generate_json_error(500, str(e)) | 270 | self._generate_json_error(500, str(e)) |
| 258 | 271 | ||
| @@ -270,9 +283,9 @@ class BEINCCustomHandler(BaseHTTPRequestHandler): | |||
| 270 | self.send_header('Content-type', 'application/json; charset=utf-8') | 283 | self.send_header('Content-type', 'application/json; charset=utf-8') |
| 271 | self.end_headers() | 284 | self.end_headers() |
| 272 | msg = {'code': code, 'message': message, 'data': {}} | 285 | msg = {'code': code, 'message': message, 'data': {}} |
| 273 | self.wfile.write(json.dumps(msg, | 286 | self.wfile.write( |
| 274 | sort_keys=True, | 287 | json.dumps(msg, sort_keys=True, indent=4).encode('utf-8') |
| 275 | indent=4).encode('utf-8')) | 288 | ) |
| 276 | 289 | ||
| 277 | def _render_to_JSON_response(self, context): | 290 | def _render_to_JSON_response(self, context): |
| 278 | """ | 291 | """ |
| @@ -284,13 +297,14 @@ class BEINCCustomHandler(BaseHTTPRequestHandler): | |||
| 284 | self.send_response(200) | 297 | self.send_response(200) |
| 285 | self.send_header('Content-type', 'application/json; charset=utf-8') | 298 | self.send_header('Content-type', 'application/json; charset=utf-8') |
| 286 | self.end_headers() | 299 | self.end_headers() |
| 287 | self.wfile.write(json.dumps(response, | 300 | self.wfile.write( |
| 288 | sort_keys=True, | 301 | json.dumps(response, sort_keys=True, indent=4).encode('utf-8') |
| 289 | indent=4).encode('utf-8')) | 302 | ) |
| 290 | 303 | ||
| 291 | 304 | ||
| 292 | class BEINCNotifyServer(HTTPServer): | 305 | class BEINCNotifyServer(HTTPServer): |
| 293 | """BEINCNotifyServer class""" | 306 | """BEINCNotifyServer class""" |
| 307 | |||
| 294 | def __init__(self, *arg, **kwargs): | 308 | def __init__(self, *arg, **kwargs): |
| 295 | """Default constructor""" | 309 | """Default constructor""" |
| 296 | super().__init__(*arg, **kwargs) | 310 | super().__init__(*arg, **kwargs) |
| @@ -319,9 +333,7 @@ class BEINCNotifyServer(HTTPServer): | |||
| 319 | self._instances[instance['name']] = BEINCInstance(instance) | 333 | self._instances[instance['name']] = BEINCInstance(instance) |
| 320 | logger.info('Instance %s added', instance['name']) | 334 | logger.info('Instance %s added', instance['name']) |
| 321 | except Exception as e: | 335 | except Exception as e: |
| 322 | eprint('Unable to create instance "{0}": {1}'.format( | 336 | eprint(f"Unable to create instance \"{instance['name']}\": {e}") |
| 323 | instance['name'], | ||
| 324 | e)) | ||
| 325 | sys.exit(1) | 337 | sys.exit(1) |
| 326 | 338 | ||
| 327 | @property | 339 | @property |
| @@ -334,51 +346,66 @@ class BEINCNotifyServer(HTTPServer): | |||
| 334 | 346 | ||
| 335 | if __name__ == '__main__': | 347 | if __name__ == '__main__': |
| 336 | parser = argparse.ArgumentParser( | 348 | parser = argparse.ArgumentParser( |
| 337 | description='The following options are available') | 349 | description='The following options are available' |
| 350 | ) | ||
| 338 | parser.add_argument( | 351 | parser.add_argument( |
| 339 | '-H', '--hostname', | 352 | '-H', |
| 353 | '--hostname', | ||
| 340 | metavar='HOSTNAME', | 354 | metavar='HOSTNAME', |
| 341 | type=str, | 355 | type=str, |
| 342 | dest='hostname', | 356 | dest='hostname', |
| 343 | default='127.0.0.1', | 357 | default='127.0.0.1', |
| 344 | help='BEINC server IP / hostname (default: 127.0.0.1)') | 358 | help='BEINC server IP / hostname (default: 127.0.0.1)', |
| 359 | ) | ||
| 345 | parser.add_argument( | 360 | parser.add_argument( |
| 346 | '-L', '--logger-name', | 361 | '-L', |
| 362 | '--logger-name', | ||
| 347 | metavar='NAME', | 363 | metavar='NAME', |
| 348 | type=str, | 364 | type=str, |
| 349 | dest='logger_name', | 365 | dest='logger_name', |
| 350 | default='', | 366 | default='', |
| 351 | help="BEINC logger name (default: 'beinc')") | 367 | help="BEINC logger name (default: 'beinc')", |
| 368 | ) | ||
| 352 | parser.add_argument( | 369 | parser.add_argument( |
| 353 | '-l', '--logger-config', | 370 | '-l', |
| 371 | '--logger-config', | ||
| 354 | metavar='CONFIG', | 372 | metavar='CONFIG', |
| 355 | type=str, | 373 | type=str, |
| 356 | dest='logger_config', | 374 | dest='logger_config', |
| 357 | default=os.path.expanduser('~/.beinc_server_logger.ini'), | 375 | default=os.path.expanduser('~/.beinc_server_logger.ini'), |
| 358 | help=('BEINC logger config (.ini) ' | 376 | help=( |
| 359 | '(default: ~/.beinc_server_logger.ini)')) | 377 | 'BEINC logger config (.ini) ' |
| 378 | '(default: ~/.beinc_server_logger.ini)' | ||
| 379 | ), | ||
| 380 | ) | ||
| 360 | parser.add_argument( | 381 | parser.add_argument( |
| 361 | '-p', '--port', | 382 | '-p', |
| 383 | '--port', | ||
| 362 | metavar='PORT', | 384 | metavar='PORT', |
| 363 | type=int, | 385 | type=int, |
| 364 | dest='port', | 386 | dest='port', |
| 365 | default=9998, | 387 | default=9998, |
| 366 | help='BEINC server port (default: 9998)') | 388 | help='BEINC server port (default: 9998)', |
| 389 | ) | ||
| 367 | parser.add_argument( | 390 | parser.add_argument( |
| 368 | '-f', '--config-file', | 391 | '-f', |
| 392 | '--config-file', | ||
| 369 | metavar='FILE', | 393 | metavar='FILE', |
| 370 | type=str, | 394 | type=str, |
| 371 | default=os.path.expanduser('~/.beinc_server.json'), | 395 | default=os.path.expanduser('~/.beinc_server.json'), |
| 372 | dest='config_file', | 396 | dest='config_file', |
| 373 | help='BEINC config file (default: ~/.beinc_server.json)') | 397 | help='BEINC config file (default: ~/.beinc_server.json)', |
| 398 | ) | ||
| 374 | parser.add_argument( | 399 | parser.add_argument( |
| 375 | '-v', '--version', | 400 | '-v', |
| 401 | '--version', | ||
| 376 | action='version', | 402 | action='version', |
| 377 | version=f'%(prog)s {__version__}', | 403 | version=f'%(prog)s {__version__}', |
| 378 | help='Display program-version and exit') | 404 | help='Display program-version and exit', |
| 405 | ) | ||
| 379 | args = parser.parse_args() | 406 | args = parser.parse_args() |
| 380 | try: | 407 | try: |
| 381 | with open(args.config_file, 'r') as fp: | 408 | with io.open(args.config_file, 'r', encoding='utf-8') as fp: |
| 382 | config_dict = json.load(fp) | 409 | config_dict = json.load(fp) |
| 383 | except Exception as e: | 410 | except Exception as e: |
| 384 | eprint(f'Unable to parse {args.config_file}: {e}') | 411 | eprint(f'Unable to parse {args.config_file}: {e}') |
| @@ -390,35 +417,42 @@ if __name__ == '__main__': | |||
| 390 | else: | 417 | else: |
| 391 | logging.basicConfig( | 418 | logging.basicConfig( |
| 392 | format='%(asctime)s - %(levelname)s - %(message)s', | 419 | format='%(asctime)s - %(levelname)s - %(message)s', |
| 393 | level=logging.DEBUG) | 420 | level=logging.DEBUG, |
| 421 | ) | ||
| 394 | logger = logging.getLogger('beinc') | 422 | logger = logging.getLogger('beinc') |
| 395 | logger.info('BEINC starting. Loading config...') | 423 | logger.info('BEINC starting. Loading config...') |
| 396 | if config_dict.get('config_version') != BEINC_CURRENT_CONFIG_VERSION: | 424 | if config_dict.get('config_version') != BEINC_CURRENT_CONFIG_VERSION: |
| 397 | eprint( | 425 | eprint( |
| 398 | 'WARNING: The version of the config-file: {0} ({1}) ' | 426 | f'WARNING: The version of the config-file: {args.config_file} ' |
| 399 | 'does not correspond to the latest version supported ' | 427 | f'({config_dict.get("config_version", "Not set")}) does not ' |
| 400 | 'by this program ({2})\nCheck beinc_config_sample.json ' | 428 | 'correspond to the latest version supported by this program ' |
| 401 | 'for the newest features!'.format( | 429 | f'({BEINC_CURRENT_CONFIG_VERSION})\n' |
| 402 | args.config_file, | 430 | 'Check beinc_config_sample.json for the newest features!' |
| 403 | config_dict.get('config_version', 'Not set'), | 431 | ) |
| 404 | BEINC_CURRENT_CONFIG_VERSION)) | ||
| 405 | ssl_certificate = config_dict['server']['general'].get( | 432 | ssl_certificate = config_dict['server']['general'].get( |
| 406 | 'ssl_certificate') | 433 | 'ssl_certificate' |
| 434 | ) | ||
| 407 | ssl_private_key = config_dict['server']['general'].get( | 435 | ssl_private_key = config_dict['server']['general'].get( |
| 408 | 'ssl_private_key') | 436 | 'ssl_private_key' |
| 437 | ) | ||
| 409 | ssl_acceptable_ciphers_str = config_dict['server']['general'].get( | 438 | ssl_acceptable_ciphers_str = config_dict['server']['general'].get( |
| 410 | 'ssl_ciphers') | 439 | 'ssl_ciphers' |
| 411 | beinc_server = BEINCNotifyServer((args.hostname, args.port), | 440 | ) |
| 412 | BEINCCustomHandler) | 441 | beinc_server = BEINCNotifyServer( |
| 442 | (args.hostname, args.port), | ||
| 443 | BEINCCustomHandler, | ||
| 444 | ) | ||
| 413 | beinc_server.set_config(config_dict) | 445 | beinc_server.set_config(config_dict) |
| 414 | if ssl_certificate and ssl_private_key: | 446 | if ssl_certificate and ssl_private_key: |
| 415 | beinc_server.socket = ssl.wrap_socket( | 447 | context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) |
| 416 | beinc_server.socket, | 448 | context.load_cert_chain( |
| 417 | keyfile=ssl_private_key, | 449 | certfile=ssl_certificate, keyfile=ssl_private_key |
| 418 | certfile=ssl_certificate, | 450 | ) |
| 419 | server_side=True, | 451 | if ssl_acceptable_ciphers_str is not None: |
| 420 | ssl_version=ssl.PROTOCOL_TLSv1_2, | 452 | context.set_ciphers(ssl_acceptable_ciphers_str) |
| 421 | ciphers=ssl_acceptable_ciphers_str) | 453 | beinc_server.socket = context.wrap_socket( |
| 454 | beinc_server.socket, server_side=True | ||
| 455 | ) | ||
| 422 | logger.info('Done!') | 456 | logger.info('Done!') |
| 423 | beinc_server.serve_forever() | 457 | beinc_server.serve_forever() |
| 424 | except KeyboardInterrupt: | 458 | except KeyboardInterrupt: |
