summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimeon Simeonov2017-05-11 11:36:59 +0200
committerSimeon Simeonov2017-05-11 11:36:59 +0200
commit0bf37c734a03d6ed643c355dc0fcbdfcc788854b (patch)
treea457f744f77727246fcffc23b3a5a6d51118541c
parentfec60828bc210af96a5c04de6965c5f5499c8c4c (diff)
BEINC 3.0 server - py3k version implemented
-rwxr-xr-xbeinc_server.py287
-rw-r--r--dot.beinc_server_logger.ini34
2 files changed, 216 insertions, 105 deletions
diff --git a/beinc_server.py b/beinc_server.py
index 84f7a82..aa68a32 100755
--- a/beinc_server.py
+++ b/beinc_server.py
@@ -1,8 +1,8 @@
1#!/usr/bin/env python 1#!/usr/bin/env python3
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) v3.0
5# Copyright (C) 2013-2015 Simeon Simeonov 5# Copyright (C) 2013-2017 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
@@ -19,19 +19,18 @@
19 19
20 20
21import argparse 21import argparse
22import cgi
22import errno 23import errno
23import getpass 24import getpass
25import http.server
24import json 26import json
27import logging
25import os 28import os
29import ssl
26import sys 30import sys
27 31
28import OpenSSL
29
30from functools import wraps 32from functools import wraps
31 33from logging.config import fileConfig
32from twisted.web import xmlrpc, server
33from twisted.internet import protocol, reactor, ssl
34from twisted.python import filepath, log
35 34
36try: 35try:
37 import pynotify 36 import pynotify
@@ -51,7 +50,7 @@ except ImportError as e:
51 50
52 51
53__author__ = 'Simeon Simeonov' 52__author__ = 'Simeon Simeonov'
54__version__ = '2.0' 53__version__ = '3.0'
55__license__ = 'GPL3' 54__license__ = 'GPL3'
56 55
57 56
@@ -59,19 +58,44 @@ BEINC_OSD_TYPE_NONE = 0
59BEINC_OSD_TYPE_PYNOTIFY = 1 58BEINC_OSD_TYPE_PYNOTIFY = 1
60BEINC_OSD_TYPE_PYOSD = 2 59BEINC_OSD_TYPE_PYOSD = 2
61 60
62BEINC_CURRENT_CONFIG_VERSION = 2 61BEINC_CURRENT_CONFIG_VERSION = 3
62
63
64class BEINCError401(Exception):
65 pass
66
67
68class BEINCError403(Exception):
69 pass
70
71
72class BEINCError404(Exception):
73 pass
63 74
64BEINC_SSL_METHODS = {'SSLv3': OpenSSL.SSL.SSLv3_METHOD, 75
65 'TLSv1': OpenSSL.SSL.TLSv1_METHOD} 76class BEINCError405(Exception):
66try: 77 pass
67 errstr = ("Warning: Current Twisted / " 78
68 "OpenSSL version doesn't support TLSv1.1") 79
69 BEINC_SSL_METHODS.update({'TLSv1_1': OpenSSL.SSL.TLSv1_1_METHOD}) 80def beinc_login_required(method):
70 errstr = ("Warning: Current Twisted / " + 81 """
71 "OpenSSL version doesn't support TLSv1.2") 82 Decorator for checking login credentials
72 BEINC_SSL_METHODS.update({'TLSv1_2': OpenSSL.SSL.TLSv1_2_METHOD}) 83 """
73except: 84
74 sys.stderr.write(errstr + '\n') 85 @wraps(method)
86 def wrapper(self, data, *args, **kwargs):
87 if data.get('resource_name') is None:
88 raise BEINCError403('Resource-name missing')
89 if data.get('password') is None:
90 raise BEINCError401('Password missing')
91 try:
92 instance = self.server.instances[data.get('resource_name')]
93 except Exception as e:
94 raise BEINCError401('Wrong instance or password')
95 if not instance.password_match(data.get('password')):
96 raise BEINCError401('Wrong instance or password')
97 return method(self, data, *args, **kwargs)
98 return wrapper
75 99
76 100
77class BEINCInstance(object): 101class BEINCInstance(object):
@@ -190,7 +214,7 @@ class BEINCInstance(object):
190 214
191 def get_queue(self): 215 def get_queue(self):
192 """ 216 """
193 Reruens a json representation of the message queue 217 Returns a list of dict representation of the message queue
194 """ 218 """
195 r_value = self.__message_queue 219 r_value = self.__message_queue
196 self.__message_queue = list() 220 self.__message_queue = list()
@@ -219,34 +243,112 @@ class BEINCInstance(object):
219 self.__message_queue.append({'title': title, 'message': message}) 243 self.__message_queue.append({'title': title, 'message': message})
220 244
221 245
222def beinc_login_required(method): 246class BEINCCustomHandler(http.server.BaseHTTPRequestHandler):
223 """ 247 """
224 Decorator for checking login credentials
225 """ 248 """
249 def do_POST(self):
250 """
251 Handle POST requests
252 """
253 if self.path.strip('/') not in ('beinc/push', 'beinc/pull'):
254 self.__generate_json_error(404, 'Invalid resource path')
255 return
256 form = cgi.FieldStorage(
257 fp=self.rfile,
258 headers=self.headers,
259 environ={'REQUEST_METHOD': 'POST',
260 'CONTENT_TYPE': self.headers['Content-Type']})
261 # extract all known fields
262 POST_data = dict(
263 resource_name=form.getvalue('resource_name'),
264 password=form.getvalue('password'),
265 title=form.getvalue('title', ''),
266 message=form.getvalue('message', ''))
267 try:
268 result = {}
269 if self.path.strip('/') == 'beinc/push':
270 result = self.__handle_push(POST_data)
271 elif self.path.strip('/') == 'beinc/pull':
272 result = self.__handle_pull(POST_data)
273 self.__render_to_JSON_response(result)
274 except BEINCError401 as e:
275 self.__generate_json_error(401, str(e))
276 except BEINCError403 as e:
277 self.__generate_json_error(403, str(e))
278 except BEINCError404 as e:
279 self.__generate_json_error(404, str(e))
280 except BEINCError405 as e:
281 self.__generate_json_error(405, str(e))
282 except Exception as e:
283 self.__generate_json_error(500,
284 'Unexpected error: {}'.format(str(e)))
226 285
227 @wraps(method) 286 def do_GET(self):
228 def wrapper(self, resource_name, password, *args, **kwargs): 287 """
288 Handle GET Requests
289 """
290 self.__generate_json_error(405, 'Unsupported method')
291
292 @beinc_login_required
293 def __handle_push(self, data):
294 """
295 """
296 instance = self.server.instances[data.get('resource_name')]
229 try: 297 try:
230 instance = self.instances[resource_name] 298 instance.send_message(data.get('title'), data.get('message'))
299 return {'message': 'OK. Sent.'}
231 except Exception as e: 300 except Exception as e:
232 raise xmlrpc.Fault(401, 301 self.__generate_json_error(500, str(e))
233 'Wrong instance or password')
234 if not instance.password_match(password):
235 raise xmlrpc.Fault(401,
236 'Wrong instance or password')
237 return method(self, resource_name, password, *args, **kwargs)
238 return wrapper
239 302
303 @beinc_login_required
304 def __handle_pull(self, data):
305 """
306 """
307 return {}
308
309 def __generate_json_error(self, code, message):
310 """
311 Generates response header and json content for errors
240 312
241class XMLRPCNotifyServer(xmlrpc.XMLRPC): 313 Keyword Arguments:
314 :param code: the HTTP code
315 :type code: int
316
317 :param message: the return message set in the .json response
318 :type message: str
319 """
320 self.send_response(code)
321 self.send_header('Content-type', 'application/json; charset=utf-8')
322 self.end_headers()
323 msg = {'code': code, 'message': message, 'data': {}}
324 self.wfile.write(json.dumps(msg,
325 sort_keys=True,
326 indent=4).encode('utf-8'))
327
328 def __render_to_JSON_response(self, context):
329 """
330 Keyword Arguments:
331 :param context: the context-dict to be converted to json
332 :type context: dict
333 """
334 response = {'code': 200, 'message': 'OK', 'data': {}}
335 response.update(context)
336 self.send_response(200)
337 self.send_header('Content-type', 'application/json; charset=utf-8')
338 self.end_headers()
339 self.wfile.write(json.dumps(response,
340 sort_keys=True,
341 indent=4).encode('utf-8'))
342
343
344class BEINCNotifyServer(http.server.HTTPServer):
242 """ 345 """
243 A class representing the entire server
244 """ 346 """
245 347 def set_config(self, config):
246 def __init__(self, config):
247 """ 348 """
349 Sets the configuration dict for the server, instantiates
350 the BEINC instances and initiates the defined OSD backends
248 """ 351 """
249 xmlrpc.XMLRPC.__init__(self)
250 self.__config = config 352 self.__config = config
251 self.__instances = dict() 353 self.__instances = dict()
252 # initialize pynotify if the module exists and if needed 354 # initialize pynotify if the module exists and if needed
@@ -262,7 +364,7 @@ class XMLRPCNotifyServer(xmlrpc.XMLRPC):
262 try: 364 try:
263 for instance in self.__config['server']['instances']: 365 for instance in self.__config['server']['instances']:
264 self.__instances[instance['name']] = BEINCInstance(instance) 366 self.__instances[instance['name']] = BEINCInstance(instance)
265 print('Instance "{0}" added'.format(instance['name'])) 367 logger.info('Instance "{0}" added'.format(instance['name']))
266 except Exception as e: 368 except Exception as e:
267 sys.stderr.write('Unable to create instance "{0}": {1}\n'.format( 369 sys.stderr.write('Unable to create instance "{0}": {1}\n'.format(
268 instance['name'], 370 instance['name'],
@@ -276,39 +378,8 @@ class XMLRPCNotifyServer(xmlrpc.XMLRPC):
276 """ 378 """
277 return self.__instances 379 return self.__instances
278 380
279 @beinc_login_required
280 def xmlrpc_push(self, resource_name, password, title, message):
281 """
282 Return all passed args.
283 """
284 instance = self.__instances[resource_name]
285 try:
286 instance.send_message(title, message)
287 return 'OK'
288 except Exception as e:
289 sys.stderr.write(
290 'Unable to handle message in {0}: ({1})\n'.format(
291 instance.name,
292 e))
293 raise xmlrpc.Fault(500, 'Unable to send message')
294
295 @beinc_login_required
296 def xmlrpc_pull(self, resource_name, password):
297 """
298 Return sum of arguments.
299 """
300 instance = self.__instances[resource_name]
301 if not instance.queueable:
302 raise xmlrpc.Fault(
303 405,
304 'BEINC instance "{0}" does not support queuing'.format(
305 instance.name))
306 return instance.get_queue()
307
308 381
309def main(): 382if __name__ == '__main__':
310 """
311 """
312 parser = argparse.ArgumentParser( 383 parser = argparse.ArgumentParser(
313 description='The following options are available') 384 description='The following options are available')
314 parser.add_argument( 385 parser.add_argument(
@@ -325,6 +396,21 @@ def main():
325 default='127.0.0.1', 396 default='127.0.0.1',
326 help='BEINC server IP / hostname (default: 127.0.0.1)') 397 help='BEINC server IP / hostname (default: 127.0.0.1)')
327 parser.add_argument( 398 parser.add_argument(
399 '-L', '--logger-name',
400 metavar='NAME',
401 type=str,
402 dest='logger_name',
403 default='',
404 help="BEINC logger name (default: 'beinc')")
405 parser.add_argument(
406 '-l', '--logger-config',
407 metavar='CONFIG',
408 type=str,
409 dest='logger_config',
410 default=os.path.expanduser('~/.beinc_server_logger.ini'),
411 help=('BEINC logger config (.ini) '
412 '(default: ~/.beinc_server_logger.ini)'))
413 parser.add_argument(
328 '-p', '--port', 414 '-p', '--port',
329 metavar='PORT', 415 metavar='PORT',
330 type=int, 416 type=int,
@@ -344,7 +430,6 @@ def main():
344 version='%(prog)s {0}'.format(__version__), 430 version='%(prog)s {0}'.format(__version__),
345 help='Display program-version and exit') 431 help='Display program-version and exit')
346 args = parser.parse_args() 432 args = parser.parse_args()
347 log.startLogging(sys.stdout)
348 try: 433 try:
349 with open(args.config_file, 'r') as fp: 434 with open(args.config_file, 'r') as fp:
350 config_dict = json.load(fp) 435 config_dict = json.load(fp)
@@ -353,6 +438,15 @@ def main():
353 e)) 438 e))
354 sys.exit(errno.EIO) 439 sys.exit(errno.EIO)
355 try: 440 try:
441 if os.path.isfile(args.logger_config):
442 fileConfig(args.logger_config)
443 logger = logging.getLogger(args.logger_name)
444 else:
445 logging.basicConfig(
446 format='%(asctime)s - %(levelname)s - %(message)s',
447 level=logging.DEBUG)
448 logger = logging.getLogger('beinc')
449 logger.info('BEINC starting. Loading config...')
356 if config_dict.get('config_version') != BEINC_CURRENT_CONFIG_VERSION: 450 if config_dict.get('config_version') != BEINC_CURRENT_CONFIG_VERSION:
357 sys.stderr.write( 451 sys.stderr.write(
358 'WARNING: The version of the config-file: {0} ({1}) ' 452 'WARNING: The version of the config-file: {0} ({1}) '
@@ -366,39 +460,22 @@ def main():
366 'ssl_certificate') 460 'ssl_certificate')
367 ssl_private_key = config_dict['server']['general'].get( 461 ssl_private_key = config_dict['server']['general'].get(
368 'ssl_private_key') 462 'ssl_private_key')
369 ssl_method_str = config_dict['server']['general'].get(
370 'ssl_version', 'auto')
371 ssl_acceptable_ciphers_str = config_dict['server']['general'].get( 463 ssl_acceptable_ciphers_str = config_dict['server']['general'].get(
372 'ssl_ciphers', 'auto') 464 'ssl_ciphers')
373 beinc_server = XMLRPCNotifyServer(config_dict) 465 beinc_server = BEINCNotifyServer((args.hostname, args.port),
466 BEINCCustomHandler)
467 beinc_server.set_config(config_dict)
374 if ssl_certificate and ssl_private_key: 468 if ssl_certificate and ssl_private_key:
375 # SSL connection 469 beinc_server.socket = ssl.wrap_socket(
376 cert_path = filepath.FilePath(ssl_certificate) 470 beinc_server.socket,
377 key_path = filepath.FilePath(ssl_private_key) 471 keyfile=ssl_private_key,
378 private_certificate = ssl.PrivateCertificate.loadPEM( 472 certfile=ssl_certificate,
379 key_path.getContent() + cert_path.getContent()) 473 server_side=True,
380 options = private_certificate.options() 474 ssl_version=ssl.PROTOCOL_TLSv1_2,
381 ssl_method = BEINC_SSL_METHODS.get(ssl_method_str) 475 ciphers=ssl_acceptable_ciphers_str)
382 if ssl_method: 476 logger.info('Done!')
383 options.method = ssl_method 477 beinc_server.serve_forever()
384 if ssl_acceptable_ciphers_str.lower() != 'auto':
385 options.acceptableCiphers = (
386 ssl.AcceptableCiphers.fromOpenSSLCipherString(
387 ssl_acceptable_ciphers_str))
388 reactor.listenSSL(args.port,
389 server.Site(beinc_server),
390 options,
391 interface=args.hostname)
392 else:
393 reactor.listenTCP(args.port,
394 server.Site(beinc_server),
395 interface=args.hostname)
396 reactor.run()
397 except Exception as e: 478 except Exception as e:
398 sys.stderr.write('WebServer error: {0}\n'.format(e)) 479 sys.stderr.write('BEINCServer critical error: {0}\n'.format(e))
399 sys.exit(1) 480 sys.exit(1)
400 sys.exit(0) 481 sys.exit(0)
401
402
403if __name__ == "__main__":
404 main()
diff --git a/dot.beinc_server_logger.ini b/dot.beinc_server_logger.ini
new file mode 100644
index 0000000..5b0f89f
--- /dev/null
+++ b/dot.beinc_server_logger.ini
@@ -0,0 +1,34 @@
1[loggers]
2keys=root,beinc_logger
3
4[handlers]
5keys=console_handler
6
7[formatters]
8keys=simple_formatter,verbose_formatter
9
10[formatter_simple_formatter]
11format=%(asctime)s - %(levelname)s - %(message)s
12datefmt=
13class=logging.Formatter
14
15[formatter_verbose_formatter]
16format=%(asctime)s %(levelname)s %(module)s %(process)d %(thread)d %(message)s
17datefmt=
18class=logging.Formatter
19
20[handler_console_handler]
21class=StreamHandler
22level=DEBUG
23formatter=simple_formatter
24args=(sys.stdout,)
25
26[logger_root]
27level=NOTSET
28handlers=file_handler
29
30[logger_beinc_logger]
31level=DEBUG
32handlers=console_handler
33propagate=0
34qualname=beinc