aihdcontroler: fix encoding
[enigma2-plugins.git] / webinterface / src / plugin.py
1 Version = '$Header$';
2
3 from enigma import eConsoleAppContainer
4 from Plugins.Plugin import PluginDescriptor
5
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
12
13 from Tools.Directories import copyfile, resolveFilename, SCOPE_PLUGINS, SCOPE_CONFIG
14 from Tools.IO import saveFile
15 from Tools.Log import Log
16
17 from twisted.internet import reactor, ssl
18 from twisted.internet.error import CannotListenError
19 from twisted.web import server, http, util, static, resource
20
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
26
27 from __init__ import __version__
28
29 import random, uuid, time, hashlib
30
31 from netaddr import IPNetwork
32
33 hw = HardwareInfo()
34 #CONFIG INIT
35
36 #init the config
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.
45
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)
50
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)
55
56 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
57 config.plugins.Webinterface.localauth = ConfigOnOff(default=False)
58
59 config.plugins.Webinterface.anti_hijack = ConfigOnOff(default=True)
60 config.plugins.Webinterface.extended_security = ConfigOnOff(default=True)
61
62 global running_defered, waiting_shutdown, toplevel
63
64 running_defered = []
65 waiting_shutdown = 0
66 toplevel = None
67 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
68
69 KEY_FILE = resolveFilename(SCOPE_CONFIG, "key.pem")
70 CERT_FILE = resolveFilename(SCOPE_CONFIG, "cert.pem")
71
72 #===============================================================================
73 # Helperclass to close running Instances of the Webinterface
74 #===============================================================================
75 class Closer:
76         counter = 0
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 #===============================================================================
83         def stop(self):
84                 global running_defered
85                 for d in running_defered:
86                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
87                         x = d.stopListening()
88
89                         try:
90                                 x.addCallback(self.isDown)
91                                 self.counter += 1
92                         except AttributeError:
93                                 pass
94                 running_defered = []
95                 if self.counter < 1:
96                         if self.callback is not None:
97                                 self.callback(self.session)
98
99 #===============================================================================
100 # #Is it already down?
101 #===============================================================================
102         def isDown(self, s):
103                 self.counter -= 1
104                 if self.counter < 1:
105                         if self.callback is not None:
106                                 self.callback(self.session)
107
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"
112                 # create a key pair
113                 k = crypto.PKey()
114                 k.generate_key(crypto.TYPE_RSA, 2048)
115
116                 # create a self-signed cert
117                 cert = crypto.X509()
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())
128                 cert.set_pubkey(k)
129                 print "[Webinterface].installCertificates :: Signing SSL key pair with new CACert"
130                 cert.sign(k, 'sha256')
131
132                 try:
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))
136                 except IOError, e:
137                         #Disable https
138                         config.plugins.Webinterface.https.enabled.value = False
139                         config.plugins.Webinterface.https.enabled.save()
140                         #Inform the user
141                         session.open(MessageBox, "Couldn't install generated SSL-Certifactes for https access\nHttps access is disabled!", MessageBox.TYPE_ERROR)
142
143
144 #===============================================================================
145 # restart the Webinterface for all configured Interfaces
146 #===============================================================================
147 def restartWebserver(session):
148         try:
149                 del session.mediaplayer
150                 del session.messageboxanswer
151         except NameError:
152                 pass
153         except AttributeError:
154                 pass
155
156         global running_defered
157         if len(running_defered) > 0:
158                 Closer(session, startWebserver).stop()
159         else:
160                 startWebserver(session)
161
162 #===============================================================================
163 # start the Webinterface for all configured Interfaces
164 #===============================================================================
165 def startWebserver(session):
166         global running_defered
167         global toplevel
168
169         session.mediaplayer = None
170         session.messageboxanswer = None
171         if toplevel is None:
172                 toplevel = getToplevel(session)
173
174         errors = ""
175
176         if config.plugins.Webinterface.enabled.value is not True:
177                 print "[Webinterface] is disabled!"
178
179         else:
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)
185
186                 # Listen on all Interfaces
187
188                 #HTTP
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)
193                         if not ret:
194                                 errors = "%s port %i\n" %(errors, port)
195                         else:
196                                 registerBonjourService('http', port)
197
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)
201                         local4 = "127.0.0.1"
202                         local4mapped = "::ffff:127.0.0.1"
203                         local6 = "::1"
204
205                         ret = startServerInstance(session, 80, useauth=auth, ipaddress=local4)
206                         if not ret:
207                                 errors = "%s%s:%i\n" %(errors, local4, 80)
208                         ret = startServerInstance(session, 80, useauth=auth, ipaddress=local4mapped, ipaddress2=local6)
209                         #ip6 is optional
210 #                       if not ret:
211 #                               errors = "%s%s/%s:%i\n" %(errors, local4mapped, local6, 80)
212
213                 #HTTPS
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
217
218                         ret = startServerInstance(session, sport, useauth=sauth, usessl=True)
219                         if not ret:
220                                 errors = "%s%s:%i\n" %(errors, "0.0.0.0 / ::", sport)
221                         else:
222                                 registerBonjourService('https', sport)
223
224                 if errors:
225                         session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), type=MessageBox.TYPE_ERROR, timeout=30)
226
227 #===============================================================================
228 # stop the Webinterface for all configured Interfaces
229 #===============================================================================
230 def stopWebserver(session):
231         try:
232                 del session.mediaplayer
233                 del session.messageboxanswer
234         except NameError:
235                 pass
236         except AttributeError:
237                 pass
238
239         global running_defered
240         if len(running_defered) > 0:
241                 Closer(session).stop()
242
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):
249         if useauth:
250 # HTTPAuthResource handles the authentication for every Resource you want it to
251                 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
252                 site = server.Site(root)
253         else:
254                 root = HTTPRootResource(toplevel)
255                 site = server.Site(root)
256
257         result = False
258
259         def logFail(addr, exception=None):
260                 print "[Webinterface] FAILED to listen on %s:%i auth=%s ssl=%s" % (addr, port, useauth, usessl)
261                 if exception:
262                         print exception
263
264         if usessl:
265                 ctx = ChainedOpenSSLContextFactory(KEY_FILE, CERT_FILE)
266                 try:
267                         d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
268                         result = True
269                         running_defered.append(d)
270                 except CannotListenError as e:
271                         logFail(ipaddress, e)
272                 if ipaddress2:
273                         try:
274                                 d = reactor.listenSSL(port, site, ctx, interface=ipaddress2)
275                                 result = True
276                                 running_defered.append(d)
277                         except CannotListenError as e:
278                                 logFail(ipaddress2, e)
279         else:
280                 try:
281                         d = reactor.listenTCP(port, site, interface=ipaddress)
282                         result = True
283                         running_defered.append(d)
284                 except CannotListenError as e:
285                         logFail(ipaddress, e)
286                 if ipaddress2:
287                         try:
288                                 d = reactor.listenTCP(port, site, interface=ipaddress2)
289                                 result = True
290                                 running_defered.append(d)
291                         except CannotListenError as e:
292                                 logFail(ipaddress2, e)
293         
294         print "[Webinterface] started on %s:%i auth=%s ssl=%s" % (ipaddress, port, useauth, usessl)
295         return result
296
297         #except Exception, e:
298                 #print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
299                 #return False
300
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
306                 self.cacheContext()
307
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)
313                 self._context = ctx
314
315 class SimpleSession(object):
316         def __init__(self, expires=0):
317                 self._id = "0"
318                 self._expires = time.time() + expires if expires > 0 else 0
319
320         def _generateId(self):
321                 if config.plugins.Webinterface.extended_security.value:
322                         self._id = str ( uuid.uuid4() )
323                 else:
324                         self._id = "0"
325
326         def _getId(self):
327                 if self.expired():
328                         self._generateId()
329                 return self._id
330
331         def expired(self):
332                 expired = False
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"
336                 else:
337                         expired = self._id != "0"
338                 return expired
339
340         id = property(_getId)
341
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']
349
350         def __init__(self, res):
351                 print "[HTTPRootResource}.__init__"
352                 resource.Resource.__init__(self)
353                 self.resource = res
354                 self.sessionInvalidResource = resource.ErrorPage(http.PRECONDITION_FAILED, "Precondition failed!", "sessionid is missing, invalid or expired!")
355                 self._sessions = {}
356
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()
361
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
369
370                 request.enigma2_session = session
371
372                 if config.plugins.Webinterface.extended_security.value and not request.path in self.SESSION_EXCEPTIONS:
373                         protected = False
374                         for path in self.SESSION_PROTECTED_PATHS:
375                                 if request.path.startswith(path):
376                                         protected = True
377
378                         if protected:
379                                 rsid = request.args.get('sessionid', None)
380                                 if rsid:
381                                         rsid = rsid[0]
382                                 return session and session.id == rsid
383
384                 return True
385
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")
390
391                 if self.isSessionValid(request):
392                         return self.resource.render(request)
393                 else:
394                         return self.sessionInvalidResource.render(request)
395
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")
400
401                 if self.isSessionValid(request):
402                         return self.resource.getChildWithDefault(path, request)
403                 else:
404                         print "[Webinterface.HTTPRootResource.render] !!! session invalid !!!"
405                         return self.sessionInvalidResource
406
407 #===============================================================================
408 # HTTPAuthResource
409 # Handles HTTP Authorization for a given Resource
410 #===============================================================================
411 class HTTPAuthResource(HTTPRootResource):
412         LOCALHOSTS = (IPNetwork("127.0.0.1"), IPNetwork("::1"))
413
414         def __init__(self, res, realm):
415                 HTTPRootResource.__init__(self, res)
416                 self.realm = realm
417                 self.authorized = False
418                 self.unauthorizedResource = resource.ErrorPage(http.UNAUTHORIZED, "Access denied", "Authentication credentials invalid!")
419                 self._localNetworks = []
420
421         def _assignLocalNetworks(self, ifaces):
422                         if self._localNetworks:
423                                 return
424                         self._localNetworks = []
425                         #LAN
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)
434
435         def unauthorized(self, request):
436                 request.setHeader('WWW-authenticate', 'Basic realm="%s"' % self.realm)
437                 request.setResponseCode(http.UNAUTHORIZED)
438                 return self.unauthorizedResource
439
440         def _isLocalClient(self, clientip):
441                 if self._isLocalHost(clientip):
442                         return True
443                 for lnw in self._localNetworks:
444                         if self._networkContains(lnw, clientip):
445                                 return True
446                 return False
447
448         def _isLocalHost(self, clientip):
449                 for host in self.LOCALHOSTS:
450                         if self._networkContains(host, clientip):
451                                 return True
452                 return False
453
454         def _networkContains(self, network, ip):
455                 if network.__contains__(ip):
456                         return True
457                 try:
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()):
461                                 return True
462                 except:
463                         pass
464                 return False
465
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)
474                                         return True
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)
478                                         return True
479
480                 # get the Session from the Request
481                 http_session = request.getSession().sessionNamespaces
482
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())
487                         else:
488                                 http_session['authenticated'] = False
489
490                 #if the auth-information already is in the http_session
491                 else:
492                         if http_session['authenticated'] is False:
493                                 http_session['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
494
495                 #return the current authentication status
496                 return http_session['authenticated']
497
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)
504                 else:
505                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
506                         return self.unauthorized(request).render(request)
507
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)
514                 else:
515                         print "[Webinterface.HTTPAuthResource.getChildWithDefault] !!! unauthorized !!!"
516                         return self.unauthorized(request)
517
518 from auth import check_passwd
519
520 global_session = None
521
522 #===============================================================================
523 # sessionstart
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)
530
531
532 def registerBonjourService(protocol, port):
533         try:
534                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
535
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)
539                 return True
540
541         except ImportError, e:
542                 print "[WebInterface.registerBonjourService] %s" %e
543                 return False
544
545 def unregisterBonjourService(protocol):
546         try:
547                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
548
549                 bonjour.unregisterService(protocol)
550                 print "[WebInterface.unregisterBonjourService] Service for protocol '%s' unregistered!" %(protocol)
551                 return True
552
553         except ImportError, e:
554                 print "[WebInterface.unregisterBonjourService] %s" %e
555                 return False
556
557 def checkBonjour():
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')
562
563 #===============================================================================
564 # networkstart
565 # Actions to take place after Network is up (startup the Webserver)
566 #===============================================================================
567 #def networkstart(reason, **kwargs):
568 def networkstart(reason, session):
569         if reason is True:
570                 startWebserver(session)
571                 checkBonjour()
572
573         elif reason is False:
574                 stopWebserver(session)
575                 checkBonjour()
576
577 def openconfig(session, **kwargs):
578         session.openWithCallback(configCB, WebIfConfigScreen)
579
580 def menu_config(menuid, **kwargs):
581         if menuid == "network":
582                 return [(_("Webinterface"), openconfig, "webif", 60)]
583         else:
584                 return []
585
586 def configCB(result, session):
587         if result:
588                 print "[WebIf] config changed"
589                 restartWebserver(session)
590                 checkBonjour()
591         else:
592                 print "[WebIf] config not changed"
593
594 def Plugins(**kwargs):
595         p = PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart)
596         p.weight = 100 #webif should start as last plugin
597         list = [p,
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))
604         return list