Update plugin.py
[enigma2-plugins.git] / growlee / src / GNTP.py
1 from __future__ import print_function
2
3 from twisted.internet.protocol import Protocol, ReconnectingClientFactory, ServerFactory
4 from twisted.internet.defer import Deferred
5 from twisted.internet import reactor
6 import hashlib
7 import uuid
8 import re
9 import threading
10 import collections
11
12 our_print = lambda *args, **kwargs: print("[growlee.GNTP]", *args, **kwargs)
13
14 try:
15         from Screens.MessageBox import MessageBox
16         from Tools import Notifications
17
18         from GrowleeConnection import emergencyDisable
19         from . import NOTIFICATIONID
20 except ImportError:
21         def emergencyDisable():
22                 our_print('Fallback emergencyDisabled called, stopping reactor')
23                 reactor.stop()
24
25 GNTP_TCP_PORT = 23053
26
27 try:
28         dict.iteritems
29         iteritems = lambda d: d.iteritems()
30 except AttributeError:
31         iteritems = lambda d: d.items()
32
33 class GNTPPacket:
34         version = '1.0'
35         password = ''
36         hashAlgorithm = None
37         encryptionAlgorithm = None
38         def encode(self):
39                 # TODO: add encryption support
40                 message = u'GNTP/%s %s ' % (self.version, self.messageType)
41
42                 if self.encryptionAlgorithm is None:
43                         message += u'NONE'
44                 else:
45                         message += u'%s:%s' % (self.encryptionAlgorithm, self.ivValue)
46
47                 if self.hashAlgorithm is not None:
48                         message += u' %s:%s.%s' % (self.hashAlgorithm, self.keyHash, self.salt)
49
50                 message += u'\r\n'
51                 return message
52
53         def set_password(self, password, hashAlgorithm='MD5', encryptionAlgorithm=None):
54                 if password is None:
55                         self.password = None
56                         self.hashAlgorithm = None
57                         self.encryptionAlgorithm = None
58
59                 hashes = {
60                                 'MD5': hashlib.md5,
61                                 'SHA1': hashlib.sha1,
62                                 'SHA256': hashlib.sha256,
63                                 'SHA512': hashlib.sha512,
64                 }
65                 hashAlgorithm = hashAlgorithm.upper()
66                 
67                 if not hashAlgorithm in hashes:
68                         raise Exception('Unsupported hash algorithm: %s' % hashAlgorithm)
69                 if encryptionAlgorithm is not None:
70                         raise Exception('Unsupported encryption algorithm: %s' % encryptionAlgorithm)
71
72                 hashfunction = hashes.get(hashAlgorithm)
73                 password = password.encode('utf8')
74                 seed = uuid.uuid4().hex
75                 salt = hashfunction(seed).hexdigest()
76                 saltHash = hashfunction(seed).digest()
77                 keyBasis = password+saltHash
78                 key = hashfunction(keyBasis).digest()
79                 keyHash = hashfunction(key).hexdigest()
80
81                 self.hashAlgorithm = hashAlgorithm
82                 self.keyHash = keyHash.upper()
83                 self.salt = salt.upper()
84
85 class GNTPRegister(GNTPPacket):
86         messageType = 'REGISTER'
87         def __init__(self, applicationName=None):
88                 assert applicationName, "There needs to be an application name set"
89                 self.applicationName = applicationName
90                 self.notifications = []
91         
92         def add_notification(self, name, displayName=None, enabled=True):
93                 assert name, "Notifications need a name"
94                 note = {
95                         'Notification-Name': name,
96                         'Notification-Enabled': enabled,
97                 }
98                 if displayName is not None:
99                         note['Notification-Display-Name'] = displayName
100                 self.notifications.append(note)
101
102         def encode(self):
103                 assert self.notifications, "At least one notification needs to be registered"
104                 base = GNTPPacket.encode(self)
105                 base += u"Application-Name: %s\r\n" % self.applicationName
106                 base += u"Notifications-Count: %d\r\n" % len(self.notifications)
107
108                 for note in self.notifications:
109                         base += u'\r\n'
110                         for key, value in iteritems(note):
111                                 base += u'%s: %s\r\n' % (key, value)
112                 base += u'\r\n'
113                 return base.encode('utf8', 'replace')
114
115 class GNTPNotice(GNTPPacket):
116         messageType = 'NOTIFY'
117         def __init__(self, applicationName, name, title, text='', sticky=False, priority=0):
118                 assert priority > -3 and priority < 3, "Priority has to be between -2 and 2"
119                 self.applicationName = applicationName
120                 self.name = name
121                 self.title = title
122                 self.text = text
123                 self.sticky = sticky
124                 self.priority = priority
125                 self.pendingVerification = None
126
127         def encode(self):
128                 base = GNTPPacket.encode(self)
129                 base += u"Application-Name: %s\r\n" % self.applicationName
130                 base += u"Notification-Name: %s\r\n" % self.name
131                 base += u"Notification-Text: %s\r\n" % self.text.replace('\r\n', '\n') # NOTE: just in case replace CRLF by LF so we don't break protocol
132                 base += u"Notification-Title: %s\r\n" % self.title
133                 base += u"Notification-Sticky: %s\r\n" % self.sticky
134                 base += u"Notification-Priority: %s\r\n" % self.priority
135                 base += u"Notifications-Count: 1\r\n"
136                 base += u'\r\n'
137                 return base.encode('utf8', 'replace')
138
139 class GNTP(Protocol):
140         def __init__(self, client=False, host=None, registered=False):
141                 self.client = client
142                 self.host = host
143                 self.registered = registered
144                 self.__buffer = ''
145                 self.defer = None
146                 self.messageLock = threading.Lock()
147                 self.messageQueue = collections.deque()
148
149         def connectionMade(self):
150                 if self.client and not self.registered:
151                         self.messageLock.acquire()
152
153                         register = GNTPRegister('growlee')
154                         register.set_password(self.host.password.value, 'MD5', None)
155                         register.add_notification("Notifications from your Dreambox", enabled=True)
156
157                         our_print("about to send packet:", register.encode().replace('\r\n', '<CRLF>\n'))
158                         self.transport.write(register.encode())
159
160         def sendNotification(self, title='No title.', description='No description.', sticky=False, priority=0):
161                 if not self.client or not self.transport:
162                         return
163
164                 note = GNTPNotice('growlee', "Notifications from your Dreambox", title, text=description, sticky=sticky, priority=priority)
165                 note.set_password(self.host.password.value, 'MD5', None)
166                 self.messageQueue.append(note)
167                 self.sendQueuedMessage()
168
169         def sendQueuedMessage(self):
170                 if not self.registered:
171                         our_print("not registered though there are queued messages... something is weird!")
172                         return
173                 if not self.messageLock.acquire(False):
174                         return
175                 try:
176                         note = self.messageQueue.popleft()
177                 except IndexError:
178                         self.messageLock.release()
179                 else:
180                         msg = note.encode()
181                         our_print("about to send packet:", msg.replace('\r\n', '<CRLF>\n'))
182                         self.transport.write(msg)
183                         def writeAgain():
184                                 note.set_password(self.host.password.value, 'MD5', None)
185                                 msg = note.encode()
186                                 our_print("about to re-send packet:", msg.replace('\r\n', '<CRLF>\n'))
187                                 self.transport.write(msg)
188                                 # return to "normal" operation in 5 seconds regardless of state
189                                 self.pendingVerification = None
190                                 self.messageLock.release()
191                                 reactor.callLater(5, self.sendQueuedMessage)
192                         self.pendingVerification = reactor.callLater(30, writeAgain)
193
194         def dataReceived(self, data):
195                 # only parse complete packages
196                 self.__buffer += data
197                 if data[-4:] != '\r\n\r\n': return
198                 data = self.__buffer
199                 self.__buffer = ''
200
201                 # TODO: proper implementation
202                 our_print(data.replace('\r\n', '<CRLF>\n'))
203                 match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)', data, re.IGNORECASE)
204                 if not match:
205                         our_print('invalid/partial return')
206                         try:
207                                 self.messageLock.release()
208                         except Exception as e:
209                                 our_print("error releasing lock, something is wierd!", e)
210                         return
211                 type = match.group('messagetype')
212                 if type == '-OK' or type == '-ERROR':
213                         try:
214                                 self.messageLock.release()
215                         except Exception as e:
216                                 our_print("error releasing lock, something is wierd!", e)
217                         match = re.search('Response-Action: (?P<messagetype>.*?)\r', data, re.IGNORECASE)
218                         if not match:
219                                 our_print('no action found in data')
220                                 return
221                         rtype = match.group('messagetype')
222                         if rtype == 'REGISTER':
223                                 self.registered = type == '-OK'
224                         elif rtype == 'NOTIFY':
225                                 if self.pendingVerification and type == '-OK':
226                                         self.pendingVerification.cancel()
227                                         self.pendingVerification = None
228                         reactor.callLater(10, self.sendQueuedMessage)
229
230
231 class GNTPClientFactory(ReconnectingClientFactory):
232         client = None
233         registered = False
234
235         def __init__(self, host):
236                 self.host = host
237
238         def buildProtocol(self, addr):
239                 self.client = p = GNTP(client=True, host=self.host, registered=self.registered)
240                 p.factory = self
241                 return p
242
243         def clientConnectionLost(self, connector, reason):
244                 self.registered = self.client.registered
245                 ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
246
247         def sendNotification(self, *args, **kwargs):
248                 if self.client:
249                         self.client.sendNotification(*args, **kwargs)
250
251 class GNTPServerFactory(ServerFactory):
252         protocol = GNTP
253
254         def __init__(self):
255                 self.clients = []
256
257         def addClient(self, client):
258                 self.clients.append(client)
259
260         def removeClient(self, client):
261                 self.clients.remove(client)
262
263         def sendNotification(self, *args, **kwargs):
264                 pass
265
266         def stopFactory(self):
267                 for client in self.clients:
268                         client.stop()
269
270 class GNTPAbstraction:
271         clientPort = None
272         serverPort = None
273         pending = 0
274
275         def __init__(self, host):
276                 self.clientFactory = GNTPClientFactory(host)
277                 self.serverFactory = GNTPServerFactory()
278
279                 if host.enable_outgoing.value:
280                         reactor.resolve(host.address.value).addCallback(self.gotIP).addErrback(self.noIP)
281
282                 if host.enable_incoming.value:
283                         self.serverPort = reactor.listenTCP(GNTP_TCP_PORT, self.serverFactory)
284                         self.pending += 1
285
286         def gotIP(self, ip):
287                 self.clientPort = reactor.connectTCP(ip, GNTP_TCP_PORT, self.clientFactory)
288                 self.pending += 1
289
290         def noIP(self, error):
291                 emergencyDisable()
292
293         def sendNotification(self, title='No title.', description='No description.', priority=-1, timeout=-1):
294                 self.clientFactory.sendNotification(title=title, description=description, sticky=timeout==-1, priority=priority)
295
296         def maybeClose(self, resOrFail, defer = None):
297                 self.pending -= 1
298                 if self.pending == 0:
299                         if defer:
300                                 defer.callback(True)
301
302         def stop(self):
303                 defer = Deferred()
304                 if self.clientPort:
305                         d = self.clientPort.disconnect()
306                         if d:
307                                 d.addBoth(self.maybeClose, defer = defer)
308                         else:
309                                 self.pending -= 1
310
311                 if self.serverPort:
312                         d = self.serverPort.stopListening()
313                         if d:
314                                 d.addBoth(self.maybeClose, defer = defer)
315                         else:
316                                 self.pending -= 1
317
318                 if self.pending == 0:
319                         reactor.callLater(1, defer.callback, True)
320                 return defer
321
322 if __name__ == '__main__':
323         class Value:
324                 def __init__(self, value):
325                         self.value = value
326         class Config:
327                 address = Value('moritz-venns-macbook-pro')
328                 password = Value('')
329                 enable_outgoing = Value(True)
330                 enable_incoming = Value(True)
331
332         def callLater():
333                 gntp = GNTPAbstraction(Config)
334                 reactor.callLater(3, gntp.sendNotification)
335                 reactor.callLater(5, lambda: gntp.sendNotification('Dreambox', 'A record has been started:\nDummy recoding.', priority=0, timeout=5))
336         reactor.callLater(1, callLater)
337         reactor.run()