* fix headers for stream.m3u - this fixes IE showing plain text
[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  
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             if not salt:
225                 salt = str(whrandom.choice(DES_SALT)) + str(whrandom.choice(DES_SALT))
226             return crypt.crypt(passwd, salt)
227     elif method.lower() == 'md5':
228             return passcrypt_md5(passwd, salt, magic)
229     elif method.lower() == 'clear':
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     except (KeyError, IOError):
243         return 0
244     if not enc_passwd:
245         return 0
246     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
247         salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
248         return enc_passwd == passcrypt(passwd, salt=salt, method='md5')
249     else:
250         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
251
252 def _to64(v, n):
253     r = ''
254     while (n-1 >= 0):
255         r = r + DES_SALT[v & 0x3F]
256         v = v >> 6
257         n = n - 1
258     return r
259                         
260 def passcrypt_md5(passwd, salt=None, magic='$1$'):
261     """Encrypt passwd with MD5 algorithm."""
262     
263     if not salt:
264         salt = repr(int(time.time()))[-8:]
265     elif salt[:len(magic)] == magic:
266         # remove magic from salt if present
267         salt = salt[len(magic):]
268
269     # salt only goes up to first '$'
270     salt = string.split(salt, '$')[0]
271     # limit length of salt to 8
272     salt = salt[:8]
273
274     ctx = md5.new(passwd)
275     ctx.update(magic)
276     ctx.update(salt)
277     
278     ctx1 = md5.new(passwd)
279     ctx1.update(salt)
280     ctx1.update(passwd)
281     
282     final = ctx1.digest()
283     
284     for i in range(len(passwd), 0 , -16):
285         if i > 16:
286             ctx.update(final)
287         else:
288             ctx.update(final[:i])
289     
290     i = len(passwd)
291     while i:
292         if i & 1:
293             ctx.update('\0')
294         else:
295             ctx.update(passwd[:1])
296         i = i >> 1
297     final = ctx.digest()
298     
299     for i in range(1000):
300         ctx1 = md5.new()
301         if i & 1:
302             ctx1.update(passwd)
303         else:
304             ctx1.update(final)
305         if i % 3: ctx1.update(salt)
306         if i % 7: ctx1.update(passwd)
307         if i & 1:
308             ctx1.update(final)
309         else:
310             ctx1.update(passwd)
311         final = ctx1.digest()
312     
313     rv = magic + salt + '$'
314     final = map(ord, final)
315     l = (final[0] << 16) + (final[6] << 8) + final[12]
316     rv = rv + _to64(l, 4)
317     l = (final[1] << 16) + (final[7] << 8) + final[13]
318     rv = rv + _to64(l, 4)
319     l = (final[2] << 16) + (final[8] << 8) + final[14]
320     rv = rv + _to64(l, 4)
321     l = (final[3] << 16) + (final[9] << 8) + final[15]
322     rv = rv + _to64(l, 4)
323     l = (final[4] << 16) + (final[10] << 8) + final[5]
324     rv = rv + _to64(l, 4)
325     l = final[11]
326     rv = rv + _to64(l, 2)
327     
328     return rv
329
330