[WebIf] * Fix certificates validity dates by setting fixed validFrom and validTo...
[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("201201010000Z");
122                 cert.set_notAfter("203012312359Z")
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 # HTTPAuthResource
301 # Handles HTTP Authorization for a given Resource
302 #===============================================================================
303 class HTTPAuthResource(resource.Resource):
304         def __init__(self, res, realm):
305                 resource.Resource.__init__(self)
306                 self.resource = res
307                 self.realm = realm
308                 self.authorized = False
309                 self.tries = 0
310                 self.unauthorizedResource = UnauthorizedResource(self.realm)
311
312         def unautorized(self, request):
313                 request.setResponseCode(http.UNAUTHORIZED)
314                 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
315
316                 return self.unauthorizedResource
317
318         def isAuthenticated(self, request):
319                 host = request.getHost().host
320                 #If streamauth is disabled allow all acces from localhost
321                 if not config.plugins.Webinterface.streamauth.value:
322                         if( host == "127.0.0.1" or host == "localhost" ):
323                                 print "[WebInterface.plugin.isAuthenticated] Streaming auth is disabled bypassing authcheck because host is '%s'" %host
324                                 return True
325
326                 # get the Session from the Request
327                 sessionNs = request.getSession().sessionNamespaces
328
329                 # if the auth-information has not yet been stored to the session
330                 if not sessionNs.has_key('authenticated'):
331                         if request.getUser() != '':
332                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
333                         else:
334                                 sessionNs['authenticated'] = False
335
336                 #if the auth-information already is in the session
337                 else:
338                         if sessionNs['authenticated'] is False:
339                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
340
341                 #return the current authentication status
342                 return sessionNs['authenticated']
343
344 #===============================================================================
345 # Call render of self.resource (if authenticated)
346 #===============================================================================
347         def render(self, request):
348                 if self.isAuthenticated(request) is True:
349                         return self.resource.render(request)
350
351                 else:
352                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
353                         return self.unautorized(request).render(request)
354
355 #===============================================================================
356 # Override to call getChildWithDefault of self.resource (if authenticated)
357 #===============================================================================
358         def getChildWithDefault(self, path, request):
359                 if self.isAuthenticated(request) is True:
360                         return self.resource.getChildWithDefault(path, request)
361
362                 else:
363                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
364                         return self.unautorized(request)
365
366 #===============================================================================
367 # UnauthorizedResource
368 # Returns a simple html-ified "Access Denied"
369 #===============================================================================
370 class UnauthorizedResource(resource.Resource):
371         def __init__(self, realm):
372                 resource.Resource.__init__(self)
373                 self.realm = realm
374                 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
375
376         def getChild(self, path, request):
377                 return self.errorpage
378
379         def render(self, request):
380                 return self.errorpage.render(request)
381
382
383
384 # Password verfication stuff
385 from crypt import crypt
386 from pwd import getpwnam
387 from spwd import getspnam
388
389
390 def check_passwd(name, passwd):
391         cryptedpass = None
392         try:
393                 cryptedpass = getpwnam(name)[1]
394         except:
395                 return False
396
397         if cryptedpass:
398                 #shadowed or not, that's the questions here
399                 if cryptedpass == 'x' or cryptedpass == '*':
400                         try:
401                                 cryptedpass = getspnam(name)[1]
402                         except:
403                                 return False
404
405                 return crypt(passwd, cryptedpass) == cryptedpass
406         return False
407
408 global_session = None
409
410 #===============================================================================
411 # sessionstart
412 # Actions to take place on Session start
413 #===============================================================================
414 def sessionstart(reason, session):
415         global global_session
416         global_session = session
417         networkstart(True, session)
418
419
420 def registerBonjourService(protocol, port):
421         try:
422                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
423
424                 service = bonjour.buildService(protocol, port)
425                 bonjour.registerService(service, True)
426                 print "[WebInterface.registerBonjourService] Service for protocol '%s' with port '%i' registered!" %(protocol, port)
427                 return True
428
429         except ImportError, e:
430                 print "[WebInterface.registerBonjourService] %s" %e
431                 return False
432
433 def unregisterBonjourService(protocol):
434         try:
435                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
436
437                 bonjour.unregisterService(protocol)
438                 print "[WebInterface.unregisterBonjourService] Service for protocol '%s' unregistered!" %(protocol)
439                 return True
440
441         except ImportError, e:
442                 print "[WebInterface.unregisterBonjourService] %s" %e
443                 return False
444
445 def checkBonjour():
446         if ( not config.plugins.Webinterface.http.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
447                 unregisterBonjourService('http')
448         if ( not config.plugins.Webinterface.https.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
449                 unregisterBonjourService('https')
450
451 #===============================================================================
452 # networkstart
453 # Actions to take place after Network is up (startup the Webserver)
454 #===============================================================================
455 #def networkstart(reason, **kwargs):
456 def networkstart(reason, session):
457         l2r = False
458         l2k = None
459         if hw.get_device_name().lower() != "dm7025":
460                 l2c = tpm.getCert(eTPM.TPMD_DT_LEVEL2_CERT)
461
462                 if l2c is None:
463                         return
464
465                 l2k = validate_certificate(l2c, rootkey)
466                 if l2k is None:
467                         return
468
469                 l2r = True
470         else:
471                 l2r = True
472
473         if l2r:
474                 if reason is True:
475                         startWebserver(session, l2k)
476                         checkBonjour()
477
478                 elif reason is False:
479                         stopWebserver(session)
480                         checkBonjour()
481
482 def openconfig(session, **kwargs):
483         session.openWithCallback(configCB, WebIfConfigScreen)
484
485 def configCB(result, session):
486         l2r = False
487         l2k = None
488         if hw.get_device_name().lower() != "dm7025":
489                 l2c = tpm.getCert(eTPM.TPMD_DT_LEVEL2_CERT)
490
491                 if l2c is None:
492                         return
493
494                 l2k = validate_certificate(l2c, rootkey)
495                 if l2k is None:
496                         return
497
498                 l2r = True
499         else:
500                 l2r = True
501
502         if l2r:
503                 if result:
504                         print "[WebIf] config changed"
505                         restartWebserver(session, l2k)
506                         checkBonjour()
507                 else:
508                         print "[WebIf] config not changed"
509
510 def Plugins(**kwargs):
511         p = PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart)
512         p.weight = 100 #webif should start as last plugin
513         list = [p,
514 #                       PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
515                         PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
516                                                         where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]
517         if config.plugins.Webinterface.show_in_extensionsmenu.value:
518                 list.append(PluginDescriptor(name="Webinterface", description=_("Configuration for the Webinterface"),
519                         where = PluginDescriptor.WHERE_EXTENSIONSMENU, icon="plugin.png", fnc=openconfig))
520         return list