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