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