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