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