[webif] HTTPS: set proper ASN1 Timestamp to keep android from exploding when trying...
[enigma2-plugins.git] / webinterface / src / plugin.py
1 Version = '$Header$';
2
3 from enigma import eConsoleAppContainer, eTPM
4 from Plugins.Plugin import PluginDescriptor
5
6 from Components.config import config, ConfigBoolean, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText
7 from Components.Network import iNetwork
8 from Screens.MessageBox import MessageBox
9 from WebIfConfig import WebIfConfigScreen
10 from WebChilds.Toplevel import getToplevel
11 from Tools.HardwareInfo import HardwareInfo
12
13 from Tools.Directories import copyfile, resolveFilename, SCOPE_PLUGINS, SCOPE_CONFIG
14
15 from twisted.internet import reactor, ssl
16 from twisted.internet.error import CannotListenError
17 from twisted.web import server, http, util, static, resource
18
19 from zope.interface import Interface, implements
20 from socket import gethostname as socket_gethostname
21 from OpenSSL import SSL, crypto
22 from time import gmtime
23 from os.path import isfile as os_isfile, exists as os_exists
24
25 from __init__ import _, __version__, decrypt_block
26 from webif import get_random, validate_certificate
27
28 import random
29
30 tpm = eTPM()
31 rootkey = ['\x9f', '|', '\xe4', 'G', '\xc9', '\xb4', '\xf4', '#', '&', '\xce', '\xb3', '\xfe', '\xda', '\xc9', 'U', '`', '\xd8', '\x8c', 's', 'o', '\x90', '\x9b', '\\', 'b', '\xc0', '\x89', '\xd1', '\x8c', '\x9e', 'J', 'T', '\xc5', 'X', '\xa1', '\xb8', '\x13', '5', 'E', '\x02', '\xc9', '\xb2', '\xe6', 't', '\x89', '\xde', '\xcd', '\x9d', '\x11', '\xdd', '\xc7', '\xf4', '\xe4', '\xe4', '\xbc', '\xdb', '\x9c', '\xea', '}', '\xad', '\xda', 't', 'r', '\x9b', '\xdc', '\xbc', '\x18', '3', '\xe7', '\xaf', '|', '\xae', '\x0c', '\xe3', '\xb5', '\x84', '\x8d', '\r', '\x8d', '\x9d', '2', '\xd0', '\xce', '\xd5', 'q', '\t', '\x84', 'c', '\xa8', ')', '\x99', '\xdc', '<', '"', 'x', '\xe8', '\x87', '\x8f', '\x02', ';', 'S', 'm', '\xd5', '\xf0', '\xa3', '_', '\xb7', 'T', '\t', '\xde', '\xa7', '\xf1', '\xc9', '\xae', '\x8a', '\xd7', '\xd2', '\xcf', '\xb2', '.', '\x13', '\xfb', '\xac', 'j', '\xdf', '\xb1', '\x1d', ':', '?']
32 hw = HardwareInfo()
33 #CONFIG INIT
34
35 #init the config
36 config.plugins.Webinterface = ConfigSubsection()
37 config.plugins.Webinterface.enabled = ConfigYesNo(default=True)
38 config.plugins.Webinterface.show_in_extensionsmenu = ConfigYesNo(default = False)
39 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
40 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
41 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
42 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=True)
43 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
44
45 config.plugins.Webinterface.http = ConfigSubsection()
46 config.plugins.Webinterface.http.enabled = ConfigYesNo(default=True)
47 config.plugins.Webinterface.http.port = ConfigInteger(default = 80, limits=(1, 65535) )
48 config.plugins.Webinterface.http.auth = ConfigYesNo(default=False)
49
50 config.plugins.Webinterface.https = ConfigSubsection()
51 config.plugins.Webinterface.https.enabled = ConfigYesNo(default=True)
52 config.plugins.Webinterface.https.port = ConfigInteger(default = 443, limits=(1, 65535) )
53 config.plugins.Webinterface.https.auth = ConfigYesNo(default=True)
54
55 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
56
57 global running_defered, waiting_shutdown, toplevel
58
59 running_defered = []
60 waiting_shutdown = 0
61 toplevel = None
62 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
63
64 KEY_FILE = resolveFilename(SCOPE_CONFIG, "key.pem")
65 CERT_FILE = resolveFilename(SCOPE_CONFIG, "cert.pem")
66
67 #===============================================================================
68 # Helperclass to close running Instances of the Webinterface
69 #===============================================================================
70 class Closer:
71         counter = 0
72         def __init__(self, session, callback=None, l2k=None):
73                 self.callback = callback
74                 self.session = session
75                 self.l2k = l2k
76 #===============================================================================
77 # Closes all running Instances of the Webinterface
78 #===============================================================================
79         def stop(self):
80                 global running_defered
81                 for d in running_defered:
82                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
83                         x = d.stopListening()
84
85                         try:
86                                 x.addCallback(self.isDown)
87                                 self.counter += 1
88                         except AttributeError:
89                                 pass
90                 running_defered = []
91                 if self.counter < 1:
92                         if self.callback is not None:
93                                 self.callback(self.session, self.l2k)
94
95 #===============================================================================
96 # #Is it already down?
97 #===============================================================================
98         def isDown(self, s):
99                 self.counter -= 1
100                 if self.counter < 1:
101                         if self.callback is not None:
102                                 self.callback(self.session, self.l2k)
103
104 def installCertificates(session):
105         if not os_exists(CERT_FILE) \
106                         or not os_exists(KEY_FILE):
107                 print "[Webinterface].installCertificates :: Generating SSL key pair and CACert"
108                 # create a key pair
109                 k = crypto.PKey()
110                 k.generate_key(crypto.TYPE_RSA, 1024)
111
112                 # create a self-signed cert
113                 cert = crypto.X509()
114                 cert.get_subject().C = "DE"
115                 cert.get_subject().ST = "Home"
116                 cert.get_subject().L = "Home"
117                 cert.get_subject().O = "Dreambox"
118                 cert.get_subject().OU = "STB"
119                 cert.get_subject().CN = socket_gethostname()
120                 cert.set_serial_number(random.randint(1000000,1000000000))
121                 cert.set_notBefore("20120101000000Z");
122                 cert.set_notAfter("20301231235900Z")
123                 cert.set_issuer(cert.get_subject())
124                 cert.set_pubkey(k)
125                 print "[Webinterface].installCertificates :: Signing SSL key pair with new CACert"
126                 cert.sign(k, 'sha1')
127
128                 try:
129                         print "[Webinterface].installCertificates ::  Installing newly generated certificate and key pair"
130                         open(CERT_FILE, "wt").write(
131                                 crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
132                         open(KEY_FILE, "wt").write(
133                                 crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
134                 except IOError, e:
135                         #Disable https
136                         config.plugins.Webinterface.https.enabled.value = False
137                         config.plugins.Webinterface.https.enabled.save()
138                         #Inform the user
139                         session.open(MessageBox, "Couldn't install generated SSL-Certifactes for https access\nHttps access is disabled!", MessageBox.TYPE_ERROR)
140
141
142 #===============================================================================
143 # restart the Webinterface for all configured Interfaces
144 #===============================================================================
145 def restartWebserver(session, l2k):
146         try:
147                 del session.mediaplayer
148                 del session.messageboxanswer
149         except NameError:
150                 pass
151         except AttributeError:
152                 pass
153
154         global running_defered
155         if len(running_defered) > 0:
156                 Closer(session, startWebserver, l2k).stop()
157         else:
158                 startWebserver(session, l2k)
159
160 #===============================================================================
161 # start the Webinterface for all configured Interfaces
162 #===============================================================================
163 def startWebserver(session, l2k):
164         global running_defered
165         global toplevel
166
167         session.mediaplayer = None
168         session.messageboxanswer = None
169         if toplevel is None:
170                 toplevel = getToplevel(session)
171
172         errors = ""
173
174         if config.plugins.Webinterface.enabled.value is not True:
175                 print "[Webinterface] is disabled!"
176
177         else:
178                 # IF SSL is enabled we need to check for the certs first
179                 # If they're not there we'll exit via return here
180                 # and get called after Certificates are installed properly
181                 if config.plugins.Webinterface.https.enabled.value:
182                         installCertificates(session)
183
184                 # Listen on all Interfaces
185                 ip = "0.0.0.0"
186                 #HTTP
187                 if config.plugins.Webinterface.http.enabled.value is True:
188                         ret = startServerInstance(session, ip, config.plugins.Webinterface.http.port.value, config.plugins.Webinterface.http.auth.value, l2k)
189                         if ret == False:
190                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
191                         else:
192                                 registerBonjourService('http', config.plugins.Webinterface.http.port.value)
193
194                 #Streaming requires listening on 127.0.0.1:80 no matter what, ensure it its available
195                 if config.plugins.Webinterface.http.port.value != 80 or not config.plugins.Webinterface.http.enabled.value:
196                         #LOCAL HTTP Connections (Streamproxy)
197                         ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.http.auth.value, l2k)
198                         if ret == False:
199                                 errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
200
201                         if errors != "":
202                                 session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), type=MessageBox.TYPE_ERROR, timeout=30)
203
204                 #HTTPS
205                 if config.plugins.Webinterface.https.enabled.value is True:
206                         ret = startServerInstance(session, ip, config.plugins.Webinterface.https.port.value, config.plugins.Webinterface.https.auth.value, l2k, True)
207                         if ret == False:
208                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
209                         else:
210                                 registerBonjourService('https', config.plugins.Webinterface.https.port.value)
211
212 #===============================================================================
213 # stop the Webinterface for all configured Interfaces
214 #===============================================================================
215 def stopWebserver(session):
216         try:
217                 del session.mediaplayer
218                 del session.messageboxanswer
219         except NameError:
220                 pass
221         except AttributeError:
222                 pass
223
224         global running_defered
225         if len(running_defered) > 0:
226                 Closer(session).stop()
227
228 #===============================================================================
229 # startServerInstance
230 # Starts an Instance of the Webinterface
231 # on given ipaddress, port, w/o auth, w/o ssl
232 #===============================================================================
233 def startServerInstance(session, ipaddress, port, useauth=False, l2k=None, usessl=False):
234         if hw.get_device_name().lower() != "dm7025":
235                 l3k = None
236                 l3c = tpm.getCert(eTPM.TPMD_DT_LEVEL3_CERT)
237
238                 if l3c is None:
239                         return False
240
241                 l3k = validate_certificate(l3c, l2k)
242                 if l3k is None:
243                         return False
244
245                 random = get_random()
246                 if random is None:
247                         return False
248
249                 value = tpm.challenge(random)
250                 result = decrypt_block(value, l3k)
251
252                 if result is None:
253                         return False
254                 else:
255                         if result [80:88] != random:
256                                 return False
257
258         if useauth:
259 # HTTPAuthResource handles the authentication for every Resource you want it to
260                 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
261                 site = server.Site(root)
262         else:
263                 site = server.Site(toplevel)
264
265         if usessl:
266                 ctx = ChainedOpenSSLContextFactory(KEY_FILE, CERT_FILE)
267                 try:
268                         d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
269                 except CannotListenError:
270                         print "[Webinterface] FAILED to listen on %s:%i auth=%s ssl=%s" % (ipaddress, port, useauth, usessl)
271                         return False
272         else:
273                 try:
274                         d = reactor.listenTCP(port, site, interface=ipaddress)
275                 except CannotListenError:
276                         print "[Webinterface] FAILED to listen on %s:%i auth=%s ssl=%s" % (ipaddress, port, useauth, usessl)
277                         return False
278
279         running_defered.append(d)
280         print "[Webinterface] started on %s:%i auth=%s ssl=%s" % (ipaddress, port, useauth, usessl)
281         return True
282
283         #except Exception, e:
284                 #print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
285                 #return False
286
287 class ChainedOpenSSLContextFactory(ssl.DefaultOpenSSLContextFactory):
288         def __init__(self, privateKeyFileName, certificateChainFileName, sslmethod=SSL.SSLv23_METHOD):
289                 self.privateKeyFileName = privateKeyFileName
290                 self.certificateChainFileName = certificateChainFileName
291                 self.sslmethod = sslmethod
292                 self.cacheContext()
293
294         def cacheContext(self):
295                 ctx = SSL.Context(self.sslmethod)
296                 ctx.use_certificate_chain_file(self.certificateChainFileName)
297                 ctx.use_privatekey_file(self.privateKeyFileName)
298                 self._context = ctx
299
300 #===============================================================================
301 # HTTPAuthResource
302 # Handles HTTP Authorization for a given Resource
303 #===============================================================================
304 class HTTPAuthResource(resource.Resource):
305         def __init__(self, res, realm):
306                 resource.Resource.__init__(self)
307                 self.resource = res
308                 self.realm = realm
309                 self.authorized = False
310                 self.tries = 0
311                 self.unauthorizedResource = UnauthorizedResource(self.realm)
312
313         def unautorized(self, request):
314                 request.setResponseCode(http.UNAUTHORIZED)
315                 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
316
317                 return self.unauthorizedResource
318
319         def isAuthenticated(self, request):
320                 host = request.getHost().host
321                 #If streamauth is disabled allow all acces from localhost
322                 if not config.plugins.Webinterface.streamauth.value:
323                         if( host == "127.0.0.1" or host == "localhost" ):
324                                 print "[WebInterface.plugin.isAuthenticated] Streaming auth is disabled bypassing authcheck because host is '%s'" %host
325                                 return True
326
327                 # get the Session from the Request
328                 sessionNs = request.getSession().sessionNamespaces
329
330                 # if the auth-information has not yet been stored to the session
331                 if not sessionNs.has_key('authenticated'):
332                         if request.getUser() != '':
333                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
334                         else:
335                                 sessionNs['authenticated'] = False
336
337                 #if the auth-information already is in the session
338                 else:
339                         if sessionNs['authenticated'] is False:
340                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
341
342                 #return the current authentication status
343                 return sessionNs['authenticated']
344
345 #===============================================================================
346 # Call render of self.resource (if authenticated)
347 #===============================================================================
348         def render(self, request):
349                 if self.isAuthenticated(request) is True:
350                         return self.resource.render(request)
351
352                 else:
353                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
354                         return self.unautorized(request).render(request)
355
356 #===============================================================================
357 # Override to call getChildWithDefault of self.resource (if authenticated)
358 #===============================================================================
359         def getChildWithDefault(self, path, request):
360                 if self.isAuthenticated(request) is True:
361                         return self.resource.getChildWithDefault(path, request)
362
363                 else:
364                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
365                         return self.unautorized(request)
366
367 #===============================================================================
368 # UnauthorizedResource
369 # Returns a simple html-ified "Access Denied"
370 #===============================================================================
371 class UnauthorizedResource(resource.Resource):
372         def __init__(self, realm):
373                 resource.Resource.__init__(self)
374                 self.realm = realm
375                 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
376
377         def getChild(self, path, request):
378                 return self.errorpage
379
380         def render(self, request):
381                 return self.errorpage.render(request)
382
383
384
385 # Password verfication stuff
386 from crypt import crypt
387 from pwd import getpwnam
388 from spwd import getspnam
389
390
391 def check_passwd(name, passwd):
392         cryptedpass = None
393         try:
394                 cryptedpass = getpwnam(name)[1]
395         except:
396                 return False
397
398         if cryptedpass:
399                 #shadowed or not, that's the questions here
400                 if cryptedpass == 'x' or cryptedpass == '*':
401                         try:
402                                 cryptedpass = getspnam(name)[1]
403                         except:
404                                 return False
405
406                 return crypt(passwd, cryptedpass) == cryptedpass
407         return False
408
409 global_session = None
410
411 #===============================================================================
412 # sessionstart
413 # Actions to take place on Session start
414 #===============================================================================
415 def sessionstart(reason, session):
416         global global_session
417         global_session = session
418         networkstart(True, session)
419
420
421 def registerBonjourService(protocol, port):
422         try:
423                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
424
425                 service = bonjour.buildService(protocol, port)
426                 bonjour.registerService(service, True)
427                 print "[WebInterface.registerBonjourService] Service for protocol '%s' with port '%i' registered!" %(protocol, port)
428                 return True
429
430         except ImportError, e:
431                 print "[WebInterface.registerBonjourService] %s" %e
432                 return False
433
434 def unregisterBonjourService(protocol):
435         try:
436                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
437
438                 bonjour.unregisterService(protocol)
439                 print "[WebInterface.unregisterBonjourService] Service for protocol '%s' unregistered!" %(protocol)
440                 return True
441
442         except ImportError, e:
443                 print "[WebInterface.unregisterBonjourService] %s" %e
444                 return False
445
446 def checkBonjour():
447         if ( not config.plugins.Webinterface.http.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
448                 unregisterBonjourService('http')
449         if ( not config.plugins.Webinterface.https.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
450                 unregisterBonjourService('https')
451
452 #===============================================================================
453 # networkstart
454 # Actions to take place after Network is up (startup the Webserver)
455 #===============================================================================
456 #def networkstart(reason, **kwargs):
457 def networkstart(reason, session):
458         l2r = False
459         l2k = None
460         if hw.get_device_name().lower() != "dm7025":
461                 l2c = tpm.getCert(eTPM.TPMD_DT_LEVEL2_CERT)
462
463                 if l2c is None:
464                         return
465
466                 l2k = validate_certificate(l2c, rootkey)
467                 if l2k is None:
468                         return
469
470                 l2r = True
471         else:
472                 l2r = True
473
474         if l2r:
475                 if reason is True:
476                         startWebserver(session, l2k)
477                         checkBonjour()
478
479                 elif reason is False:
480                         stopWebserver(session)
481                         checkBonjour()
482
483 def openconfig(session, **kwargs):
484         session.openWithCallback(configCB, WebIfConfigScreen)
485
486 def configCB(result, session):
487         l2r = False
488         l2k = None
489         if hw.get_device_name().lower() != "dm7025":
490                 l2c = tpm.getCert(eTPM.TPMD_DT_LEVEL2_CERT)
491
492                 if l2c is None:
493                         return
494
495                 l2k = validate_certificate(l2c, rootkey)
496                 if l2k is None:
497                         return
498
499                 l2r = True
500         else:
501                 l2r = True
502
503         if l2r:
504                 if result:
505                         print "[WebIf] config changed"
506                         restartWebserver(session, l2k)
507                         checkBonjour()
508                 else:
509                         print "[WebIf] config not changed"
510
511 def Plugins(**kwargs):
512         p = PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart)
513         p.weight = 100 #webif should start as last plugin
514         list = [p,
515 #                       PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
516                         PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
517                                                         where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]
518         if config.plugins.Webinterface.show_in_extensionsmenu.value:
519                 list.append(PluginDescriptor(name="Webinterface", description=_("Configuration for the Webinterface"),
520                         where = PluginDescriptor.WHERE_EXTENSIONSMENU, icon="plugin.png", fnc=openconfig))
521         return list