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