switch to enable debug moved to e2-config-system. this is not configurable in GUI...
[vuplus_dvbapp-plugin] / webinterface / src / plugin.py
1 from Plugins.Plugin import PluginDescriptor
2
3 from twisted.internet import reactor, defer
4
5 from twisted.web2 import server, channel, static, resource, stream, http_headers, responsecode, http
6 from twisted.web2.auth import digest, basic, wrapper
7
8 from twisted.python import util
9 from twisted.python.log import startLogging,discardLogs
10
11 from twisted.cred.portal import Portal, IRealm
12 from twisted.cred import checkers, credentials, error
13
14 from zope.interface import Interface, implements
15
16 import webif
17 import WebIfConfig  
18 import os
19
20 from Components.config import config, ConfigSubsection, ConfigInteger,ConfigYesNo
21
22 config.plugins.Webinterface = ConfigSubsection()
23 config.plugins.Webinterface.enable = ConfigYesNo(default = True)
24 config.plugins.Webinterface.port = ConfigInteger(80,limits = (1, 65536))
25 config.plugins.Webinterface.includehdd = ConfigYesNo(default = False)
26 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
27 config.plugins.Webinterface.debug = ConfigYesNo(default = False) # False by default, not confgurable in GUI. Edit settingsfile directly if needed
28
29 sessions = [ ]
30
31 """
32         define all files in /web to send no  XML-HTTP-Headers here
33         all files not listed here will get an Content-Type: application/xhtml+xml charset: UTF-8
34 """
35 AppTextHeaderFiles = ['stream.m3u.xml','ts.m3u.xml',] 
36
37 """
38  Actualy, the TextHtmlHeaderFiles should contain the updates.html.xml, but the IE then
39  has problems with unicode-characters
40 """
41 TextHtmlHeaderFiles = ['wapremote.xml',] 
42
43 """
44         define all files in /web to send no  XML-HTTP-Headers here
45         all files not listed here will get an Content-Type: text/html charset: UTF-8
46 """
47 NoExplicitHeaderFiles = ['getpid.xml','tvbrowser.xml',] 
48
49 """
50  set DEBUG to True, if twisted should write logoutput to a file.
51  in normal console output, twisted will print only the first Traceback.
52  is this a bug in twisted or a conflict with enigma2?
53  with this option enabled, twisted will print all TB to the logfile
54  use tail -f <file> to view this log
55 """
56                         
57
58 DEBUGFILE= "/tmp/twisted.log"
59
60 def stopWebserver():
61         reactor.disconnectAll()
62
63 def restartWebserver():
64         stopWebserver()
65         startWebserver()
66
67 def startWebserver():
68         if config.plugins.Webinterface.enable.value is not True:
69                 print "not starting Werbinterface"
70                 return False
71         if config.plugins.Webinterface.debug.value:
72                 print "start twisted logfile, writing to %s" % DEBUGFILE 
73                 import sys
74                 startLogging(open(DEBUGFILE,'w'))
75
76         class ScreenPage(resource.Resource):
77                 def __init__(self, path):
78                         self.path = path
79
80                 def render(self, req):
81                         global sessions
82                         if sessions == [ ]:
83                                 return http.Response(responsecode.OK, stream="please wait until enigma has booted")
84
85                         class myProducerStream(stream.ProducerStream):
86                                 def __init__(self):
87                                         stream.ProducerStream.__init__(self)
88                                         self.closed_callback = None
89
90                                 def close(self):
91                                         if self.closed_callback:
92                                                 self.closed_callback()
93                                                 self.closed_callback = None
94                                         stream.ProducerStream.close(self)
95
96                         if os.path.isfile(self.path):
97                                 s=myProducerStream()
98                                 webif.renderPage(s, self.path, req, sessions[0])  # login?
99                                 if self.path.split("/")[-1] in AppTextHeaderFiles:
100                                         return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'text', (('charset', 'UTF-8'),))},stream=s)
101                                 elif self.path.split("/")[-1] in TextHtmlHeaderFiles:
102                                         return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('text', 'html', (('charset', 'UTF-8'),))},stream=s)
103                                 elif self.path.split("/")[-1] in NoExplicitHeaderFiles:
104                                         return http.Response(responsecode.OK,stream=s)
105                                 else:
106                                         return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'xhtml+xml', (('charset', 'UTF-8'),))},stream=s)
107                         else:
108                                 return http.Response(responsecode.NOT_FOUND)
109
110                 def locateChild(self, request, segments):
111                         path = self.path+'/'+'/'.join(segments)
112                         if path[-1:] == "/":
113                                 path += "index.html"
114                         path +=".xml"
115                         return ScreenPage(path), ()
116
117         class Toplevel(resource.Resource):
118                 addSlash = True
119                 child_web = ScreenPage(util.sibpath(__file__, "web")) # "/web/*"
120                 child_webdata = static.File(util.sibpath(__file__, "web-data")) # FIXME: web-data appears as webdata
121                 child_wap = static.File(util.sibpath(__file__, "wap")) # static pages for wap
122                 child_movie = MovieStreamer()
123                 def render(self, req):
124                         fp = open(util.sibpath(__file__, "web-data")+"/index.html")
125                         s = fp.read()
126                         fp.close()
127                         return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
128
129
130         toplevel = Toplevel()
131         if config.plugins.Webinterface.includehdd.value:
132                 toplevel.putChild("hdd",static.File("/hdd"))
133         
134         if config.plugins.Webinterface.useauth.value is False:
135                 site = server.Site(toplevel)
136         else:
137                 portal = Portal(HTTPAuthRealm())
138                 portal.registerChecker(PasswordDatabase())
139                 root = ModifiedHTTPAuthResource(toplevel,(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
140                 site = server.Site(root)
141         print "[WebIf] starting Webinterface on port",config.plugins.Webinterface.port.value
142         reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site))
143
144 class MovieStreamer(resource.Resource):
145         addSlash = True
146         
147         def render(self, req):
148                 class myFileStream(stream.FileStream):
149                     """
150                         because os.fstat(f.fileno()).st_size returns negative values on 
151                         large file, we set read() to a fix value
152                     """
153                     readsize = 10000    
154                     
155                     def read(self, sendfile=False):
156                         if self.f is None:
157                             return None
158                 
159                         length = self.length
160                         if length == 0:
161                             self.f = None
162                             return None
163                 
164                         if sendfile and length > SENDFILE_THRESHOLD:
165                             # XXX: Yay using non-existent sendfile support!
166                             # FIXME: if we return a SendfileBuffer, and then sendfile
167                             #        fails, then what? Or, what if file is too short?
168                             readSize = min(length, SENDFILE_LIMIT)
169                             res = SendfileBuffer(self.f, self.start, readSize)
170                             self.length -= readSize
171                             self.start += readSize
172                             return res
173                 
174                         if self.useMMap and length > MMAP_THRESHOLD:
175                             readSize = min(length, MMAP_LIMIT)
176                             try:
177                                 res = mmapwrapper(self.f.fileno(), readSize,
178                                                   access=mmap.ACCESS_READ, offset=self.start)
179                                 #madvise(res, MADV_SEQUENTIAL)
180                                 self.length -= readSize
181                                 self.start += readSize
182                                 return res
183                             except mmap.error:
184                                 pass
185                         # Fall back to standard read.
186                         readSize = self.readsize #this is the only changed line :} 3c5x9 #min(length, self.CHUNK_SIZE)
187                         
188                         self.f.seek(self.start)
189                         b = self.f.read(readSize)
190                         bytesRead = len(b)
191                         if not bytesRead:
192                             raise RuntimeError("Ran out of data reading file %r, expected %d more bytes" % (self.f, length))
193                         else:
194                             self.length -= bytesRead
195                             self.start += bytesRead
196                             return b
197                 try:
198                         w1 = req.uri.split("?")[1]
199                         w2 = w1.split("&")
200                         parts= {}
201                         for i in w2:
202                                 w3 = i.split("=")
203                                 parts[w3[0]] = w3[1]
204                 except:
205                         return http.Response(responsecode.OK, stream="no file given with file=???")                     
206                 if parts.has_key("file"):
207                         path = "/hdd/movie/"+parts["file"].replace("%20"," ").replace("+"," ")
208                         if os.path.exists(path):
209                                 self.filehandler = open(path,"r")
210                                 s = myFileStream(self.filehandler)
211                                 return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
212                         else:
213                                 return http.Response(responsecode.OK, stream="file was not found in /media/hdd/movie/")                 
214                 else:
215                         return http.Response(responsecode.OK, stream="no file given with file=???")                     
216
217 def autostart(reason, **kwargs):
218         if "session" in kwargs:
219                 global sessions
220                 sessions.append(kwargs["session"])
221                 return
222         if reason == 0:
223                 try:
224                         startWebserver()
225                 except ImportError,e:
226                         print "[WebIf] twisted not available, not starting web services",e
227                         
228 def openconfig(session, **kwargs):
229         session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
230
231 def configCB(result):
232         if result is True:
233                 print "[WebIf] config changed"
234                 restartWebserver()
235         else:
236                 print "[WebIf] config not changed"
237                 
238
239 def Plugins(**kwargs):
240         return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
241                     PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
242         
243         
244 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
245         """
246                 set it only to True, if you have a patched wrapper.py
247                 see http://twistedmatrix.com/trac/ticket/2041
248                 so, the solution for us is to make a new class an override ne faulty func
249         """
250
251         def locateChild(self, req, seg):
252                 return self.authenticate(req), seg
253         
254 class PasswordDatabase:
255     """
256         this checks webiflogins agains /etc/passwd
257     """
258     passwordfile = "/etc/passwd"
259     implements(checkers.ICredentialsChecker)
260     credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
261
262     def _cbPasswordMatch(self, matched, username):
263         if matched:
264             return username
265         else:
266             return failure.Failure(error.UnauthorizedLogin())
267
268     def requestAvatarId(self, credentials):     
269         if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
270                 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
271         else:
272                 return defer.fail(error.UnauthorizedLogin())
273
274 class IHTTPUser(Interface):
275         pass
276
277 class HTTPUser(object):
278         implements(IHTTPUser)
279
280 class HTTPAuthRealm(object):
281         implements(IRealm)
282         def requestAvatar(self, avatarId, mind, *interfaces):
283                 if IHTTPUser in interfaces:
284                         return IHTTPUser, HTTPUser()
285                 raise NotImplementedError("Only IHTTPUser interface is supported")
286
287         
288 import md5,time,string,crypt
289 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') 
290 def getpwnam(name, pwfile=None):
291     """Return pasword database entry for the given user name.
292     
293     Example from the Python Library Reference.
294     """
295     
296     if not pwfile:
297         pwfile = '/etc/passwd'
298
299     f = open(pwfile)
300     while 1:
301         line = f.readline()
302         if not line:
303             f.close()
304             raise KeyError, name
305         entry = tuple(line.strip().split(':', 6))
306         if entry[0] == name:
307             f.close()
308             return entry
309
310 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
311     """Encrypt a string according to rules in crypt(3)."""
312     if method.lower() == 'des':
313             return crypt.crypt(passwd, salt)
314     elif method.lower() == 'md5':
315         return passcrypt_md5(passwd, salt, magic)
316     elif method.lower() == 'clear':
317         return passwd
318
319 def check_passwd(name, passwd, pwfile=None):
320     """Validate given user, passwd pair against password database."""
321     
322     if not pwfile or type(pwfile) == type(''):
323         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
324     else:
325         getuser = pwfile.get_passwd
326
327     try:
328         enc_passwd = getuser(name)
329     except (KeyError, IOError):
330         return 0
331     if not enc_passwd:
332         return 0
333     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
334         salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
335         return enc_passwd == passcrypt(passwd, salt, 'md5')
336        
337     else:
338         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
339
340 def _to64(v, n):
341     r = ''
342     while (n-1 >= 0):
343         r = r + DES_SALT[v & 0x3F]
344         v = v >> 6
345         n = n - 1
346     return r
347                         
348 def passcrypt_md5(passwd, salt=None, magic='$1$'):
349     """Encrypt passwd with MD5 algorithm."""
350     
351     if not salt:
352         pass
353     elif salt[:len(magic)] == magic:
354         # remove magic from salt if present
355         salt = salt[len(magic):]
356
357     # salt only goes up to first '$'
358     salt = string.split(salt, '$')[0]
359     # limit length of salt to 8
360     salt = salt[:8]
361
362     ctx = md5.new(passwd)
363     ctx.update(magic)
364     ctx.update(salt)
365     
366     ctx1 = md5.new(passwd)
367     ctx1.update(salt)
368     ctx1.update(passwd)
369     
370     final = ctx1.digest()
371     
372     for i in range(len(passwd), 0 , -16):
373         if i > 16:
374             ctx.update(final)
375         else:
376             ctx.update(final[:i])
377     
378     i = len(passwd)
379     while i:
380         if i & 1:
381             ctx.update('\0')
382         else:
383             ctx.update(passwd[:1])
384         i = i >> 1
385     final = ctx.digest()
386     
387     for i in range(1000):
388         ctx1 = md5.new()
389         if i & 1:
390             ctx1.update(passwd)
391         else:
392             ctx1.update(final)
393         if i % 3: ctx1.update(salt)
394         if i % 7: ctx1.update(passwd)
395         if i & 1:
396             ctx1.update(final)
397         else:
398             ctx1.update(passwd)
399         final = ctx1.digest()
400     
401     rv = magic + salt + '$'
402     final = map(ord, final)
403     l = (final[0] << 16) + (final[6] << 8) + final[12]
404     rv = rv + _to64(l, 4)
405     l = (final[1] << 16) + (final[7] << 8) + final[13]
406     rv = rv + _to64(l, 4)
407     l = (final[2] << 16) + (final[8] << 8) + final[14]
408     rv = rv + _to64(l, 4)
409     l = (final[3] << 16) + (final[9] << 8) + final[15]
410     rv = rv + _to64(l, 4)
411     l = (final[4] << 16) + (final[10] << 8) + final[5]
412     rv = rv + _to64(l, 4)
413     l = final[11]
414     rv = rv + _to64(l, 2)
415     
416     return rv
417
418