1 from Plugins.Plugin import PluginDescriptor
3 from twisted.internet import reactor, defer
5 from twisted.web2 import server, channel, static, resource, stream, http_headers, responsecode, http
6 from twisted.web2.auth import digest, basic, wrapper
8 from twisted.python import util
9 from twisted.python.log import startLogging,discardLogs
11 from twisted.cred.portal import Portal, IRealm
12 from twisted.cred import checkers, credentials, error
14 from zope.interface import Interface, implements
20 from Components.config import config, ConfigSubsection, ConfigInteger,ConfigYesNo
22 config.plugins.Webinterface = ConfigSubsection()
23 config.plugins.Webinterface.enable = ConfigYesNo(default = True)
24 config.plugins.Webinterface.port = ConfigInteger(80,limits = (1, 65536))
25 config.plugins.Webinterface.includehdd = ConfigYesNo(default = False)
26 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
31 define all files in /web to send no XML-HTTP-Headers here
32 all files not listed here will get an Content-Type: application/xhtml+xml charset: UTF-8
34 AppTextHeaderFiles = ['stream.m3u.xml','ts.m3u.xml','wapwrapper.xml',]
37 Actualle, the TextHtmlHeaderFiles should contain the updates.html.xml, but the IE then
38 has problems with unicode-characters
40 TextHtmlHeaderFiles = []
43 define all files in /web to send no XML-HTTP-Headers here
44 all files not listed here will get an Content-Type: text/html charset: UTF-8
46 NoExplicitHeaderFiles = ['getpid.xml','tvbrowser.xml',]
49 set DEBUG to True, if twisted should write logoutput to a file.
50 in normal console output, twisted will print only the first Traceback.
51 is this a bug in twisted or a conflict with enigma2?
52 with this option enabled, twisted will print all TB to the logfile
53 use tail -f <file> to view this log
59 DEBUGFILE= "/tmp/twisted.log"
62 reactor.disconnectAll()
64 def restartWebserver():
69 if config.plugins.Webinterface.enable.value is not True:
70 print "not starting Werbinterface"
73 print "start twisted logfile, writing to %s" % DEBUGFILE
75 startLogging(open(DEBUGFILE,'w'))
77 class ScreenPage(resource.Resource):
78 def __init__(self, path):
81 def render(self, req):
84 return http.Response(responsecode.OK, stream="please wait until enigma has booted")
86 class myProducerStream(stream.ProducerStream):
88 stream.ProducerStream.__init__(self)
89 self.closed_callback = None
92 if self.closed_callback:
93 self.closed_callback()
94 self.closed_callback = None
95 stream.ProducerStream.close(self)
97 if os.path.isfile(self.path):
99 webif.renderPage(s, self.path, req, sessions[0]) # login?
100 if self.path.split("/")[-1] in AppTextHeaderFiles:
101 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'text', (('charset', 'UTF-8'),))},stream=s)
102 elif self.path.split("/")[-1] in TextHtmlHeaderFiles:
103 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('text', 'html', (('charset', 'UTF-8'),))},stream=s)
104 elif self.path.split("/")[-1] in NoExplicitHeaderFiles:
105 return http.Response(responsecode.OK,stream=s)
107 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'xhtml+xml', (('charset', 'UTF-8'),))},stream=s)
109 return http.Response(responsecode.NOT_FOUND)
111 def locateChild(self, request, segments):
112 path = self.path+'/'+'/'.join(segments)
116 return ScreenPage(path), ()
118 class Toplevel(resource.Resource):
120 child_web = ScreenPage(util.sibpath(__file__, "web")) # "/web/*"
121 child_webdata = static.File(util.sibpath(__file__, "web-data")) # FIXME: web-data appears as webdata
122 child_wap = static.File(util.sibpath(__file__, "wap")) # static pages for wap
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 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):
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)