disable debug :\
[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 = False
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         if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
171                 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
172         else:
173                 return defer.fail(error.UnauthorizedLogin())
174
175 class IHTTPUser(Interface):
176         pass
177
178 class HTTPUser(object):
179         implements(IHTTPUser)
180
181 class HTTPAuthRealm(object):
182         implements(portal.IRealm)
183         def requestAvatar(self, avatarId, mind, *interfaces):
184                 if IHTTPUser in interfaces:
185                         return IHTTPUser, HTTPUser()
186                 raise NotImplementedError("Only IHTTPUser interface is supported")
187
188         
189 import md5,time,string,crypt
190 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') 
191 def getpwnam(name, pwfile=None):
192     """Return pasword database entry for the given user name.
193     
194     Example from the Python Library Reference.
195     """
196     
197     if not pwfile:
198         pwfile = '/etc/passwd'
199
200     f = open(pwfile)
201     while 1:
202         line = f.readline()
203         if not line:
204             f.close()
205             raise KeyError, name
206         entry = tuple(line.strip().split(':', 6))
207         if entry[0] == name:
208             f.close()
209             return entry
210
211 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
212     """Encrypt a string according to rules in crypt(3)."""
213     if method.lower() == 'des':
214             if not salt:
215                 salt = str(whrandom.choice(DES_SALT)) + str(whrandom.choice(DES_SALT))
216             return crypt.crypt(passwd, salt)
217     elif method.lower() == 'md5':
218             return passcrypt_md5(passwd, salt, magic)
219     elif method.lower() == 'clear':
220         return passwd
221
222 def check_passwd(name, passwd, pwfile=None):
223     """Validate given user, passwd pair against password database."""
224     
225     if not pwfile or type(pwfile) == type(''):
226         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
227     else:
228         getuser = pwfile.get_passwd
229
230     try:
231         enc_passwd = getuser(name)
232     except (KeyError, IOError):
233         return 0
234     if not enc_passwd:
235         return 0
236     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
237         salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
238         return enc_passwd == passcrypt(passwd, salt=salt, method='md5')
239     else:
240         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
241
242 def _to64(v, n):
243     r = ''
244     while (n-1 >= 0):
245         r = r + DES_SALT[v & 0x3F]
246         v = v >> 6
247         n = n - 1
248     return r
249                         
250 def passcrypt_md5(passwd, salt=None, magic='$1$'):
251     """Encrypt passwd with MD5 algorithm."""
252     
253     if not salt:
254         salt = repr(int(time.time()))[-8:]
255     elif salt[:len(magic)] == magic:
256         # remove magic from salt if present
257         salt = salt[len(magic):]
258
259     # salt only goes up to first '$'
260     salt = string.split(salt, '$')[0]
261     # limit length of salt to 8
262     salt = salt[:8]
263
264     ctx = md5.new(passwd)
265     ctx.update(magic)
266     ctx.update(salt)
267     
268     ctx1 = md5.new(passwd)
269     ctx1.update(salt)
270     ctx1.update(passwd)
271     
272     final = ctx1.digest()
273     
274     for i in range(len(passwd), 0 , -16):
275         if i > 16:
276             ctx.update(final)
277         else:
278             ctx.update(final[:i])
279     
280     i = len(passwd)
281     while i:
282         if i & 1:
283             ctx.update('\0')
284         else:
285             ctx.update(passwd[:1])
286         i = i >> 1
287     final = ctx.digest()
288     
289     for i in range(1000):
290         ctx1 = md5.new()
291         if i & 1:
292             ctx1.update(passwd)
293         else:
294             ctx1.update(final)
295         if i % 3: ctx1.update(salt)
296         if i % 7: ctx1.update(passwd)
297         if i & 1:
298             ctx1.update(final)
299         else:
300             ctx1.update(passwd)
301         final = ctx1.digest()
302     
303     rv = magic + salt + '$'
304     final = map(ord, final)
305     l = (final[0] << 16) + (final[6] << 8) + final[12]
306     rv = rv + _to64(l, 4)
307     l = (final[1] << 16) + (final[7] << 8) + final[13]
308     rv = rv + _to64(l, 4)
309     l = (final[2] << 16) + (final[8] << 8) + final[14]
310     rv = rv + _to64(l, 4)
311     l = (final[3] << 16) + (final[9] << 8) + final[15]
312     rv = rv + _to64(l, 4)
313     l = (final[4] << 16) + (final[10] << 8) + final[5]
314     rv = rv + _to64(l, 4)
315     l = final[11]
316     rv = rv + _to64(l, 2)
317     
318     return rv
319
320