summaryrefslogtreecommitdiff
path: root/beinc_server_.py
diff options
context:
space:
mode:
authorSimeon Simeonov2015-04-14 19:22:40 +0200
committerSimeon Simeonov2015-04-14 19:22:40 +0200
commit36dda1cca9503fc00c15ed22c4c5969e80edc1f7 (patch)
tree046887857d1268c12e6022e04a7441bf5750ac36 /beinc_server_.py
parent56c09c10fd7dd1cb3a83a8d4b1bdcd98459e8175 (diff)
beinc_server_.py v.2.0 finished
Diffstat (limited to 'beinc_server_.py')
-rwxr-xr-xbeinc_server_.py394
1 files changed, 394 insertions, 0 deletions
diff --git a/beinc_server_.py b/beinc_server_.py
new file mode 100755
index 0000000..36cbcd5
--- /dev/null
+++ b/beinc_server_.py
@@ -0,0 +1,394 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Blackmore's Enhanced IRC-Notification Collection (BEINC) v2.0
5# Copyright (C) 2013-2015 Simeon Simeonov
6
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
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16
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/>.
19
20
21import argparse
22import errno
23import getpass
24import json
25import os
26import sys
27
28import OpenSSL
29
30from functools import wraps
31
32from twisted.web import xmlrpc, server
33from twisted.internet import protocol, reactor, ssl
34from twisted.python.filepath import FilePath
35
36try:
37 import pynotify
38except ImportError as e:
39 pynotify = None
40
41try:
42 import pyosd
43 pyosd_positions = {'top': pyosd.POS_TOP,
44 'middle': pyosd.POS_MID,
45 'bottom': pyosd.POS_BOT}
46 pyosd_alignments = {'left': pyosd.ALIGN_LEFT,
47 'center': pyosd.ALIGN_CENTER,
48 'right': pyosd.ALIGN_RIGHT}
49except ImportError as e:
50 pyosd = None
51
52
53__author__ = 'Simeon Simeonov'
54__version__ = '2.0'
55__license__ = 'GPL3'
56
57
58BEINC_OSD_TYPE_NONE = 0
59BEINC_OSD_TYPE_PYNOTIFY = 1
60BEINC_OSD_TYPE_PYOSD = 2
61
62BEINC_SSL_METHODS = {'SSLv3': OpenSSL.SSL.SSLv3_METHOD,
63 'TLSv1': OpenSSL.SSL.TLSv1_METHOD}
64try:
65 errstr = "Warning: Current Twisted / OpenSSL version doesn't support TLSv1.1"
66 BEINC_SSL_METHODS.update({'TLSv1_1': OpenSSL.SSL.TLSv1_1_METHOD})
67 errstr = "Warning: Current Twisted / OpenSSL version doesn't support TLSv1.2"
68 BEINC_SSL_METHODS.update({'TLSv1_2': OpenSSL.SSL.TLSv1_2_METHOD})
69except:
70 sys.stderr.write(errstr + '\n')
71
72
73class BEINCInstance(object):
74 """
75 Represents a single server-instance
76 """
77
78 def __init__(self, instance_dict):
79 """
80 instance_dict: the config-dictionary node that represents this instance
81 """
82 self.__message_queue = list()
83 self.__osd_type = BEINC_OSD_TYPE_NONE
84 self.__osd_notification = None
85
86 self.__name = instance_dict.get('name')
87 self.__password = instance_dict.get('password', '')
88 self.__queue_size = int(instance_dict.get('queue_size', 3))
89 if instance_dict['osd_system'].lower() == 'pynotify':
90 self.__queue_size = 0 # disable queueing
91 if not pynotify:
92 sys.stderr.write(
93 'This server does not possess pynotify capability\n')
94 sys.stderr.write(
95 'Remove the instance {0}'.format(self.__name))
96 sys.stderr.write(
97 'or define it with "osd_system": "none" '
98 'or other available backend\n')
99 sys.exit(errno.EPERM)
100 try:
101 self.__osd_notification = pynotify.Notification(' ')
102 self.__osd_notification.set_timeout(
103 1000 * int(instance_dict.get('osd_timeout', 5)))
104 self.__osd_notification.set_property(
105 'app_name',
106 '{0} {1}'.format(sys.argv[0], __version__))
107 except Exception as e:
108 sys.stderr.write(
109 'Unable to set up a '
110 'pynotify notification object for "{0}" ({1})\n'.format(
111 self.__name,
112 e))
113 sys.exit(errno.EPERM)
114 self.__osd_type = BEINC_OSD_TYPE_PYNOTIFY
115 elif instance_dict['osd_system'].lower() == 'pyosd':
116 self.__queue_size = 0 # disable queueing
117 if not pyosd:
118 sys.stderr.write(
119 'This server does not possess pyosd capability\n')
120 sys.stderr.write(
121 'Remove the instance {0}'.format(self.__name))
122 sys.stderr.write(
123 'or define it with "osd_system": "none" '
124 'or other available backend\n')
125 sys.exit(errno.EPERM)
126 try:
127 self.__osd_notification = pyosd.osd()
128 self.__osd_notification.set_timeout(
129 int(instance_dict.get('osd_timeout', 5)))
130 pyosd_font = instance_dict.get('pyosd_font')
131 if pyosd_font:
132 self.__osd_notification.set_font(pyosd_font)
133 self.__osd_notification.set_vertical_offset(
134 instance_dict.get('pyosd_vertical_offset', 120))
135 self.__osd_notification.set_horizontal_offset(
136 instance_dict.get('pyosd_horizontal_offset', 30))
137 align_str = instance_dict.get('pyosd_align', 'left')
138 self.__osd_notification.set_align(
139 pyosd_alignments.get(align_str, pyosd.ALIGN_LEFT))
140 position_str = instance_dict.get('pyosd_position', 'bottom')
141 self.__osd_notification.set_pos(
142 pyosd_positions.get(position_str, pyosd.POS_BOT))
143 self.__osd_notification.set_colour(
144 instance_dict.get('pyosd_color', 'blue'))
145 except Exception as e:
146 sys.stderr.write(
147 'Unable to set up a pyosd '
148 'notification object for "{0}" ({1})\n'.format(
149 self.__name,
150 e))
151 sys.exit(errno.EPERM)
152 self.__osd_type = BEINC_OSD_TYPE_PYOSD
153
154 @property
155 def name(self):
156 """
157 name-property for the server instance (read-only)
158 """
159 return self.__name
160
161 @property
162 def queueable(self):
163 """
164 True if this instance has a queueing capability (read-only)
165 """
166 return bool(self.__queue_size)
167
168 def password_match(self, password):
169 """
170 Returns True if 'passowrd' matches the instance-password,
171 otherwise - False
172 """
173 return True if self.__password == password else False
174
175 def send_message(self, title, message):
176 """
177 Displays or enqueues the message,
178 depending on the instance's type in regard to the osd_system
179 """
180 if self.__osd_type == BEINC_OSD_TYPE_PYNOTIFY:
181 self.__send_pynotify_messaage(title, message)
182 elif self.__osd_type == BEINC_OSD_TYPE_PYOSD:
183 self.__send_pyosd_message(title, message)
184 else:
185 self.__send_message_to_queue(title, message)
186
187 def get_queue(self):
188 """
189 Reruens a json representation of the message queue
190 """
191 jstr = json.dumps(self.__message_queue)
192 self.__message_queue = list()
193 return jstr
194
195 def __send_pynotify_messaage(self, title, message):
196 """
197 Displays pynotify message
198 """
199 self.__osd_notification.set_properties(summary=title, body=message)
200 self.__osd_notification.show()
201
202 def __send_pyosd_message(self, title, message):
203 """
204 Displays pyosd message
205 """
206 self.__osd_notification.display(title, line=0)
207 self.__osd_notification.display(message, line=1)
208
209 def __send_message_to_queue(self, title, message):
210 """
211 Enqueues the message
212 """
213 if len(self.__message_queue) >= self.__queue_size:
214 self.__message_queue.pop(0)
215 self.__message_queue.append({'title': title, 'message': message})
216
217
218def beinc_login_required(method):
219 """
220 decorator for checking login credentials
221 """
222
223 @wraps(method)
224 def wrapper(self, resource_name, password, *args, **kwargs):
225 try:
226 instance = self.instances[resource_name]
227 except Exception as e:
228 raise xmlrpc.Fault(401,
229 'Wrong instance or password')
230 if not instance.password_match(password):
231 raise xmlrpc.Fault(401,
232 'Wrong instance or password')
233 return method(self, resource_name, password, *args, **kwargs)
234 return wrapper
235
236
237class XMLRPCNotifyServer(xmlrpc.XMLRPC):
238 """
239 A class representing the entire server
240 """
241
242 def __init__(self, config):
243 """
244 """
245 xmlrpc.XMLRPC.__init__(self)
246 self.__config = config
247 self.__instances = dict()
248 # initialize pynotify if the module exists and if needed
249 if pynotify:
250 for instance in self.__config['server']['instances']:
251 # check if we have at least one instance that uses pynotify
252 # before initializing it
253 if instance.get('osd_system', '').lower() == 'pynotify':
254 if not pynotify.init('BEINC Notify'):
255 sys.stderr.write('pynotify.init failed! Exiting...\n')
256 sys.exit(1)
257 break
258 try:
259 for instance in self.__config['server']['instances']:
260 self.__instances[instance['name']] = BEINCInstance(instance)
261 print('Instance "{0}" added'.format(instance['name']))
262 except Exception as e:
263 sys.stderr.write('Unable to create instance "{0}": {1}\n'.format(
264 instance['name'],
265 e))
266 sys.exit(1)
267
268 @property
269 def instances(self):
270 """
271 a property that returns the instance list (read-only)
272 """
273 return self.__instances
274
275 @beinc_login_required
276 def xmlrpc_push(self, resource_name, password, title, message):
277 """
278 Return all passed args.
279 """
280 instance = self.__instances[resource_name]
281 try:
282 instance.send_message(title, message)
283 return 'OK'
284 except Exception as e:
285 sys.stderr.write(
286 'Unable to handle message in {0}: ({1})\n'.format(
287 instance.name,
288 e))
289 raise xmlrpc.Fault(500, 'Unable to send message')
290
291 @beinc_login_required
292 def xmlrpc_pull(self, resource_name, password):
293 """
294 Return sum of arguments.
295 """
296 instance = self.__instances[resource_name]
297 if not instance.queueable:
298 raise xmlrpc.Fault(
299 405,
300 'BEINC instance "{0}" does not support queuing'.format(
301 instance.name))
302 return instance.get_queue()
303
304
305def main():
306 """
307 """
308 parser = argparse.ArgumentParser(
309 description='The following options are available')
310 parser.add_argument(
311 '-d',
312 action='store_true',
313 dest='daemonize',
314 default=False,
315 help='Run the BEINC-server in the background')
316 parser.add_argument(
317 '-H', '--hostname',
318 metavar='HOSTNAME',
319 type=str,
320 dest='hostname',
321 default='127.0.0.1',
322 help='BEINC server IP / hostname (default: 127.0.0.1)')
323 parser.add_argument(
324 '-p', '--port',
325 metavar='PORT',
326 type=int,
327 dest='port',
328 default=9998,
329 help='BEINC server port (default: 9998)')
330 parser.add_argument(
331 '-f', '--config-file',
332 metavar='FILE',
333 type=str,
334 default=os.path.expanduser('~/.beinc_server.json'),
335 dest='config_file',
336 help='BEINC config file (default: ~/.beinc_server.json)')
337 parser.add_argument(
338 '-v', '--version',
339 action='version',
340 version='%(prog)s {0}'.format(__version__),
341 help='Display program-version and exit')
342 args = parser.parse_args()
343 try:
344 with open(args.config_file, 'r') as fp:
345 config_dict = json.load(fp)
346 except Exception as e:
347 sys.stderr.write('Unable to parse {0}: {1}\n'.format(args.config_file,
348 e))
349 sys.exit(errno.EIO)
350 try:
351 if config_dict.get('config_version') != 2:
352 sys.stderr.write(
353 'Incompatible or missing config-file version for {0}\n'.format(
354 args.config_file))
355 sys.exit(1)
356 ssl_certificate = config_dict['server']['general'].get(
357 'ssl_certificate')
358 ssl_private_key = config_dict['server']['general'].get(
359 'ssl_private_key')
360 ssl_method_str = config_dict['server']['general'].get(
361 'ssl_method', 'auto')
362 ssl_acceptable_ciphers_str = config_dict['server']['general'].get(
363 'ssl_acceptable_ciphers', 'auto')
364 beinc_server = XMLRPCNotifyServer(config_dict)
365 if ssl_certificate and ssl_private_key:
366 # SSL connection
367 cert_path = FilePath(ssl_certificate)
368 key_path = FilePath(ssl_private_key)
369 private_certificate = ssl.PrivateCertificate.loadPEM(
370 key_path.getContent() + cert_path.getContent())
371 options = private_certificate.options()
372 ssl_method = BEINC_SSL_METHODS.get(ssl_method_str)
373 if ssl_method:
374 options.method = ssl_method
375 if ssl_acceptable_ciphers_str.lower() != 'auto':
376 options.acceptableCiphers = ssl.AcceptableCiphers.fromOpenSSLCipherString(
377 ssl_acceptable_ciphers_str)
378 reactor.listenSSL(args.port,
379 server.Site(beinc_server),
380 options,
381 interface=args.hostname)
382 else:
383 reactor.listenTCP(args.port,
384 server.Site(beinc_server),
385 interface=args.hostname)
386 reactor.run()
387 except Exception as e:
388 sys.stderr.write('WebServer error: {0}\n'.format(e))
389 sys.exit(1)
390 sys.exit(0)
391
392
393if __name__ == "__main__":
394 main()