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