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