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