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