Many changes, commented in
[enigma2-plugins.git] / 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',] 
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 DEBUG = False
49 #DEBUG = True
50 DEBUGFILE= "/tmp/twisted.log"
51
52 from twisted.cred.portal import Portal
53 from twisted.cred import checkers
54 from twisted.web2.auth import digest, basic, wrapper
55 from zope.interface import Interface, implements
56 from twisted.cred import portal
57 from twisted.cred import credentials, error
58 from twisted.internet import defer
59 from zope import interface
60
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 DEBUG:
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
124                 def render(self, req):
125                         fp = open(util.sibpath(__file__, "web-data")+"/index.html")
126                         s = fp.read()
127                         fp.close()
128                         return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
129
130         toplevel = Toplevel()
131         if config.plugins.Webinterface.includehdd.value:
132                 toplevel.putChild("hdd",static.File("/hdd"))
133         
134         if config.plugins.Webinterface.useauth.value is False:
135                 site = server.Site(toplevel)
136         else:
137                 portal = Portal(HTTPAuthRealm())
138                 portal.registerChecker(PasswordDatabase())
139                 root = ModifiedHTTPAuthResource(toplevel,(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
140                 site = server.Site(root)
141         print "[WebIf] starting Webinterface on port",config.plugins.Webinterface.port.value
142         reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site))
143
144
145 def autostart(reason, **kwargs):
146         if "session" in kwargs:
147                 global sessions
148                 sessions.append(kwargs["session"])
149                 return
150         if reason == 0:
151                 try:
152                         startWebserver()
153                 except ImportError,e:
154                         print "[WebIf] twisted not available, not starting web services",e
155                         
156 def openconfig(session, **kwargs):
157         session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
158
159 def configCB(result):
160         if result is True:
161                 print "[WebIf] config changed"
162                 restartWebserver()
163         else:
164                 print "[WebIf] config not changed"
165                 
166
167 def Plugins(**kwargs):
168         return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
169                     PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
170         
171         
172 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
173         """
174                 set it only to True, if you have a patched wrapper.py
175                 see http://twistedmatrix.com/trac/ticket/2041
176                 so, the solution for us is to make a new class an override ne faulty func
177         """
178
179         def locateChild(self, req, seg):
180                 return self.authenticate(req), seg
181         
182 class PasswordDatabase:
183     """
184         this checks webiflogins agains /etc/passwd
185     """
186     passwordfile = "/etc/passwd"
187     interface.implements(checkers.ICredentialsChecker)
188     credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
189
190     def _cbPasswordMatch(self, matched, username):
191         if matched:
192             return username
193         else:
194             return failure.Failure(error.UnauthorizedLogin())
195
196     def requestAvatarId(self, credentials):     
197         if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
198                 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
199         else:
200                 return defer.fail(error.UnauthorizedLogin())
201
202 class IHTTPUser(Interface):
203         pass
204
205 class HTTPUser(object):
206         implements(IHTTPUser)
207
208 class HTTPAuthRealm(object):
209         implements(portal.IRealm)
210         def requestAvatar(self, avatarId, mind, *interfaces):
211                 if IHTTPUser in interfaces:
212                         return IHTTPUser, HTTPUser()
213                 raise NotImplementedError("Only IHTTPUser interface is supported")
214
215         
216 import md5,time,string,crypt
217 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') 
218 def getpwnam(name, pwfile=None):
219     """Return pasword database entry for the given user name.
220     
221     Example from the Python Library Reference.
222     """
223     
224     if not pwfile:
225         pwfile = '/etc/passwd'
226
227     f = open(pwfile)
228     while 1:
229         line = f.readline()
230         if not line:
231             f.close()
232             raise KeyError, name
233         entry = tuple(line.strip().split(':', 6))
234         if entry[0] == name:
235             f.close()
236             return entry
237
238 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
239     """Encrypt a string according to rules in crypt(3)."""
240     if method.lower() == 'des':
241             return crypt.crypt(passwd, salt)
242     elif method.lower() == 'md5':
243         return passcrypt_md5(passwd, salt, magic)
244     elif method.lower() == 'clear':
245         return passwd
246
247 def check_passwd(name, passwd, pwfile=None):
248     """Validate given user, passwd pair against password database."""
249     
250     if not pwfile or type(pwfile) == type(''):
251         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
252     else:
253         getuser = pwfile.get_passwd
254
255     try:
256         enc_passwd = getuser(name)
257     except (KeyError, IOError):
258         return 0
259     if not enc_passwd:
260         return 0
261     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
262         salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
263         return enc_passwd == passcrypt(passwd, salt, 'md5')
264        
265     else:
266         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
267
268 def _to64(v, n):
269     r = ''
270     while (n-1 >= 0):
271         r = r + DES_SALT[v & 0x3F]
272         v = v >> 6
273         n = n - 1
274     return r
275                         
276 def passcrypt_md5(passwd, salt=None, magic='$1$'):
277     """Encrypt passwd with MD5 algorithm."""
278     
279     if not salt:
280         pass
281     elif salt[:len(magic)] == magic:
282         # remove magic from salt if present
283         salt = salt[len(magic):]
284
285     # salt only goes up to first '$'
286     salt = string.split(salt, '$')[0]
287     # limit length of salt to 8
288     salt = salt[:8]
289
290     ctx = md5.new(passwd)
291     ctx.update(magic)
292     ctx.update(salt)
293     
294     ctx1 = md5.new(passwd)
295     ctx1.update(salt)
296     ctx1.update(passwd)
297     
298     final = ctx1.digest()
299     
300     for i in range(len(passwd), 0 , -16):
301         if i > 16:
302             ctx.update(final)
303         else:
304             ctx.update(final[:i])
305     
306     i = len(passwd)
307     while i:
308         if i & 1:
309             ctx.update('\0')
310         else:
311             ctx.update(passwd[:1])
312         i = i >> 1
313     final = ctx.digest()
314     
315     for i in range(1000):
316         ctx1 = md5.new()
317         if i & 1:
318             ctx1.update(passwd)
319         else:
320             ctx1.update(final)
321         if i % 3: ctx1.update(salt)
322         if i % 7: ctx1.update(passwd)
323         if i & 1:
324             ctx1.update(final)
325         else:
326             ctx1.update(passwd)
327         final = ctx1.digest()
328     
329     rv = magic + salt + '$'
330     final = map(ord, final)
331     l = (final[0] << 16) + (final[6] << 8) + final[12]
332     rv = rv + _to64(l, 4)
333     l = (final[1] << 16) + (final[7] << 8) + final[13]
334     rv = rv + _to64(l, 4)
335     l = (final[2] << 16) + (final[8] << 8) + final[14]
336     rv = rv + _to64(l, 4)
337     l = (final[3] << 16) + (final[9] << 8) + final[15]
338     rv = rv + _to64(l, 4)
339     l = (final[4] << 16) + (final[10] << 8) + final[5]
340     rv = rv + _to64(l, 4)
341     l = final[11]
342     rv = rv + _to64(l, 2)
343     
344     return rv
345
346