fix first start on fresh installations
[vuplus_dvbapp-plugin] / 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.web import server, http, util, static, resource
17
18 from zope.interface import Interface, implements
19 from socket import gethostname as socket_gethostname
20 from OpenSSL import SSL
21
22 from os.path import isfile as os_isfile
23 from __init__ import _, __version__, decrypt_block
24 from webif import get_random, validate_certificate
25
26 tpm = eTPM()
27 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', ':', '?']
28 hw = HardwareInfo()
29 #CONFIG INIT
30
31 #init the config
32 config.plugins.Webinterface = ConfigSubsection()
33 config.plugins.Webinterface.enabled = ConfigYesNo(default=True)
34 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
35 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
36 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
37 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=True)
38 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
39
40 config.plugins.Webinterface.http = ConfigSubsection()
41 config.plugins.Webinterface.http.enabled = ConfigYesNo(default=True)
42 config.plugins.Webinterface.http.port = ConfigInteger(default = 80, limits=(1, 65535) )
43 config.plugins.Webinterface.http.auth = ConfigYesNo(default=False)
44
45 config.plugins.Webinterface.https = ConfigSubsection()
46 config.plugins.Webinterface.https.enabled = ConfigYesNo(default=True)
47 config.plugins.Webinterface.https.port = ConfigInteger(default = 443, limits=(1, 65535) )
48 config.plugins.Webinterface.https.auth = ConfigYesNo(default=True)
49
50 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
51
52 global running_defered, waiting_shutdown, toplevel
53
54 running_defered = []
55 waiting_shutdown = 0
56 toplevel = None
57 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
58
59 #===============================================================================
60 # Helperclass to close running Instances of the Webinterface
61 #===============================================================================
62 class Closer:
63         counter = 0
64         def __init__(self, session, callback=None):
65                 self.callback = callback
66                 self.session = session
67
68 #===============================================================================
69 # Closes all running Instances of the Webinterface
70 #===============================================================================
71         def stop(self):
72                 global running_defered
73                 for d in running_defered:
74                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
75                         x = d.stopListening()
76                         
77                         try:
78                                 x.addCallback(self.isDown)
79                                 self.counter += 1
80                         except AttributeError:
81                                 pass
82                 running_defered = []
83                 if self.counter < 1:
84                         if self.callback is not None:
85                                 self.callback(self.session)
86
87 #===============================================================================
88 # #Is it already down?
89 #===============================================================================
90         def isDown(self, s):
91                 self.counter -= 1
92                 if self.counter < 1:
93                         if self.callback is not None:
94                                 self.callback(self.session)
95
96 def checkCertificates():
97         print "[WebInterface] checking for SSL Certificates"
98         srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG) 
99         cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)
100
101         # Check whether there are regular certificates, if not copy the default ones over
102         if not os_isfile(srvcert) or not os_isfile(cacert):
103                 return False
104         
105         else:
106                 return True
107                 
108 def installCertificates(session, callback = None, l2k = None):
109         print "[WebInterface] Installing SSL Certificates to %s" %resolveFilename(SCOPE_CONFIG)
110         
111         srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG) 
112         cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)  
113         scope_webif = '%sExtensions/WebInterface/' %resolveFilename(SCOPE_PLUGINS)
114         
115         source = '%setc/server.pem' %scope_webif
116         target = srvcert
117         ret = copyfile(source, target)
118         
119         if ret == 0:
120                 source = '%setc/cacert.pem' %scope_webif
121                 target = cacert
122                 ret = copyfile(source, target)
123                 
124                 if ret == 0 and callback != None:
125                         callback(session, l2k)
126         
127         if ret < 0:
128                 config.plugins.Webinterface.https.enabled.value = False
129                 config.plugins.Webinterface.https.enabled.save()
130                 
131                 # Start without https
132                 callback(session, l2k)
133                 
134                 #Inform the user
135                 session.open(MessageBox, "Couldn't install SSL-Certifactes for https access\nHttps access is now disabled!", MessageBox.TYPE_ERROR)
136         
137 #===============================================================================
138 # restart the Webinterface for all configured Interfaces
139 #===============================================================================
140 def restartWebserver(session):
141         try:
142                 del session.mediaplayer
143                 del session.messageboxanswer
144         except NameError:
145                 pass
146         except AttributeError:
147                 pass
148
149         global running_defered
150         if len(running_defered) > 0:
151                 Closer(session, startWebserver).stop()
152         else:
153                 startWebserver(session)
154         
155 #===============================================================================
156 # start the Webinterface for all configured Interfaces
157 #===============================================================================
158 def startWebserver(session, l2k):
159         global running_defered
160         global toplevel
161         
162         session.mediaplayer = None
163         session.messageboxanswer = None
164         if toplevel is None:
165                 toplevel = getToplevel(session)
166         
167         errors = ""
168         
169         if config.plugins.Webinterface.enabled.value is not True:
170                 print "[Webinterface] is disabled!"
171         
172         else:
173                 # IF SSL is enabled we need to check for the certs first
174                 # If they're not there we'll exit via return here 
175                 # and get called after Certificates are installed properly
176                 if config.plugins.Webinterface.https.enabled.value:
177                         if not checkCertificates():
178                                 print "[Webinterface] Installing Webserver Certificates for SSL encryption"
179                                 installCertificates(session, startWebserver, l2k)
180                                 return
181                 # Listen on all Interfaces
182                 ip = "0.0.0.0"
183                 #HTTP
184                 if config.plugins.Webinterface.http.enabled.value is True:
185                         ret = startServerInstance(session, ip, config.plugins.Webinterface.http.port.value, config.plugins.Webinterface.http.auth.value, l2k)
186                         if ret == False:
187                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
188                         else:
189                                 registerBonjourService('http', config.plugins.Webinterface.http.port.value)
190                         
191                 #Streaming requires listening on 127.0.0.1:80 no matter what, ensure it its available
192                 if config.plugins.Webinterface.http.port.value != 80 or not config.plugins.Webinterface.http.enabled.value:
193                         #LOCAL HTTP Connections (Streamproxy)
194                         ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.http.auth.value, l2k)                   
195                         if ret == False:
196                                 errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
197                         
198                         if errors != "":
199                                 session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), type=MessageBox.TYPE_ERROR, timeout=30)
200                                 
201                 #HTTPS          
202                 if config.plugins.Webinterface.https.enabled.value is True:                                     
203                         ret = startServerInstance(session, ip, config.plugins.Webinterface.https.port.value, config.plugins.Webinterface.https.auth.value, l2k, True)
204                         if ret == False:
205                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
206                         else:
207                                 registerBonjourService('https', config.plugins.Webinterface.https.port.value)
208                 
209 #===============================================================================
210 # stop the Webinterface for all configured Interfaces
211 #===============================================================================
212 def stopWebserver(session):
213         try:
214                 del session.mediaplayer
215                 del session.messageboxanswer
216         except NameError:
217                 pass
218         except AttributeError:
219                 pass
220
221         global running_defered
222         if len(running_defered) > 0:
223                 Closer(session).stop()
224
225 #===============================================================================
226 # startServerInstance
227 # Starts an Instance of the Webinterface
228 # on given ipaddress, port, w/o auth, w/o ssl
229 #===============================================================================
230 def startServerInstance(session, ipaddress, port, useauth=False, l2k=None, usessl=False):
231         if hw.get_device_name().lower() != "dm7025":
232                 l3k = None              
233                 l3c = tpm.getCert(eTPM.TPMD_DT_LEVEL3_CERT)
234                 
235                 if l3c is None:
236                         return False                    
237                 
238                 l3k = validate_certificate(l3c, l2k)
239                 if l3k is None:                 
240                         return False
241                 
242                 random = get_random()
243                 if random is None:
244                         return False
245         
246                 value = tpm.challenge(random)
247                 result = decrypt_block(value, l3k)
248                 
249                 if result is None:
250                         return False
251                 else:
252                         if result [80:88] != random:            
253                                 return False
254                 
255         if useauth:
256 # HTTPAuthResource handles the authentication for every Resource you want it to                 
257                 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
258                 site = server.Site(root)                        
259         else:
260                 site = server.Site(toplevel)
261
262         if usessl:
263                 
264                 ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem', '/etc/enigma2/cacert.pem', sslmethod=SSL.SSLv23_METHOD)
265                 d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
266         else:
267                 d = reactor.listenTCP(port, site, interface=ipaddress)
268         running_defered.append(d)               
269         print "[Webinterface] started on %s:%i" % (ipaddress, port), "auth=", useauth, "ssl=", usessl
270         return True
271         
272         #except Exception, e:
273                 #print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e                
274                 #return False
275 #===============================================================================
276 # HTTPAuthResource
277 # Handles HTTP Authorization for a given Resource
278 #===============================================================================
279 class HTTPAuthResource(resource.Resource):
280         def __init__(self, res, realm):
281                 resource.Resource.__init__(self)
282                 self.resource = res
283                 self.realm = realm
284                 self.authorized = False
285                 self.tries = 0
286                 self.unauthorizedResource = UnauthorizedResource(self.realm)
287         
288         def unautorized(self, request):
289                 request.setResponseCode(http.UNAUTHORIZED)
290                 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
291
292                 return self.unauthorizedResource
293         
294         def isAuthenticated(self, request):             
295                 host = request.getHost().host
296                 #If streamauth is disabled allow all acces from localhost
297                 if not config.plugins.Webinterface.streamauth.value:                    
298                         if( host == "127.0.0.1" or host == "localhost" ):
299                                 print "[WebInterface.plugin.isAuthenticated] Streaming auth is disabled bypassing authcheck because host is '%s'" %host
300                                 return True
301                                         
302                 # get the Session from the Request
303                 sessionNs = request.getSession().sessionNamespaces
304                 
305                 # if the auth-information has not yet been stored to the session
306                 if not sessionNs.has_key('authenticated'):
307                         sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
308                 
309                 #if the auth-information already is in the session                              
310                 else:
311                         if sessionNs['authenticated'] is False:
312                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
313                 
314                 #return the current authentication status                                               
315                 return sessionNs['authenticated']
316                                                                                                         
317 #===============================================================================
318 # Call render of self.resource (if authenticated)                                                                                                       
319 #===============================================================================
320         def render(self, request):                      
321                 if self.isAuthenticated(request) is True:       
322                         return self.resource.render(request)
323                 
324                 else:
325                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
326                         return self.unautorized(request).render(request)
327
328 #===============================================================================
329 # Override to call getChildWithDefault of self.resource (if authenticated)      
330 #===============================================================================
331         def getChildWithDefault(self, path, request):
332                 if self.isAuthenticated(request) is True:
333                         return self.resource.getChildWithDefault(path, request)
334                 
335                 else:
336                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
337                         return self.unautorized(request)
338
339 #===============================================================================
340 # UnauthorizedResource
341 # Returns a simple html-ified "Access Denied"
342 #===============================================================================
343 class UnauthorizedResource(resource.Resource):
344         def __init__(self, realm):
345                 resource.Resource.__init__(self)
346                 self.realm = realm
347                 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
348         
349         def getChild(self, path, request):
350                 return self.errorpage
351                 
352         def render(self, request):      
353                 return self.errorpage.render(request)
354
355 # Password verfication stuff
356
357 from hashlib import md5 as md5_new
358 from crypt import crypt
359
360 #===============================================================================
361 # getpwnam
362
363 # Get a password database entry for the given user name
364 # Example from the Python Library Reference.
365 #===============================================================================
366 def getpwnam(name, pwfile=None):
367         if not pwfile:
368                 pwfile = '/etc/passwd'
369
370         f = open(pwfile)
371         while 1:
372                 line = f.readline()
373                 if not line:
374                         f.close()
375                         raise KeyError, name
376                 entry = tuple(line.strip().split(':', 6))
377                 if entry[0] == name:
378                         f.close()
379                         return entry
380
381 #===============================================================================
382 # passcrypt
383 #
384 # Encrypt a password
385 #===============================================================================
386 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
387         """Encrypt a string according to rules in crypt(3)."""
388         if method.lower() == 'des':
389                 return crypt(passwd, salt)
390         elif method.lower() == 'md5':
391                 return passcrypt_md5(passwd, salt, magic)
392         elif method.lower() == 'clear':
393                 return passwd
394
395 #===============================================================================
396 # check_passwd
397 #
398 # Checks username and Password against a given Unix Password file 
399 # The default path is '/etc/passwd'
400 #===============================================================================
401 def check_passwd(name, passwd, pwfile='/etc/passwd'):
402         """Validate given user, passwd pair against password database."""
403
404         if not pwfile or type(pwfile) == type(''):
405                 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
406         else:
407                 getuser = pwfile.get_passwd
408
409         try:
410                 enc_passwd = getuser(name)
411         except (KeyError, IOError):
412                 print "[Webinterface.check_passwd] No such user!"
413                 return False
414         if not enc_passwd:
415                 "[Webinterface.check_passwd] NOT ENC_PASSWD"
416                 return False
417         elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
418                 salt = enc_passwd[3:enc_passwd.find('$', 3)]
419                 return enc_passwd == passcrypt(passwd, salt, 'md5')
420         else:
421                 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
422
423 def _to64(v, n):
424         DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
425         r = ''
426         while (n - 1 >= 0):
427                 r = r + DES_SALT[v & 0x3F]
428                 v = v >> 6
429                 n = n - 1
430         return r
431
432 #===============================================================================
433 # passcrypt_md5
434 # Encrypt a password via md5
435 #===============================================================================
436 def passcrypt_md5(passwd, salt=None, magic='$1$'):
437         if not salt:
438                 pass
439         elif salt[:len(magic)] == magic:
440                 # remove magic from salt if present
441                 salt = salt[len(magic):]
442
443         # salt only goes up to first '$'
444         salt = salt.split('$')[0]
445         # limit length of salt to 8
446         salt = salt[:8]
447
448         ctx = md5_new(passwd)
449         ctx.update(magic)
450         ctx.update(salt)
451
452         ctx1 = md5_new(passwd)
453         ctx1.update(salt)
454         ctx1.update(passwd)
455
456         final = ctx1.digest()
457
458         for i in range(len(passwd), 0 , -16):
459                 if i > 16:
460                         ctx.update(final)
461                 else:
462                         ctx.update(final[:i])
463
464         i = len(passwd)
465         while i:
466                 if i & 1:
467                         ctx.update('\0')
468                 else:
469                         ctx.update(passwd[:1])
470                 i = i >> 1
471         final = ctx.digest()
472
473         for i in range(1000):
474                 ctx1 = md5_new()
475                 if i & 1:
476                         ctx1.update(passwd)
477                 else:
478                         ctx1.update(final)
479                 if i % 3: ctx1.update(salt)
480                 if i % 7: ctx1.update(passwd)
481                 if i & 1:
482                         ctx1.update(final)
483                 else:
484                         ctx1.update(passwd)
485                 final = ctx1.digest()
486
487         rv = magic + salt + '$'
488         final = map(ord, final)
489         l = (final[0] << 16) + (final[6] << 8) + final[12]
490         rv = rv + _to64(l, 4)
491         l = (final[1] << 16) + (final[7] << 8) + final[13]
492         rv = rv + _to64(l, 4)
493         l = (final[2] << 16) + (final[8] << 8) + final[14]
494         rv = rv + _to64(l, 4)
495         l = (final[3] << 16) + (final[9] << 8) + final[15]
496         rv = rv + _to64(l, 4)
497         l = (final[4] << 16) + (final[10] << 8) + final[5]
498         rv = rv + _to64(l, 4)
499         l = final[11]
500         rv = rv + _to64(l, 2)
501
502         return rv
503
504 global_session = None
505
506 #===============================================================================
507 # sessionstart
508 # Actions to take place on Session start 
509 #===============================================================================
510 def sessionstart(reason, session):
511         global global_session
512         global_session = session
513
514
515 def registerBonjourService(protocol, port):     
516         try:
517                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
518                                 
519                 service = bonjour.buildService(protocol, port)
520                 bonjour.registerService(service, True)
521                 print "[WebInterface.registerBonjourService] Service for protocol '%s' with port '%i' registered!" %(protocol, port) 
522                 return True
523                 
524         except ImportError, e:
525                 print "[WebInterface.registerBonjourService] %s" %e
526                 return False
527
528 def unregisterBonjourService(protocol): 
529         try:
530                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
531                                                 
532                 bonjour.unregisterService(protocol)
533                 print "[WebInterface.unregisterBonjourService] Service for protocol '%s' unregistered!" %(protocol) 
534                 return True
535                 
536         except ImportError, e:
537                 print "[WebInterface.unregisterBonjourService] %s" %e
538                 return False
539         
540 def checkBonjour():
541         if ( not config.plugins.Webinterface.http.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
542                 unregisterBonjourService('http')
543         if ( not config.plugins.Webinterface.https.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
544                 unregisterBonjourService('https')
545                 
546 #===============================================================================
547 # networkstart
548 # Actions to take place after Network is up (startup the Webserver)
549 #===============================================================================
550 def networkstart(reason, **kwargs):
551         l2r = False
552         if hw.get_device_name().lower() != "dm7025":            
553                 l2c = tpm.getCert(eTPM.TPMD_DT_LEVEL2_CERT)
554                 
555                 if l2c is None:
556                         return
557                 
558                 l2k = validate_certificate(l2c, rootkey)
559                 if l2k is None:
560                         return
561                         
562                 l2r = True
563         else:
564                 l2r = True
565                 
566         if l2r: 
567                 if reason is True:
568                         startWebserver(global_session, l2k)
569                         checkBonjour()
570                         
571                 elif reason is False:
572                         stopWebserver(global_session)
573                         checkBonjour()
574                 
575 def openconfig(session, **kwargs):
576         session.openWithCallback(configCB, WebIfConfigScreen)
577
578 def configCB(result, session):
579         if result is True:
580                 print "[WebIf] config changed"
581                 restartWebserver(session)
582                 checkBonjour()
583         else:
584                 print "[WebIf] config not changed"
585
586 def Plugins(**kwargs):
587         return [PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart),
588                         PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
589                         PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
590                                                         where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]