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