fix BS with not existing session
[enigma2-plugins.git] / webinterface / src / plugin.py
1 Version = '$Header$';
2 __version__ = "Beta 0.95.1"
3 from Plugins.Plugin import PluginDescriptor
4 from Components.config import config, ConfigSubsection, ConfigInteger,ConfigYesNo,ConfigText
5 from Components.Network import Network
6
7 from twisted.internet import reactor, defer
8 from twisted.web2 import server, channel, static, resource, stream, http_headers, responsecode, http
9 from twisted.web2.auth import digest, basic, wrapper
10 #from twisted.python import util
11 from twisted.python.log import startLogging,discardLogs
12 from twisted.cred.portal import Portal, IRealm
13 from twisted.cred import checkers, credentials, error
14 from zope.interface import Interface, implements
15
16 import webif
17 import WebIfConfig  
18 import os
19
20 from WebChilds.Toplevel import Toplevel
21 config.plugins.Webinterface = ConfigSubsection()
22 config.plugins.Webinterface.enable = ConfigYesNo(default = True)
23 config.plugins.Webinterface.port = ConfigInteger(80,limits = (1, 65536))
24 config.plugins.Webinterface.includehdd = ConfigYesNo(default = False)
25 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
26 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default = False)
27 config.plugins.Webinterface.debug = ConfigYesNo(default = False) # False by default, not confgurable in GUI. Edit settingsfile directly if needed
28 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI. 
29  
30
31 """
32  set DEBUG to True, if twisted should write logoutput to a file.
33  in normal console output, twisted will print only the first Traceback.
34  is this a bug in twisted or a conflict with enigma2?
35  with this option enabled, twisted will print all TB to the logfile
36  use tail -f <file> to view this log
37 """
38                         
39
40 DEBUGFILE= "/tmp/twisted.log"
41
42 def stopWebserver(session):
43         reactor.disconnectAll()
44         try:# got BS with "global name session is not defined" so i catched it, but please make it safe if you use globals! thx
45                 del session.mediaplayer
46                 del session.messageboxanswer
47         except NameError:
48                 pass
49 def restartWebserver(session):
50         stopWebserver(session)
51         startWebserver(session)
52
53 def startWebserver(session):
54         # variables, that are needed in the process
55         session.mediaplayer = None
56         session.messageboxanswer = None
57         
58         if config.plugins.Webinterface.enable.value is not True:
59                 print "not starting Werbinterface"
60                 return False
61         if config.plugins.Webinterface.debug.value:
62                 print "start twisted logfile, writing to %s" % DEBUGFILE 
63                 import sys
64                 startLogging(open(DEBUGFILE,'w'))
65
66         toplevel = Toplevel(session)
67         if config.plugins.Webinterface.useauth.value is False:
68                 site = server.Site(toplevel)
69         else:
70                 portal = Portal(HTTPAuthRealm())
71                 portal.registerChecker(PasswordDatabase())
72                 root = ModifiedHTTPAuthResource(toplevel,(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
73                 site = server.Site(root)
74         
75         # here we start the Toplevel without any username or password
76         # this allows access to all request over the iface 127.0.0.1 without any auth
77         localsite = server.Site(toplevel)
78         reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(localsite),interface='127.0.0.1')
79         
80         # and here we make the Toplevel public to our external ifaces
81         # it depends on the config, if this is with auth support
82         # keep in mind, if we have a second external ip (like a wlan device), we have to do it in the same way for this iface too
83         nw = Network()
84         for adaptername in nw.ifaces:
85                 extip = nw.ifaces[adaptername]['ip']
86                 if nw.ifaces[adaptername]['up'] is True:
87                         extip = "%i.%i.%i.%i"%(extip[0],extip[1],extip[2],extip[3])
88                         print "[WebIf] starting Webinterface on port %s on interface %s with address %s"%(str(config.plugins.Webinterface.port.value),adaptername,extip)
89                         try:
90                                 reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site),interface=extip)
91                         except Exception,e:
92                                 print "[WebIf] Error starting Webinterface on port %s on interface %s with address %s,because \n%s"%(str(config.plugins.Webinterface.port.value),adaptername,extip,e)
93                 else:
94                         print "[WebIf] found configured interface %s, but it is not running. so not starting a server on it ..." % adaptername
95         
96 ####            
97 def autostart(reason, **kwargs):
98         if "session" in kwargs:
99                 try:
100                         startWebserver(kwargs["session"])
101                 except ImportError,e:
102                         print "[WebIf] twisted not available, not starting web services",e
103                         
104 def openconfig(session, **kwargs):
105         session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
106
107 def configCB(result,session):
108         if result is True:
109                 print "[WebIf] config changed"
110                 restartWebserver(session)
111         else:
112                 print "[WebIf] config not changed"
113                 
114
115 def Plugins(**kwargs):
116         return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
117                     PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
118         
119         
120 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
121         """
122                 set it only to True, if you have a patched wrapper.py
123                 see http://twistedmatrix.com/trac/ticket/2041
124                 so, the solution for us is to make a new class an override ne faulty func
125         """
126
127         def locateChild(self, req, seg):
128                 return self.authenticate(req), seg
129         
130 class PasswordDatabase:
131     """
132         this checks webiflogins agains /etc/passwd
133     """
134     passwordfile = "/etc/passwd"
135     implements(checkers.ICredentialsChecker)
136     credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
137
138     def _cbPasswordMatch(self, matched, username):
139         if matched:
140             return username
141         else:
142             return failure.Failure(error.UnauthorizedLogin())
143
144     def requestAvatarId(self, credentials):     
145         if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
146                 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
147         else:
148                 return defer.fail(error.UnauthorizedLogin())
149
150 class IHTTPUser(Interface):
151         pass
152
153 class HTTPUser(object):
154         implements(IHTTPUser)
155
156 class HTTPAuthRealm(object):
157         implements(IRealm)
158         def requestAvatar(self, avatarId, mind, *interfaces):
159                 if IHTTPUser in interfaces:
160                         return IHTTPUser, HTTPUser()
161                 raise NotImplementedError("Only IHTTPUser interface is supported")
162
163         
164 import md5,time,string,crypt
165 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') 
166 def getpwnam(name, pwfile=None):
167     """Return pasword database entry for the given user name.
168     
169     Example from the Python Library Reference.
170     """
171     
172     if not pwfile:
173         pwfile = '/etc/passwd'
174
175     f = open(pwfile)
176     while 1:
177         line = f.readline()
178         if not line:
179             f.close()
180             raise KeyError, name
181         entry = tuple(line.strip().split(':', 6))
182         if entry[0] == name:
183             f.close()
184             return entry
185
186 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
187     """Encrypt a string according to rules in crypt(3)."""
188     if method.lower() == 'des':
189             return crypt.crypt(passwd, salt)
190     elif method.lower() == 'md5':
191         return passcrypt_md5(passwd, salt, magic)
192     elif method.lower() == 'clear':
193         return passwd
194
195 def check_passwd(name, passwd, pwfile=None):
196     """Validate given user, passwd pair against password database."""
197     
198     if not pwfile or type(pwfile) == type(''):
199         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
200     else:
201         getuser = pwfile.get_passwd
202
203     try:
204         enc_passwd = getuser(name)
205     except (KeyError, IOError):
206         return 0
207     if not enc_passwd:
208         return 0
209     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
210         salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
211         return enc_passwd == passcrypt(passwd, salt, 'md5')
212        
213     else:
214         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
215
216 def _to64(v, n):
217     r = ''
218     while (n-1 >= 0):
219         r = r + DES_SALT[v & 0x3F]
220         v = v >> 6
221         n = n - 1
222     return r
223                         
224 def passcrypt_md5(passwd, salt=None, magic='$1$'):
225     """Encrypt passwd with MD5 algorithm."""
226     
227     if not salt:
228         pass
229     elif salt[:len(magic)] == magic:
230         # remove magic from salt if present
231         salt = salt[len(magic):]
232
233     # salt only goes up to first '$'
234     salt = string.split(salt, '$')[0]
235     # limit length of salt to 8
236     salt = salt[:8]
237
238     ctx = md5.new(passwd)
239     ctx.update(magic)
240     ctx.update(salt)
241     
242     ctx1 = md5.new(passwd)
243     ctx1.update(salt)
244     ctx1.update(passwd)
245     
246     final = ctx1.digest()
247     
248     for i in range(len(passwd), 0 , -16):
249         if i > 16:
250             ctx.update(final)
251         else:
252             ctx.update(final[:i])
253     
254     i = len(passwd)
255     while i:
256         if i & 1:
257             ctx.update('\0')
258         else:
259             ctx.update(passwd[:1])
260         i = i >> 1
261     final = ctx.digest()
262     
263     for i in range(1000):
264         ctx1 = md5.new()
265         if i & 1:
266             ctx1.update(passwd)
267         else:
268             ctx1.update(final)
269         if i % 3: ctx1.update(salt)
270         if i % 7: ctx1.update(passwd)
271         if i & 1:
272             ctx1.update(final)
273         else:
274             ctx1.update(passwd)
275         final = ctx1.digest()
276     
277     rv = magic + salt + '$'
278     final = map(ord, final)
279     l = (final[0] << 16) + (final[6] << 8) + final[12]
280     rv = rv + _to64(l, 4)
281     l = (final[1] << 16) + (final[7] << 8) + final[13]
282     rv = rv + _to64(l, 4)
283     l = (final[2] << 16) + (final[8] << 8) + final[14]
284     rv = rv + _to64(l, 4)
285     l = (final[3] << 16) + (final[9] << 8) + final[15]
286     rv = rv + _to64(l, 4)
287     l = (final[4] << 16) + (final[10] << 8) + final[5]
288     rv = rv + _to64(l, 4)
289     l = final[11]
290     rv = rv + _to64(l, 2)
291     
292     return rv
293
294