3 from Plugins.Plugin import PluginDescriptor
5 from twisted.internet import reactor, defer
7 from twisted.web2 import server, channel, static, resource, stream, http_headers, responsecode, http
8 from twisted.web2.auth import digest, basic, wrapper
10 from twisted.python import util
11 from twisted.python.log import startLogging,discardLogs
13 from twisted.cred.portal import Portal, IRealm
14 from twisted.cred import checkers, credentials, error
16 from zope.interface import Interface, implements
22 from Components.config import config, ConfigSubsection, ConfigInteger,ConfigYesNo
24 config.plugins.Webinterface = ConfigSubsection()
25 config.plugins.Webinterface.enable = ConfigYesNo(default = True)
26 config.plugins.Webinterface.port = ConfigInteger(80,limits = (1, 65536))
27 config.plugins.Webinterface.includehdd = ConfigYesNo(default = False)
28 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
29 config.plugins.Webinterface.debug = ConfigYesNo(default = False) # False by default, not confgurable in GUI. Edit settingsfile directly if needed
34 define all files in /web to send no XML-HTTP-Headers here
35 all files not listed here will get an Content-Type: application/xhtml+xml charset: UTF-8
37 AppTextHeaderFiles = ['stream.m3u.xml','ts.m3u.xml',]
40 Actualy, the TextHtmlHeaderFiles should contain the updates.html.xml, but the IE then
41 has problems with unicode-characters
43 TextHtmlHeaderFiles = ['wapremote.xml',]
46 define all files in /web to send no XML-HTTP-Headers here
47 all files not listed here will get an Content-Type: text/html charset: UTF-8
49 NoExplicitHeaderFiles = ['getpid.xml','tvbrowser.xml',]
52 set DEBUG to True, if twisted should write logoutput to a file.
53 in normal console output, twisted will print only the first Traceback.
54 is this a bug in twisted or a conflict with enigma2?
55 with this option enabled, twisted will print all TB to the logfile
56 use tail -f <file> to view this log
60 DEBUGFILE= "/tmp/twisted.log"
63 reactor.disconnectAll()
65 def restartWebserver():
70 if config.plugins.Webinterface.enable.value is not True:
71 print "not starting Werbinterface"
73 if config.plugins.Webinterface.debug.value:
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
123 child_wap = static.File(util.sibpath(__file__, "wap")) # static pages for wap
124 child_movie = MovieStreamer()
125 def render(self, req):
126 fp = open(util.sibpath(__file__, "web-data")+"/index.html")
129 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))
146 class MovieStreamer(resource.Resource):
149 def render(self, req):
150 class myFileStream(stream.FileStream):
152 because os.fstat(f.fileno()).st_size returns negative values on
153 large file, we set read() to a fix value
157 def read(self, sendfile=False):
166 if sendfile and length > SENDFILE_THRESHOLD:
167 # XXX: Yay using non-existent sendfile support!
168 # FIXME: if we return a SendfileBuffer, and then sendfile
169 # fails, then what? Or, what if file is too short?
170 readSize = min(length, SENDFILE_LIMIT)
171 res = SendfileBuffer(self.f, self.start, readSize)
172 self.length -= readSize
173 self.start += readSize
176 if self.useMMap and length > MMAP_THRESHOLD:
177 readSize = min(length, MMAP_LIMIT)
179 res = mmapwrapper(self.f.fileno(), readSize,
180 access=mmap.ACCESS_READ, offset=self.start)
181 #madvise(res, MADV_SEQUENTIAL)
182 self.length -= readSize
183 self.start += readSize
187 # Fall back to standard read.
188 readSize = self.readsize #this is the only changed line :} 3c5x9 #min(length, self.CHUNK_SIZE)
190 self.f.seek(self.start)
191 b = self.f.read(readSize)
194 raise RuntimeError("Ran out of data reading file %r, expected %d more bytes" % (self.f, length))
196 self.length -= bytesRead
197 self.start += bytesRead
200 w1 = req.uri.split("?")[1]
207 return http.Response(responsecode.OK, stream="no file given with file=???")
208 if parts.has_key("file"):
209 path = "/hdd/movie/"+parts["file"].replace("%20"," ").replace("+"," ")
210 if os.path.exists(path):
211 self.filehandler = open(path,"r")
212 s = myFileStream(self.filehandler)
213 return http.Response(responsecode.OK, {'Content-type': http_headers.MimeType('text', 'html')},stream=s)
215 return http.Response(responsecode.OK, stream="file was not found in /media/hdd/movie/")
217 return http.Response(responsecode.OK, stream="no file given with file=???")
219 def autostart(reason, **kwargs):
220 if "session" in kwargs:
222 sessions.append(kwargs["session"])
227 except ImportError,e:
228 print "[WebIf] twisted not available, not starting web services",e
230 def openconfig(session, **kwargs):
231 session.openWithCallback(configCB,WebIfConfig.WebIfConfigScreen)
233 def configCB(result):
235 print "[WebIf] config changed"
238 print "[WebIf] config not changed"
241 def Plugins(**kwargs):
242 return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
243 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
246 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
248 set it only to True, if you have a patched wrapper.py
249 see http://twistedmatrix.com/trac/ticket/2041
250 so, the solution for us is to make a new class an override ne faulty func
253 def locateChild(self, req, seg):
254 return self.authenticate(req), seg
256 class PasswordDatabase:
258 this checks webiflogins agains /etc/passwd
260 passwordfile = "/etc/passwd"
261 implements(checkers.ICredentialsChecker)
262 credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
264 def _cbPasswordMatch(self, matched, username):
268 return failure.Failure(error.UnauthorizedLogin())
270 def requestAvatarId(self, credentials):
271 if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
272 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
274 return defer.fail(error.UnauthorizedLogin())
276 class IHTTPUser(Interface):
279 class HTTPUser(object):
280 implements(IHTTPUser)
282 class HTTPAuthRealm(object):
284 def requestAvatar(self, avatarId, mind, *interfaces):
285 if IHTTPUser in interfaces:
286 return IHTTPUser, HTTPUser()
287 raise NotImplementedError("Only IHTTPUser interface is supported")
290 import md5,time,string,crypt
291 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
292 def getpwnam(name, pwfile=None):
293 """Return pasword database entry for the given user name.
295 Example from the Python Library Reference.
299 pwfile = '/etc/passwd'
307 entry = tuple(line.strip().split(':', 6))
312 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
313 """Encrypt a string according to rules in crypt(3)."""
314 if method.lower() == 'des':
315 return crypt.crypt(passwd, salt)
316 elif method.lower() == 'md5':
317 return passcrypt_md5(passwd, salt, magic)
318 elif method.lower() == 'clear':
321 def check_passwd(name, passwd, pwfile=None):
322 """Validate given user, passwd pair against password database."""
324 if not pwfile or type(pwfile) == type(''):
325 getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
327 getuser = pwfile.get_passwd
330 enc_passwd = getuser(name)
331 except (KeyError, IOError):
335 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
336 salt = enc_passwd[3:string.find(enc_passwd, '$', 3)]
337 return enc_passwd == passcrypt(passwd, salt, 'md5')
340 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
345 r = r + DES_SALT[v & 0x3F]
350 def passcrypt_md5(passwd, salt=None, magic='$1$'):
351 """Encrypt passwd with MD5 algorithm."""
355 elif salt[:len(magic)] == magic:
356 # remove magic from salt if present
357 salt = salt[len(magic):]
359 # salt only goes up to first '$'
360 salt = string.split(salt, '$')[0]
361 # limit length of salt to 8
364 ctx = md5.new(passwd)
368 ctx1 = md5.new(passwd)
372 final = ctx1.digest()
374 for i in range(len(passwd), 0 , -16):
378 ctx.update(final[:i])
385 ctx.update(passwd[:1])
389 for i in range(1000):
395 if i % 3: ctx1.update(salt)
396 if i % 7: ctx1.update(passwd)
401 final = ctx1.digest()
403 rv = magic + salt + '$'
404 final = map(ord, final)
405 l = (final[0] << 16) + (final[6] << 8) + final[12]
406 rv = rv + _to64(l, 4)
407 l = (final[1] << 16) + (final[7] << 8) + final[13]
408 rv = rv + _to64(l, 4)
409 l = (final[2] << 16) + (final[8] << 8) + final[14]
410 rv = rv + _to64(l, 4)
411 l = (final[3] << 16) + (final[9] << 8) + final[15]
412 rv = rv + _to64(l, 4)
413 l = (final[4] << 16) + (final[10] << 8) + final[5]
414 rv = rv + _to64(l, 4)
416 rv = rv + _to64(l, 2)