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