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