2 from Plugins.Plugin import PluginDescriptor
3 from Components.config import config, getConfigListEntry, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText, ConfigSelection, ConfigSubList
4 from Screens.MessageBox import MessageBox
5 from WebIfConfig import WebIfConfigScreen, initConfig
6 from WebChilds.Toplevel import Toplevel
7 from twisted.internet import reactor, defer, ssl
8 from twisted.internet.error import CannotListenError
9 from twisted.web2 import server, channel, http
10 from twisted.web2.auth import digest, basic, wrapper
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
15 from socket import gethostname as socket_gethostname
16 from OpenSSL import SSL
18 from __init__ import _, __version__
20 DEBUG_TO_FILE=False # PLEASE DONT ENABLE LOGGING BY DEFAULT (OR COMMIT TO PLUGIN CVS)
22 DEBUGFILE= "/tmp/twisted.log"
27 config.plugins.Webinterface = ConfigSubsection()
28 config.plugins.Webinterface.enable = ConfigYesNo(default = True)
29 config.plugins.Webinterface.allowzapping = ConfigYesNo(default = True)
30 config.plugins.Webinterface.includehdd = ConfigYesNo(default = False)
31 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default = False)
32 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default = False)
33 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
34 config.plugins.Webinterface.interfacecount = ConfigInteger(0)
35 config.plugins.Webinterface.interfaces = ConfigSubList()
36 config.plugins.Webinterface.warningsslsend = ConfigYesNo(default = False)
39 global running_defered,waiting_shutdown
42 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi","").replace("sion: ","").replace("$","")
46 def __init__(self,session, callback = None):
47 self.callback = callback
48 self.session = session
51 global running_defered
52 for d in running_defered:
53 print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
56 x.addCallback(self.isDown)
58 except AttributeError:
62 if self.callback is not None:
63 self.callback(self.session)
68 if self.callback is not None:
69 self.callback(self.session)
72 def restartWebserver(session):
74 del session.mediaplayer
75 del session.messageboxanswer
78 except AttributeError:
81 global running_defered
82 if len(running_defered) >0:
83 Closer(session,startWebserver).stop()
85 startWebserver(session)
87 def startWebserver(session):
88 global running_defered
89 session.mediaplayer = None
90 session.messageboxanswer = None
92 if config.plugins.Webinterface.enable.value is not True:
93 print "not starting Werbinterface"
96 print "start twisted logfile, writing to %s" % DEBUGFILE
97 startLogging(open(DEBUGFILE,'w'))
99 for i in range(0, config.plugins.Webinterface.interfacecount.value):
100 c = config.plugins.Webinterface.interfaces[i]
101 if c.disabled.value is False:
102 startServerInstance(session,c.address.value,c.port.value,c.useauth.value,c.usessl.value)
104 print "[Webinterface] not starting disabled interface on %s:%i"%(c.address.value,c.port.value)
107 def stopWebserver(session):
109 del session.mediaplayer
110 del session.messageboxanswer
113 except AttributeError:
116 global running_defered
117 if len(running_defered) > 0:
118 Closer(session).stop()
120 def startServerInstance(session,ipaddress,port,useauth=False,usessl=False):
122 toplevel = Toplevel(session)
124 portal = Portal(HTTPAuthRealm())
125 portal.registerChecker(PasswordDatabase())
126 root = wrapper.HTTPAuthResource(toplevel,(basic.BasicCredentialFactory(socket_gethostname()),),portal, (IHTTPUser,))
127 site = server.Site(root)
129 site = server.Site(toplevel)
132 ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem','/etc/enigma2/cacert.pem',sslmethod=SSL.SSLv23_METHOD)
133 d = reactor.listenSSL(port, channel.HTTPFactory(site),ctx,interface=ipaddress)
135 d = reactor.listenTCP(port, channel.HTTPFactory(site),interface=ipaddress)
136 running_defered.append(d)
137 print "[Webinterface] started on %s:%i"%(ipaddress,port),"auth=",useauth,"ssl=",usessl
138 except CannotListenError, e:
139 print "[Webinterface] Could not Listen on %s:%i!"%(ipaddress,port)
140 session.open(MessageBox,'Could not Listen on %s:%i!\n\n%s'%(ipaddress,port,str(e)), MessageBox.TYPE_ERROR)
142 print "[Webinterface] starting FAILED on %s:%i!"%(ipaddress,port),e
143 session.open(MessageBox,'starting FAILED on %s:%i!\n\n%s'%(ipaddress,port,str(e)), MessageBox.TYPE_ERROR)
146 class PasswordDatabase:
148 this checks webiflogins agains /etc/passwd
150 passwordfile = "/etc/passwd"
151 implements(checkers.ICredentialsChecker)
152 credentialInterfaces = (credentials.IUsernamePassword,credentials.IUsernameHashedPassword)
154 def _cbPasswordMatch(self, matched, username):
158 return failure.Failure(error.UnauthorizedLogin())
160 def requestAvatarId(self, credentials):
161 if check_passwd(credentials.username,credentials.password,self.passwordfile) is True:
162 return defer.maybeDeferred(credentials.checkPassword,credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
164 return defer.fail(error.UnauthorizedLogin())
166 class IHTTPUser(Interface):
169 class HTTPUser(object):
170 implements(IHTTPUser)
172 def __init__(self,username):
173 self.username = username
175 class HTTPAuthRealm(object):
177 def requestAvatar(self, avatarId, mind, *interfaces):
178 if IHTTPUser in interfaces:
179 return IHTTPUser, HTTPUser(avatarId)
180 raise NotImplementedError("Only IHTTPUser interface is supported")
182 from hashlib import md5 as md5_new
183 from crypt import crypt
185 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
186 def getpwnam(name, pwfile=None):
187 """Return pasword database entry for the given user name.
189 Example from the Python Library Reference.
193 pwfile = '/etc/passwd'
201 entry = tuple(line.strip().split(':', 6))
206 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
207 """Encrypt a string according to rules in crypt(3)."""
208 if method.lower() == 'des':
209 return crypt(passwd, salt)
210 elif method.lower() == 'md5':
211 return passcrypt_md5(passwd, salt, magic)
212 elif method.lower() == 'clear':
215 def check_passwd(name, passwd, pwfile=None):
216 """Validate given user, passwd pair against password database."""
218 if not pwfile or type(pwfile) == type(''):
219 getuser = lambda x,pwfile=pwfile: getpwnam(x,pwfile)[1]
221 getuser = pwfile.get_passwd
224 enc_passwd = getuser(name)
225 except (KeyError, IOError):
229 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
230 salt = enc_passwd[3:enc_passwd.find('$', 3)]
231 return enc_passwd == passcrypt(passwd, salt, 'md5')
234 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
239 r = r + DES_SALT[v & 0x3F]
244 def passcrypt_md5(passwd, salt=None, magic='$1$'):
245 """Encrypt passwd with MD5 algorithm."""
249 elif salt[:len(magic)] == magic:
250 # remove magic from salt if present
251 salt = salt[len(magic):]
253 # salt only goes up to first '$'
254 salt = salt.split('$')[0]
255 # limit length of salt to 8
258 ctx = md5_new(passwd)
262 ctx1 = md5_new(passwd)
266 final = ctx1.digest()
268 for i in range(len(passwd), 0 , -16):
272 ctx.update(final[:i])
279 ctx.update(passwd[:1])
283 for i in range(1000):
289 if i % 3: ctx1.update(salt)
290 if i % 7: ctx1.update(passwd)
295 final = ctx1.digest()
297 rv = magic + salt + '$'
298 final = map(ord, final)
299 l = (final[0] << 16) + (final[6] << 8) + final[12]
300 rv = rv + _to64(l, 4)
301 l = (final[1] << 16) + (final[7] << 8) + final[13]
302 rv = rv + _to64(l, 4)
303 l = (final[2] << 16) + (final[8] << 8) + final[14]
304 rv = rv + _to64(l, 4)
305 l = (final[3] << 16) + (final[9] << 8) + final[15]
306 rv = rv + _to64(l, 4)
307 l = (final[4] << 16) + (final[10] << 8) + final[5]
308 rv = rv + _to64(l, 4)
310 rv = rv + _to64(l, 2)
314 #### stuff for SSL Support
315 def makeSSLContext(myKey,trustedCA):
316 '''Returns an ssl Context Object
317 @param myKey a pem formated key and certifcate with for my current host
318 the other end of this connection must have the cert from the CA
320 @param trustedCA a pem formated certificat from a CA you trust
321 you will only allow connections from clients signed by this CA
322 and you will only allow connections to a server signed by this CA
325 # our goal in here is to make a SSLContext object to pass to connectSSL
328 # Why these functioins... Not sure...
331 theCert = ssl.PrivateCertificate.loadPEM(ss)
333 fd = open(trustedCA,'r')
334 theCA = ssl.Certificate.loadPEM(fd.read())
336 #ctx = theCert.options(theCA)
337 ctx = theCert.options()
339 # Now the options you can set look like Standard OpenSSL Library options
341 # The SSL protocol to use, one of SSLv23_METHOD, SSLv2_METHOD,
342 # SSLv3_METHOD, TLSv1_METHOD. Defaults to TLSv1_METHOD.
343 ctx.method = ssl.SSL.TLSv1_METHOD
345 # If True, verify certificates received from the peer and fail
346 # the handshake if verification fails. Otherwise, allow anonymous
347 # sessions and sessions with certificates which fail validation.
350 # Depth in certificate chain down to which to verify.
353 # If True, do not allow anonymous sessions.
354 ctx.requireCertification = True
356 # If True, do not re-verify the certificate on session resumption.
357 ctx.verifyOnce = True
359 # If True, generate a new key whenever ephemeral DH parameters are used
360 # to prevent small subgroup attacks.
361 ctx.enableSingleUseKeys = True
363 # If True, set a session ID on each context. This allows a shortened
364 # handshake to be used when a known client reconnects.
365 ctx.enableSessions = True
367 # If True, enable various non-spec protocol fixes for broken
368 # SSL implementations.
369 ctx.fixBrokenPeers = False
374 global_session = None
376 def sessionstart(reason, session):
377 global global_session
378 global_session = session
380 def autostart(reason, **kwargs):
384 startWebserver(global_session)
385 except ImportError,e:
386 print "[Webinterface] twisted not available, not starting web services", e
387 elif reason is False:
392 def openconfig(session, **kwargs):
393 session.openWithCallback(configCB, WebIfConfigScreen)
395 def configCB(result, session):
397 print "[WebIf] config changed"
398 restartWebserver(session)
400 print "[WebIf] config not changed"
403 def Plugins(**kwargs):
404 return [PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART], fnc = sessionstart),
405 PluginDescriptor(where = [PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc = autostart),
406 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
407 where = [PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png",fnc = openconfig)]