major changes for authmethods
[enigma2-plugins.git] / webinterface / src / plugin.py
1 from Plugins.Plugin import PluginDescriptor
2
3 from twisted.internet import reactor
4 from twisted.web2 import server, channel, static, resource, stream, http_headers, responsecode, http
5 from twisted.python import util
6 from twisted.python.log import startLogging,discardLogs
7
8 import webif
9 import WebIfConfig  
10 import os
11
12 from Components.config import config, ConfigSubsection, ConfigInteger,ConfigYesNo
13
14 config.plugins.Webinterface = ConfigSubsection()
15 config.plugins.Webinterface.enable = ConfigYesNo(default = True)
16 config.plugins.Webinterface.port = ConfigInteger(80,limits = (1, 999))
17 config.plugins.Webinterface.includehdd = ConfigYesNo(default = False)
18 config.plugins.Webinterface.useauth = ConfigYesNo(default = True)
19
20 sessions = [ ]
21
22  
23 """
24  set DEBUG to True, if twisted should write logoutput to a file.
25  in normal console output, twisted will print only the first Traceback.
26  is this a bug in twisted or a conflict with enigma2?
27  with this option enabled, twisted will print all TB to the logfile
28  use tail -f <file> to view this log
29 """
30                         
31 DEBUG = True
32 DEBUGFILE= "/tmp/twisted.log"
33
34 from twisted.cred.portal import Portal
35 from twisted.cred import checkers
36 from twisted.web2.auth import digest, basic, wrapper
37 from zope.interface import Interface, implements
38 from twisted.cred import portal
39 from twisted.cred import credentials, error
40 from twisted.internet import defer
41 from zope import interface
42
43
44 def stopWebserver():
45         reactor.disconnectAll()
46
47 def restartWebserver():
48         stopWebserver()
49         startWebserver()
50
51 def startWebserver():
52         if config.plugins.Webinterface.enable.value is not True:
53                 print "not starting Werbinterface"
54                 return False
55         if DEBUG:
56                 print "start twisted logfile, writing to %s" % DEBUGFILE 
57                 import sys
58                 startLogging(open(DEBUGFILE,'w'))
59
60         class ScreenPage(resource.Resource):
61                 def __init__(self, path):
62                         self.path = path
63                         
64                         
65                 def render(self, req):
66                         global sessions
67                         if sessions == [ ]:
68                                 return http.Response(responsecode.OK, stream="please wait until enigma has booted")
69
70                         class myProducerStream(stream.ProducerStream):
71                                 closed_callback = None
72
73                                 def close(self):
74                                         if self.closed_callback:
75                                                 self.closed_callback()
76                                         stream.ProducerStream.close(self)
77
78                         if os.path.isfile(self.path):
79                                 s=myProducerStream()
80                                 webif.renderPage(s, self.path, req, sessions[0])  # login?
81                                 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'xhtml+xml', (('charset', 'UTF-8'),))},stream=s)
82                         else:
83                                 return http.Response(responsecode.NOT_FOUND)
84                         
85                 def locateChild(self, request, segments):
86                         path = self.path+'/'+'/'.join(segments)
87                         if path[-1:] == "/":
88                                 path += "index.html"
89                         path +=".xml"
90                         return ScreenPage(path), ()
91                         
92         class Toplevel(resource.Resource):
93                 addSlash = True
94                 child_web = ScreenPage(util.sibpath(__file__, "web")) # "/web/*"
95                 child_webdata = static.File(util.sibpath(__file__, "web-data")) # FIXME: web-data appears as webdata
96
97                 def render(self, req):
98                         fp = open(util.sibpath(__file__, "web-data")+"/index.html")
99                         s = fp.read()
100                         fp.close()
101                         return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
102
103
104         if config.plugins.Webinterface.includehdd.value:
105                 toplevel.putChild("hdd",static.File("/hdd"))
106         
107         if config.plugins.Webinterface.useauth.value is False:
108                 site = server.Site(Toplevel())
109         else:
110                 portal = Portal(HTTPAuthRealm())
111                 portal.registerChecker(PasswordDatabase())
112                 root = ModifiedHTTPAuthResource(Toplevel(),(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
113                 site = server.Site(root)
114         print "[WebIf] starting Webinterface on port",config.plugins.Webinterface.port.value
115         reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site))
116
117
118 def autostart(reason, **kwargs):
119         if "session" in kwargs:
120                 global sessions
121                 sessions.append(kwargs["session"])
122                 return
123         if reason == 0:
124                 try:
125                         startWebserver()
126                 except ImportError,e:
127                         print "[WebIf] twisted not available, not starting web services",e
128                         
129 def openconfig(session, **kwargs):
130         session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
131
132 def configCB(result):
133         if result is True:
134                 print "[WebIf] config changed"
135                 restartWebserver()
136         else:
137                 print "[WebIf] config not changed"
138                 
139
140 def Plugins(**kwargs):
141         return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
142                     PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
143         
144         
145 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
146         """
147                 set it only to True, if you have a patched wrapper.py
148                 see http://twistedmatrix.com/trac/ticket/2041
149                 so, the solution for us is to make a new class an override ne faulty func
150         """
151
152         def locateChild(self, req, seg):
153                 return self.authenticate(req), seg
154         
155 class PasswordDatabase:
156     """
157         this checks webiflogins agains /etc/passwd
158     """
159     passwordfile = "/etc/passwd"
160     interface.implements(checkers.ICredentialsChecker)
161     credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
162
163     def _cbPasswordMatch(self, matched, username):
164         if matched:
165             return username
166         else:
167             return failure.Failure(error.UnauthorizedLogin())
168
169     def requestAvatarId(self, credentials):     
170         try:
171                 if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
172                         return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
173                 else:
174                         return defer.fail(error.UnauthorizedLogin())
175         except Exception,e:
176                 print e
177
178 class IHTTPUser(Interface):
179         pass
180
181 class HTTPUser(object):
182         implements(IHTTPUser)
183
184 class HTTPAuthRealm(object):
185         implements(portal.IRealm)
186         def requestAvatar(self, avatarId, mind, *interfaces):
187                 if IHTTPUser in interfaces:
188                         return IHTTPUser, HTTPUser()
189                 raise NotImplementedError("Only IHTTPUser interface is supported")
190
191         
192 import md5,time,string,crypt
193 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') 
194 def getpwnam(name, pwfile=None):
195     """Return pasword database entry for the given user name.
196     
197     Example from the Python Library Reference.
198     """
199     
200     if not pwfile:
201         pwfile = '/etc/passwd'
202
203     f = open(pwfile)
204     while 1:
205         line = f.readline()
206         if not line:
207             f.close()
208             raise KeyError, name
209         entry = tuple(line.strip().split(':', 6))
210         if entry[0] == name:
211             f.close()
212             return entry
213
214 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
215     """Encrypt a string according to rules in crypt(3)."""
216     if method.lower() == 'des':
217             if not salt:
218                 salt = str(whrandom.choice(DES_SALT)) + str(whrandom.choice(DES_SALT))
219             return crypt.crypt(passwd, salt)
220     elif method.lower() == 'md5':
221             return passcrypt_md5(passwd, salt, magic)
222     elif method.lower() == 'clear':
223         return passwd
224
225 def check_passwd(name, passwd, pwfile=None):
226     """Validate given user, passwd pair against password database."""
227     
228     if not pwfile or type(pwfile) == type(''):
229         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
230     else:
231         getuser = pwfile.get_passwd
232
233     try:
234         enc_passwd = getuser(name)
235     except (KeyError, IOError):
236         return 0
237     if not enc_passwd:
238         return 0
239     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
240         salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
241         return enc_passwd == passcrypt(passwd, salt=salt, method='md5')
242     else:
243         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
244
245 def _to64(v, n):
246     r = ''
247     while (n-1 >= 0):
248         r = r + DES_SALT[v & 0x3F]
249         v = v >> 6
250         n = n - 1
251     return r
252                         
253 def passcrypt_md5(passwd, salt=None, magic='$1$'):
254     """Encrypt passwd with MD5 algorithm."""
255     
256     if not salt:
257         salt = repr(int(time.time()))[-8:]
258     elif salt[:len(magic)] == magic:
259         # remove magic from salt if present
260         salt = salt[len(magic):]
261
262     # salt only goes up to first '$'
263     salt = string.split(salt, '$')[0]
264     # limit length of salt to 8
265     salt = salt[:8]
266
267     ctx = md5.new(passwd)
268     ctx.update(magic)
269     ctx.update(salt)
270     
271     ctx1 = md5.new(passwd)
272     ctx1.update(salt)
273     ctx1.update(passwd)
274     
275     final = ctx1.digest()
276     
277     for i in range(len(passwd), 0 , -16):
278         if i > 16:
279             ctx.update(final)
280         else:
281             ctx.update(final[:i])
282     
283     i = len(passwd)
284     while i:
285         if i & 1:
286             ctx.update('\0')
287         else:
288             ctx.update(passwd[:1])
289         i = i >> 1
290     final = ctx.digest()
291     
292     for i in range(1000):
293         ctx1 = md5.new()
294         if i & 1:
295             ctx1.update(passwd)
296         else:
297             ctx1.update(final)
298         if i % 3: ctx1.update(salt)
299         if i % 7: ctx1.update(passwd)
300         if i & 1:
301             ctx1.update(final)
302         else:
303             ctx1.update(passwd)
304         final = ctx1.digest()
305     
306     rv = magic + salt + '$'
307     final = map(ord, final)
308     l = (final[0] << 16) + (final[6] << 8) + final[12]
309     rv = rv + _to64(l, 4)
310     l = (final[1] << 16) + (final[7] << 8) + final[13]
311     rv = rv + _to64(l, 4)
312     l = (final[2] << 16) + (final[8] << 8) + final[14]
313     rv = rv + _to64(l, 4)
314     l = (final[3] << 16) + (final[9] << 8) + final[15]
315     rv = rv + _to64(l, 4)
316     l = (final[4] << 16) + (final[10] << 8) + final[5]
317     rv = rv + _to64(l, 4)
318     l = final[11]
319     rv = rv + _to64(l, 2)
320     
321     return rv
322
323