3 from enigma import eConsoleAppContainer
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
12 from Tools.Directories import copyfile, resolveFilename, SCOPE_PLUGINS, SCOPE_CONFIG
14 from twisted.internet import reactor, ssl
15 from twisted.web import server, http, util, static, resource
17 from zope.interface import Interface, implements
18 from socket import gethostname as socket_gethostname
19 from OpenSSL import SSL
21 from os.path import isfile as os_isfile
25 from __init__ import _, __version__
30 config.plugins.Webinterface = ConfigSubsection()
31 config.plugins.Webinterface.enabled = ConfigYesNo(default=True)
32 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
33 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
34 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
35 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=True)
36 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
38 config.plugins.Webinterface.http = ConfigSubsection()
39 config.plugins.Webinterface.http.enabled = ConfigYesNo(default=True)
40 config.plugins.Webinterface.http.port = ConfigInteger(default = 80, limits=(1, 65535) )
41 config.plugins.Webinterface.http.auth = ConfigYesNo(default=False)
43 config.plugins.Webinterface.https = ConfigSubsection()
44 config.plugins.Webinterface.https.enabled = ConfigYesNo(default=True)
45 config.plugins.Webinterface.https.port = ConfigInteger(default = 443, limits=(1, 65535) )
46 config.plugins.Webinterface.https.auth = ConfigYesNo(default=True)
48 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
50 global running_defered, waiting_shutdown, toplevel
55 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
57 #===============================================================================
58 # Helperclass to close running Instances of the Webinterface
59 #===============================================================================
62 def __init__(self, session, callback=None):
63 self.callback = callback
64 self.session = session
66 #===============================================================================
67 # Closes all running Instances of the Webinterface
68 #===============================================================================
70 global running_defered
71 for d in running_defered:
72 print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
76 x.addCallback(self.isDown)
78 except AttributeError:
82 if self.callback is not None:
83 self.callback(self.session)
85 #===============================================================================
86 # #Is it already down?
87 #===============================================================================
91 if self.callback is not None:
92 self.callback(self.session)
94 def checkCertificates():
95 print "[WebInterface] checking for SSL Certificates"
96 srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG)
97 cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)
99 # Check whether there are regular certificates, if not copy the default ones over
100 if not os_isfile(srvcert) or not os_isfile(cacert):
106 def installCertificates(session, callback = None):
107 print "[WebInterface] Installing SSL Certificates to %s" %resolveFilename(SCOPE_CONFIG)
109 srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG)
110 cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)
111 scope_webif = '%sExtensions/WebInterface/' %resolveFilename(SCOPE_PLUGINS)
113 source = '%setc/server.pem' %scope_webif
115 ret = copyfile(source, target)
118 source = '%setc/cacert.pem' %scope_webif
120 ret = copyfile(source, target)
122 if ret == 0 and callback != None:
126 config.plugins.Webinterface.https.enabled.value = False
127 config.plugins.Webinterface.https.enabled.save()
129 # Start without https
133 session.open(MessageBox, "Couldn't install SSL-Certifactes for https access\nHttps access is now disabled!", MessageBox.TYPE_ERROR)
135 #===============================================================================
136 # restart the Webinterface for all configured Interfaces
137 #===============================================================================
138 def restartWebserver(session):
140 del session.mediaplayer
141 del session.messageboxanswer
144 except AttributeError:
147 global running_defered
148 if len(running_defered) > 0:
149 Closer(session, startWebserver).stop()
151 startWebserver(session)
153 #===============================================================================
154 # start the Webinterface for all configured Interfaces
155 #===============================================================================
156 def startWebserver(session):
157 global running_defered
160 session.mediaplayer = None
161 session.messageboxanswer = None
163 toplevel = getToplevel(session)
167 if config.plugins.Webinterface.enabled.value is not True:
168 print "[Webinterface] is disabled!"
171 # IF SSL is enabled we need to check for the certs first
172 # If they're not there we'll exit via return here
173 # and get called after Certificates are installed properly
174 if config.plugins.Webinterface.https.enabled.value:
175 if not checkCertificates():
176 print "[Webinterface] Installing Webserver Certificates for SSL encryption"
177 installCertificates(session, startWebserver)
179 # Listen on all Interfaces
182 if config.plugins.Webinterface.http.enabled.value is True:
183 ret = startServerInstance(session, ip, config.plugins.Webinterface.http.port.value, config.plugins.Webinterface.http.auth.value)
185 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
187 registerBonjourService('http', config.plugins.Webinterface.http.port.value)
189 if config.plugins.Webinterface.https.enabled.value is True:
190 ret = startServerInstance(session, ip, config.plugins.Webinterface.https.port.value, config.plugins.Webinterface.https.auth.value, True)
192 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
194 registerBonjourService('https', config.plugins.Webinterface.https.port.value)
196 # #LOCAL HTTP Connections (Streamproxy)
197 # ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.streamauth.value)
199 # errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
202 # session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), type=MessageBox.TYPE_ERROR, timeout=30)
204 #===============================================================================
205 # stop the Webinterface for all configured Interfaces
206 #===============================================================================
207 def stopWebserver(session):
209 del session.mediaplayer
210 del session.messageboxanswer
213 except AttributeError:
216 global running_defered
217 if len(running_defered) > 0:
218 Closer(session).stop()
220 #===============================================================================
221 # startServerInstance
222 # Starts an Instance of the Webinterface
223 # on given ipaddress, port, w/o auth, w/o ssl
224 #===============================================================================
225 def startServerInstance(session, ipaddress, port, useauth=False, usessl=False):
228 # HTTPAuthResource handles the authentication for every Resource you want it to
229 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
230 site = server.Site(root)
232 site = server.Site(toplevel)
236 ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem', '/etc/enigma2/cacert.pem', sslmethod=SSL.SSLv23_METHOD)
237 d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
239 d = reactor.listenTCP(port, site, interface=ipaddress)
240 running_defered.append(d)
241 print "[Webinterface] started on %s:%i" % (ipaddress, port), "auth=", useauth, "ssl=", usessl
245 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
247 #===============================================================================
249 # Handles HTTP Authorization for a given Resource
250 #===============================================================================
251 class HTTPAuthResource(resource.Resource):
252 def __init__(self, res, realm):
253 resource.Resource.__init__(self)
256 self.authorized = False
258 self.unauthorizedResource = UnauthorizedResource(self.realm)
260 def unautorized(self, request):
261 request.setResponseCode(http.UNAUTHORIZED)
262 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
264 return self.unauthorizedResource
266 def isAuthenticated(self, request):
267 host = request.getHost().host
268 #If streamauth is disabled allow all acces from localhost
269 if not config.plugins.Webinterface.streamauth.value:
270 if( host == "127.0.0.1" or host == "localhost" ):
271 print "[WebInterface.plugin.isAuthenticated] Streaming auth is disabled bypassing authcheck because host is '%s'" %host
274 # get the Session from the Request
275 sessionNs = request.getSession().sessionNamespaces
277 # if the auth-information has not yet been stored to the session
278 if not sessionNs.has_key('authenticated'):
279 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
281 #if the auth-information already is in the session
283 if sessionNs['authenticated'] is False:
284 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
286 #return the current authentication status
287 return sessionNs['authenticated']
289 #===============================================================================
290 # Call render of self.resource (if authenticated)
291 #===============================================================================
292 def render(self, request):
293 if self.isAuthenticated(request) is True:
294 return self.resource.render(request)
297 print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
298 return self.unautorized(request).render(request)
300 #===============================================================================
301 # Override to call getChildWithDefault of self.resource (if authenticated)
302 #===============================================================================
303 def getChildWithDefault(self, path, request):
304 if self.isAuthenticated(request) is True:
305 return self.resource.getChildWithDefault(path, request)
308 print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
309 return self.unautorized(request)
311 #===============================================================================
312 # UnauthorizedResource
313 # Returns a simple html-ified "Access Denied"
314 #===============================================================================
315 class UnauthorizedResource(resource.Resource):
316 def __init__(self, realm):
317 resource.Resource.__init__(self)
319 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
321 def getChild(self, path, request):
322 return self.errorpage
324 def render(self, request):
325 return self.errorpage.render(request)
327 # Password verfication stuff
329 from hashlib import md5 as md5_new
330 from crypt import crypt
332 #===============================================================================
335 # Get a password database entry for the given user name
336 # Example from the Python Library Reference.
337 #===============================================================================
338 def getpwnam(name, pwfile=None):
340 pwfile = '/etc/passwd'
348 entry = tuple(line.strip().split(':', 6))
353 #===============================================================================
357 #===============================================================================
358 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
359 """Encrypt a string according to rules in crypt(3)."""
360 if method.lower() == 'des':
361 return crypt(passwd, salt)
362 elif method.lower() == 'md5':
363 return passcrypt_md5(passwd, salt, magic)
364 elif method.lower() == 'clear':
367 #===============================================================================
370 # Checks username and Password against a given Unix Password file
371 # The default path is '/etc/passwd'
372 #===============================================================================
373 def check_passwd(name, passwd, pwfile='/etc/passwd'):
374 """Validate given user, passwd pair against password database."""
376 if not pwfile or type(pwfile) == type(''):
377 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
379 getuser = pwfile.get_passwd
382 enc_passwd = getuser(name)
383 except (KeyError, IOError):
384 print "[Webinterface.check_passwd] No such user!"
387 "[Webinterface.check_passwd] NOT ENC_PASSWD"
389 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
390 salt = enc_passwd[3:enc_passwd.find('$', 3)]
391 return enc_passwd == passcrypt(passwd, salt, 'md5')
393 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
396 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
399 r = r + DES_SALT[v & 0x3F]
404 #===============================================================================
406 # Encrypt a password via md5
407 #===============================================================================
408 def passcrypt_md5(passwd, salt=None, magic='$1$'):
411 elif salt[:len(magic)] == magic:
412 # remove magic from salt if present
413 salt = salt[len(magic):]
415 # salt only goes up to first '$'
416 salt = salt.split('$')[0]
417 # limit length of salt to 8
420 ctx = md5_new(passwd)
424 ctx1 = md5_new(passwd)
428 final = ctx1.digest()
430 for i in range(len(passwd), 0 , -16):
434 ctx.update(final[:i])
441 ctx.update(passwd[:1])
445 for i in range(1000):
451 if i % 3: ctx1.update(salt)
452 if i % 7: ctx1.update(passwd)
457 final = ctx1.digest()
459 rv = magic + salt + '$'
460 final = map(ord, final)
461 l = (final[0] << 16) + (final[6] << 8) + final[12]
462 rv = rv + _to64(l, 4)
463 l = (final[1] << 16) + (final[7] << 8) + final[13]
464 rv = rv + _to64(l, 4)
465 l = (final[2] << 16) + (final[8] << 8) + final[14]
466 rv = rv + _to64(l, 4)
467 l = (final[3] << 16) + (final[9] << 8) + final[15]
468 rv = rv + _to64(l, 4)
469 l = (final[4] << 16) + (final[10] << 8) + final[5]
470 rv = rv + _to64(l, 4)
472 rv = rv + _to64(l, 2)
476 global_session = None
478 #===============================================================================
480 # Actions to take place on Session start
481 #===============================================================================
482 def sessionstart(reason, session):
483 global global_session
484 global_session = session
487 def registerBonjourService(protocol, port):
489 from Plugins.Extensions.Bonjour.Bonjour import bonjour
491 service = bonjour.buildService(protocol, port)
492 bonjour.registerService(service, True)
493 print "[WebInterface.registerBonjourService] Service for protocol '%s' with port '%i' registered!" %(protocol, port)
496 except ImportError, e:
497 print "[WebInterface.registerBonjourService] %s" %e
500 def unregisterBonjourService(protocol):
502 from Plugins.Extensions.Bonjour.Bonjour import bonjour
504 bonjour.unregisterService(protocol)
505 print "[WebInterface.unregisterBonjourService] Service for protocol '%s' unregistered!" %(protocol)
508 except ImportError, e:
509 print "[WebInterface.unregisterBonjourService] %s" %e
513 if ( not config.plugins.Webinterface.http.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
514 unregisterBonjourService('http')
515 if ( not config.plugins.Webinterface.https.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
516 unregisterBonjourService('https')
518 #===============================================================================
520 # Actions to take place after Network is up (startup the Webserver)
521 #===============================================================================
522 def networkstart(reason, **kwargs):
524 startWebserver(global_session)
527 elif reason is False:
528 stopWebserver(global_session)
531 def openconfig(session, **kwargs):
532 session.openWithCallback(configCB, WebIfConfigScreen)
534 def configCB(result, session):
536 print "[WebIf] config changed"
537 restartWebserver(session)
540 print "[WebIf] config not changed"
542 def Plugins(**kwargs):
543 return [PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart),
544 PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
545 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
546 where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]