1 from Plugins.Plugin import PluginDescriptor
3 from twisted.internet import reactor
4 from twisted.web2 import server, channel, static, resource, stream, http_headers, responsecode, http
5 from twisted.python import util
6 from twisted.python.log import startLogging,discardLogs
12 from Components.config import config, ConfigSubsection, ConfigInteger,ConfigYesNo
14 config.plugins.Webinterface = ConfigSubsection()
15 config.plugins.Webinterface.enable = ConfigYesNo(default = True)
16 config.plugins.Webinterface.port = ConfigInteger(80,limits = (1, 999))
17 config.plugins.Webinterface.includehdd = ConfigYesNo(default = False)
18 config.plugins.Webinterface.useauth = ConfigYesNo(default = True)
24 set DEBUG to True, if twisted should write logoutput to a file.
25 in normal console output, twisted will print only the first Traceback.
26 is this a bug in twisted or a conflict with enigma2?
27 with this option enabled, twisted will print all TB to the logfile
28 use tail -f <file> to view this log
32 DEBUGFILE= "/tmp/twisted.log"
34 from twisted.cred.portal import Portal
35 from twisted.cred import checkers
36 from twisted.web2.auth import digest, basic, wrapper
37 from zope.interface import Interface, implements
38 from twisted.cred import portal
39 from twisted.cred import credentials, error
40 from twisted.internet import defer
41 from zope import interface
45 reactor.disconnectAll()
47 def restartWebserver():
52 if config.plugins.Webinterface.enable.value is not True:
53 print "not starting Werbinterface"
56 print "start twisted logfile, writing to %s" % DEBUGFILE
58 startLogging(open(DEBUGFILE,'w'))
60 class ScreenPage(resource.Resource):
61 def __init__(self, path):
65 def render(self, req):
68 return http.Response(responsecode.OK, stream="please wait until enigma has booted")
70 class myProducerStream(stream.ProducerStream):
71 closed_callback = None
74 if self.closed_callback:
75 self.closed_callback()
76 stream.ProducerStream.close(self)
78 if os.path.isfile(self.path):
80 webif.renderPage(s, self.path, req, sessions[0]) # login?
81 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'xhtml+xml', (('charset', 'UTF-8'),))},stream=s)
83 return http.Response(responsecode.NOT_FOUND)
85 def locateChild(self, request, segments):
86 path = self.path+'/'+'/'.join(segments)
90 return ScreenPage(path), ()
92 class Toplevel(resource.Resource):
94 child_web = ScreenPage(util.sibpath(__file__, "web")) # "/web/*"
95 child_webdata = static.File(util.sibpath(__file__, "web-data")) # FIXME: web-data appears as webdata
97 def render(self, req):
98 fp = open(util.sibpath(__file__, "web-data")+"/index.html")
101 return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
104 if config.plugins.Webinterface.includehdd.value:
105 toplevel.putChild("hdd",static.File("/hdd"))
107 if config.plugins.Webinterface.useauth.value is False:
108 site = server.Site(Toplevel())
110 portal = Portal(HTTPAuthRealm())
111 portal.registerChecker(PasswordDatabase())
112 root = ModifiedHTTPAuthResource(Toplevel(),(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
113 site = server.Site(root)
114 print "[WebIf] starting Webinterface on port",config.plugins.Webinterface.port.value
115 reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site))
118 def autostart(reason, **kwargs):
119 if "session" in kwargs:
121 sessions.append(kwargs["session"])
126 except ImportError,e:
127 print "[WebIf] twisted not available, not starting web services",e
129 def openconfig(session, **kwargs):
130 session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
132 def configCB(result):
134 print "[WebIf] config changed"
137 print "[WebIf] config not changed"
140 def Plugins(**kwargs):
141 return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
142 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
145 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
147 set it only to True, if you have a patched wrapper.py
148 see http://twistedmatrix.com/trac/ticket/2041
149 so, the solution for us is to make a new class an override ne faulty func
152 def locateChild(self, req, seg):
153 return self.authenticate(req), seg
155 class PasswordDatabase:
157 this checks webiflogins agains /etc/passwd
159 passwordfile = "/etc/passwd"
160 interface.implements(checkers.ICredentialsChecker)
161 credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
163 def _cbPasswordMatch(self, matched, username):
167 return failure.Failure(error.UnauthorizedLogin())
169 def requestAvatarId(self, credentials):
170 if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
171 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
173 return defer.fail(error.UnauthorizedLogin())
175 class IHTTPUser(Interface):
178 class HTTPUser(object):
179 implements(IHTTPUser)
181 class HTTPAuthRealm(object):
182 implements(portal.IRealm)
183 def requestAvatar(self, avatarId, mind, *interfaces):
184 if IHTTPUser in interfaces:
185 return IHTTPUser, HTTPUser()
186 raise NotImplementedError("Only IHTTPUser interface is supported")
189 import md5,time,string,crypt
190 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
191 def getpwnam(name, pwfile=None):
192 """Return pasword database entry for the given user name.
194 Example from the Python Library Reference.
198 pwfile = '/etc/passwd'
206 entry = tuple(line.strip().split(':', 6))
211 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
212 """Encrypt a string according to rules in crypt(3)."""
213 if method.lower() == 'des':
215 salt = str(whrandom.choice(DES_SALT)) + str(whrandom.choice(DES_SALT))
216 return crypt.crypt(passwd, salt)
217 elif method.lower() == 'md5':
218 return passcrypt_md5(passwd, salt, magic)
219 elif method.lower() == 'clear':
222 def check_passwd(name, passwd, pwfile=None):
223 """Validate given user, passwd pair against password database."""
225 if not pwfile or type(pwfile) == type(''):
226 getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
228 getuser = pwfile.get_passwd
231 enc_passwd = getuser(name)
232 except (KeyError, IOError):
236 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
237 salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
238 return enc_passwd == passcrypt(passwd, salt=salt, method='md5')
240 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
245 r = r + DES_SALT[v & 0x3F]
250 def passcrypt_md5(passwd, salt=None, magic='$1$'):
251 """Encrypt passwd with MD5 algorithm."""
254 salt = repr(int(time.time()))[-8:]
255 elif salt[:len(magic)] == magic:
256 # remove magic from salt if present
257 salt = salt[len(magic):]
259 # salt only goes up to first '$'
260 salt = string.split(salt, '$')[0]
261 # limit length of salt to 8
264 ctx = md5.new(passwd)
268 ctx1 = md5.new(passwd)
272 final = ctx1.digest()
274 for i in range(len(passwd), 0 , -16):
278 ctx.update(final[:i])
285 ctx.update(passwd[:1])
289 for i in range(1000):
295 if i % 3: ctx1.update(salt)
296 if i % 7: ctx1.update(passwd)
301 final = ctx1.digest()
303 rv = magic + salt + '$'
304 final = map(ord, final)
305 l = (final[0] << 16) + (final[6] << 8) + final[12]
306 rv = rv + _to64(l, 4)
307 l = (final[1] << 16) + (final[7] << 8) + final[13]
308 rv = rv + _to64(l, 4)
309 l = (final[2] << 16) + (final[8] << 8) + final[14]
310 rv = rv + _to64(l, 4)
311 l = (final[3] << 16) + (final[9] << 8) + final[15]
312 rv = rv + _to64(l, 4)
313 l = (final[4] << 16) + (final[10] << 8) + final[5]
314 rv = rv + _to64(l, 4)
316 rv = rv + _to64(l, 2)