3 from enigma import eConsoleAppContainer, eTPM
4 from Plugins.Plugin import PluginDescriptor
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
13 from Tools.Directories import copyfile, resolveFilename, SCOPE_PLUGINS, SCOPE_CONFIG
15 from twisted.internet import reactor, ssl
16 from twisted.web import server, http, util, static, resource
18 from zope.interface import Interface, implements
19 from socket import gethostname as socket_gethostname
20 from OpenSSL import SSL
22 from os.path import isfile as os_isfile
23 from __init__ import _, __version__, decrypt_block
24 from webif import get_random, validate_certificate
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', ':', '?']
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.
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)
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)
50 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
52 global running_defered, waiting_shutdown, toplevel
57 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
59 #===============================================================================
60 # Helperclass to close running Instances of the Webinterface
61 #===============================================================================
64 def __init__(self, session, callback=None):
65 self.callback = callback
66 self.session = session
68 #===============================================================================
69 # Closes all running Instances of the Webinterface
70 #===============================================================================
72 global running_defered
73 for d in running_defered:
74 print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
78 x.addCallback(self.isDown)
80 except AttributeError:
84 if self.callback is not None:
85 self.callback(self.session)
87 #===============================================================================
88 # #Is it already down?
89 #===============================================================================
93 if self.callback is not None:
94 self.callback(self.session)
96 def checkCertificates():
97 print "[WebInterface] checking for SSL Certificates"
98 srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG)
99 cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)
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):
108 def installCertificates(session, callback = None, l2k = None):
109 print "[WebInterface] Installing SSL Certificates to %s" %resolveFilename(SCOPE_CONFIG)
111 srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG)
112 cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)
113 scope_webif = '%sExtensions/WebInterface/' %resolveFilename(SCOPE_PLUGINS)
115 source = '%setc/server.pem' %scope_webif
117 ret = copyfile(source, target)
120 source = '%setc/cacert.pem' %scope_webif
122 ret = copyfile(source, target)
124 if ret == 0 and callback != None:
125 callback(session, l2k)
128 config.plugins.Webinterface.https.enabled.value = False
129 config.plugins.Webinterface.https.enabled.save()
131 # Start without https
132 callback(session, l2k)
135 session.open(MessageBox, "Couldn't install SSL-Certifactes for https access\nHttps access is now disabled!", MessageBox.TYPE_ERROR)
137 #===============================================================================
138 # restart the Webinterface for all configured Interfaces
139 #===============================================================================
140 def restartWebserver(session):
142 del session.mediaplayer
143 del session.messageboxanswer
146 except AttributeError:
149 global running_defered
150 if len(running_defered) > 0:
151 Closer(session, startWebserver).stop()
153 startWebserver(session)
155 #===============================================================================
156 # start the Webinterface for all configured Interfaces
157 #===============================================================================
158 def startWebserver(session, l2k):
159 global running_defered
162 session.mediaplayer = None
163 session.messageboxanswer = None
165 toplevel = getToplevel(session)
169 if config.plugins.Webinterface.enabled.value is not True:
170 print "[Webinterface] is disabled!"
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)
181 # Listen on all Interfaces
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)
187 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
189 registerBonjourService('http', config.plugins.Webinterface.http.port.value)
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)
196 errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
199 session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), type=MessageBox.TYPE_ERROR, timeout=30)
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)
205 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
207 registerBonjourService('https', config.plugins.Webinterface.https.port.value)
209 #===============================================================================
210 # stop the Webinterface for all configured Interfaces
211 #===============================================================================
212 def stopWebserver(session):
214 del session.mediaplayer
215 del session.messageboxanswer
218 except AttributeError:
221 global running_defered
222 if len(running_defered) > 0:
223 Closer(session).stop()
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":
233 l3c = tpm.getCert(eTPM.TPMD_DT_LEVEL3_CERT)
238 l3k = validate_certificate(l3c, l2k)
242 random = get_random()
246 value = tpm.challenge(random)
247 result = decrypt_block(value, l3k)
252 if result [80:88] != random:
256 # HTTPAuthResource handles the authentication for every Resource you want it to
257 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
258 site = server.Site(root)
260 site = server.Site(toplevel)
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)
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
272 #except Exception, e:
273 #print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
275 #===============================================================================
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)
284 self.authorized = False
286 self.unauthorizedResource = UnauthorizedResource(self.realm)
288 def unautorized(self, request):
289 request.setResponseCode(http.UNAUTHORIZED)
290 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
292 return self.unauthorizedResource
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
302 # get the Session from the Request
303 sessionNs = request.getSession().sessionNamespaces
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())
309 #if the auth-information already is in the session
311 if sessionNs['authenticated'] is False:
312 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
314 #return the current authentication status
315 return sessionNs['authenticated']
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)
325 print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
326 return self.unautorized(request).render(request)
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)
336 print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
337 return self.unautorized(request)
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)
347 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
349 def getChild(self, path, request):
350 return self.errorpage
352 def render(self, request):
353 return self.errorpage.render(request)
355 # Password verfication stuff
357 from hashlib import md5 as md5_new
358 from crypt import crypt
360 #===============================================================================
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):
368 pwfile = '/etc/passwd'
376 entry = tuple(line.strip().split(':', 6))
381 #===============================================================================
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':
395 #===============================================================================
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."""
404 if not pwfile or type(pwfile) == type(''):
405 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
407 getuser = pwfile.get_passwd
410 enc_passwd = getuser(name)
411 except (KeyError, IOError):
412 print "[Webinterface.check_passwd] No such user!"
415 "[Webinterface.check_passwd] NOT ENC_PASSWD"
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')
421 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
424 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
427 r = r + DES_SALT[v & 0x3F]
432 #===============================================================================
434 # Encrypt a password via md5
435 #===============================================================================
436 def passcrypt_md5(passwd, salt=None, magic='$1$'):
439 elif salt[:len(magic)] == magic:
440 # remove magic from salt if present
441 salt = salt[len(magic):]
443 # salt only goes up to first '$'
444 salt = salt.split('$')[0]
445 # limit length of salt to 8
448 ctx = md5_new(passwd)
452 ctx1 = md5_new(passwd)
456 final = ctx1.digest()
458 for i in range(len(passwd), 0 , -16):
462 ctx.update(final[:i])
469 ctx.update(passwd[:1])
473 for i in range(1000):
479 if i % 3: ctx1.update(salt)
480 if i % 7: ctx1.update(passwd)
485 final = ctx1.digest()
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)
500 rv = rv + _to64(l, 2)
504 global_session = None
506 #===============================================================================
508 # Actions to take place on Session start
509 #===============================================================================
510 def sessionstart(reason, session):
511 global global_session
512 global_session = session
515 def registerBonjourService(protocol, port):
517 from Plugins.Extensions.Bonjour.Bonjour import bonjour
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)
524 except ImportError, e:
525 print "[WebInterface.registerBonjourService] %s" %e
528 def unregisterBonjourService(protocol):
530 from Plugins.Extensions.Bonjour.Bonjour import bonjour
532 bonjour.unregisterService(protocol)
533 print "[WebInterface.unregisterBonjourService] Service for protocol '%s' unregistered!" %(protocol)
536 except ImportError, e:
537 print "[WebInterface.unregisterBonjourService] %s" %e
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')
546 #===============================================================================
548 # Actions to take place after Network is up (startup the Webserver)
549 #===============================================================================
550 def networkstart(reason, **kwargs):
552 if hw.get_device_name().lower() != "dm7025":
553 l2c = tpm.getCert(eTPM.TPMD_DT_LEVEL2_CERT)
558 l2k = validate_certificate(l2c, rootkey)
568 startWebserver(global_session, l2k)
571 elif reason is False:
572 stopWebserver(global_session)
575 def openconfig(session, **kwargs):
576 session.openWithCallback(configCB, WebIfConfigScreen)
578 def configCB(result, session):
580 print "[WebIf] config changed"
581 restartWebserver(session)
584 print "[WebIf] config not changed"
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)]