2 __version__ = "Beta 0.98.5"
3 from Plugins.Plugin import PluginDescriptor
4 from Components.config import config, ConfigSubsection, ConfigInteger,ConfigYesNo,ConfigText
5 from Components.Network import Network
7 from twisted.internet import reactor, defer
8 from twisted.web2 import server, channel, http
9 from twisted.web2.auth import digest, basic, wrapper
10 #from twisted.python import util
11 from twisted.python.log import startLogging
12 from twisted.cred.portal import Portal, IRealm
13 from twisted.cred import checkers, credentials, error
14 from zope.interface import Interface, implements
16 from WebIfConfig import WebIfConfigScreen
18 from WebChilds.Toplevel import Toplevel
20 from Tools.BoundFunction import boundFunction
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.autowritetimer = ConfigYesNo(default = False)
28 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default = False)
29 config.plugins.Webinterface.debug = ConfigYesNo(default = False) # False by default, not confgurable in GUI. Edit settingsfile directly if needed
30 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
34 set DEBUG to True, if twisted should write logoutput to a file.
35 in normal console output, twisted will print only the first Traceback.
36 is this a bug in twisted or a conflict with enigma2?
37 with this option enabled, twisted will print all TB to the logfile
38 use tail -f <file> to view this log
42 DEBUGFILE= "/tmp/twisted.log"
44 global running_defered,waiting_shutdown
50 def __init__(self,session, callback):
51 self.callback = callback
52 self.session = session
55 global running_defered
56 for d in running_defered:
57 print "[WebIf] STOPPING reactor on interface ",d.interface," with port",d.port
60 x.addCallback(self.isDown)
62 except AttributeError:
66 self.callback(self.session)
71 self.callback(self.session)
74 def restartWebserver(session):
76 del session.mediaplayer
77 del session.messageboxanswer
80 except AttributeError:
83 global running_defered
84 if len(running_defered) >0:
85 Closer(session,startWebserver).stop()
87 startWebserver(session)
89 def startWebserver(session):
90 global running_defered
92 # variables, that are needed in the process
93 session.mediaplayer = None
94 session.messageboxanswer = None
96 if config.plugins.Webinterface.enable.value is not True:
97 print "not starting Werbinterface"
99 if config.plugins.Webinterface.debug.value:
100 print "start twisted logfile, writing to %s" % DEBUGFILE
101 startLogging(open(DEBUGFILE,'w'))
103 toplevel = Toplevel(session)
104 if config.plugins.Webinterface.useauth.value is False:
105 site = server.Site(toplevel)
107 portal = Portal(HTTPAuthRealm())
108 portal.registerChecker(PasswordDatabase())
109 root = ModifiedHTTPAuthResource(toplevel,(basic.BasicCredentialFactory('DM7025'),),portal, (IHTTPUser,))
110 site = server.Site(root)
112 # here we start the Toplevel without any username or password
113 # this allows access to all request over the iface 127.0.0.1 without any auth
114 localsite = server.Site(toplevel)
115 d = reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(localsite),interface='127.0.0.1')
116 running_defered.append(d)
117 # and here we make the Toplevel public to our external ifaces
118 # it depends on the config, if this is with auth support
119 # keep in mind, if we have a second external ip (like a wlan device), we have to do it in the same way for this iface too
121 for adaptername in nw.ifaces:
122 extip = nw.ifaces[adaptername]['ip']
123 if nw.ifaces[adaptername]['up'] is True:
124 extip = "%i.%i.%i.%i"%(extip[0],extip[1],extip[2],extip[3])
125 print "[WebIf] starting Webinterface on port %s on interface %s with address %s"%(str(config.plugins.Webinterface.port.value),adaptername,extip)
127 d = reactor.listenTCP(config.plugins.Webinterface.port.value, channel.HTTPFactory(site),interface=extip)
128 running_defered.append(d)
130 print "[WebIf] Error starting Webinterface on port %s on interface %s with address %s,because \n%s"%(str(config.plugins.Webinterface.port.value),adaptername,extip,e)
132 print "[WebIf] found configured interface %s, but it is not running. so not starting a server on it ..." % adaptername
134 print "\n\nSomething went wrong on starting the webif. May the following Line can help to find the error:\n",e,"\n\n"
136 def autostart(reason, **kwargs):
137 if "session" in kwargs:
139 startWebserver(kwargs["session"])
140 except ImportError,e:
141 print "[WebIf] twisted not available, not starting web services",e
143 def openconfig(session, **kwargs):
144 session.openWithCallback(configCB,WebIfConfigScreen)
146 def configCB(result,session):
148 print "[WebIf] config changed"
149 restartWebserver(session)
151 print "[WebIf] config not changed"
154 def Plugins(**kwargs):
155 return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
156 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]
159 class ModifiedHTTPAuthResource(wrapper.HTTPAuthResource):
161 set it only to True, if you have a patched wrapper.py
162 see http://twistedmatrix.com/trac/ticket/2041
163 so, the solution for us is to make a new class an override ne faulty func
166 def locateChild(self, req, seg):
167 return self.authenticate(req), seg
169 class PasswordDatabase:
171 this checks webiflogins agains /etc/passwd
173 passwordfile = "/etc/passwd"
174 implements(checkers.ICredentialsChecker)
175 credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
177 def _cbPasswordMatch(self, matched, username):
181 return failure.Failure(error.UnauthorizedLogin())
183 def requestAvatarId(self, credentials):
184 if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
185 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
187 return defer.fail(error.UnauthorizedLogin())
189 class IHTTPUser(Interface):
192 class HTTPUser(object):
193 implements(IHTTPUser)
195 class HTTPAuthRealm(object):
197 def requestAvatar(self, avatarId, mind, *interfaces):
198 if IHTTPUser in interfaces:
199 return IHTTPUser, HTTPUser()
200 raise NotImplementedError("Only IHTTPUser interface is supported")
203 from string import find, split
204 from md5 import new as md5_new
205 from crypt import crypt
207 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
208 def getpwnam(name, pwfile=None):
209 """Return pasword database entry for the given user name.
211 Example from the Python Library Reference.
215 pwfile = '/etc/passwd'
223 entry = tuple(line.strip().split(':', 6))
228 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
229 """Encrypt a string according to rules in crypt(3)."""
230 if method.lower() == 'des':
231 return crypt(passwd, salt)
232 elif method.lower() == 'md5':
233 return passcrypt_md5(passwd, salt, magic)
234 elif method.lower() == 'clear':
237 def check_passwd(name, passwd, pwfile=None):
238 """Validate given user, passwd pair against password database."""
240 if not pwfile or type(pwfile) == type(''):
241 getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
243 getuser = pwfile.get_passwd
246 enc_passwd = getuser(name)
247 except (KeyError, IOError):
251 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
252 salt = enc_passwd[3:find(enc_passwd, '$', 3)]
253 return enc_passwd == passcrypt(passwd, salt, 'md5')
256 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
261 r = r + DES_SALT[v & 0x3F]
266 def passcrypt_md5(passwd, salt=None, magic='$1$'):
267 """Encrypt passwd with MD5 algorithm."""
271 elif salt[:len(magic)] == magic:
272 # remove magic from salt if present
273 salt = salt[len(magic):]
275 # salt only goes up to first '$'
276 salt = split(salt, '$')[0]
277 # limit length of salt to 8
280 ctx = md5_new(passwd)
284 ctx1 = md5_new(passwd)
288 final = ctx1.digest()
290 for i in range(len(passwd), 0 , -16):
294 ctx.update(final[:i])
301 ctx.update(passwd[:1])
305 for i in range(1000):
311 if i % 3: ctx1.update(salt)
312 if i % 7: ctx1.update(passwd)
317 final = ctx1.digest()
319 rv = magic + salt + '$'
320 final = map(ord, final)
321 l = (final[0] << 16) + (final[6] << 8) + final[12]
322 rv = rv + _to64(l, 4)
323 l = (final[1] << 16) + (final[7] << 8) + final[13]
324 rv = rv + _to64(l, 4)
325 l = (final[2] << 16) + (final[8] << 8) + final[14]
326 rv = rv + _to64(l, 4)
327 l = (final[3] << 16) + (final[9] << 8) + final[15]
328 rv = rv + _to64(l, 4)
329 l = (final[4] << 16) + (final[10] << 8) + final[5]
330 rv = rv + _to64(l, 4)
332 rv = rv + _to64(l, 2)