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 = False) # False, because a std. images hasnt a rootpasswd set and so no login. and a login with a empty pwd makes no sense
23 define all files in /web to send no XML-HTTP-Headers here
24 all files not listed here will get an Content-Type: application/xhtml+xml charset: UTF-8
26 AppTextHeaderFiles = ['stream.m3u.xml','ts.m3u.xml','wapwrapper.xml',]
29 Actualle, the TextHtmlHeaderFiles should contain the updates.html.xml, but the IE then
30 has problems with unicode-characters
32 TextHtmlHeaderFiles = []
35 define all files in /web to send no XML-HTTP-Headers here
36 all files not listed here will get an Content-Type: text/html charset: UTF-8
38 NoExplicitHeaderFiles = ['getpid.xml','tvbrowser.xml',]
41 set DEBUG to True, if twisted should write logoutput to a file.
42 in normal console output, twisted will print only the first Traceback.
43 is this a bug in twisted or a conflict with enigma2?
44 with this option enabled, twisted will print all TB to the logfile
45 use tail -f <file> to view this log
51 DEBUGFILE= "/tmp/twisted.log"
53 from twisted.cred.portal import Portal
54 from twisted.cred import checkers
55 from twisted.web2.auth import digest, basic, wrapper
56 from zope.interface import Interface, implements
57 from twisted.cred import portal
58 from twisted.cred import credentials, error
59 from twisted.internet import defer
60 from zope import interface
64 reactor.disconnectAll()
66 def restartWebserver():
71 if config.plugins.Webinterface.enable.value is not True:
72 print "not starting Werbinterface"
75 print "start twisted logfile, writing to %s" % DEBUGFILE
77 startLogging(open(DEBUGFILE,'w'))
79 class ScreenPage(resource.Resource):
80 def __init__(self, path):
83 def render(self, req):
86 return http.Response(responsecode.OK, stream="please wait until enigma has booted")
88 class myProducerStream(stream.ProducerStream):
90 stream.ProducerStream.__init__(self)
91 self.closed_callback = None
94 if self.closed_callback:
95 self.closed_callback()
96 self.closed_callback = None
97 stream.ProducerStream.close(self)
99 if os.path.isfile(self.path):
101 webif.renderPage(s, self.path, req, sessions[0]) # login?
102 if self.path.split("/")[-1] in AppTextHeaderFiles:
103 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'text', (('charset', 'UTF-8'),))},stream=s)
104 elif self.path.split("/")[-1] in TextHtmlHeaderFiles:
105 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('text', 'html', (('charset', 'UTF-8'),))},stream=s)
106 elif self.path.split("/")[-1] in NoExplicitHeaderFiles:
107 return http.Response(responsecode.OK,stream=s)
109 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'xhtml+xml', (('charset', 'UTF-8'),))},stream=s)
111 return http.Response(responsecode.NOT_FOUND)
113 def locateChild(self, request, segments):
114 path = self.path+'/'+'/'.join(segments)
118 return ScreenPage(path), ()
120 class Toplevel(resource.Resource):
122 child_web = ScreenPage(util.sibpath(__file__, "web")) # "/web/*"
123 child_webdata = static.File(util.sibpath(__file__, "web-data")) # FIXME: web-data appears as webdata
124 child_wap = static.File(util.sibpath(__file__, "wap")) # static pages for wap
126 def render(self, req):
127 fp = open(util.sibpath(__file__, "web-data")+"/index.html")
130 return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
132 toplevel = Toplevel()
133 if config.plugins.Webinterface.includehdd.value:
134 toplevel.putChild("hdd",static.File("/hdd"))
136 if config.plugins.Webinterface.useauth.value is False:
137 site = server.Site(toplevel)
139 portal = Portal(HTTPAuthRealm())
140 portal.registerChecker(PasswordDatabase())
141 root = ModifiedHTTPAuthResource(toplevel,(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
142 site = server.Site(root)
143 print "[WebIf] starting Webinterface on port",config.plugins.Webinterface.port.value
144 reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site))
147 def autostart(reason, **kwargs):
148 if "session" in kwargs:
150 sessions.append(kwargs["session"])
155 except ImportError,e:
156 print "[WebIf] twisted not available, not starting web services",e
158 def openconfig(session, **kwargs):
159 session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
161 def configCB(result):
163 print "[WebIf] config changed"
166 print "[WebIf] config not changed"
169 def Plugins(**kwargs):
170 return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
171 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
174 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
176 set it only to True, if you have a patched wrapper.py
177 see http://twistedmatrix.com/trac/ticket/2041
178 so, the solution for us is to make a new class an override ne faulty func
181 def locateChild(self, req, seg):
182 return self.authenticate(req), seg
184 class PasswordDatabase:
186 this checks webiflogins agains /etc/passwd
188 passwordfile = "/etc/passwd"
189 interface.implements(checkers.ICredentialsChecker)
190 credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
192 def _cbPasswordMatch(self, matched, username):
196 return failure.Failure(error.UnauthorizedLogin())
198 def requestAvatarId(self, credentials):
199 if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
200 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
202 return defer.fail(error.UnauthorizedLogin())
204 class IHTTPUser(Interface):
207 class HTTPUser(object):
208 implements(IHTTPUser)
210 class HTTPAuthRealm(object):
211 implements(portal.IRealm)
212 def requestAvatar(self, avatarId, mind, *interfaces):
213 if IHTTPUser in interfaces:
214 return IHTTPUser, HTTPUser()
215 raise NotImplementedError("Only IHTTPUser interface is supported")
218 import md5,time,string,crypt
219 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
220 def getpwnam(name, pwfile=None):
221 """Return pasword database entry for the given user name.
223 Example from the Python Library Reference.
227 pwfile = '/etc/passwd'
235 entry = tuple(line.strip().split(':', 6))
240 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
241 """Encrypt a string according to rules in crypt(3)."""
242 if method.lower() == 'des':
243 return crypt.crypt(passwd, salt)
244 elif method.lower() == 'md5':
245 return passcrypt_md5(passwd, salt, magic)
246 elif method.lower() == 'clear':
249 def check_passwd(name, passwd, pwfile=None):
250 """Validate given user, passwd pair against password database."""
252 if not pwfile or type(pwfile) == type(''):
253 getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
255 getuser = pwfile.get_passwd
258 enc_passwd = getuser(name)
259 except (KeyError, IOError):
263 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
264 salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
265 return enc_passwd == passcrypt(passwd, salt, 'md5')
268 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
273 r = r + DES_SALT[v & 0x3F]
278 def passcrypt_md5(passwd, salt=None, magic='$1$'):
279 """Encrypt passwd with MD5 algorithm."""
283 elif salt[:len(magic)] == magic:
284 # remove magic from salt if present
285 salt = salt[len(magic):]
287 # salt only goes up to first '$'
288 salt = string.split(salt, '$')[0]
289 # limit length of salt to 8
292 ctx = md5.new(passwd)
296 ctx1 = md5.new(passwd)
300 final = ctx1.digest()
302 for i in range(len(passwd), 0 , -16):
306 ctx.update(final[:i])
313 ctx.update(passwd[:1])
317 for i in range(1000):
323 if i % 3: ctx1.update(salt)
324 if i % 7: ctx1.update(passwd)
329 final = ctx1.digest()
331 rv = magic + salt + '$'
332 final = map(ord, final)
333 l = (final[0] << 16) + (final[6] << 8) + final[12]
334 rv = rv + _to64(l, 4)
335 l = (final[1] << 16) + (final[7] << 8) + final[13]
336 rv = rv + _to64(l, 4)
337 l = (final[2] << 16) + (final[8] << 8) + final[14]
338 rv = rv + _to64(l, 4)
339 l = (final[3] << 16) + (final[9] << 8) + final[15]
340 rv = rv + _to64(l, 4)
341 l = (final[4] << 16) + (final[10] << 8) + final[5]
342 rv = rv + _to64(l, 4)
344 rv = rv + _to64(l, 2)