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