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