3 from enigma import eConsoleAppContainer
4 from Plugins.Plugin import PluginDescriptor
6 from Components.config import config, ConfigBoolean, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText, ConfigOnOff
7 from Components.Network import iNetworkInfo
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
14 from Tools.IO import saveFile
15 from Tools.Log import Log
17 from twisted.internet import reactor, ssl
18 from twisted.internet.error import CannotListenError
19 from twisted.web import server, http, util, static, resource
21 from zope.interface import Interface, implements
22 from socket import gethostname as socket_gethostname
23 from OpenSSL import SSL, crypto
24 from time import gmtime
25 from os.path import isfile as os_isfile, exists as os_exists
27 from __init__ import __version__
29 import random, uuid, time, hashlib
31 from netaddr import IPNetwork
37 config.plugins.Webinterface = ConfigSubsection()
38 config.plugins.Webinterface.enabled = ConfigYesNo(default=True)
39 config.plugins.Webinterface.show_in_extensionsmenu = ConfigYesNo(default = False)
40 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
41 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
42 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
43 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=True)
44 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
46 config.plugins.Webinterface.http = ConfigSubsection()
47 config.plugins.Webinterface.http.enabled = ConfigYesNo(default=True)
48 config.plugins.Webinterface.http.port = ConfigInteger(default = 80, limits=(1, 65535) )
49 config.plugins.Webinterface.http.auth = ConfigYesNo(default=True)
51 config.plugins.Webinterface.https = ConfigSubsection()
52 config.plugins.Webinterface.https.enabled = ConfigYesNo(default=True)
53 config.plugins.Webinterface.https.port = ConfigInteger(default = 443, limits=(1, 65535) )
54 config.plugins.Webinterface.https.auth = ConfigYesNo(default=True)
56 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
57 config.plugins.Webinterface.localauth = ConfigOnOff(default=False)
59 config.plugins.Webinterface.anti_hijack = ConfigOnOff(default=True)
60 config.plugins.Webinterface.extended_security = ConfigOnOff(default=True)
62 global running_defered, waiting_shutdown, toplevel
67 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
69 KEY_FILE = resolveFilename(SCOPE_CONFIG, "key.pem")
70 CERT_FILE = resolveFilename(SCOPE_CONFIG, "cert.pem")
72 #===============================================================================
73 # Helperclass to close running Instances of the Webinterface
74 #===============================================================================
77 def __init__(self, session, callback=None):
78 self.callback = callback
79 self.session = session
80 #===============================================================================
81 # Closes all running Instances of the Webinterface
82 #===============================================================================
84 global running_defered
85 for d in running_defered:
86 print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
90 x.addCallback(self.isDown)
92 except AttributeError:
96 if self.callback is not None:
97 self.callback(self.session)
99 #===============================================================================
100 # #Is it already down?
101 #===============================================================================
105 if self.callback is not None:
106 self.callback(self.session)
108 def installCertificates(session):
109 if not os_exists(CERT_FILE) \
110 or not os_exists(KEY_FILE):
111 print "[Webinterface].installCertificates :: Generating SSL key pair and CACert"
114 k.generate_key(crypto.TYPE_RSA, 2048)
116 # create a self-signed cert
118 cert.get_subject().C = "DE"
119 cert.get_subject().ST = "Home"
120 cert.get_subject().L = "Home"
121 cert.get_subject().O = "Dreambox"
122 cert.get_subject().OU = "STB"
123 cert.get_subject().CN = socket_gethostname()
124 cert.set_serial_number(random.randint(1000000,1000000000))
125 cert.set_notBefore("20120101000000Z");
126 cert.set_notAfter("20301231235900Z")
127 cert.set_issuer(cert.get_subject())
129 print "[Webinterface].installCertificates :: Signing SSL key pair with new CACert"
130 cert.sign(k, 'sha256')
133 print "[Webinterface].installCertificates :: Installing newly generated certificate and key pair"
134 saveFile(CERT_FILE, crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
135 saveFile(KEY_FILE, crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
138 config.plugins.Webinterface.https.enabled.value = False
139 config.plugins.Webinterface.https.enabled.save()
141 session.open(MessageBox, "Couldn't install generated SSL-Certifactes for https access\nHttps access is disabled!", MessageBox.TYPE_ERROR)
144 #===============================================================================
145 # restart the Webinterface for all configured Interfaces
146 #===============================================================================
147 def restartWebserver(session):
149 del session.mediaplayer
150 del session.messageboxanswer
153 except AttributeError:
156 global running_defered
157 if len(running_defered) > 0:
158 Closer(session, startWebserver).stop()
160 startWebserver(session)
162 #===============================================================================
163 # start the Webinterface for all configured Interfaces
164 #===============================================================================
165 def startWebserver(session):
166 global running_defered
169 session.mediaplayer = None
170 session.messageboxanswer = None
172 toplevel = getToplevel(session)
176 if config.plugins.Webinterface.enabled.value is not True:
177 print "[Webinterface] is disabled!"
180 # IF SSL is enabled we need to check for the certs first
181 # If they're not there we'll exit via return here
182 # and get called after Certificates are installed properly
183 if config.plugins.Webinterface.https.enabled.value:
184 installCertificates(session)
186 # Listen on all Interfaces
189 port = config.plugins.Webinterface.http.port.value
190 auth = config.plugins.Webinterface.http.auth.value
191 if config.plugins.Webinterface.http.enabled.value is True:
192 ret = startServerInstance(session, port, useauth=auth)
194 errors = "%s port %i\n" %(errors, port)
196 registerBonjourService('http', port)
198 #Streaming requires listening on localhost:80 no matter what, ensure it its available
199 if config.plugins.Webinterface.http.port.value != 80 or not config.plugins.Webinterface.http.enabled.value:
200 #LOCAL HTTP Connections (Streamproxy)
202 local4mapped = "::ffff:127.0.0.1"
205 ret = startServerInstance(session, 80, useauth=auth, ipaddress=local4)
207 errors = "%s%s:%i\n" %(errors, local4, 80)
208 ret = startServerInstance(session, 80, useauth=auth, ipaddress=local4mapped, ipaddress2=local6)
211 # errors = "%s%s/%s:%i\n" %(errors, local4mapped, local6, 80)
214 if config.plugins.Webinterface.https.enabled.value is True:
215 sport = config.plugins.Webinterface.https.port.value
216 sauth = config.plugins.Webinterface.https.auth.value
218 ret = startServerInstance(session, sport, useauth=sauth, usessl=True)
220 errors = "%s%s:%i\n" %(errors, "0.0.0.0 / ::", sport)
222 registerBonjourService('https', sport)
225 session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), type=MessageBox.TYPE_ERROR, timeout=30)
227 #===============================================================================
228 # stop the Webinterface for all configured Interfaces
229 #===============================================================================
230 def stopWebserver(session):
232 del session.mediaplayer
233 del session.messageboxanswer
236 except AttributeError:
239 global running_defered
240 if len(running_defered) > 0:
241 Closer(session).stop()
243 #===============================================================================
244 # startServerInstance
245 # Starts an Instance of the Webinterface
246 # on given ipaddress, port, w/o auth, w/o ssl
247 #===============================================================================
248 def startServerInstance(session, port, useauth=False, usessl=False, ipaddress="::", ipaddress2=None):
250 # HTTPAuthResource handles the authentication for every Resource you want it to
251 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
252 site = server.Site(root)
254 root = HTTPRootResource(toplevel)
255 site = server.Site(root)
259 def logFail(addr, exception=None):
260 print "[Webinterface] FAILED to listen on %s:%i auth=%s ssl=%s" % (addr, port, useauth, usessl)
265 ctx = ChainedOpenSSLContextFactory(KEY_FILE, CERT_FILE)
267 d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
269 running_defered.append(d)
270 except CannotListenError as e:
271 logFail(ipaddress, e)
274 d = reactor.listenSSL(port, site, ctx, interface=ipaddress2)
276 running_defered.append(d)
277 except CannotListenError as e:
278 logFail(ipaddress2, e)
281 d = reactor.listenTCP(port, site, interface=ipaddress)
283 running_defered.append(d)
284 except CannotListenError as e:
285 logFail(ipaddress, e)
288 d = reactor.listenTCP(port, site, interface=ipaddress2)
290 running_defered.append(d)
291 except CannotListenError as e:
292 logFail(ipaddress2, e)
294 print "[Webinterface] started on %s:%i auth=%s ssl=%s" % (ipaddress, port, useauth, usessl)
297 #except Exception, e:
298 #print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
301 class ChainedOpenSSLContextFactory(ssl.DefaultOpenSSLContextFactory):
302 def __init__(self, privateKeyFileName, certificateChainFileName, sslmethod=SSL.SSLv23_METHOD):
303 self.privateKeyFileName = privateKeyFileName
304 self.certificateChainFileName = certificateChainFileName
305 self.sslmethod = sslmethod
308 def cacheContext(self):
309 ctx = SSL.Context(self.sslmethod)
310 ctx.set_options(SSL.OP_NO_SSLv3|SSL.OP_NO_SSLv2)
311 ctx.use_certificate_chain_file(self.certificateChainFileName)
312 ctx.use_privatekey_file(self.privateKeyFileName)
315 class SimpleSession(object):
316 def __init__(self, expires=0):
318 self._expires = time.time() + expires if expires > 0 else 0
320 def _generateId(self):
321 if config.plugins.Webinterface.extended_security.value:
322 self._id = str ( uuid.uuid4() )
333 if config.plugins.Webinterface.extended_security.value:
334 expired = self._expires > 0 and self._expires < time.time()
335 expired = expired or self._id == "0"
337 expired = self._id != "0"
340 id = property(_getId)
342 #Every request made will pass this Resource (as it is the root resource)
343 #Any "global" checks should be done here
344 class HTTPRootResource(resource.Resource):
345 SESSION_PROTECTED_PATHS = ['/web/', '/opkg', '/ipkg']
346 SESSION_EXCEPTIONS = [
347 '/web/epgsearch.rss', '/web/movielist.m3u', '/web/movielist.rss', '/web/services.m3u', '/web/session',
348 '/web/stream.m3u', '/web/stream', '/web/streamcurrent.m3u', '/web/strings.js', '/web/ts.m3u']
350 def __init__(self, res):
351 print "[HTTPRootResource}.__init__"
352 resource.Resource.__init__(self)
354 self.sessionInvalidResource = resource.ErrorPage(http.PRECONDITION_FAILED, "Precondition failed!", "sessionid is missing, invalid or expired!")
357 def getClientToken(self, request):
358 ip = request.getClientIP()
359 ua = request.getHeader("User-Agent") or "Default UA"
360 return hashlib.sha1("%s/%s" %(ip, ua)).hexdigest()
362 def isSessionValid(self, request):
363 session = self._sessions.get( self.getClientToken(request), None )
364 if session is None or session.expired():
365 session = SimpleSession()
366 key = self.getClientToken(request)
367 print "[HTTPRootResource].isSessionValid :: created session with id '%s' for client with token '%s'" %(session.id, key)
368 self._sessions[ key ] = session
370 request.enigma2_session = session
372 if config.plugins.Webinterface.extended_security.value and not request.path in self.SESSION_EXCEPTIONS:
374 for path in self.SESSION_PROTECTED_PATHS:
375 if request.path.startswith(path):
379 rsid = request.args.get('sessionid', None)
382 return session and session.id == rsid
386 def render(self, request):
387 #enable SAMEORIGIN policy for iframes
388 if config.plugins.Webinterface.anti_hijack.value:
389 request.setHeader("X-Frame-Options", "SAMEORIGIN")
391 if self.isSessionValid(request):
392 return self.resource.render(request)
394 return self.sessionInvalidResource.render(request)
396 def getChildWithDefault(self, path, request):
397 #enable SAMEORIGIN policy for iframes
398 if config.plugins.Webinterface.anti_hijack.value:
399 request.setHeader("X-Frame-Options", "SAMEORIGIN")
401 if self.isSessionValid(request):
402 return self.resource.getChildWithDefault(path, request)
404 print "[Webinterface.HTTPRootResource.render] !!! session invalid !!!"
405 return self.sessionInvalidResource
407 #===============================================================================
409 # Handles HTTP Authorization for a given Resource
410 #===============================================================================
411 class HTTPAuthResource(HTTPRootResource):
412 LOCALHOSTS = (IPNetwork("127.0.0.1"), IPNetwork("::1"))
414 def __init__(self, res, realm):
415 HTTPRootResource.__init__(self, res)
417 self.authorized = False
418 self.unauthorizedResource = resource.ErrorPage(http.UNAUTHORIZED, "Access denied", "Authentication credentials invalid!")
419 self._localNetworks = []
421 def _assignLocalNetworks(self, ifaces):
422 if self._localNetworks:
424 self._localNetworks = []
426 for key, iface in ifaces.iteritems():
427 if iface.ipv4.address != "0.0.0.0":
428 v4net = IPNetwork("%s/%s" %(iface.ipv4.address, iface.ipv4.netmask))
429 self._localNetworks.append(v4net)
430 if iface.ipv6.address != "::":
431 v6net = IPNetwork("%s/%s" %(iface.ipv6.address, iface.ipv6.netmask))
432 self._localNetworks.append(v6net)
433 Log.w(self._localNetworks)
435 def unauthorized(self, request):
436 request.setHeader('WWW-authenticate', 'Basic realm="%s"' % self.realm)
437 request.setResponseCode(http.UNAUTHORIZED)
438 return self.unauthorizedResource
440 def _isLocalClient(self, clientip):
441 if self._isLocalHost(clientip):
443 for lnw in self._localNetworks:
444 if self._networkContains(lnw, clientip):
448 def _isLocalHost(self, clientip):
449 for host in self.LOCALHOSTS:
450 if self._networkContains(host, clientip):
454 def _networkContains(self, network, ip):
455 if network.__contains__(ip):
458 # You may get an ipv6 noted ipv4 address like "::ffff:192.168.0.2"
459 # In that case it won't match the ipv4 local network so we have to try converting it to plain ipv4
460 if network.__contains__(ip.ipv4()):
466 def isAuthenticated(self, request):
467 self._assignLocalNetworks(iNetworkInfo.getConfiguredInterfaces())
468 if request.transport:
469 host = IPNetwork(request.transport.getPeer().host)
470 #If streamauth is disabled allow all acces from localhost
471 if not config.plugins.Webinterface.streamauth.value:
472 if self._isLocalHost(host.ip):
473 Log.d("Streaming auth is disabled - Bypassing Authcheck because host '%s' is local!" %host)
475 if not config.plugins.Webinterface.localauth.value:
476 if self._isLocalClient(host.ip):
477 Log.d("Local auth is disabled - Bypassing Authcheck because host '%s' is local!" %host)
480 # get the Session from the Request
481 http_session = request.getSession().sessionNamespaces
483 # if the auth-information has not yet been stored to the http_session
484 if not http_session.has_key('authenticated'):
485 if request.getUser() and request.getPassword():
486 http_session['authenticated'] = check_passwd(request.getUser(), request.getPassword())
488 http_session['authenticated'] = False
490 #if the auth-information already is in the http_session
492 if http_session['authenticated'] is False:
493 http_session['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
495 #return the current authentication status
496 return http_session['authenticated']
498 #===============================================================================
499 # Call render of self.resource (if authenticated)
500 #===============================================================================
501 def render(self, request):
502 if self.isAuthenticated(request) is True:
503 return HTTPRootResource.render(self, request)
505 print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
506 return self.unauthorized(request).render(request)
508 #===============================================================================
509 # Override to call getChildWithDefault of self.resource (if authenticated)
510 #===============================================================================
511 def getChildWithDefault(self, path, request):
512 if self.isAuthenticated(request) is True:
513 return HTTPRootResource.getChildWithDefault(self, path, request)
515 print "[Webinterface.HTTPAuthResource.getChildWithDefault] !!! unauthorized !!!"
516 return self.unauthorized(request)
518 from auth import check_passwd
520 global_session = None
522 #===============================================================================
524 # Actions to take place on Session start
525 #===============================================================================
526 def sessionstart(reason, session):
527 global global_session
528 global_session = session
529 networkstart(True, session)
532 def registerBonjourService(protocol, port):
534 from Plugins.Extensions.Bonjour.Bonjour import bonjour
536 service = bonjour.buildService(protocol, port)
537 bonjour.registerService(service, True)
538 print "[WebInterface.registerBonjourService] Service for protocol '%s' with port '%i' registered!" %(protocol, port)
541 except ImportError, e:
542 print "[WebInterface.registerBonjourService] %s" %e
545 def unregisterBonjourService(protocol):
547 from Plugins.Extensions.Bonjour.Bonjour import bonjour
549 bonjour.unregisterService(protocol)
550 print "[WebInterface.unregisterBonjourService] Service for protocol '%s' unregistered!" %(protocol)
553 except ImportError, e:
554 print "[WebInterface.unregisterBonjourService] %s" %e
558 if ( not config.plugins.Webinterface.http.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
559 unregisterBonjourService('http')
560 if ( not config.plugins.Webinterface.https.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
561 unregisterBonjourService('https')
563 #===============================================================================
565 # Actions to take place after Network is up (startup the Webserver)
566 #===============================================================================
567 #def networkstart(reason, **kwargs):
568 def networkstart(reason, session):
570 startWebserver(session)
573 elif reason is False:
574 stopWebserver(session)
577 def openconfig(session, **kwargs):
578 session.openWithCallback(configCB, WebIfConfigScreen)
580 def menu_config(menuid, **kwargs):
581 if menuid == "network":
582 return [(_("Webinterface"), openconfig, "webif", 60)]
586 def configCB(result, session):
588 print "[WebIf] config changed"
589 restartWebserver(session)
592 print "[WebIf] config not changed"
594 def Plugins(**kwargs):
595 p = PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart)
596 p.weight = 100 #webif should start as last plugin
598 # PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
599 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
600 where=PluginDescriptor.WHERE_MENU, icon="plugin.png", fnc=menu_config)]
601 if config.plugins.Webinterface.show_in_extensionsmenu.value:
602 list.append(PluginDescriptor(name="Webinterface", description=_("Configuration for the Webinterface"),
603 where=PluginDescriptor.WHERE_EXTENSIONSMENU, icon="plugin.png", fnc=openconfig))