small changes
[vuplus_dvbapp-plugin] / webinterface / src / plugin.py
1 from Plugins.Plugin import PluginDescriptor
2
3 from twisted.internet import reactor
4 from twisted.web2 import server, channel, static, resource, stream, http_headers, responsecode, http
5 from twisted.python import util
6 from twisted.python.log import startLogging,discardLogs
7
8 import webif
9 import WebIfConfig  
10 import os
11
12 from Components.config import config, ConfigSubsection, ConfigInteger,ConfigYesNo
13
14 config.plugins.Webinterface = ConfigSubsection()
15 config.plugins.Webinterface.enable = ConfigYesNo(default = True)
16 config.plugins.Webinterface.port = ConfigInteger(80,limits = (1, 999))
17 config.plugins.Webinterface.includehdd = ConfigYesNo(default = False)
18 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
19
20 sessions = [ ]
21
22 """
23         define all files in /web to send no  XML-HTTP-Headers here
24         all files not listed here will get an Content-Type: application/xhtml+xml charset: UTF-8
25 """
26 AppTextHeaderFiles = ['stream.m3u.xml','ts.m3u.xml','wapwrapper.xml',] 
27
28 """
29  Actualle, the TextHtmlHeaderFiles should contain the updates.html.xml, but the IE then
30  has problems with unicode-characters
31 """
32 TextHtmlHeaderFiles = [] 
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: text/html charset: UTF-8
37 """
38 NoExplicitHeaderFiles = ['getpid.xml','tvbrowser.xml',] 
39
40 """
41  set DEBUG to True, if twisted should write logoutput to a file.
42  in normal console output, twisted will print only the first Traceback.
43  is this a bug in twisted or a conflict with enigma2?
44  with this option enabled, twisted will print all TB to the logfile
45  use tail -f <file> to view this log
46 """
47                         
48
49 DEBUG = True
50 #DEBUG = False
51 DEBUGFILE= "/tmp/twisted.log"
52
53 from twisted.cred.portal import Portal
54 from twisted.cred import checkers
55 from twisted.web2.auth import digest, basic, wrapper
56 from zope.interface import Interface, implements
57 from twisted.cred import portal
58 from twisted.cred import credentials, error
59 from twisted.internet import defer
60 from zope import interface
61
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 DEBUG:
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
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         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
147 def autostart(reason, **kwargs):
148         if "session" in kwargs:
149                 global sessions
150                 sessions.append(kwargs["session"])
151                 return
152         if reason == 0:
153                 try:
154                         startWebserver()
155                 except ImportError,e:
156                         print "[WebIf] twisted not available, not starting web services",e
157                         
158 def openconfig(session, **kwargs):
159         session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
160
161 def configCB(result):
162         if result is True:
163                 print "[WebIf] config changed"
164                 restartWebserver()
165         else:
166                 print "[WebIf] config not changed"
167                 
168
169 def Plugins(**kwargs):
170         return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
171                     PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
172         
173         
174 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
175         """
176                 set it only to True, if you have a patched wrapper.py
177                 see http://twistedmatrix.com/trac/ticket/2041
178                 so, the solution for us is to make a new class an override ne faulty func
179         """
180
181         def locateChild(self, req, seg):
182                 return self.authenticate(req), seg
183         
184 class PasswordDatabase:
185     """
186         this checks webiflogins agains /etc/passwd
187     """
188     passwordfile = "/etc/passwd"
189     interface.implements(checkers.ICredentialsChecker)
190     credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
191
192     def _cbPasswordMatch(self, matched, username):
193         if matched:
194             return username
195         else:
196             return failure.Failure(error.UnauthorizedLogin())
197
198     def requestAvatarId(self, credentials):     
199         if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
200                 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
201         else:
202                 return defer.fail(error.UnauthorizedLogin())
203
204 class IHTTPUser(Interface):
205         pass
206
207 class HTTPUser(object):
208         implements(IHTTPUser)
209
210 class HTTPAuthRealm(object):
211         implements(portal.IRealm)
212         def requestAvatar(self, avatarId, mind, *interfaces):
213                 if IHTTPUser in interfaces:
214                         return IHTTPUser, HTTPUser()
215                 raise NotImplementedError("Only IHTTPUser interface is supported")
216
217         
218 import md5,time,string,crypt
219 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') 
220 def getpwnam(name, pwfile=None):
221     """Return pasword database entry for the given user name.
222     
223     Example from the Python Library Reference.
224     """
225     
226     if not pwfile:
227         pwfile = '/etc/passwd'
228
229     f = open(pwfile)
230     while 1:
231         line = f.readline()
232         if not line:
233             f.close()
234             raise KeyError, name
235         entry = tuple(line.strip().split(':', 6))
236         if entry[0] == name:
237             f.close()
238             return entry
239
240 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
241     """Encrypt a string according to rules in crypt(3)."""
242     if method.lower() == 'des':
243             return crypt.crypt(passwd, salt)
244     elif method.lower() == 'md5':
245         return passcrypt_md5(passwd, salt, magic)
246     elif method.lower() == 'clear':
247         return passwd
248
249 def check_passwd(name, passwd, pwfile=None):
250     """Validate given user, passwd pair against password database."""
251     
252     if not pwfile or type(pwfile) == type(''):
253         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
254     else:
255         getuser = pwfile.get_passwd
256
257     try:
258         enc_passwd = getuser(name)
259     except (KeyError, IOError):
260         return 0
261     if not enc_passwd:
262         return 0
263     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
264         salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
265         return enc_passwd == passcrypt(passwd, salt, 'md5')
266        
267     else:
268         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
269
270 def _to64(v, n):
271     r = ''
272     while (n-1 >= 0):
273         r = r + DES_SALT[v & 0x3F]
274         v = v >> 6
275         n = n - 1
276     return r
277                         
278 def passcrypt_md5(passwd, salt=None, magic='$1$'):
279     """Encrypt passwd with MD5 algorithm."""
280     
281     if not salt:
282         pass
283     elif salt[:len(magic)] == magic:
284         # remove magic from salt if present
285         salt = salt[len(magic):]
286
287     # salt only goes up to first '$'
288     salt = string.split(salt, '$')[0]
289     # limit length of salt to 8
290     salt = salt[:8]
291
292     ctx = md5.new(passwd)
293     ctx.update(magic)
294     ctx.update(salt)
295     
296     ctx1 = md5.new(passwd)
297     ctx1.update(salt)
298     ctx1.update(passwd)
299     
300     final = ctx1.digest()
301     
302     for i in range(len(passwd), 0 , -16):
303         if i > 16:
304             ctx.update(final)
305         else:
306             ctx.update(final[:i])
307     
308     i = len(passwd)
309     while i:
310         if i & 1:
311             ctx.update('\0')
312         else:
313             ctx.update(passwd[:1])
314         i = i >> 1
315     final = ctx.digest()
316     
317     for i in range(1000):
318         ctx1 = md5.new()
319         if i & 1:
320             ctx1.update(passwd)
321         else:
322             ctx1.update(final)
323         if i % 3: ctx1.update(salt)
324         if i % 7: ctx1.update(passwd)
325         if i & 1:
326             ctx1.update(final)
327         else:
328             ctx1.update(passwd)
329         final = ctx1.digest()
330     
331     rv = magic + salt + '$'
332     final = map(ord, final)
333     l = (final[0] << 16) + (final[6] << 8) + final[12]
334     rv = rv + _to64(l, 4)
335     l = (final[1] << 16) + (final[7] << 8) + final[13]
336     rv = rv + _to64(l, 4)
337     l = (final[2] << 16) + (final[8] << 8) + final[14]
338     rv = rv + _to64(l, 4)
339     l = (final[3] << 16) + (final[9] << 8) + final[15]
340     rv = rv + _to64(l, 4)
341     l = (final[4] << 16) + (final[10] << 8) + final[5]
342     rv = rv + _to64(l, 4)
343     l = final[11]
344     rv = rv + _to64(l, 2)
345     
346     return rv
347
348