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