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