Change listening behaviour.
[vuplus_dvbapp-plugin] / 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
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
23
24
25 from __init__ import _, __version__
26
27 #CONFIG INIT
28
29 #init the config
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.
37
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)
42
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)
47
48 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
49
50 global running_defered, waiting_shutdown, toplevel
51
52 running_defered = []
53 waiting_shutdown = 0
54 toplevel = None
55 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
56
57 #===============================================================================
58 # Helperclass to close running Instances of the Webinterface
59 #===============================================================================
60 class Closer:
61         counter = 0
62         def __init__(self, session, callback=None):
63                 self.callback = callback
64                 self.session = session
65
66 #===============================================================================
67 # Closes all running Instances of the Webinterface
68 #===============================================================================
69         def stop(self):
70                 global running_defered
71                 for d in running_defered:
72                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
73                         x = d.stopListening()
74                         
75                         try:
76                                 x.addCallback(self.isDown)
77                                 self.counter += 1
78                         except AttributeError:
79                                 pass
80                 running_defered = []
81                 if self.counter < 1:
82                         if self.callback is not None:
83                                 self.callback(self.session)
84
85 #===============================================================================
86 # #Is it already down?
87 #===============================================================================
88         def isDown(self, s):
89                 self.counter -= 1
90                 if self.counter < 1:
91                         if self.callback is not None:
92                                 self.callback(self.session)
93
94 def checkCertificates():
95         print "[WebInterface] checking for SSL Certificates"
96         srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG) 
97         cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)
98
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):
101                 return False
102         
103         else:
104                 return True
105                 
106 def installCertificates(session, callback = None):
107         print "[WebInterface] Installing SSL Certificates to %s" %resolveFilename(SCOPE_CONFIG)
108         
109         srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG) 
110         cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)  
111         scope_webif = '%sExtensions/WebInterface/' %resolveFilename(SCOPE_PLUGINS)
112         
113         source = '%setc/server.pem' %scope_webif
114         target = srvcert
115         ret = copyfile(source, target)
116         
117         if ret == 0:
118                 source = '%setc/cacert.pem' %scope_webif
119                 target = cacert
120                 ret = copyfile(source, target)
121                 
122                 if ret == 0 and callback != None:
123                         callback(session)
124         
125         if ret < 0:
126                 config.plugins.Webinterface.https.enabled.value = False
127                 config.plugins.Webinterface.https.enabled.save()
128                 
129                 # Start without https
130                 callback(session)
131                 
132                 #Inform the user
133                 session.open(MessageBox, "Couldn't install SSL-Certifactes for https access\nHttps access is now disabled!", MessageBox.TYPE_ERROR)
134         
135 #===============================================================================
136 # restart the Webinterface for all configured Interfaces
137 #===============================================================================
138 def restartWebserver(session):
139         try:
140                 del session.mediaplayer
141                 del session.messageboxanswer
142         except NameError:
143                 pass
144         except AttributeError:
145                 pass
146
147         global running_defered
148         if len(running_defered) > 0:
149                 Closer(session, startWebserver).stop()
150         else:
151                 startWebserver(session)
152         
153 #===============================================================================
154 # start the Webinterface for all configured Interfaces
155 #===============================================================================
156 def startWebserver(session):
157         global running_defered
158         global toplevel
159         
160         session.mediaplayer = None
161         session.messageboxanswer = None
162         if toplevel is None:
163                 toplevel = getToplevel(session)
164         
165         errors = ""
166         
167         if config.plugins.Webinterface.enabled.value is not True:
168                 print "[Webinterface] is disabled!"
169         
170         else:
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)
178                                 return
179                 # Listen on all Interfaces
180                 ip = "0.0.0.0"
181                 #HTTP
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)
184                         if ret == False:
185                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
186                         else:
187                                 registerBonjourService('http', config.plugins.Webinterface.http.port.value)
188                 #HTTPS          
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)
191                         if ret == False:
192                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
193                         else:
194                                 registerBonjourService('https', config.plugins.Webinterface.https.port.value)
195         
196 #               #LOCAL HTTP Connections (Streamproxy)
197 #               ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.streamauth.value)                       
198 #               if ret == False:
199 #                       errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
200 #               
201 #               if errors != "":
202 #                       session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), type=MessageBox.TYPE_ERROR, timeout=30)
203                 
204 #===============================================================================
205 # stop the Webinterface for all configured Interfaces
206 #===============================================================================
207 def stopWebserver(session):
208         try:
209                 del session.mediaplayer
210                 del session.messageboxanswer
211         except NameError:
212                 pass
213         except AttributeError:
214                 pass
215
216         global running_defered
217         if len(running_defered) > 0:
218                 Closer(session).stop()
219
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):
226         try:
227                 if useauth:
228 # HTTPAuthResource handles the authentication for every Resource you want it to                 
229                         root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
230                         site = server.Site(root)                        
231                 else:
232                         site = server.Site(toplevel)
233         
234                 if usessl:
235                         
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)
238                 else:
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
242                 return True
243         
244         except Exception, e:
245                 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e         
246                 return False
247 #===============================================================================
248 # HTTPAuthResource
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)
254                 self.resource = res
255                 self.realm = realm
256                 self.authorized = False
257                 self.tries = 0
258                 self.unauthorizedResource = UnauthorizedResource(self.realm)            
259         
260         def unautorized(self, request):
261                 request.setResponseCode(http.UNAUTHORIZED)
262                 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
263
264                 return self.unauthorizedResource
265         
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
272                                 return True
273                                         
274                 # get the Session from the Request
275                 sessionNs = request.getSession().sessionNamespaces
276                 
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())
280                 
281                 #if the auth-information already is in the session                              
282                 else:
283                         if sessionNs['authenticated'] is False:
284                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
285                 
286                 #return the current authentication status                                               
287                 return sessionNs['authenticated']
288                                                                                                         
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)
295                 
296                 else:
297                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
298                         return self.unautorized(request).render(request)
299
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)
306                 
307                 else:
308                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
309                         return self.unautorized(request)
310
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)
318                 self.realm = realm
319                 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
320         
321         def getChild(self, path, request):
322                 return self.errorpage
323                 
324         def render(self, request):      
325                 return self.errorpage.render(request)
326
327 # Password verfication stuff
328
329 from hashlib import md5 as md5_new
330 from crypt import crypt
331
332 #===============================================================================
333 # getpwnam
334
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):
339         if not pwfile:
340                 pwfile = '/etc/passwd'
341
342         f = open(pwfile)
343         while 1:
344                 line = f.readline()
345                 if not line:
346                         f.close()
347                         raise KeyError, name
348                 entry = tuple(line.strip().split(':', 6))
349                 if entry[0] == name:
350                         f.close()
351                         return entry
352
353 #===============================================================================
354 # passcrypt
355 #
356 # Encrypt a password
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':
365                 return passwd
366
367 #===============================================================================
368 # check_passwd
369 #
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."""
375
376         if not pwfile or type(pwfile) == type(''):
377                 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
378         else:
379                 getuser = pwfile.get_passwd
380
381         try:
382                 enc_passwd = getuser(name)
383         except (KeyError, IOError):
384                 print "[Webinterface.check_passwd] No such user!"
385                 return False
386         if not enc_passwd:
387                 "[Webinterface.check_passwd] NOT ENC_PASSWD"
388                 return False
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')
392         else:
393                 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
394
395 def _to64(v, n):
396         DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
397         r = ''
398         while (n - 1 >= 0):
399                 r = r + DES_SALT[v & 0x3F]
400                 v = v >> 6
401                 n = n - 1
402         return r
403
404 #===============================================================================
405 # passcrypt_md5
406 # Encrypt a password via md5
407 #===============================================================================
408 def passcrypt_md5(passwd, salt=None, magic='$1$'):
409         if not salt:
410                 pass
411         elif salt[:len(magic)] == magic:
412                 # remove magic from salt if present
413                 salt = salt[len(magic):]
414
415         # salt only goes up to first '$'
416         salt = salt.split('$')[0]
417         # limit length of salt to 8
418         salt = salt[:8]
419
420         ctx = md5_new(passwd)
421         ctx.update(magic)
422         ctx.update(salt)
423
424         ctx1 = md5_new(passwd)
425         ctx1.update(salt)
426         ctx1.update(passwd)
427
428         final = ctx1.digest()
429
430         for i in range(len(passwd), 0 , -16):
431                 if i > 16:
432                         ctx.update(final)
433                 else:
434                         ctx.update(final[:i])
435
436         i = len(passwd)
437         while i:
438                 if i & 1:
439                         ctx.update('\0')
440                 else:
441                         ctx.update(passwd[:1])
442                 i = i >> 1
443         final = ctx.digest()
444
445         for i in range(1000):
446                 ctx1 = md5_new()
447                 if i & 1:
448                         ctx1.update(passwd)
449                 else:
450                         ctx1.update(final)
451                 if i % 3: ctx1.update(salt)
452                 if i % 7: ctx1.update(passwd)
453                 if i & 1:
454                         ctx1.update(final)
455                 else:
456                         ctx1.update(passwd)
457                 final = ctx1.digest()
458
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)
471         l = final[11]
472         rv = rv + _to64(l, 2)
473
474         return rv
475
476 global_session = None
477
478 #===============================================================================
479 # sessionstart
480 # Actions to take place on Session start 
481 #===============================================================================
482 def sessionstart(reason, session):
483         global global_session
484         global_session = session
485
486
487 def registerBonjourService(protocol, port):     
488         try:
489                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
490                                 
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) 
494                 return True
495                 
496         except ImportError, e:
497                 print "[WebInterface.registerBonjourService] %s" %e
498                 return False
499
500 def unregisterBonjourService(protocol): 
501         try:
502                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
503                                                 
504                 bonjour.unregisterService(protocol)
505                 print "[WebInterface.unregisterBonjourService] Service for protocol '%s' unregistered!" %(protocol) 
506                 return True
507                 
508         except ImportError, e:
509                 print "[WebInterface.unregisterBonjourService] %s" %e
510                 return False
511         
512 def checkBonjour():
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')
517                 
518 #===============================================================================
519 # networkstart
520 # Actions to take place after Network is up (startup the Webserver)
521 #===============================================================================
522 def networkstart(reason, **kwargs):
523         if reason is True:
524                 startWebserver(global_session)
525                 checkBonjour()
526                 
527         elif reason is False:
528                 stopWebserver(global_session)
529                 checkBonjour()
530
531 def openconfig(session, **kwargs):
532         session.openWithCallback(configCB, WebIfConfigScreen)
533
534 def configCB(result, session):
535         if result is True:
536                 print "[WebIf] config changed"
537                 restartWebserver(session)
538                 checkBonjour()
539         else:
540                 print "[WebIf] config not changed"
541
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)]