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',]
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
50 DEBUGFILE= "/tmp/twisted.log"
52 from twisted.cred.portal import Portal
53 from twisted.cred import checkers
54 from twisted.web2.auth import digest, basic, wrapper
55 from zope.interface import Interface, implements
56 from twisted.cred import portal
57 from twisted.cred import credentials, error
58 from twisted.internet import defer
59 from zope import interface
63 reactor.disconnectAll()
65 def restartWebserver():
70 if config.plugins.Webinterface.enable.value is not True:
71 print "not starting Werbinterface"
74 print "start twisted logfile, writing to %s" % DEBUGFILE
76 startLogging(open(DEBUGFILE,'w'))
78 class ScreenPage(resource.Resource):
79 def __init__(self, path):
82 def render(self, req):
85 return http.Response(responsecode.OK, stream="please wait until enigma has booted")
87 class myProducerStream(stream.ProducerStream):
89 stream.ProducerStream.__init__(self)
90 self.closed_callback = None
93 if self.closed_callback:
94 self.closed_callback()
95 self.closed_callback = None
96 stream.ProducerStream.close(self)
98 if os.path.isfile(self.path):
100 webif.renderPage(s, self.path, req, sessions[0]) # login?
101 if self.path.split("/")[-1] in AppTextHeaderFiles:
102 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'text', (('charset', 'UTF-8'),))},stream=s)
103 # elif self.path.split("/")[-1] in TextHtmlHeaderFiles:
104 # return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('text', 'html', (('charset', 'UTF-8'),))},stream=s)
105 elif self.path.split("/")[-1] in NoExplicitHeaderFiles:
106 return http.Response(responsecode.OK,stream=s)
108 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'xhtml+xml', (('charset', 'UTF-8'),))},stream=s)
110 return http.Response(responsecode.NOT_FOUND)
112 def locateChild(self, request, segments):
113 path = self.path+'/'+'/'.join(segments)
117 return ScreenPage(path), ()
119 class Toplevel(resource.Resource):
121 child_web = ScreenPage(util.sibpath(__file__, "web")) # "/web/*"
122 child_webdata = static.File(util.sibpath(__file__, "web-data")) # FIXME: web-data appears as webdata
124 def render(self, req):
125 fp = open(util.sibpath(__file__, "web-data")+"/index.html")
128 return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
130 toplevel = Toplevel()
131 if config.plugins.Webinterface.includehdd.value:
132 toplevel.putChild("hdd",static.File("/hdd"))
134 if config.plugins.Webinterface.useauth.value is False:
135 site = server.Site(toplevel)
137 portal = Portal(HTTPAuthRealm())
138 portal.registerChecker(PasswordDatabase())
139 root = ModifiedHTTPAuthResource(toplevel,(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
140 site = server.Site(root)
141 print "[WebIf] starting Webinterface on port",config.plugins.Webinterface.port.value
142 reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site))
145 def autostart(reason, **kwargs):
146 if "session" in kwargs:
148 sessions.append(kwargs["session"])
153 except ImportError,e:
154 print "[WebIf] twisted not available, not starting web services",e
156 def openconfig(session, **kwargs):
157 session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
159 def configCB(result):
161 print "[WebIf] config changed"
164 print "[WebIf] config not changed"
167 def Plugins(**kwargs):
168 return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
169 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
172 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
174 set it only to True, if you have a patched wrapper.py
175 see http://twistedmatrix.com/trac/ticket/2041
176 so, the solution for us is to make a new class an override ne faulty func
179 def locateChild(self, req, seg):
180 return self.authenticate(req), seg
182 class PasswordDatabase:
184 this checks webiflogins agains /etc/passwd
186 passwordfile = "/etc/passwd"
187 interface.implements(checkers.ICredentialsChecker)
188 credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
190 def _cbPasswordMatch(self, matched, username):
194 return failure.Failure(error.UnauthorizedLogin())
196 def requestAvatarId(self, credentials):
197 if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
198 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
200 return defer.fail(error.UnauthorizedLogin())
202 class IHTTPUser(Interface):
205 class HTTPUser(object):
206 implements(IHTTPUser)
208 class HTTPAuthRealm(object):
209 implements(portal.IRealm)
210 def requestAvatar(self, avatarId, mind, *interfaces):
211 if IHTTPUser in interfaces:
212 return IHTTPUser, HTTPUser()
213 raise NotImplementedError("Only IHTTPUser interface is supported")
216 import md5,time,string,crypt
217 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
218 def getpwnam(name, pwfile=None):
219 """Return pasword database entry for the given user name.
221 Example from the Python Library Reference.
225 pwfile = '/etc/passwd'
233 entry = tuple(line.strip().split(':', 6))
238 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
239 """Encrypt a string according to rules in crypt(3)."""
240 if method.lower() == 'des':
241 return crypt.crypt(passwd, salt)
242 elif method.lower() == 'md5':
243 return passcrypt_md5(passwd, salt, magic)
244 elif method.lower() == 'clear':
247 def check_passwd(name, passwd, pwfile=None):
248 """Validate given user, passwd pair against password database."""
250 if not pwfile or type(pwfile) == type(''):
251 getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
253 getuser = pwfile.get_passwd
256 enc_passwd = getuser(name)
257 except (KeyError, IOError):
261 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
262 salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
263 return enc_passwd == passcrypt(passwd, salt, 'md5')
266 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
271 r = r + DES_SALT[v & 0x3F]
276 def passcrypt_md5(passwd, salt=None, magic='$1$'):
277 """Encrypt passwd with MD5 algorithm."""
281 elif salt[:len(magic)] == magic:
282 # remove magic from salt if present
283 salt = salt[len(magic):]
285 # salt only goes up to first '$'
286 salt = string.split(salt, '$')[0]
287 # limit length of salt to 8
290 ctx = md5.new(passwd)
294 ctx1 = md5.new(passwd)
298 final = ctx1.digest()
300 for i in range(len(passwd), 0 , -16):
304 ctx.update(final[:i])
311 ctx.update(passwd[:1])
315 for i in range(1000):
321 if i % 3: ctx1.update(salt)
322 if i % 7: ctx1.update(passwd)
327 final = ctx1.digest()
329 rv = magic + salt + '$'
330 final = map(ord, final)
331 l = (final[0] << 16) + (final[6] << 8) + final[12]
332 rv = rv + _to64(l, 4)
333 l = (final[1] << 16) + (final[7] << 8) + final[13]
334 rv = rv + _to64(l, 4)
335 l = (final[2] << 16) + (final[8] << 8) + final[14]
336 rv = rv + _to64(l, 4)
337 l = (final[3] << 16) + (final[9] << 8) + final[15]
338 rv = rv + _to64(l, 4)
339 l = (final[4] << 16) + (final[10] << 8) + final[5]
340 rv = rv + _to64(l, 4)
342 rv = rv + _to64(l, 2)