chmod from 775 to 755
[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.loadmovielength = ConfigYesNo(default = False)
28 config.plugins.Webinterface.debug = ConfigYesNo(default = False) # False by default, not confgurable in GUI. Edit settingsfile directly if needed
29 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI. 
30  
31
32 """
33  set DEBUG to True, if twisted should write logoutput to a file.
34  in normal console output, twisted will print only the first Traceback.
35  is this a bug in twisted or a conflict with enigma2?
36  with this option enabled, twisted will print all TB to the logfile
37  use tail -f <file> to view this log
38 """
39                         
40
41 DEBUGFILE= "/tmp/twisted.log"
42
43 def stopWebserver(session):
44         reactor.disconnectAll()
45         try:# got BS with "global name session is not defined" so i catched it, but please make it safe if you use globals! thx
46                 del session.mediaplayer
47                 del session.messageboxanswer
48         except NameError:
49                 pass
50         
51 def restartWebserver(session):
52         stopWebserver(session)
53         startWebserver(session)
54
55 def startWebserver(session):
56         # variables, that are needed in the process
57         session.mediaplayer = None
58         session.messageboxanswer = None
59         
60         if config.plugins.Webinterface.enable.value is not True:
61                 print "not starting Werbinterface"
62                 return False
63         if config.plugins.Webinterface.debug.value:
64                 print "start twisted logfile, writing to %s" % DEBUGFILE 
65                 import sys
66                 startLogging(open(DEBUGFILE,'w'))
67
68         toplevel = Toplevel(session)
69         if config.plugins.Webinterface.useauth.value is False:
70                 site = server.Site(toplevel)
71         else:
72                 portal = Portal(HTTPAuthRealm())
73                 portal.registerChecker(PasswordDatabase())
74                 root = ModifiedHTTPAuthResource(toplevel,(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
75                 site = server.Site(root)
76         
77         # here we start the Toplevel without any username or password
78         # this allows access to all request over the iface 127.0.0.1 without any auth
79         localsite = server.Site(toplevel)
80         reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(localsite),interface='127.0.0.1')
81         
82         # and here we make the Toplevel public to our external ifaces
83         # it depends on the config, if this is with auth support
84         # 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
85         nw = Network()
86         for adaptername in nw.ifaces:
87                 extip = nw.ifaces[adaptername]['ip']
88                 if nw.ifaces[adaptername]['up'] is True:
89                         extip = "%i.%i.%i.%i"%(extip[0],extip[1],extip[2],extip[3])
90                         print "[WebIf] starting Webinterface on port %s on interface %s with address %s"%(str(config.plugins.Webinterface.port.value),adaptername,extip)
91                         try:
92                                 reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site),interface=extip)
93                         except Exception,e:
94                                 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)
95                 else:
96                         print "[WebIf] found configured interface %s, but it is not running. so not starting a server on it ..." % adaptername
97         
98 ####            
99 def autostart(reason, **kwargs):
100         if "session" in kwargs:
101                 try:
102                         startWebserver(kwargs["session"])
103                 except ImportError,e:
104                         print "[WebIf] twisted not available, not starting web services",e
105                         
106 def openconfig(session, **kwargs):
107         session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
108
109 def configCB(result,session):
110         if result is True:
111                 print "[WebIf] config changed"
112                 restartWebserver(session)
113         else:
114                 print "[WebIf] config not changed"
115                 
116
117 def Plugins(**kwargs):
118         return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
119                     PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
120         
121         
122 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
123         """
124                 set it only to True, if you have a patched wrapper.py
125                 see http://twistedmatrix.com/trac/ticket/2041
126                 so, the solution for us is to make a new class an override ne faulty func
127         """
128
129         def locateChild(self, req, seg):
130                 return self.authenticate(req), seg
131         
132 class PasswordDatabase:
133     """
134         this checks webiflogins agains /etc/passwd
135     """
136     passwordfile = "/etc/passwd"
137     implements(checkers.ICredentialsChecker)
138     credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
139
140     def _cbPasswordMatch(self, matched, username):
141         if matched:
142             return username
143         else:
144             return failure.Failure(error.UnauthorizedLogin())
145
146     def requestAvatarId(self, credentials):     
147         if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
148                 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
149         else:
150                 return defer.fail(error.UnauthorizedLogin())
151
152 class IHTTPUser(Interface):
153         pass
154
155 class HTTPUser(object):
156         implements(IHTTPUser)
157
158 class HTTPAuthRealm(object):
159         implements(IRealm)
160         def requestAvatar(self, avatarId, mind, *interfaces):
161                 if IHTTPUser in interfaces:
162                         return IHTTPUser, HTTPUser()
163                 raise NotImplementedError("Only IHTTPUser interface is supported")
164
165         
166 import md5,time,string,crypt
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.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:string.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 = string.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