small changes in "RemotePower" and the new class getPid, untill port 8001 is open...
[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',] 
27
28 """
29         define all files in /web to send no  XML-HTTP-Headers here
30         all files not listed here will get an Content-Type: text/html charset: UTF-8
31 """
32 NoExplicitHeaderFiles = ['getpid.xml','tvbrowser.xml',] 
33  
34 """
35  set DEBUG to True, if twisted should write logoutput to a file.
36  in normal console output, twisted will print only the first Traceback.
37  is this a bug in twisted or a conflict with enigma2?
38  with this option enabled, twisted will print all TB to the logfile
39  use tail -f <file> to view this log
40 """
41                         
42 DEBUG = False
43 #DEBUG = True
44 DEBUGFILE= "/tmp/twisted.log"
45
46 from twisted.cred.portal import Portal
47 from twisted.cred import checkers
48 from twisted.web2.auth import digest, basic, wrapper
49 from zope.interface import Interface, implements
50 from twisted.cred import portal
51 from twisted.cred import credentials, error
52 from twisted.internet import defer
53 from zope import interface
54
55
56 def stopWebserver():
57         reactor.disconnectAll()
58
59 def restartWebserver():
60         stopWebserver()
61         startWebserver()
62
63 def startWebserver():
64         if config.plugins.Webinterface.enable.value is not True:
65                 print "not starting Werbinterface"
66                 return False
67         if DEBUG:
68                 print "start twisted logfile, writing to %s" % DEBUGFILE 
69                 import sys
70                 startLogging(open(DEBUGFILE,'w'))
71
72         class ScreenPage(resource.Resource):
73                 def __init__(self, path):
74                         self.path = path
75
76                 def render(self, req):
77                         global sessions
78                         if sessions == [ ]:
79                                 return http.Response(responsecode.OK, stream="please wait until enigma has booted")
80
81                         class myProducerStream(stream.ProducerStream):
82                                 def __init__(self):
83                                         stream.ProducerStream.__init__(self)
84                                         self.closed_callback = None
85
86                                 def close(self):
87                                         if self.closed_callback:
88                                                 self.closed_callback()
89                                                 self.closed_callback = None
90                                         stream.ProducerStream.close(self)
91
92                         if os.path.isfile(self.path):
93                                 s=myProducerStream()
94                                 webif.renderPage(s, self.path, req, sessions[0])  # login?
95                                 if self.path.split("/")[-1] in AppTextHeaderFiles:
96                                         return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'text', (('charset', 'UTF-8'),))},stream=s)
97                                 elif self.path.split("/")[-1] in NoExplicitHeaderFiles:
98                                         return http.Response(responsecode.OK,stream=s)
99                                 else:
100                                         return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'xhtml+xml', (('charset', 'UTF-8'),))},stream=s)
101                         else:
102                                 return http.Response(responsecode.NOT_FOUND)
103
104                 def locateChild(self, request, segments):
105                         path = self.path+'/'+'/'.join(segments)
106                         if path[-1:] == "/":
107                                 path += "index.html"
108                         path +=".xml"
109                         return ScreenPage(path), ()
110
111         class Toplevel(resource.Resource):
112                 addSlash = True
113                 child_web = ScreenPage(util.sibpath(__file__, "web")) # "/web/*"
114                 child_webdata = static.File(util.sibpath(__file__, "web-data")) # FIXME: web-data appears as webdata
115
116                 def render(self, req):
117                         fp = open(util.sibpath(__file__, "web-data")+"/index.html")
118                         s = fp.read()
119                         fp.close()
120                         return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
121
122         toplevel = Toplevel()
123         if config.plugins.Webinterface.includehdd.value:
124                 toplevel.putChild("hdd",static.File("/hdd"))
125         
126         if config.plugins.Webinterface.useauth.value is False:
127                 site = server.Site(toplevel)
128         else:
129                 portal = Portal(HTTPAuthRealm())
130                 portal.registerChecker(PasswordDatabase())
131                 root = ModifiedHTTPAuthResource(toplevel,(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
132                 site = server.Site(root)
133         print "[WebIf] starting Webinterface on port",config.plugins.Webinterface.port.value
134         reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site))
135
136
137 def autostart(reason, **kwargs):
138         if "session" in kwargs:
139                 global sessions
140                 sessions.append(kwargs["session"])
141                 return
142         if reason == 0:
143                 try:
144                         startWebserver()
145                 except ImportError,e:
146                         print "[WebIf] twisted not available, not starting web services",e
147                         
148 def openconfig(session, **kwargs):
149         session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
150
151 def configCB(result):
152         if result is True:
153                 print "[WebIf] config changed"
154                 restartWebserver()
155         else:
156                 print "[WebIf] config not changed"
157                 
158
159 def Plugins(**kwargs):
160         return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
161                     PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
162         
163         
164 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
165         """
166                 set it only to True, if you have a patched wrapper.py
167                 see http://twistedmatrix.com/trac/ticket/2041
168                 so, the solution for us is to make a new class an override ne faulty func
169         """
170
171         def locateChild(self, req, seg):
172                 return self.authenticate(req), seg
173         
174 class PasswordDatabase:
175     """
176         this checks webiflogins agains /etc/passwd
177     """
178     passwordfile = "/etc/passwd"
179     interface.implements(checkers.ICredentialsChecker)
180     credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
181
182     def _cbPasswordMatch(self, matched, username):
183         if matched:
184             return username
185         else:
186             return failure.Failure(error.UnauthorizedLogin())
187
188     def requestAvatarId(self, credentials):     
189         if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
190                 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
191         else:
192                 return defer.fail(error.UnauthorizedLogin())
193
194 class IHTTPUser(Interface):
195         pass
196
197 class HTTPUser(object):
198         implements(IHTTPUser)
199
200 class HTTPAuthRealm(object):
201         implements(portal.IRealm)
202         def requestAvatar(self, avatarId, mind, *interfaces):
203                 if IHTTPUser in interfaces:
204                         return IHTTPUser, HTTPUser()
205                 raise NotImplementedError("Only IHTTPUser interface is supported")
206
207         
208 import md5,time,string,crypt
209 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') 
210 def getpwnam(name, pwfile=None):
211     """Return pasword database entry for the given user name.
212     
213     Example from the Python Library Reference.
214     """
215     
216     if not pwfile:
217         pwfile = '/etc/passwd'
218
219     f = open(pwfile)
220     while 1:
221         line = f.readline()
222         if not line:
223             f.close()
224             raise KeyError, name
225         entry = tuple(line.strip().split(':', 6))
226         if entry[0] == name:
227             f.close()
228             return entry
229
230 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
231     """Encrypt a string according to rules in crypt(3)."""
232     if method.lower() == 'des':
233             return crypt.crypt(passwd, salt)
234     elif method.lower() == 'md5':
235         return passcrypt_md5(passwd, salt, magic)
236     elif method.lower() == 'clear':
237         return passwd
238
239 def check_passwd(name, passwd, pwfile=None):
240     """Validate given user, passwd pair against password database."""
241     
242     if not pwfile or type(pwfile) == type(''):
243         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
244     else:
245         getuser = pwfile.get_passwd
246
247     try:
248         enc_passwd = getuser(name)
249     except (KeyError, IOError):
250         return 0
251     if not enc_passwd:
252         return 0
253     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
254         salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
255         return enc_passwd == passcrypt(passwd, salt, 'md5')
256        
257     else:
258         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
259
260 def _to64(v, n):
261     r = ''
262     while (n-1 >= 0):
263         r = r + DES_SALT[v & 0x3F]
264         v = v >> 6
265         n = n - 1
266     return r
267                         
268 def passcrypt_md5(passwd, salt=None, magic='$1$'):
269     """Encrypt passwd with MD5 algorithm."""
270     
271     if not salt:
272         pass
273     elif salt[:len(magic)] == magic:
274         # remove magic from salt if present
275         salt = salt[len(magic):]
276
277     # salt only goes up to first '$'
278     salt = string.split(salt, '$')[0]
279     # limit length of salt to 8
280     salt = salt[:8]
281
282     ctx = md5.new(passwd)
283     ctx.update(magic)
284     ctx.update(salt)
285     
286     ctx1 = md5.new(passwd)
287     ctx1.update(salt)
288     ctx1.update(passwd)
289     
290     final = ctx1.digest()
291     
292     for i in range(len(passwd), 0 , -16):
293         if i > 16:
294             ctx.update(final)
295         else:
296             ctx.update(final[:i])
297     
298     i = len(passwd)
299     while i:
300         if i & 1:
301             ctx.update('\0')
302         else:
303             ctx.update(passwd[:1])
304         i = i >> 1
305     final = ctx.digest()
306     
307     for i in range(1000):
308         ctx1 = md5.new()
309         if i & 1:
310             ctx1.update(passwd)
311         else:
312             ctx1.update(final)
313         if i % 3: ctx1.update(salt)
314         if i % 7: ctx1.update(passwd)
315         if i & 1:
316             ctx1.update(final)
317         else:
318             ctx1.update(passwd)
319         final = ctx1.digest()
320     
321     rv = magic + salt + '$'
322     final = map(ord, final)
323     l = (final[0] << 16) + (final[6] << 8) + final[12]
324     rv = rv + _to64(l, 4)
325     l = (final[1] << 16) + (final[7] << 8) + final[13]
326     rv = rv + _to64(l, 4)
327     l = (final[2] << 16) + (final[8] << 8) + final[14]
328     rv = rv + _to64(l, 4)
329     l = (final[3] << 16) + (final[9] << 8) + final[15]
330     rv = rv + _to64(l, 4)
331     l = (final[4] << 16) + (final[10] << 8) + final[5]
332     rv = rv + _to64(l, 4)
333     l = final[11]
334     rv = rv + _to64(l, 2)
335     
336     return rv
337
338