* fix Webinterface Networkinterface Config Issue
[vuplus_dvbapp-plugin] / webinterface / src / plugin.py
1 Version = '$Header$';
2 from Plugins.Plugin import PluginDescriptor
3 from Components.config import config, getConfigListEntry, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText, ConfigSelection, ConfigSubList
4 from Screens.MessageBox import MessageBox
5 from WebIfConfig import WebIfConfigScreen, initConfig
6 from WebChilds.Toplevel import Toplevel
7 from twisted.internet import reactor, defer, ssl
8 from twisted.internet.error import CannotListenError
9 from twisted.web2 import server, channel, http
10 from twisted.web2.auth import digest, basic, wrapper
11 from twisted.python.log import startLogging
12 from twisted.cred.portal import Portal, IRealm
13 from twisted.cred import checkers, credentials, error
14 from zope.interface import Interface, implements
15 from socket import gethostname as socket_gethostname
16 from OpenSSL import SSL
17
18 from __init__ import _, __version__
19
20 DEBUG_TO_FILE=False # PLEASE DONT ENABLE LOGGING BY DEFAULT (OR COMMIT TO PLUGIN CVS)
21
22 DEBUGFILE= "/tmp/twisted.log"
23
24 #CONFIG INIT
25
26 #init the config
27 config.plugins.Webinterface = ConfigSubsection()
28 config.plugins.Webinterface.enable = ConfigYesNo(default = True)
29 config.plugins.Webinterface.allowzapping = ConfigYesNo(default = True)
30 config.plugins.Webinterface.includehdd = ConfigYesNo(default = False)
31 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default = False)
32 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default = False)
33 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
34 config.plugins.Webinterface.interfacecount = ConfigInteger(0)
35 config.plugins.Webinterface.interfaces = ConfigSubList()
36 config.plugins.Webinterface.warningsslsend = ConfigYesNo(default = False)
37
38
39 global running_defered,waiting_shutdown
40 running_defered = []
41 waiting_shutdown = 0
42 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi","").replace("sion: ","").replace("$","")
43
44 class Closer:
45         counter = 0
46         def __init__(self,session, callback = None):
47                 self.callback = callback
48                 self.session = session
49                 
50         def stop(self):
51                 global running_defered
52                 for d in running_defered:
53                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
54                         x = d.stopListening()
55                         try:
56                                 x.addCallback(self.isDown)
57                                 self.counter +=1
58                         except AttributeError:
59                                 pass
60                 running_defered = []
61                 if self.counter <1:
62                         if self.callback is not None:
63                                 self.callback(self.session)
64                 
65         def isDown(self,s):
66                 self.counter-=1
67                 if self.counter <1:
68                         if self.callback is not None:
69                                 self.callback(self.session)
70                         
71                 
72 def restartWebserver(session):
73         try:
74                 del session.mediaplayer
75                 del session.messageboxanswer
76         except NameError:
77                 pass
78         except AttributeError:
79                 pass
80
81         global running_defered
82         if len(running_defered) >0:
83                 Closer(session,startWebserver).stop()
84         else:
85                 startWebserver(session)
86
87 def startWebserver(session):
88         global running_defered
89         session.mediaplayer = None
90         session.messageboxanswer = None
91         
92         if config.plugins.Webinterface.enable.value is not True:
93                 print "not starting Werbinterface"
94                 return False
95         if DEBUG_TO_FILE:
96                 print "start twisted logfile, writing to %s" % DEBUGFILE 
97                 startLogging(open(DEBUGFILE,'w'))
98         
99         for i in range(0, config.plugins.Webinterface.interfacecount.value):
100                 c = config.plugins.Webinterface.interfaces[i]
101                 if c.disabled.value is False:
102                         startServerInstance(session,c.address.value,c.port.value,c.useauth.value,c.usessl.value)
103                 else:
104                         print "[Webinterface] not starting disabled interface on %s:%i"%(c.address.value,c.port.value)
105
106
107 def stopWebserver(session):
108         try:
109                 del session.mediaplayer
110                 del session.messageboxanswer
111         except NameError:
112                 pass
113         except AttributeError:
114                 pass
115
116         global running_defered
117         if len(running_defered) > 0:
118                 Closer(session).stop()
119                         
120 def startServerInstance(session,ipaddress,port,useauth=False,usessl=False):
121         try:
122                 toplevel = Toplevel(session)
123                 if useauth:
124                         portal = Portal(HTTPAuthRealm())
125                         portal.registerChecker(PasswordDatabase())
126                         root = wrapper.HTTPAuthResource(toplevel,(basic.BasicCredentialFactory(socket_gethostname()),),portal, (IHTTPUser,))
127                         site = server.Site(root)        
128                 else:
129                         site = server.Site(toplevel)
130                 try:
131                         if usessl:                              
132                                 ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem','/etc/enigma2/cacert.pem',sslmethod=SSL.SSLv23_METHOD)
133                                 d = reactor.listenSSL(port, channel.HTTPFactory(site),ctx,interface=ipaddress)
134                         else:
135                                 d = reactor.listenTCP(port, channel.HTTPFactory(site),interface=ipaddress)
136                         running_defered.append(d)
137                         print "[Webinterface] started on %s:%i"%(ipaddress,port),"auth=",useauth,"ssl=",usessl
138                 except CannotListenError, e:
139                         print "[Webinterface] Could not Listen on %s:%i!"%(ipaddress,port)
140                         session.open(MessageBox,'Could not Listen on %s:%i!\n\n%s'%(ipaddress,port,str(e)), MessageBox.TYPE_ERROR)
141         except Exception,e:
142                 print "[Webinterface] starting FAILED on %s:%i!"%(ipaddress,port),e
143                 session.open(MessageBox,'starting FAILED on %s:%i!\n\n%s'%(ipaddress,port,str(e)), MessageBox.TYPE_ERROR)       
144         
145         
146 class PasswordDatabase:
147     """
148         this checks webiflogins agains /etc/passwd
149     """
150     passwordfile = "/etc/passwd"
151     implements(checkers.ICredentialsChecker)
152     credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
153
154     def _cbPasswordMatch(self, matched, username):
155         if matched:
156             return username
157         else:
158             return failure.Failure(error.UnauthorizedLogin())
159
160     def requestAvatarId(self, credentials):     
161         if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
162                 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
163         else:
164                 return defer.fail(error.UnauthorizedLogin())
165
166 class IHTTPUser(Interface):
167         pass
168
169 class HTTPUser(object):
170         implements(IHTTPUser)
171         username = None
172         def __init__(self,username):
173                 self.username = username
174
175 class HTTPAuthRealm(object):
176         implements(IRealm)
177         def requestAvatar(self, avatarId, mind, *interfaces):
178                 if IHTTPUser in interfaces:
179                         return IHTTPUser, HTTPUser(avatarId)
180                 raise NotImplementedError("Only IHTTPUser interface is supported")
181
182 from hashlib import md5 as md5_new
183 from crypt import crypt
184
185 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') 
186 def getpwnam(name, pwfile=None):
187     """Return pasword database entry for the given user name.
188     
189     Example from the Python Library Reference.
190     """
191     
192     if not pwfile:
193         pwfile = '/etc/passwd'
194
195     f = open(pwfile)
196     while 1:
197         line = f.readline()
198         if not line:
199             f.close()
200             raise KeyError, name
201         entry = tuple(line.strip().split(':', 6))
202         if entry[0] == name:
203             f.close()
204             return entry
205
206 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
207     """Encrypt a string according to rules in crypt(3)."""
208     if method.lower() == 'des':
209             return crypt(passwd, salt)
210     elif method.lower() == 'md5':
211         return passcrypt_md5(passwd, salt, magic)
212     elif method.lower() == 'clear':
213         return passwd
214
215 def check_passwd(name, passwd, pwfile=None):
216     """Validate given user, passwd pair against password database."""
217     
218     if not pwfile or type(pwfile) == type(''):
219         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
220     else:
221         getuser = pwfile.get_passwd
222
223     try:
224         enc_passwd = getuser(name)
225     except (KeyError, IOError):
226         return 0
227     if not enc_passwd:
228         return 0
229     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
230         salt = enc_passwd[3:enc_passwd.find('$', 3)]
231         return enc_passwd == passcrypt(passwd, salt, 'md5')
232        
233     else:
234         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
235
236 def _to64(v, n):
237     r = ''
238     while (n-1 >= 0):
239         r = r + DES_SALT[v & 0x3F]
240         v = v >> 6
241         n = n - 1
242     return r
243                         
244 def passcrypt_md5(passwd, salt=None, magic='$1$'):
245     """Encrypt passwd with MD5 algorithm."""
246     
247     if not salt:
248         pass
249     elif salt[:len(magic)] == magic:
250         # remove magic from salt if present
251         salt = salt[len(magic):]
252
253     # salt only goes up to first '$'
254     salt = salt.split('$')[0]
255     # limit length of salt to 8
256     salt = salt[:8]
257
258     ctx = md5_new(passwd)
259     ctx.update(magic)
260     ctx.update(salt)
261     
262     ctx1 = md5_new(passwd)
263     ctx1.update(salt)
264     ctx1.update(passwd)
265     
266     final = ctx1.digest()
267     
268     for i in range(len(passwd), 0 , -16):
269         if i > 16:
270             ctx.update(final)
271         else:
272             ctx.update(final[:i])
273     
274     i = len(passwd)
275     while i:
276         if i & 1:
277             ctx.update('\0')
278         else:
279             ctx.update(passwd[:1])
280         i = i >> 1
281     final = ctx.digest()
282     
283     for i in range(1000):
284         ctx1 = md5_new()
285         if i & 1:
286             ctx1.update(passwd)
287         else:
288             ctx1.update(final)
289         if i % 3: ctx1.update(salt)
290         if i % 7: ctx1.update(passwd)
291         if i & 1:
292             ctx1.update(final)
293         else:
294             ctx1.update(passwd)
295         final = ctx1.digest()
296     
297     rv = magic + salt + '$'
298     final = map(ord, final)
299     l = (final[0] << 16) + (final[6] << 8) + final[12]
300     rv = rv + _to64(l, 4)
301     l = (final[1] << 16) + (final[7] << 8) + final[13]
302     rv = rv + _to64(l, 4)
303     l = (final[2] << 16) + (final[8] << 8) + final[14]
304     rv = rv + _to64(l, 4)
305     l = (final[3] << 16) + (final[9] << 8) + final[15]
306     rv = rv + _to64(l, 4)
307     l = (final[4] << 16) + (final[10] << 8) + final[5]
308     rv = rv + _to64(l, 4)
309     l = final[11]
310     rv = rv + _to64(l, 2)
311     
312     return rv
313
314 #### stuff for SSL Support
315 def makeSSLContext(myKey,trustedCA):
316      '''Returns an ssl Context Object
317     @param myKey a pem formated key and certifcate with for my current host
318            the other end of this connection must have the cert from the CA
319            that signed this key
320     @param trustedCA a pem formated certificat from a CA you trust
321            you will only allow connections from clients signed by this CA
322            and you will only allow connections to a server signed by this CA
323      '''
324
325      # our goal in here is to make a SSLContext object to pass to connectSSL
326      # or listenSSL
327
328      # Why these functioins... Not sure...
329      fd = open(myKey,'r')
330      ss = fd.read()
331      theCert = ssl.PrivateCertificate.loadPEM(ss)
332      fd.close()
333      fd = open(trustedCA,'r')
334      theCA = ssl.Certificate.loadPEM(fd.read())
335      fd.close()
336      #ctx = theCert.options(theCA)
337      ctx = theCert.options()
338
339      # Now the options you can set look like Standard OpenSSL Library options
340
341      # The SSL protocol to use, one of SSLv23_METHOD, SSLv2_METHOD,
342      # SSLv3_METHOD, TLSv1_METHOD. Defaults to TLSv1_METHOD.
343      ctx.method = ssl.SSL.TLSv1_METHOD
344
345      # If True, verify certificates received from the peer and fail
346      # the handshake if verification fails. Otherwise, allow anonymous
347      # sessions and sessions with certificates which fail validation.
348      ctx.verify = True
349
350      # Depth in certificate chain down to which to verify.
351      ctx.verifyDepth = 1
352
353      # If True, do not allow anonymous sessions.
354      ctx.requireCertification = True
355
356      # If True, do not re-verify the certificate on session resumption.
357      ctx.verifyOnce = True
358
359      # If True, generate a new key whenever ephemeral DH parameters are used
360      # to prevent small subgroup attacks.
361      ctx.enableSingleUseKeys = True
362
363      # If True, set a session ID on each context. This allows a shortened
364      # handshake to be used when a known client reconnects.
365      ctx.enableSessions = True
366
367      # If True, enable various non-spec protocol fixes for broken
368      # SSL implementations.
369      ctx.fixBrokenPeers = False
370
371      return ctx
372
373
374 global_session = None
375
376 def sessionstart(reason, session):
377         global global_session
378         global_session = session
379
380 def autostart(reason, **kwargs):
381         if reason is True:
382                 try:
383                         initConfig()
384                         startWebserver(global_session)
385                 except ImportError,e:
386                         print "[Webinterface] twisted not available, not starting web services", e
387         elif reason is False:
388                 stopWebserver()
389                 
390                 
391                         
392 def openconfig(session, **kwargs):
393         session.openWithCallback(configCB, WebIfConfigScreen)
394
395 def configCB(result, session):
396         if result is True:
397                 print "[WebIf] config changed"
398                 restartWebserver(session)
399         else:
400                 print "[WebIf] config not changed"
401                 
402
403 def Plugins(**kwargs):
404         return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART], fnc = sessionstart),
405                     PluginDescriptor(where = [PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc = autostart),
406                     PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
407                                                          where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]