2 from Plugins.Plugin import PluginDescriptor
3 from Components.config import config, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText, ConfigSubList
4 from Screens.MessageBox import MessageBox
5 from WebIfConfig import WebIfConfigScreen, initConfig, updateConfig
6 from WebChilds.Toplevel import getToplevel
8 from twisted.internet import reactor, ssl
9 from twisted.web import server, http, util, static, resource
10 from twisted.python.log import startLogging
12 from zope.interface import Interface, implements
13 from socket import gethostname as socket_gethostname
14 from OpenSSL import SSL
16 from __init__ import _, __version__
18 DEBUG_TO_FILE = False # PLEASE DONT ENABLE LOGGING BY DEFAULT (OR COMMIT TO PLUGIN CVS)
20 DEBUGFILE = "/tmp/twisted.log"
25 config.plugins.Webinterface = ConfigSubsection()
26 config.plugins.Webinterface.enable = ConfigYesNo(default=True)
27 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
28 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
29 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
30 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=False)
31 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
32 config.plugins.Webinterface.interfacecount = ConfigInteger(0)
33 config.plugins.Webinterface.interfaces = ConfigSubList()
35 config.plugins.Webinterface.warningsslsend = ConfigYesNo(default=False)
37 global running_defered, waiting_shutdown
40 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
44 def __init__(self, session, callback=None):
45 self.callback = callback
46 self.session = session
49 global running_defered
50 for d in running_defered:
51 print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
54 x.addCallback(self.isDown)
56 except AttributeError:
60 if self.callback is not None:
61 self.callback(self.session)
66 if self.callback is not None:
67 self.callback(self.session)
69 def restartWebserver(session):
71 del session.mediaplayer
72 del session.messageboxanswer
75 except AttributeError:
78 global running_defered
79 if len(running_defered) > 0:
80 Closer(session, startWebserver).stop()
82 startWebserver(session)
84 def startWebserver(session):
85 global running_defered
86 session.mediaplayer = None
87 session.messageboxanswer = None
89 if config.plugins.Webinterface.enable.value is not True:
90 print "[Webinterface] is disabled!"
93 print "[Webinterface] start twisted logfile, writing to %s" % DEBUGFILE
94 startLogging(open(DEBUGFILE, 'w'))
96 for c in config.plugins.Webinterface.interfaces:
97 if c.disabled.value is False:
98 startServerInstance(session, c.address.value, c.port.value, c.useauth.value, c.usessl.value)
100 print "[Webinterface] not starting disabled interface on %s:%i" % (c.address.value, c.port.value)
102 def stopWebserver(session):
104 del session.mediaplayer
105 del session.messageboxanswer
108 except AttributeError:
111 global running_defered
112 if len(running_defered) > 0:
113 Closer(session).stop()
115 def startServerInstance(session, ipaddress, port, useauth=False, usessl=False):
117 toplevel = getToplevel(session)
119 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
120 site = server.Site(root)
122 site = server.Site(toplevel)
125 ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem', '/etc/enigma2/cacert.pem', sslmethod=SSL.SSLv23_METHOD)
126 d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
128 d = reactor.listenTCP(port, site, interface=ipaddress)
129 running_defered.append(d)
130 print "[Webinterface] started on %s:%i" % (ipaddress, port), "auth=", useauth, "ssl=", usessl
133 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
134 session.open(MessageBox, 'starting FAILED on %s:%i!\n\n%s' % (ipaddress, port, str(e)), MessageBox.TYPE_ERROR)
136 class HTTPAuthResource(resource.Resource):
137 def __init__(self, res, realm):
138 resource.Resource.__init__(self)
141 self.authorized = False
143 self.unauthorizedResource = UnauthorizedResource(self.realm)
145 def unautorized(self, request):
146 print "[Webinterface] Unauthorized!"
147 request.setResponseCode(http.UNAUTHORIZED)
148 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
150 return self.unauthorizedResource
152 def isAuthenticated(self, request):
153 # get the Session from the Request
154 sessionNs = request.getSession().sessionNamespaces
156 # if the auth-information has not yet been stored to the session
157 if not sessionNs.has_key('authenticated'):
158 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
160 #if the auth-information already is in the session
162 if sessionNs['authenticated'] is False:
163 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
165 #return the current authentication status
166 return sessionNs['authenticated']
169 def render(self, request):
170 if self.isAuthenticated(request) is True:
171 return self.resource.render(request)
174 return self.unautorized(request)
177 def getChildWithDefault(self, path, request):
178 if self.isAuthenticated(request) is True:
179 return self.resource.getChildWithDefault(path, request)
182 return self.unautorized(request)
184 class UnauthorizedResource(resource.Resource):
185 def __init__(self, realm):
186 resource.Resource.__init__(self)
188 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
190 def render(self, request):
191 return self.errorpage.render(request)
193 from hashlib import md5 as md5_new
194 from crypt import crypt
196 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
197 def getpwnam(name, pwfile=None):
198 """Return pasword database entry for the given user name.
200 Example from the Python Library Reference.
204 pwfile = '/etc/passwd'
212 entry = tuple(line.strip().split(':', 6))
217 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
218 """Encrypt a string according to rules in crypt(3)."""
219 if method.lower() == 'des':
220 return crypt(passwd, salt)
221 elif method.lower() == 'md5':
222 return passcrypt_md5(passwd, salt, magic)
223 elif method.lower() == 'clear':
226 def check_passwd(name, passwd, pwfile='/etc/passwd'):
227 """Validate given user, passwd pair against password database."""
229 if not pwfile or type(pwfile) == type(''):
230 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
232 getuser = pwfile.get_passwd
235 enc_passwd = getuser(name)
236 except (KeyError, IOError):
242 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
243 salt = enc_passwd[3:enc_passwd.find('$', 3)]
244 return enc_passwd == passcrypt(passwd, salt, 'md5')
246 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
251 r = r + DES_SALT[v & 0x3F]
256 def passcrypt_md5(passwd, salt=None, magic='$1$'):
257 """Encrypt passwd with MD5 algorithm."""
261 elif salt[:len(magic)] == magic:
262 # remove magic from salt if present
263 salt = salt[len(magic):]
265 # salt only goes up to first '$'
266 salt = salt.split('$')[0]
267 # limit length of salt to 8
270 ctx = md5_new(passwd)
274 ctx1 = md5_new(passwd)
278 final = ctx1.digest()
280 for i in range(len(passwd), 0 , -16):
284 ctx.update(final[:i])
291 ctx.update(passwd[:1])
295 for i in range(1000):
301 if i % 3: ctx1.update(salt)
302 if i % 7: ctx1.update(passwd)
307 final = ctx1.digest()
309 rv = magic + salt + '$'
310 final = map(ord, final)
311 l = (final[0] << 16) + (final[6] << 8) + final[12]
312 rv = rv + _to64(l, 4)
313 l = (final[1] << 16) + (final[7] << 8) + final[13]
314 rv = rv + _to64(l, 4)
315 l = (final[2] << 16) + (final[8] << 8) + final[14]
316 rv = rv + _to64(l, 4)
317 l = (final[3] << 16) + (final[9] << 8) + final[15]
318 rv = rv + _to64(l, 4)
319 l = (final[4] << 16) + (final[10] << 8) + final[5]
320 rv = rv + _to64(l, 4)
322 rv = rv + _to64(l, 2)
326 #### stuff for SSL Support
327 def makeSSLContext(myKey, trustedCA):
328 '''Returns an ssl Context Object
329 @param myKey a pem formated key and certifcate with for my current host
330 the other end of this connection must have the cert from the CA
332 @param trustedCA a pem formated certificat from a CA you trust
333 you will only allow connections from clients signed by this CA
334 and you will only allow connections to a server signed by this CA
337 # our goal in here is to make a SSLContext object to pass to connectSSL
340 # Why these functioins... Not sure...
341 fd = open(myKey, 'r')
343 theCert = ssl.PrivateCertificate.loadPEM(ss)
345 fd = open(trustedCA, 'r')
346 theCA = ssl.Certificate.loadPEM(fd.read())
348 #ctx = theCert.options(theCA)
349 ctx = theCert.options()
351 # Now the options you can set look like Standard OpenSSL Library options
353 # The SSL protocol to use, one of SSLv23_METHOD, SSLv2_METHOD,
354 # SSLv3_METHOD, TLSv1_METHOD. Defaults to TLSv1_METHOD.
355 ctx.method = ssl.SSL.TLSv1_METHOD
357 # If True, verify certificates received from the peer and fail
358 # the handshake if verification fails. Otherwise, allow anonymous
359 # sessions and sessions with certificates which fail validation.
362 # Depth in certificate chain down to which to verify.
365 # If True, do not allow anonymous sessions.
366 ctx.requireCertification = True
368 # If True, do not re-verify the certificate on session resumption.
369 ctx.verifyOnce = True
371 # If True, generate a new key whenever ephemeral DH parameters are used
372 # to prevent small subgroup attacks.
373 ctx.enableSingleUseKeys = True
375 # If True, set a session ID on each context. This allows a shortened
376 # handshake to be used when a known client reconnects.
377 ctx.enableSessions = True
379 # If True, enable various non-spec protocol fixes for broken
380 # SSL implementations.
381 ctx.fixBrokenPeers = False
385 global_session = None
387 def sessionstart(reason, session):
388 global global_session
389 global_session = session
391 def autostart(reason, **kwargs):
395 startWebserver(global_session)
396 # except ImportError, e:
397 # print "[Webinterface] twisted not available, not starting web services", e
398 elif reason is False:
399 stopWebserver(global_session)
401 def openconfig(session, **kwargs):
402 session.openWithCallback(configCB, WebIfConfigScreen)
404 def configCB(result, session):
406 print "[WebIf] config changed"
407 restartWebserver(session)
409 print "[WebIf] config not changed"
411 def Plugins(**kwargs):
412 return [PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart),
413 PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=autostart),
414 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
415 where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]