small bugfix (removed html-code from tools.js) and some other improvements.
[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','getpid.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         return passcrypt_md5(passwd, salt, magic)
227     elif method.lower() == 'clear':
228         return passwd
229
230 def check_passwd(name, passwd, pwfile=None):
231     """Validate given user, passwd pair against password database."""
232     
233     if not pwfile or type(pwfile) == type(''):
234         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
235     else:
236         getuser = pwfile.get_passwd
237
238     try:
239         enc_passwd = getuser(name)
240     except (KeyError, IOError):
241         return 0
242     if not enc_passwd:
243         return 0
244     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
245         salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
246         return enc_passwd == passcrypt(passwd, salt, 'md5')
247        
248     else:
249         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
250
251 def _to64(v, n):
252     r = ''
253     while (n-1 >= 0):
254         r = r + DES_SALT[v & 0x3F]
255         v = v >> 6
256         n = n - 1
257     return r
258                         
259 def passcrypt_md5(passwd, salt=None, magic='$1$'):
260     """Encrypt passwd with MD5 algorithm."""
261     
262     if not salt:
263         pass
264     elif salt[:len(magic)] == magic:
265         # remove magic from salt if present
266         salt = salt[len(magic):]
267
268     # salt only goes up to first '$'
269     salt = string.split(salt, '$')[0]
270     # limit length of salt to 8
271     salt = salt[:8]
272
273     ctx = md5.new(passwd)
274     ctx.update(magic)
275     ctx.update(salt)
276     
277     ctx1 = md5.new(passwd)
278     ctx1.update(salt)
279     ctx1.update(passwd)
280     
281     final = ctx1.digest()
282     
283     for i in range(len(passwd), 0 , -16):
284         if i > 16:
285             ctx.update(final)
286         else:
287             ctx.update(final[:i])
288     
289     i = len(passwd)
290     while i:
291         if i & 1:
292             ctx.update('\0')
293         else:
294             ctx.update(passwd[:1])
295         i = i >> 1
296     final = ctx.digest()
297     
298     for i in range(1000):
299         ctx1 = md5.new()
300         if i & 1:
301             ctx1.update(passwd)
302         else:
303             ctx1.update(final)
304         if i % 3: ctx1.update(salt)
305         if i % 7: ctx1.update(passwd)
306         if i & 1:
307             ctx1.update(final)
308         else:
309             ctx1.update(passwd)
310         final = ctx1.digest()
311     
312     rv = magic + salt + '$'
313     final = map(ord, final)
314     l = (final[0] << 16) + (final[6] << 8) + final[12]
315     rv = rv + _to64(l, 4)
316     l = (final[1] << 16) + (final[7] << 8) + final[13]
317     rv = rv + _to64(l, 4)
318     l = (final[2] << 16) + (final[8] << 8) + final[14]
319     rv = rv + _to64(l, 4)
320     l = (final[3] << 16) + (final[9] << 8) + final[15]
321     rv = rv + _to64(l, 4)
322     l = (final[4] << 16) + (final[10] << 8) + final[5]
323     rv = rv + _to64(l, 4)
324     l = final[11]
325     rv = rv + _to64(l, 2)
326     
327     return rv
328
329