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, updateConfig
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.includemedia = 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()
37 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)
71 def restartWebserver(session):
73 del session.mediaplayer
74 del session.messageboxanswer
77 except AttributeError:
80 global running_defered
81 if len(running_defered) > 0:
82 Closer(session, startWebserver).stop()
84 startWebserver(session)
86 def startWebserver(session):
87 global running_defered
88 session.mediaplayer = None
89 session.messageboxanswer = None
91 if config.plugins.Webinterface.enable.value is not True:
92 print "not starting Werbinterface"
95 print "start twisted logfile, writing to %s" % DEBUGFILE
96 startLogging(open(DEBUGFILE, 'w'))
98 for c in config.plugins.Webinterface.interfaces:
99 if c.disabled.value is False:
100 startServerInstance(session, c.address.value, c.port.value, c.useauth.value, c.usessl.value)
102 print "[Webinterface] not starting disabled interface on %s:%i" % (c.address.value, c.port.value)
104 def stopWebserver(session):
106 del session.mediaplayer
107 del session.messageboxanswer
110 except AttributeError:
113 global running_defered
114 if len(running_defered) > 0:
115 Closer(session).stop()
117 def startServerInstance(session, ipaddress, port, useauth=False, usessl=False):
119 toplevel = Toplevel(session)
121 portal = Portal(HTTPAuthRealm())
122 portal.registerChecker(PasswordDatabase())
123 root = wrapper.HTTPAuthResource(toplevel, (basic.BasicCredentialFactory(socket_gethostname()),), portal, (IHTTPUser,))
124 site = server.Site(root)
126 site = server.Site(toplevel)
129 ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem', '/etc/enigma2/cacert.pem', sslmethod=SSL.SSLv23_METHOD)
130 d = reactor.listenSSL(port, channel.HTTPFactory(site), ctx, interface=ipaddress)
132 d = reactor.listenTCP(port, channel.HTTPFactory(site), interface=ipaddress)
133 running_defered.append(d)
134 print "[Webinterface] started on %s:%i" % (ipaddress, port), "auth=", useauth, "ssl=", usessl
135 except CannotListenError, e:
136 print "[Webinterface] Could not Listen on %s:%i!" % (ipaddress, port)
137 session.open(MessageBox, 'Could not Listen on %s:%i!\n\n%s' % (ipaddress, port, str(e)), MessageBox.TYPE_ERROR)
139 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
140 session.open(MessageBox, 'starting FAILED on %s:%i!\n\n%s' % (ipaddress, port, str(e)), MessageBox.TYPE_ERROR)
142 class PasswordDatabase:
144 this checks webiflogins agains /etc/passwd
146 passwordfile = "/etc/passwd"
147 implements(checkers.ICredentialsChecker)
148 credentialInterfaces = (credentials.IUsernamePassword, credentials.IUsernameHashedPassword)
150 def _cbPasswordMatch(self, matched, username):
154 return failure.Failure(error.UnauthorizedLogin())
156 def requestAvatarId(self, credentials):
157 if check_passwd(credentials.username, credentials.password, self.passwordfile) is True:
158 return defer.maybeDeferred(credentials.checkPassword, credentials.password).addCallback(self._cbPasswordMatch, str(credentials.username))
160 return defer.fail(error.UnauthorizedLogin())
162 class IHTTPUser(Interface):
165 class HTTPUser(object):
166 implements(IHTTPUser)
168 def __init__(self, username):
169 self.username = username
171 class HTTPAuthRealm(object):
173 def requestAvatar(self, avatarId, mind, *interfaces):
174 if IHTTPUser in interfaces:
175 return IHTTPUser, HTTPUser(avatarId)
176 raise NotImplementedError("Only IHTTPUser interface is supported")
178 from hashlib import md5 as md5_new
179 from crypt import crypt
181 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
182 def getpwnam(name, pwfile=None):
183 """Return pasword database entry for the given user name.
185 Example from the Python Library Reference.
189 pwfile = '/etc/passwd'
197 entry = tuple(line.strip().split(':', 6))
202 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
203 """Encrypt a string according to rules in crypt(3)."""
204 if method.lower() == 'des':
205 return crypt(passwd, salt)
206 elif method.lower() == 'md5':
207 return passcrypt_md5(passwd, salt, magic)
208 elif method.lower() == 'clear':
211 def check_passwd(name, passwd, pwfile=None):
212 """Validate given user, passwd pair against password database."""
214 if not pwfile or type(pwfile) == type(''):
215 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
217 getuser = pwfile.get_passwd
220 enc_passwd = getuser(name)
221 except (KeyError, IOError):
225 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
226 salt = enc_passwd[3:enc_passwd.find('$', 3)]
227 return enc_passwd == passcrypt(passwd, salt, 'md5')
229 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
234 r = r + DES_SALT[v & 0x3F]
239 def passcrypt_md5(passwd, salt=None, magic='$1$'):
240 """Encrypt passwd with MD5 algorithm."""
244 elif salt[:len(magic)] == magic:
245 # remove magic from salt if present
246 salt = salt[len(magic):]
248 # salt only goes up to first '$'
249 salt = salt.split('$')[0]
250 # limit length of salt to 8
253 ctx = md5_new(passwd)
257 ctx1 = md5_new(passwd)
261 final = ctx1.digest()
263 for i in range(len(passwd), 0 , -16):
267 ctx.update(final[:i])
274 ctx.update(passwd[:1])
278 for i in range(1000):
284 if i % 3: ctx1.update(salt)
285 if i % 7: ctx1.update(passwd)
290 final = ctx1.digest()
292 rv = magic + salt + '$'
293 final = map(ord, final)
294 l = (final[0] << 16) + (final[6] << 8) + final[12]
295 rv = rv + _to64(l, 4)
296 l = (final[1] << 16) + (final[7] << 8) + final[13]
297 rv = rv + _to64(l, 4)
298 l = (final[2] << 16) + (final[8] << 8) + final[14]
299 rv = rv + _to64(l, 4)
300 l = (final[3] << 16) + (final[9] << 8) + final[15]
301 rv = rv + _to64(l, 4)
302 l = (final[4] << 16) + (final[10] << 8) + final[5]
303 rv = rv + _to64(l, 4)
305 rv = rv + _to64(l, 2)
309 #### stuff for SSL Support
310 def makeSSLContext(myKey, trustedCA):
311 '''Returns an ssl Context Object
312 @param myKey a pem formated key and certifcate with for my current host
313 the other end of this connection must have the cert from the CA
315 @param trustedCA a pem formated certificat from a CA you trust
316 you will only allow connections from clients signed by this CA
317 and you will only allow connections to a server signed by this CA
320 # our goal in here is to make a SSLContext object to pass to connectSSL
323 # Why these functioins... Not sure...
324 fd = open(myKey, 'r')
326 theCert = ssl.PrivateCertificate.loadPEM(ss)
328 fd = open(trustedCA, 'r')
329 theCA = ssl.Certificate.loadPEM(fd.read())
331 #ctx = theCert.options(theCA)
332 ctx = theCert.options()
334 # Now the options you can set look like Standard OpenSSL Library options
336 # The SSL protocol to use, one of SSLv23_METHOD, SSLv2_METHOD,
337 # SSLv3_METHOD, TLSv1_METHOD. Defaults to TLSv1_METHOD.
338 ctx.method = ssl.SSL.TLSv1_METHOD
340 # If True, verify certificates received from the peer and fail
341 # the handshake if verification fails. Otherwise, allow anonymous
342 # sessions and sessions with certificates which fail validation.
345 # Depth in certificate chain down to which to verify.
348 # If True, do not allow anonymous sessions.
349 ctx.requireCertification = True
351 # If True, do not re-verify the certificate on session resumption.
352 ctx.verifyOnce = True
354 # If True, generate a new key whenever ephemeral DH parameters are used
355 # to prevent small subgroup attacks.
356 ctx.enableSingleUseKeys = True
358 # If True, set a session ID on each context. This allows a shortened
359 # handshake to be used when a known client reconnects.
360 ctx.enableSessions = True
362 # If True, enable various non-spec protocol fixes for broken
363 # SSL implementations.
364 ctx.fixBrokenPeers = False
368 global_session = None
370 def sessionstart(reason, session):
371 global global_session
372 global_session = session
374 def autostart(reason, **kwargs):
378 startWebserver(global_session)
379 except ImportError, e:
380 print "[Webinterface] twisted not available, not starting web services", e
381 elif reason is False:
382 stopWebserver(global_session)
384 def openconfig(session, **kwargs):
385 session.openWithCallback(configCB, WebIfConfigScreen)
387 def configCB(result, session):
389 print "[WebIf] config changed"
390 restartWebserver(session)
392 print "[WebIf] config not changed"
394 def Plugins(**kwargs):
395 return [PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart),
396 PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=autostart),
397 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
398 where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]