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