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
27 config.plugins.Webinterface.debug = ConfigYesNo(default = False) # False by default, not confgurable in GUI. Edit settingsfile directly if needed
32 define all files in /web to send no XML-HTTP-Headers here
33 all files not listed here will get an Content-Type: application/xhtml+xml charset: UTF-8
35 AppTextHeaderFiles = ['stream.m3u.xml','ts.m3u.xml',]
38 Actualy, the TextHtmlHeaderFiles should contain the updates.html.xml, but the IE then
39 has problems with unicode-characters
41 TextHtmlHeaderFiles = ['wapremote.xml',]
44 define all files in /web to send no XML-HTTP-Headers here
45 all files not listed here will get an Content-Type: text/html charset: UTF-8
47 NoExplicitHeaderFiles = ['getpid.xml','tvbrowser.xml',]
50 set DEBUG to True, if twisted should write logoutput to a file.
51 in normal console output, twisted will print only the first Traceback.
52 is this a bug in twisted or a conflict with enigma2?
53 with this option enabled, twisted will print all TB to the logfile
54 use tail -f <file> to view this log
58 DEBUGFILE= "/tmp/twisted.log"
61 reactor.disconnectAll()
63 def restartWebserver():
68 if config.plugins.Webinterface.enable.value is not True:
69 print "not starting Werbinterface"
71 if config.plugins.Webinterface.debug.value:
72 print "start twisted logfile, writing to %s" % DEBUGFILE
74 startLogging(open(DEBUGFILE,'w'))
76 class ScreenPage(resource.Resource):
77 def __init__(self, path):
80 def render(self, req):
83 return http.Response(responsecode.OK, stream="please wait until enigma has booted")
85 class myProducerStream(stream.ProducerStream):
87 stream.ProducerStream.__init__(self)
88 self.closed_callback = None
91 if self.closed_callback:
92 self.closed_callback()
93 self.closed_callback = None
94 stream.ProducerStream.close(self)
96 if os.path.isfile(self.path):
98 webif.renderPage(s, self.path, req, sessions[0]) # login?
99 if self.path.split("/")[-1] in AppTextHeaderFiles:
100 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'text', (('charset', 'UTF-8'),))},stream=s)
101 elif self.path.split("/")[-1] in TextHtmlHeaderFiles:
102 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('text', 'html', (('charset', 'UTF-8'),))},stream=s)
103 elif self.path.split("/")[-1] in NoExplicitHeaderFiles:
104 return http.Response(responsecode.OK,stream=s)
106 return http.Response(responsecode.OK,{'Content-type': http_headers.MimeType('application', 'xhtml+xml', (('charset', 'UTF-8'),))},stream=s)
108 return http.Response(responsecode.NOT_FOUND)
110 def locateChild(self, request, segments):
111 path = self.path+'/'+'/'.join(segments)
115 return ScreenPage(path), ()
117 class Toplevel(resource.Resource):
119 child_web = ScreenPage(util.sibpath(__file__, "web")) # "/web/*"
120 child_webdata = static.File(util.sibpath(__file__, "web-data")) # FIXME: web-data appears as webdata
121 child_wap = static.File(util.sibpath(__file__, "wap")) # static pages for wap
122 child_movie = MovieStreamer()
123 def render(self, req):
124 fp = open(util.sibpath(__file__, "web-data")+"/index.html")
127 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))
144 class MovieStreamer(resource.Resource):
147 def render(self, req):
148 class myFileStream(stream.FileStream):
150 because os.fstat(f.fileno()).st_size returns negative values on
151 large file, we set read() to a fix value
155 def read(self, sendfile=False):
164 if sendfile and length > SENDFILE_THRESHOLD:
165 # XXX: Yay using non-existent sendfile support!
166 # FIXME: if we return a SendfileBuffer, and then sendfile
167 # fails, then what? Or, what if file is too short?
168 readSize = min(length, SENDFILE_LIMIT)
169 res = SendfileBuffer(self.f, self.start, readSize)
170 self.length -= readSize
171 self.start += readSize
174 if self.useMMap and length > MMAP_THRESHOLD:
175 readSize = min(length, MMAP_LIMIT)
177 res = mmapwrapper(self.f.fileno(), readSize,
178 access=mmap.ACCESS_READ, offset=self.start)
179 #madvise(res, MADV_SEQUENTIAL)
180 self.length -= readSize
181 self.start += readSize
185 # Fall back to standard read.
186 readSize = self.readsize #this is the only changed line :} 3c5x9 #min(length, self.CHUNK_SIZE)
188 self.f.seek(self.start)
189 b = self.f.read(readSize)
192 raise RuntimeError("Ran out of data reading file %r, expected %d more bytes" % (self.f, length))
194 self.length -= bytesRead
195 self.start += bytesRead
198 w1 = req.uri.split("?")[1]
205 return http.Response(responsecode.OK, stream="no file given with file=???")
206 if parts.has_key("file"):
207 path = "/hdd/movie/"+parts["file"].replace("%20"," ").replace("+"," ")
208 if os.path.exists(path):
209 self.filehandler = open(path,"r")
210 s = myFileStream(self.filehandler)
211 return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
213 return http.Response(responsecode.OK, stream="file was not found in /media/hdd/movie/")
215 return http.Response(responsecode.OK, stream="no file given with file=???")
217 def autostart(reason, **kwargs):
218 if "session" in kwargs:
220 sessions.append(kwargs["session"])
225 except ImportError,e:
226 print "[WebIf] twisted not available, not starting web services",e
228 def openconfig(session, **kwargs):
229 session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
231 def configCB(result):
233 print "[WebIf] config changed"
236 print "[WebIf] config not changed"
239 def Plugins(**kwargs):
240 return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
241 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
244 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
246 set it only to True, if you have a patched wrapper.py
247 see http://twistedmatrix.com/trac/ticket/2041
248 so, the solution for us is to make a new class an override ne faulty func
251 def locateChild(self, req, seg):
252 return self.authenticate(req), seg
254 class PasswordDatabase:
256 this checks webiflogins agains /etc/passwd
258 passwordfile = "/etc/passwd"
259 implements(checkers.ICredentialsChecker)
260 credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
262 def _cbPasswordMatch(self, matched, username):
266 return failure.Failure(error.UnauthorizedLogin())
268 def requestAvatarId(self, credentials):
269 if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
270 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
272 return defer.fail(error.UnauthorizedLogin())
274 class IHTTPUser(Interface):
277 class HTTPUser(object):
278 implements(IHTTPUser)
280 class HTTPAuthRealm(object):
282 def requestAvatar(self, avatarId, mind, *interfaces):
283 if IHTTPUser in interfaces:
284 return IHTTPUser, HTTPUser()
285 raise NotImplementedError("Only IHTTPUser interface is supported")
288 import md5,time,string,crypt
289 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
290 def getpwnam(name, pwfile=None):
291 """Return pasword database entry for the given user name.
293 Example from the Python Library Reference.
297 pwfile = '/etc/passwd'
305 entry = tuple(line.strip().split(':', 6))
310 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
311 """Encrypt a string according to rules in crypt(3)."""
312 if method.lower() == 'des':
313 return crypt.crypt(passwd, salt)
314 elif method.lower() == 'md5':
315 return passcrypt_md5(passwd, salt, magic)
316 elif method.lower() == 'clear':
319 def check_passwd(name, passwd, pwfile=None):
320 """Validate given user, passwd pair against password database."""
322 if not pwfile or type(pwfile) == type(''):
323 getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
325 getuser = pwfile.get_passwd
328 enc_passwd = getuser(name)
329 except (KeyError, IOError):
333 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
334 salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
335 return enc_passwd == passcrypt(passwd, salt, 'md5')
338 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
343 r = r + DES_SALT[v & 0x3F]
348 def passcrypt_md5(passwd, salt=None, magic='$1$'):
349 """Encrypt passwd with MD5 algorithm."""
353 elif salt[:len(magic)] == magic:
354 # remove magic from salt if present
355 salt = salt[len(magic):]
357 # salt only goes up to first '$'
358 salt = string.split(salt, '$')[0]
359 # limit length of salt to 8
362 ctx = md5.new(passwd)
366 ctx1 = md5.new(passwd)
370 final = ctx1.digest()
372 for i in range(len(passwd), 0 , -16):
376 ctx.update(final[:i])
383 ctx.update(passwd[:1])
387 for i in range(1000):
393 if i % 3: ctx1.update(salt)
394 if i % 7: ctx1.update(passwd)
399 final = ctx1.digest()
401 rv = magic + salt + '$'
402 final = map(ord, final)
403 l = (final[0] << 16) + (final[6] << 8) + final[12]
404 rv = rv + _to64(l, 4)
405 l = (final[1] << 16) + (final[7] << 8) + final[13]
406 rv = rv + _to64(l, 4)
407 l = (final[2] << 16) + (final[8] << 8) + final[14]
408 rv = rv + _to64(l, 4)
409 l = (final[3] << 16) + (final[9] << 8) + final[15]
410 rv = rv + _to64(l, 4)
411 l = (final[4] << 16) + (final[10] << 8) + final[5]
412 rv = rv + _to64(l, 4)
414 rv = rv + _to64(l, 2)