Check if new_items is not None before extending
[enigma2-plugins.git] / webinterface / src / plugin.py
1 Version = '$Header$';
2 __version__ = "Beta 0.98.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, 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         
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         
134 ####            
135 def autostart(reason, **kwargs):
136         if "session" in kwargs:
137                 try:
138                         startWebserver(kwargs["session"])
139                 except ImportError,e:
140                         print "[WebIf] twisted not available, not starting web services",e
141                         
142 def openconfig(session, **kwargs):
143         session.openWithCallback(configCB,WebIfConfigScreen)
144
145 def configCB(result,session):
146         if result is True:
147                 print "[WebIf] config changed"
148                 restartWebserver(session)
149         else:
150                 print "[WebIf] config not changed"
151                 
152
153 def Plugins(**kwargs):
154         return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
155                     PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
156         
157         
158 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
159         """
160                 set it only to True, if you have a patched wrapper.py
161                 see http://twistedmatrix.com/trac/ticket/2041
162                 so, the solution for us is to make a new class an override ne faulty func
163         """
164
165         def locateChild(self, req, seg):
166                 return self.authenticate(req), seg
167         
168 class PasswordDatabase:
169     """
170         this checks webiflogins agains /etc/passwd
171     """
172     passwordfile = "/etc/passwd"
173     implements(checkers.ICredentialsChecker)
174     credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
175
176     def _cbPasswordMatch(self, matched, username):
177         if matched:
178             return username
179         else:
180             return failure.Failure(error.UnauthorizedLogin())
181
182     def requestAvatarId(self, credentials):     
183         if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
184                 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
185         else:
186                 return defer.fail(error.UnauthorizedLogin())
187
188 class IHTTPUser(Interface):
189         pass
190
191 class HTTPUser(object):
192         implements(IHTTPUser)
193
194 class HTTPAuthRealm(object):
195         implements(IRealm)
196         def requestAvatar(self, avatarId, mind, *interfaces):
197                 if IHTTPUser in interfaces:
198                         return IHTTPUser, HTTPUser()
199                 raise NotImplementedError("Only IHTTPUser interface is supported")
200
201
202 from string import find, split  
203 from md5 import new as md5_new
204 from crypt import crypt
205
206 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') 
207 def getpwnam(name, pwfile=None):
208     """Return pasword database entry for the given user name.
209     
210     Example from the Python Library Reference.
211     """
212     
213     if not pwfile:
214         pwfile = '/etc/passwd'
215
216     f = open(pwfile)
217     while 1:
218         line = f.readline()
219         if not line:
220             f.close()
221             raise KeyError, name
222         entry = tuple(line.strip().split(':', 6))
223         if entry[0] == name:
224             f.close()
225             return entry
226
227 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
228     """Encrypt a string according to rules in crypt(3)."""
229     if method.lower() == 'des':
230             return crypt(passwd, salt)
231     elif method.lower() == 'md5':
232         return passcrypt_md5(passwd, salt, magic)
233     elif method.lower() == 'clear':
234         return passwd
235
236 def check_passwd(name, passwd, pwfile=None):
237     """Validate given user, passwd pair against password database."""
238     
239     if not pwfile or type(pwfile) == type(''):
240         getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
241     else:
242         getuser = pwfile.get_passwd
243
244     try:
245         enc_passwd = getuser(name)
246     except (KeyError, IOError):
247         return 0
248     if not enc_passwd:
249         return 0
250     elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
251         salt = enc_passwd[3:find(enc_passwd, '$', 3)]
252         return enc_passwd == passcrypt(passwd, salt, 'md5')
253        
254     else:
255         return enc_passwd == passcrypt(passwd, enc_passwd[:2])
256
257 def _to64(v, n):
258     r = ''
259     while (n-1 >= 0):
260         r = r + DES_SALT[v & 0x3F]
261         v = v >> 6
262         n = n - 1
263     return r
264                         
265 def passcrypt_md5(passwd, salt=None, magic='$1$'):
266     """Encrypt passwd with MD5 algorithm."""
267     
268     if not salt:
269         pass
270     elif salt[:len(magic)] == magic:
271         # remove magic from salt if present
272         salt = salt[len(magic):]
273
274     # salt only goes up to first '$'
275     salt = split(salt, '$')[0]
276     # limit length of salt to 8
277     salt = salt[:8]
278
279     ctx = md5_new(passwd)
280     ctx.update(magic)
281     ctx.update(salt)
282     
283     ctx1 = md5_new(passwd)
284     ctx1.update(salt)
285     ctx1.update(passwd)
286     
287     final = ctx1.digest()
288     
289     for i in range(len(passwd), 0 , -16):
290         if i > 16:
291             ctx.update(final)
292         else:
293             ctx.update(final[:i])
294     
295     i = len(passwd)
296     while i:
297         if i & 1:
298             ctx.update('\0')
299         else:
300             ctx.update(passwd[:1])
301         i = i >> 1
302     final = ctx.digest()
303     
304     for i in range(1000):
305         ctx1 = md5_new()
306         if i & 1:
307             ctx1.update(passwd)
308         else:
309             ctx1.update(final)
310         if i % 3: ctx1.update(salt)
311         if i % 7: ctx1.update(passwd)
312         if i & 1:
313             ctx1.update(final)
314         else:
315             ctx1.update(passwd)
316         final = ctx1.digest()
317     
318     rv = magic + salt + '$'
319     final = map(ord, final)
320     l = (final[0] << 16) + (final[6] << 8) + final[12]
321     rv = rv + _to64(l, 4)
322     l = (final[1] << 16) + (final[7] << 8) + final[13]
323     rv = rv + _to64(l, 4)
324     l = (final[2] << 16) + (final[8] << 8) + final[14]
325     rv = rv + _to64(l, 4)
326     l = (final[3] << 16) + (final[9] << 8) + final[15]
327     rv = rv + _to64(l, 4)
328     l = (final[4] << 16) + (final[10] << 8) + final[5]
329     rv = rv + _to64(l, 4)
330     l = final[11]
331     rv = rv + _to64(l, 2)
332     
333     return rv
334
335