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