639c97d727f6f1aadec5238f6616ac5f5bf9360c
[vuplus_dvbapp-plugin] / webinterface / src / plugin.py
1 Version = '$Header$';
2 from Plugins.Plugin import PluginDescriptor
3 from Components.config import config, ConfigBoolean, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText
4 from Components.Network import iNetwork
5 from Screens.MessageBox import MessageBox
6 from WebIfConfig import WebIfConfigScreen
7 from WebChilds.Toplevel import getToplevel
8
9 from twisted.internet import reactor, ssl
10 from twisted.web import server, http, util, static, resource
11
12 from zope.interface import Interface, implements
13 from socket import gethostname as socket_gethostname
14 from OpenSSL import SSL
15
16 from __init__ import _, __version__
17
18 #CONFIG INIT
19
20 #init the config
21 config.plugins.Webinterface = ConfigSubsection()
22 config.plugins.Webinterface.enabled = ConfigYesNo(default=True)
23 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
24 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
25 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
26 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=True)
27 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
28
29 config.plugins.Webinterface.http = ConfigSubsection()
30 config.plugins.Webinterface.http.enabled = ConfigYesNo(default=True)
31 config.plugins.Webinterface.http.port = ConfigInteger(default = 80, limits=(1, 65535) )
32 config.plugins.Webinterface.http.auth = ConfigYesNo(default=False)
33
34 config.plugins.Webinterface.https = ConfigSubsection()
35 config.plugins.Webinterface.https.enabled = ConfigYesNo(default=True)
36 config.plugins.Webinterface.https.port = ConfigInteger(default = 443, limits=(1, 65535) )
37 config.plugins.Webinterface.https.auth = ConfigYesNo(default=True)
38
39 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
40
41 global running_defered, waiting_shutdown, toplevel
42
43 running_defered = []
44 waiting_shutdown = 0
45 toplevel = None
46 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
47
48 #===============================================================================
49 # Helperclass to close running Instances of the Webinterface
50 #===============================================================================
51 class Closer:
52         counter = 0
53         def __init__(self, session, callback=None):
54                 self.callback = callback
55                 self.session = session
56
57 #===============================================================================
58 # Closes all running Instances of the Webinterface
59 #===============================================================================
60         def stop(self):
61                 global running_defered
62                 for d in running_defered:
63                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
64                         x = d.stopListening()
65                         try:
66                                 x.addCallback(self.isDown)
67                                 self.counter += 1
68                         except AttributeError:
69                                 pass
70                 running_defered = []
71                 if self.counter < 1:
72                         if self.callback is not None:
73                                 self.callback(self.session)
74
75 #===============================================================================
76 # #Is it already down?
77 #===============================================================================
78         def isDown(self, s):
79                 self.counter -= 1
80                 if self.counter < 1:
81                         if self.callback is not None:
82                                 self.callback(self.session)
83
84 #===============================================================================
85 # restart the Webinterface for all configured Interfaces
86 #===============================================================================
87 def restartWebserver(session):
88         try:
89                 del session.mediaplayer
90                 del session.messageboxanswer
91         except NameError:
92                 pass
93         except AttributeError:
94                 pass
95
96         global running_defered
97         if len(running_defered) > 0:
98                 Closer(session, startWebserver).stop()
99         else:
100                 startWebserver(session)
101
102 #===============================================================================
103 # start the Webinterface for all configured Interfaces
104 #===============================================================================
105 def startWebserver(session):
106         print "[Webinterface] startWebserver - iNetwork.ifaces: %s" %(iNetwork.ifaces)
107         
108         global running_defered
109         global toplevel
110         
111         session.mediaplayer = None
112         session.messageboxanswer = None
113         if toplevel is None:
114                 toplevel = getToplevel(session)
115         
116         errors = ""
117         
118         if config.plugins.Webinterface.enabled.value is not True:
119                 print "[Webinterface] is disabled!"
120         
121         else:
122         #HTTP
123                 if config.plugins.Webinterface.http.enabled.value is True:
124                         for adaptername in iNetwork.ifaces:                             
125                                 ip = '.'.join("%d" % d for d in iNetwork.ifaces[adaptername]['ip'])
126                                 #Network.py sets the IP of inactive Adapters to 0.0.0.0, we do not want to listen on 0.0.0.0
127                                 if ip != '0.0.0.0':             
128                                         ret = startServerInstance(session, ip, config.plugins.Webinterface.http.port.value, config.plugins.Webinterface.http.auth.value)
129                                         if ret == False:
130                                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
131                 else:
132                         print "[Webinterface] HTTP is disabled - not starting!"
133         
134         #HTTPS          
135                 if config.plugins.Webinterface.https.enabled.value is True:
136                         for adaptername in iNetwork.ifaces:
137                                 ip = '.'.join("%d" % d for d in iNetwork.ifaces[adaptername]['ip'])
138                                 #Network.py sets the IP of inactive Adapters to 0.0.0.0, we do not want to listen on 0.0.0.0
139                                 if ip != '0.0.0.0':                                             
140                                         ret = startServerInstance(session, ip, config.plugins.Webinterface.https.port.value, config.plugins.Webinterface.https.auth.value, True)
141                                         if ret == False:
142                                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
143                 else:
144                         print "[Webinterface] HTTPS is disabled - not starting!"
145         
146         #LOCAL HTTP Connections (Streamproxy)
147                 ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.streamauth.value)                       
148                 if ret == False:
149                         errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
150                 
151                 if errors != "":
152                         session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), MessageBox.TYPE_ERROR)
153                 
154 #===============================================================================
155 # stop the Webinterface for all configured Interfaces
156 #===============================================================================
157 def stopWebserver(session):
158         try:
159                 del session.mediaplayer
160                 del session.messageboxanswer
161         except NameError:
162                 pass
163         except AttributeError:
164                 pass
165
166         global running_defered
167         if len(running_defered) > 0:
168                 Closer(session).stop()
169
170 #===============================================================================
171 # startServerInstance
172 # Starts an Instance of the Webinterface
173 # on given ipaddress, port, w/o auth, w/o ssl
174 #===============================================================================
175 def startServerInstance(session, ipaddress, port, useauth=False, usessl=False):
176         try:
177                 if useauth:
178 # HTTPAuthResource handles the authentication for every Resource you want it to                 
179                         root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
180                         site = server.Site(root)                        
181                 else:
182                         site = server.Site(toplevel)
183         
184                 if usessl:
185                         ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem', '/etc/enigma2/cacert.pem', sslmethod=SSL.SSLv23_METHOD)
186                         d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
187                 else:
188                         d = reactor.listenTCP(port, site, interface=ipaddress)
189                 running_defered.append(d)               
190                 print "[Webinterface] started on %s:%i" % (ipaddress, port), "auth=", useauth, "ssl=", usessl
191                 return True
192         
193         except Exception, e:
194                 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e         
195                 return False
196 #===============================================================================
197 # HTTPAuthResource
198 # Handles HTTP Authorization for a given Resource
199 #===============================================================================
200 class HTTPAuthResource(resource.Resource):
201         def __init__(self, res, realm):
202                 resource.Resource.__init__(self)
203                 self.resource = res
204                 self.realm = realm
205                 self.authorized = False
206                 self.tries = 0
207                 self.unauthorizedResource = UnauthorizedResource(self.realm)            
208         
209         def unautorized(self, request):
210                 request.setResponseCode(http.UNAUTHORIZED)
211                 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
212
213                 return self.unauthorizedResource
214         
215         def isAuthenticated(self, request):
216                 # get the Session from the Request
217                 sessionNs = request.getSession().sessionNamespaces
218                 
219                 # if the auth-information has not yet been stored to the session
220                 if not sessionNs.has_key('authenticated'):
221                         sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
222                 
223                 #if the auth-information already is in the session                              
224                 else:
225                         if sessionNs['authenticated'] is False:
226                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
227                 
228                 #return the current authentication status                                               
229                 return sessionNs['authenticated']
230                                                                                                         
231 #===============================================================================
232 # Call render of self.resource (if authenticated)                                                                                                       
233 #===============================================================================
234         def render(self, request):                      
235                 if self.isAuthenticated(request) is True:       
236                         return self.resource.render(request)
237                 
238                 else:
239                         return self.unautorized(request).render(request)
240
241 #===============================================================================
242 # Override to call getChildWithDefault of self.resource (if authenticated)      
243 #===============================================================================
244         def getChildWithDefault(self, path, request):
245                 if self.isAuthenticated(request) is True:
246                         return self.resource.getChildWithDefault(path, request)
247                 
248                 else:
249                         return self.unautorized(request)
250
251 #===============================================================================
252 # UnauthorizedResource
253 # Returns a simple html-ified "Access Denied"
254 #===============================================================================
255 class UnauthorizedResource(resource.Resource):
256         def __init__(self, realm):
257                 resource.Resource.__init__(self)
258                 self.realm = realm
259                 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
260         
261         def getChild(self, path, request):
262                 return self.errorpage
263                 
264         def render(self, request):      
265                 return self.errorpage.render(request)
266
267 # Password verfication stuff
268
269 from hashlib import md5 as md5_new
270 from crypt import crypt
271
272 #===============================================================================
273 # getpwnam
274
275 # Get a password database entry for the given user name
276 # Example from the Python Library Reference.
277 #===============================================================================
278 def getpwnam(name, pwfile=None):
279         if not pwfile:
280                 pwfile = '/etc/passwd'
281
282         f = open(pwfile)
283         while 1:
284                 line = f.readline()
285                 if not line:
286                         f.close()
287                         raise KeyError, name
288                 entry = tuple(line.strip().split(':', 6))
289                 if entry[0] == name:
290                         f.close()
291                         return entry
292
293 #===============================================================================
294 # passcrypt
295 #
296 # Encrypt a password
297 #===============================================================================
298 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
299         """Encrypt a string according to rules in crypt(3)."""
300         if method.lower() == 'des':
301                 return crypt(passwd, salt)
302         elif method.lower() == 'md5':
303                 return passcrypt_md5(passwd, salt, magic)
304         elif method.lower() == 'clear':
305                 return passwd
306
307 #===============================================================================
308 # check_passwd
309 #
310 # Checks username and Password against a given Unix Password file 
311 # The default path is '/etc/passwd'
312 #===============================================================================
313 def check_passwd(name, passwd, pwfile='/etc/passwd'):
314         """Validate given user, passwd pair against password database."""
315
316         if not pwfile or type(pwfile) == type(''):
317                 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
318         else:
319                 getuser = pwfile.get_passwd
320
321         try:
322                 enc_passwd = getuser(name)
323         except (KeyError, IOError):
324                 print "!!! EXCEPT"
325                 return False
326         if not enc_passwd:
327                 "!!! NOT ENC_PASSWD"
328                 return False
329         elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
330                 salt = enc_passwd[3:enc_passwd.find('$', 3)]
331                 return enc_passwd == passcrypt(passwd, salt, 'md5')
332         else:
333                 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
334
335 def _to64(v, n):
336         DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
337         r = ''
338         while (n - 1 >= 0):
339                 r = r + DES_SALT[v & 0x3F]
340                 v = v >> 6
341                 n = n - 1
342         return r
343
344 #===============================================================================
345 # passcrypt_md5
346 # Encrypt a password via md5
347 #===============================================================================
348 def passcrypt_md5(passwd, salt=None, magic='$1$'):
349         if not salt:
350                 pass
351         elif salt[:len(magic)] == magic:
352                 # remove magic from salt if present
353                 salt = salt[len(magic):]
354
355         # salt only goes up to first '$'
356         salt = salt.split('$')[0]
357         # limit length of salt to 8
358         salt = salt[:8]
359
360         ctx = md5_new(passwd)
361         ctx.update(magic)
362         ctx.update(salt)
363
364         ctx1 = md5_new(passwd)
365         ctx1.update(salt)
366         ctx1.update(passwd)
367
368         final = ctx1.digest()
369
370         for i in range(len(passwd), 0 , -16):
371                 if i > 16:
372                         ctx.update(final)
373                 else:
374                         ctx.update(final[:i])
375
376         i = len(passwd)
377         while i:
378                 if i & 1:
379                         ctx.update('\0')
380                 else:
381                         ctx.update(passwd[:1])
382                 i = i >> 1
383         final = ctx.digest()
384
385         for i in range(1000):
386                 ctx1 = md5_new()
387                 if i & 1:
388                         ctx1.update(passwd)
389                 else:
390                         ctx1.update(final)
391                 if i % 3: ctx1.update(salt)
392                 if i % 7: ctx1.update(passwd)
393                 if i & 1:
394                         ctx1.update(final)
395                 else:
396                         ctx1.update(passwd)
397                 final = ctx1.digest()
398
399         rv = magic + salt + '$'
400         final = map(ord, final)
401         l = (final[0] << 16) + (final[6] << 8) + final[12]
402         rv = rv + _to64(l, 4)
403         l = (final[1] << 16) + (final[7] << 8) + final[13]
404         rv = rv + _to64(l, 4)
405         l = (final[2] << 16) + (final[8] << 8) + final[14]
406         rv = rv + _to64(l, 4)
407         l = (final[3] << 16) + (final[9] << 8) + final[15]
408         rv = rv + _to64(l, 4)
409         l = (final[4] << 16) + (final[10] << 8) + final[5]
410         rv = rv + _to64(l, 4)
411         l = final[11]
412         rv = rv + _to64(l, 2)
413
414         return rv
415
416 #===============================================================================
417 # Creates an SSL Context to use with twisted.web
418 #===============================================================================
419 def makeSSLContext(myKey, trustedCA):
420          '''Returns an ssl Context Object
421         @param myKey a pem formated key and certifcate with for my current host
422                         the other end of this connection must have the cert from the CA
423                         that signed this key
424         @param trustedCA a pem formated certificat from a CA you trust
425                         you will only allow connections from clients signed by this CA
426                         and you will only allow connections to a server signed by this CA
427          '''
428
429          # our goal in here is to make a SSLContext object to pass to connectSSL
430          # or listenSSL
431
432          # Why these functioins... Not sure...
433          fd = open(myKey, 'r')
434          ss = fd.read()
435          theCert = ssl.PrivateCertificate.loadPEM(ss)
436          fd.close()
437          fd = open(trustedCA, 'r')
438          theCA = ssl.Certificate.loadPEM(fd.read())
439          fd.close()
440          #ctx = theCert.options(theCA)
441          ctx = theCert.options()
442
443          # Now the options you can set look like Standard OpenSSL Library options
444
445          # The SSL protocol to use, one of SSLv23_METHOD, SSLv2_METHOD,
446          # SSLv3_METHOD, TLSv1_METHOD. Defaults to TLSv1_METHOD.
447          ctx.method = ssl.SSL.TLSv1_METHOD
448
449          # If True, verify certificates received from the peer and fail
450          # the handshake if verification fails. Otherwise, allow anonymous
451          # sessions and sessions with certificates which fail validation.
452          ctx.verify = True
453
454          # Depth in certificate chain down to which to verify.
455          ctx.verifyDepth = 1
456
457          # If True, do not allow anonymous sessions.
458          ctx.requireCertification = True
459
460          # If True, do not re-verify the certificate on session resumption.
461          ctx.verifyOnce = True
462
463          # If True, generate a new key whenever ephemeral DH parameters are used
464          # to prevent small subgroup attacks.
465          ctx.enableSingleUseKeys = True
466
467          # If True, set a session ID on each context. This allows a shortened
468          # handshake to be used when a known client reconnects.
469          ctx.enableSessions = True
470
471          # If True, enable various non-spec protocol fixes for broken
472          # SSL implementations.
473          ctx.fixBrokenPeers = False
474
475          return ctx
476
477 global_session = None
478
479 #===============================================================================
480 # sessionstart
481 # Actions to take place on Session start 
482 #===============================================================================
483 def sessionstart(reason, session):
484         global global_session
485         global_session = session
486
487 #===============================================================================
488 # networkstart
489 # Actions to take place after Network is up (startup the Webserver)
490 #===============================================================================
491 def networkstart(reason, **kwargs):
492         if reason is True:
493                 startWebserver(global_session)
494
495         elif reason is False:
496                 stopWebserver(global_session)
497
498 def openconfig(session, **kwargs):
499         session.openWithCallback(configCB, WebIfConfigScreen)
500
501 def configCB(result, session):
502         if result is True:
503                 print "[WebIf] config changed"
504                 restartWebserver(session)
505         else:
506                 print "[WebIf] config not changed"
507
508 def Plugins(**kwargs):
509         return [PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart),
510                         PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
511                         PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
512                                                         where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]