summaryrefslogtreecommitdiff
path: root/beinc_server.py
diff options
context:
space:
mode:
Diffstat (limited to 'beinc_server.py')
-rwxr-xr-xbeinc_server.py255
1 files changed, 122 insertions, 133 deletions
diff --git a/beinc_server.py b/beinc_server.py
index ad81ce9..0f6c248 100755
--- a/beinc_server.py
+++ b/beinc_server.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) v3.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
@@ -29,26 +29,16 @@ import sys
29 29
30from functools import wraps 30from functools import wraps
31from logging.config import fileConfig 31from logging.config import fileConfig
32 32from http.server import BaseHTTPRequestHandler, HTTPServer
33PY2 = sys.version_info[0] == 2
34PY3 = sys.version_info[0] == 3
35
36if PY3:
37 from http.server import BaseHTTPRequestHandler, HTTPServer
38else:
39 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
40 33
41try: 34try:
42 if PY3: 35 import notify2 as pynotify
43 import notify2 as pynotify 36except ImportError:
44 else:
45 import pynotify
46except ImportError as e:
47 pynotify = None 37 pynotify = None
48 38
49 39
50__author__ = 'Simeon Simeonov' 40__author__ = 'Simeon Simeonov'
51__version__ = '3.0' 41__version__ = '4.0'
52__license__ = 'GPL3' 42__license__ = 'GPL3'
53 43
54 44
@@ -59,28 +49,30 @@ BEINC_CURRENT_CONFIG_VERSION = 3
59 49
60 50
61class BEINCError401(Exception): 51class BEINCError401(Exception):
62 pass 52 """BEINCError401"""
63 53
64 54
65class BEINCError403(Exception): 55class BEINCError403(Exception):
66 pass 56 """BEINCError403"""
67 57
68 58
69class BEINCError404(Exception): 59class BEINCError404(Exception):
70 pass 60 """BEINCError404"""
71 61
72 62
73class BEINCError405(Exception): 63class BEINCError405(Exception):
74 pass 64 """BEINCError405"""
75 65
76 66
77def beinc_login_required(method): 67def eprint(*arg, **kwargs):
78 """ 68 """stdderr print wrapper"""
79 Decorator for checking login credentials 69 print(*arg, file=sys.stderr, flush=True, **kwargs)
80 """ 70
81 71
72def beinc_login_required(method):
73 """Decorator for checking login credentials"""
82 @wraps(method) 74 @wraps(method)
83 def wrapper(self, data, *args, **kwargs): 75 def wrapper(self, data, *arg, **kwargs):
84 if data.get('resource_name') is None: 76 if data.get('resource_name') is None:
85 raise BEINCError403('Resource-name missing') 77 raise BEINCError403('Resource-name missing')
86 if data.get('password') is None: 78 if data.get('password') is None:
@@ -91,122 +83,122 @@ def beinc_login_required(method):
91 raise BEINCError401('Wrong instance or password') 83 raise BEINCError401('Wrong instance or password')
92 if not instance.password_match(data.get('password')): 84 if not instance.password_match(data.get('password')):
93 raise BEINCError401('Wrong instance or password') 85 raise BEINCError401('Wrong instance or password')
94 return method(self, data, *args, **kwargs) 86 return method(self, data, *arg, **kwargs)
95 return wrapper 87 return wrapper
96 88
97 89
98class BEINCInstance(object): 90class BEINCInstance:
99 """ 91 """Represents a single server-instance"""
100 Represents a single server-instance
101 """
102 92
103 def __init__(self, instance_dict): 93 def __init__(self, instance_dict):
104 """ 94 """
105 instance_dict: the config-dictionary node that represents this instance 95 instance_dict: the config-dictionary node that represents this instance
106 """ 96 """
107 self.__message_queue = list() 97 self._message_queue = []
108 self.__osd_type = BEINC_OSD_TYPE_NONE 98 self._osd_type = BEINC_OSD_TYPE_NONE
109 self.__osd_notification = None 99 self._osd_notification = None
110 100
111 self.__name = instance_dict.get('name') 101 self._name = instance_dict.get('name')
112 self.__password = instance_dict.get('password', '') 102 self._password = instance_dict.get('password', '')
113 self.__queue_size = int(instance_dict.get('queue_size', 3)) 103 self._queue_size = int(instance_dict.get('queue_size', 3))
114 if instance_dict['osd_system'].lower() == 'pynotify': 104 if instance_dict['osd_system'].lower() == 'pynotify':
115 self.__queue_size = 0 # disable queueing 105 self._queue_size = 0 # disable queueing
116 if pynotify is None: 106 if pynotify is None:
117 sys.stderr.write( 107 eprint('This server does not possess pynotify capability')
118 'This server does not possess pynotify capability\n') 108 eprint(f'Remove the instance {self._name} or define it with '
119 sys.stderr.write( 109 f'"osd_system": "none" or other '
120 'Remove the instance {0}'.format(self.__name)) 110 f'available backend')
121 sys.stderr.write(
122 'or define it with "osd_system": "none" '
123 'or other available backend\n')
124 sys.exit(errno.EPERM) 111 sys.exit(errno.EPERM)
125 try: 112 try:
126 self.__osd_notification = pynotify.Notification(' ') 113 self._osd_notification = pynotify.Notification(' ')
127 if PY3: 114 self._osd_notification.timeout = 1000 * int(
128 self.__osd_notification.timeout = 1000 * int( 115 instance_dict.get('osd_timeout', 5))
129 instance_dict.get('osd_timeout', 5)) 116 self._osd_notification.set_category('im.received')
130 self.__osd_notification.set_category('im.received') 117 self._osd_type = BEINC_OSD_TYPE_PYNOTIFY
131 else:
132 self.__osd_notification.set_timeout(
133 1000 * int(instance_dict.get('osd_timeout', 5)))
134 self.__osd_notification.set_property(
135 'app_name',
136 '{0} {1}'.format(sys.argv[0], __version__))
137 self.__osd_type = BEINC_OSD_TYPE_PYNOTIFY
138 except Exception as e: 118 except Exception as e:
139 sys.stderr.write( 119 eprint(f'Unable to set up a pynotify notification object '
140 'Unable to set up a ' 120 f'for "{self._name}" ({e})')
141 'pynotify notification object for "{0}" ({1})\n'.format(
142 self.__name,
143 e))
144 sys.exit(errno.EPERM) 121 sys.exit(errno.EPERM)
145 122
146 @property 123 @property
147 def name(self): 124 def name(self):
148 """ 125 """name-property for the server instance (read-only)"""
149 name-property for the server instance (read-only) 126 return self._name
150 """
151 return self.__name
152 127
153 @property 128 @property
154 def queueable(self): 129 def queueable(self):
155 """ 130 """True if this instance has a queueing capability (read-only)"""
156 True if this instance has a queueing capability (read-only) 131 return bool(self._queue_size)
157 """
158 return bool(self.__queue_size)
159 132
160 def password_match(self, password): 133 def password_match(self, password):
161 """ 134 """
162 Returns True if 'passowrd' matches the instance-password, 135 Returns True if 'passowrd' matches the instance-password,
163 otherwise - False 136 otherwise - False
137
138 :param password: The password to compare
139 :type password: str
140
141 :return: True if the password matches, False otherwise
142 :rtype: bool
164 """ 143 """
165 return True if self.__password == password else False 144 return self._password == password
166 145
167 def send_message(self, title, message): 146 def send_message(self, title, message):
168 """ 147 """
169 Displays or enqueues the message, 148 Displays or enqueues the message,
170 depending on the instance's type in regard to the osd_system 149 depending on the instance's type in regard to the osd_system
150
151 :param title: The title
152 :type title: str
153
154 :param message: The message
155 :type message: str
171 """ 156 """
172 if self.__osd_type == BEINC_OSD_TYPE_PYNOTIFY: 157 if self._osd_type == BEINC_OSD_TYPE_PYNOTIFY:
173 self.__send_pynotify_messaage(title, message) 158 self._send_pynotify_messaage(title, message)
174 else: 159 else:
175 self.__send_message_to_queue(title, message) 160 self._send_message_to_queue(title, message)
176 161
177 def get_queue(self): 162 def get_queue(self):
178 """ 163 """Returns a list of dict representation of the message queue"""
179 Returns a list of dict representation of the message queue 164 r_value = self._message_queue
180 """ 165 self._message_queue = []
181 r_value = self.__message_queue
182 self.__message_queue = list()
183 return r_value 166 return r_value
184 167
185 def __send_pynotify_messaage(self, title, message): 168 def _send_pynotify_messaage(self, title, message):
186 """ 169 """
187 Displays pynotify message 170 Displays pynotify message
171
172 :param title: The title
173 :type title: str
174
175 :param message: The message
176 :type message: str
188 """ 177 """
189 self.__osd_notification.update(summary=title, message=message) 178 self._osd_notification.update(summary=title, message=message)
190 self.__osd_notification.show() 179 self._osd_notification.show()
191 180
192 def __send_message_to_queue(self, title, message): 181 def _send_message_to_queue(self, title, message):
193 """ 182 """
194 Enqueues the message 183 Enqueues the message
184
185 :param title: The title
186 :type title: str
187
188 :param message: The message
189 :type message: str
195 """ 190 """
196 if len(self.__message_queue) >= self.__queue_size: 191 if len(self._message_queue) >= self._queue_size:
197 self.__message_queue.pop(0) 192 self._message_queue.pop(0)
198 self.__message_queue.append({'title': title, 'message': message}) 193 self._message_queue.append({'title': title, 'message': message})
199 194
200 195
201class BEINCCustomHandler(BaseHTTPRequestHandler): 196class BEINCCustomHandler(BaseHTTPRequestHandler):
202 """ 197 """Custom handler"""
203 """
204 def do_POST(self): 198 def do_POST(self):
205 """ 199 """Handle POST requests"""
206 Handle POST requests
207 """
208 if self.path.strip('/') not in ('beinc/push', 'beinc/pull'): 200 if self.path.strip('/') not in ('beinc/push', 'beinc/pull'):
209 self.__generate_json_error(404, 'Invalid resource path') 201 self._generate_json_error(404, 'Invalid resource path')
210 return 202 return
211 form = cgi.FieldStorage( 203 form = cgi.FieldStorage(
212 fp=self.rfile, 204 fp=self.rfile,
@@ -222,43 +214,38 @@ class BEINCCustomHandler(BaseHTTPRequestHandler):
222 try: 214 try:
223 result = {} 215 result = {}
224 if self.path.strip('/') == 'beinc/push': 216 if self.path.strip('/') == 'beinc/push':
225 result = self.__handle_push(POST_data) 217 result = self._handle_push(POST_data)
226 elif self.path.strip('/') == 'beinc/pull': 218 elif self.path.strip('/') == 'beinc/pull':
227 result = self.__handle_pull(POST_data) 219 result = self._handle_pull(POST_data)
228 self.__render_to_JSON_response(result) 220 self._render_to_JSON_response(result)
229 except BEINCError401 as e: 221 except BEINCError401 as e:
230 self.__generate_json_error(401, str(e)) 222 self._generate_json_error(401, str(e))
231 except BEINCError403 as e: 223 except BEINCError403 as e:
232 self.__generate_json_error(403, str(e)) 224 self._generate_json_error(403, str(e))
233 except BEINCError404 as e: 225 except BEINCError404 as e:
234 self.__generate_json_error(404, str(e)) 226 self._generate_json_error(404, str(e))
235 except BEINCError405 as e: 227 except BEINCError405 as e:
236 self.__generate_json_error(405, str(e)) 228 self._generate_json_error(405, str(e))
237 except Exception as e: 229 except Exception as e:
238 self.__generate_json_error(500, 230 self._generate_json_error(500, f'Unexpected error: {e}')
239 'Unexpected error: {}'.format(str(e)))
240 231
241 def do_GET(self): 232 def do_GET(self):
242 """ 233 """Handle GET Requests"""
243 Handle GET Requests 234 self._generate_json_error(405, 'Unsupported method')
244 """
245 self.__generate_json_error(405, 'Unsupported method')
246 235
247 @beinc_login_required 236 @beinc_login_required
248 def __handle_push(self, data): 237 def _handle_push(self, data):
249 """ 238 """Handle push"""
250 """
251 instance = self.server.instances[data.get('resource_name')] 239 instance = self.server.instances[data.get('resource_name')]
252 try: 240 try:
253 instance.send_message(data.get('title'), data.get('message')) 241 instance.send_message(data.get('title'), data.get('message'))
254 return {'message': 'OK. Sent.'} 242 return {'message': 'OK. Sent.'}
255 except Exception as e: 243 except Exception as e:
256 self.__generate_json_error(500, str(e)) 244 self._generate_json_error(500, str(e))
257 245
258 @beinc_login_required 246 @beinc_login_required
259 def __handle_pull(self, data): 247 def _handle_pull(self, data):
260 """ 248 """Handle pull"""
261 """
262 instance = self.server.instances[data.get('resource_name')] 249 instance = self.server.instances[data.get('resource_name')]
263 try: 250 try:
264 if not instance.queueable: 251 if not instance.queueable:
@@ -267,13 +254,12 @@ class BEINCCustomHandler(BaseHTTPRequestHandler):
267 return {'message': 'OK. Fetched.', 254 return {'message': 'OK. Fetched.',
268 'data': {'messages': instance.get_queue()}} 255 'data': {'messages': instance.get_queue()}}
269 except Exception as e: 256 except Exception as e:
270 self.__generate_json_error(500, str(e)) 257 self._generate_json_error(500, str(e))
271 258
272 def __generate_json_error(self, code, message): 259 def _generate_json_error(self, code, message):
273 """ 260 """
274 Generates response header and json content for errors 261 Generates response header and json content for errors
275 262
276 Keyword Arguments:
277 :param code: the HTTP code 263 :param code: the HTTP code
278 :type code: int 264 :type code: int
279 265
@@ -288,9 +274,8 @@ class BEINCCustomHandler(BaseHTTPRequestHandler):
288 sort_keys=True, 274 sort_keys=True,
289 indent=4).encode('utf-8')) 275 indent=4).encode('utf-8'))
290 276
291 def __render_to_JSON_response(self, context): 277 def _render_to_JSON_response(self, context):
292 """ 278 """
293 Keyword Arguments:
294 :param context: the context-dict to be converted to json 279 :param context: the context-dict to be converted to json
295 :type context: dict 280 :type context: dict
296 """ 281 """
@@ -305,31 +290,36 @@ class BEINCCustomHandler(BaseHTTPRequestHandler):
305 290
306 291
307class BEINCNotifyServer(HTTPServer): 292class BEINCNotifyServer(HTTPServer):
308 """ 293 """BEINCNotifyServer class"""
309 """ 294 def __init__(self, *arg, **kwargs):
295 """Default constructor"""
296 super().__init__(*arg, **kwargs)
297 self._config = None
298 self._instances = {}
299
310 def set_config(self, config): 300 def set_config(self, config):
311 """ 301 """
312 Sets the configuration dict for the server, instantiates 302 Sets the configuration dict for the server, instantiates
313 the BEINC instances and initiates the defined OSD backends 303 the BEINC instances and initiates the defined OSD backends
314 """ 304 """
315 self.__config = config 305 self._config = config
316 self.__instances = dict()
317 # initialize pynotify if the module exists and if needed 306 # initialize pynotify if the module exists and if needed
318 if pynotify: 307 if pynotify:
319 for instance in self.__config['server']['instances']: 308 for instance in self._config['server']['instances']:
320 # check if we have at least one instance that uses pynotify 309 # check if we have at least one instance that uses pynotify
321 # before initializing it 310 # before initializing it
322 if instance.get('osd_system', '').lower() == 'pynotify': 311 if instance.get('osd_system', '').lower() == 'pynotify':
323 if not pynotify.init('BEINC Notify'): 312 if not pynotify.init('BEINC Notify'):
324 sys.stderr.write('pynotify.init failed! Exiting...\n') 313 eprint('pynotify.init failed! Exiting...')
325 sys.exit(1) 314 sys.exit(1)
326 break 315 break
316 instance = {'name': 'Invalid'}
327 try: 317 try:
328 for instance in self.__config['server']['instances']: 318 for instance in self._config['server']['instances']:
329 self.__instances[instance['name']] = BEINCInstance(instance) 319 self._instances[instance['name']] = BEINCInstance(instance)
330 logger.info('Instance "{0}" added'.format(instance['name'])) 320 logger.info('Instance %s added', instance['name'])
331 except Exception as e: 321 except Exception as e:
332 sys.stderr.write('Unable to create instance "{0}": {1}\n'.format( 322 eprint('Unable to create instance "{0}": {1}'.format(
333 instance['name'], 323 instance['name'],
334 e)) 324 e))
335 sys.exit(1) 325 sys.exit(1)
@@ -339,7 +329,7 @@ class BEINCNotifyServer(HTTPServer):
339 """ 329 """
340 a property that returns the instance list (read-only) 330 a property that returns the instance list (read-only)
341 """ 331 """
342 return self.__instances 332 return self._instances
343 333
344 334
345if __name__ == '__main__': 335if __name__ == '__main__':
@@ -390,15 +380,14 @@ if __name__ == '__main__':
390 parser.add_argument( 380 parser.add_argument(
391 '-v', '--version', 381 '-v', '--version',
392 action='version', 382 action='version',
393 version='%(prog)s {0}'.format(__version__), 383 version=f'%(prog)s {__version__}',
394 help='Display program-version and exit') 384 help='Display program-version and exit')
395 args = parser.parse_args() 385 args = parser.parse_args()
396 try: 386 try:
397 with open(args.config_file, 'r') as fp: 387 with open(args.config_file, 'r') as fp:
398 config_dict = json.load(fp) 388 config_dict = json.load(fp)
399 except Exception as e: 389 except Exception as e:
400 sys.stderr.write('Unable to parse {0}: {1}\n'.format(args.config_file, 390 eprint(f'Unable to parse {args.config_file}: {e}')
401 e))
402 sys.exit(errno.EIO) 391 sys.exit(errno.EIO)
403 try: 392 try:
404 if os.path.isfile(args.logger_config): 393 if os.path.isfile(args.logger_config):
@@ -411,11 +400,11 @@ if __name__ == '__main__':
411 logger = logging.getLogger('beinc') 400 logger = logging.getLogger('beinc')
412 logger.info('BEINC starting. Loading config...') 401 logger.info('BEINC starting. Loading config...')
413 if config_dict.get('config_version') != BEINC_CURRENT_CONFIG_VERSION: 402 if config_dict.get('config_version') != BEINC_CURRENT_CONFIG_VERSION:
414 sys.stderr.write( 403 eprint(
415 'WARNING: The version of the config-file: {0} ({1}) ' 404 'WARNING: The version of the config-file: {0} ({1}) '
416 'does not correspond to the latest version supported ' 405 'does not correspond to the latest version supported '
417 'by this program ({2})\nCheck beinc_config_sample.json ' 406 'by this program ({2})\nCheck beinc_config_sample.json '
418 'for the newest features!\n'.format( 407 'for the newest features!'.format(
419 args.config_file, 408 args.config_file,
420 config_dict.get('config_version', 'Not set'), 409 config_dict.get('config_version', 'Not set'),
421 BEINC_CURRENT_CONFIG_VERSION)) 410 BEINC_CURRENT_CONFIG_VERSION))
@@ -441,6 +430,6 @@ if __name__ == '__main__':
441 except KeyboardInterrupt: 430 except KeyboardInterrupt:
442 print('\n\nTerminating...') 431 print('\n\nTerminating...')
443 except Exception as e: 432 except Exception as e:
444 sys.stderr.write('BEINCServer critical error: {0}\n'.format(e)) 433 eprint(f'BEINCServer critical error: {e}')
445 sys.exit(1) 434 sys.exit(1)
446 sys.exit(0) 435 sys.exit(0)