2 from Plugins.Plugin import PluginDescriptor
3 from Components.config import config, ConfigBoolean, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText
4 from Components.Network import iNetwork
5 from Screens.MessageBox import MessageBox
6 from WebIfConfig import WebIfConfigScreen
7 from WebChilds.Toplevel import getToplevel
9 from twisted.internet import reactor, ssl
10 from twisted.web import server, http, util, static, resource
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__
21 config.plugins.Webinterface = ConfigSubsection()
22 config.plugins.Webinterface.enabled = ConfigYesNo(default=True)
23 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
24 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
25 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
26 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=True)
27 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
29 config.plugins.Webinterface.http = ConfigSubsection()
30 config.plugins.Webinterface.http.enabled = ConfigYesNo(default=True)
31 config.plugins.Webinterface.http.port = ConfigInteger(default = 80, limits=(1, 65535) )
32 config.plugins.Webinterface.http.auth = ConfigYesNo(default=False)
34 config.plugins.Webinterface.https = ConfigSubsection()
35 config.plugins.Webinterface.https.enabled = ConfigYesNo(default=True)
36 config.plugins.Webinterface.https.port = ConfigInteger(default = 443, limits=(1, 65535) )
37 config.plugins.Webinterface.https.auth = ConfigYesNo(default=True)
39 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
41 global running_defered, waiting_shutdown, toplevel
46 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
48 #===============================================================================
49 # Helperclass to close running Instances of the Webinterface
50 #===============================================================================
53 def __init__(self, session, callback=None):
54 self.callback = callback
55 self.session = session
57 #===============================================================================
58 # Closes all running Instances of the Webinterface
59 #===============================================================================
61 global running_defered
62 for d in running_defered:
63 print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
66 x.addCallback(self.isDown)
68 except AttributeError:
72 if self.callback is not None:
73 self.callback(self.session)
75 #===============================================================================
76 # #Is it already down?
77 #===============================================================================
81 if self.callback is not None:
82 self.callback(self.session)
84 #===============================================================================
85 # restart the Webinterface for all configured Interfaces
86 #===============================================================================
87 def restartWebserver(session):
89 del session.mediaplayer
90 del session.messageboxanswer
93 except AttributeError:
96 global running_defered
97 if len(running_defered) > 0:
98 Closer(session, startWebserver).stop()
100 startWebserver(session)
102 #===============================================================================
103 # start the Webinterface for all configured Interfaces
104 #===============================================================================
105 def startWebserver(session):
106 print "[Webinterface] startWebserver - iNetwork.ifaces: %s" %(iNetwork.ifaces)
108 global running_defered
111 session.mediaplayer = None
112 session.messageboxanswer = None
114 toplevel = getToplevel(session)
118 if config.plugins.Webinterface.enabled.value is not True:
119 print "[Webinterface] is disabled!"
123 if config.plugins.Webinterface.http.enabled.value is True:
124 for adaptername in iNetwork.ifaces:
125 ip = '.'.join("%d" % d for d in iNetwork.ifaces[adaptername]['ip'])
126 #Network.py sets the IP of inactive Adapters to 0.0.0.0, we do not want to listen on 0.0.0.0
128 ret = startServerInstance(session, ip, config.plugins.Webinterface.http.port.value, config.plugins.Webinterface.http.auth.value)
130 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
132 print "[Webinterface] HTTP is disabled - not starting!"
135 if config.plugins.Webinterface.https.enabled.value is True:
136 for adaptername in iNetwork.ifaces:
137 ip = '.'.join("%d" % d for d in iNetwork.ifaces[adaptername]['ip'])
138 #Network.py sets the IP of inactive Adapters to 0.0.0.0, we do not want to listen on 0.0.0.0
140 ret = startServerInstance(session, ip, config.plugins.Webinterface.https.port.value, config.plugins.Webinterface.https.auth.value, True)
142 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
144 print "[Webinterface] HTTPS is disabled - not starting!"
146 #LOCAL HTTP Connections (Streamproxy)
147 ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.streamauth.value)
149 errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
152 session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), MessageBox.TYPE_ERROR)
154 #===============================================================================
155 # stop the Webinterface for all configured Interfaces
156 #===============================================================================
157 def stopWebserver(session):
159 del session.mediaplayer
160 del session.messageboxanswer
163 except AttributeError:
166 global running_defered
167 if len(running_defered) > 0:
168 Closer(session).stop()
170 #===============================================================================
171 # startServerInstance
172 # Starts an Instance of the Webinterface
173 # on given ipaddress, port, w/o auth, w/o ssl
174 #===============================================================================
175 def startServerInstance(session, ipaddress, port, useauth=False, usessl=False):
178 # HTTPAuthResource handles the authentication for every Resource you want it to
179 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
180 site = server.Site(root)
182 site = server.Site(toplevel)
185 ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem', '/etc/enigma2/cacert.pem', sslmethod=SSL.SSLv23_METHOD)
186 d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
188 d = reactor.listenTCP(port, site, interface=ipaddress)
189 running_defered.append(d)
190 print "[Webinterface] started on %s:%i" % (ipaddress, port), "auth=", useauth, "ssl=", usessl
194 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
196 #===============================================================================
198 # Handles HTTP Authorization for a given Resource
199 #===============================================================================
200 class HTTPAuthResource(resource.Resource):
201 def __init__(self, res, realm):
202 resource.Resource.__init__(self)
205 self.authorized = False
207 self.unauthorizedResource = UnauthorizedResource(self.realm)
209 def unautorized(self, request):
210 request.setResponseCode(http.UNAUTHORIZED)
211 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
213 return self.unauthorizedResource
215 def isAuthenticated(self, request):
216 # get the Session from the Request
217 sessionNs = request.getSession().sessionNamespaces
219 # if the auth-information has not yet been stored to the session
220 if not sessionNs.has_key('authenticated'):
221 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
223 #if the auth-information already is in the session
225 if sessionNs['authenticated'] is False:
226 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
228 #return the current authentication status
229 return sessionNs['authenticated']
231 #===============================================================================
232 # Call render of self.resource (if authenticated)
233 #===============================================================================
234 def render(self, request):
235 if self.isAuthenticated(request) is True:
236 return self.resource.render(request)
239 return self.unautorized(request).render(request)
241 #===============================================================================
242 # Override to call getChildWithDefault of self.resource (if authenticated)
243 #===============================================================================
244 def getChildWithDefault(self, path, request):
245 if self.isAuthenticated(request) is True:
246 return self.resource.getChildWithDefault(path, request)
249 return self.unautorized(request)
251 #===============================================================================
252 # UnauthorizedResource
253 # Returns a simple html-ified "Access Denied"
254 #===============================================================================
255 class UnauthorizedResource(resource.Resource):
256 def __init__(self, realm):
257 resource.Resource.__init__(self)
259 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
261 def getChild(self, path, request):
262 return self.errorpage
264 def render(self, request):
265 return self.errorpage.render(request)
267 # Password verfication stuff
269 from hashlib import md5 as md5_new
270 from crypt import crypt
272 #===============================================================================
275 # Get a password database entry for the given user name
276 # Example from the Python Library Reference.
277 #===============================================================================
278 def getpwnam(name, pwfile=None):
280 pwfile = '/etc/passwd'
288 entry = tuple(line.strip().split(':', 6))
293 #===============================================================================
297 #===============================================================================
298 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
299 """Encrypt a string according to rules in crypt(3)."""
300 if method.lower() == 'des':
301 return crypt(passwd, salt)
302 elif method.lower() == 'md5':
303 return passcrypt_md5(passwd, salt, magic)
304 elif method.lower() == 'clear':
307 #===============================================================================
310 # Checks username and Password against a given Unix Password file
311 # The default path is '/etc/passwd'
312 #===============================================================================
313 def check_passwd(name, passwd, pwfile='/etc/passwd'):
314 """Validate given user, passwd pair against password database."""
316 if not pwfile or type(pwfile) == type(''):
317 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
319 getuser = pwfile.get_passwd
322 enc_passwd = getuser(name)
323 except (KeyError, IOError):
329 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
330 salt = enc_passwd[3:enc_passwd.find('$', 3)]
331 return enc_passwd == passcrypt(passwd, salt, 'md5')
333 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
336 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
339 r = r + DES_SALT[v & 0x3F]
344 #===============================================================================
346 # Encrypt a password via md5
347 #===============================================================================
348 def passcrypt_md5(passwd, salt=None, magic='$1$'):
351 elif salt[:len(magic)] == magic:
352 # remove magic from salt if present
353 salt = salt[len(magic):]
355 # salt only goes up to first '$'
356 salt = salt.split('$')[0]
357 # limit length of salt to 8
360 ctx = md5_new(passwd)
364 ctx1 = md5_new(passwd)
368 final = ctx1.digest()
370 for i in range(len(passwd), 0 , -16):
374 ctx.update(final[:i])
381 ctx.update(passwd[:1])
385 for i in range(1000):
391 if i % 3: ctx1.update(salt)
392 if i % 7: ctx1.update(passwd)
397 final = ctx1.digest()
399 rv = magic + salt + '$'
400 final = map(ord, final)
401 l = (final[0] << 16) + (final[6] << 8) + final[12]
402 rv = rv + _to64(l, 4)
403 l = (final[1] << 16) + (final[7] << 8) + final[13]
404 rv = rv + _to64(l, 4)
405 l = (final[2] << 16) + (final[8] << 8) + final[14]
406 rv = rv + _to64(l, 4)
407 l = (final[3] << 16) + (final[9] << 8) + final[15]
408 rv = rv + _to64(l, 4)
409 l = (final[4] << 16) + (final[10] << 8) + final[5]
410 rv = rv + _to64(l, 4)
412 rv = rv + _to64(l, 2)
416 #===============================================================================
417 # Creates an SSL Context to use with twisted.web
418 #===============================================================================
419 def makeSSLContext(myKey, trustedCA):
420 '''Returns an ssl Context Object
421 @param myKey a pem formated key and certifcate with for my current host
422 the other end of this connection must have the cert from the CA
424 @param trustedCA a pem formated certificat from a CA you trust
425 you will only allow connections from clients signed by this CA
426 and you will only allow connections to a server signed by this CA
429 # our goal in here is to make a SSLContext object to pass to connectSSL
432 # Why these functioins... Not sure...
433 fd = open(myKey, 'r')
435 theCert = ssl.PrivateCertificate.loadPEM(ss)
437 fd = open(trustedCA, 'r')
438 theCA = ssl.Certificate.loadPEM(fd.read())
440 #ctx = theCert.options(theCA)
441 ctx = theCert.options()
443 # Now the options you can set look like Standard OpenSSL Library options
445 # The SSL protocol to use, one of SSLv23_METHOD, SSLv2_METHOD,
446 # SSLv3_METHOD, TLSv1_METHOD. Defaults to TLSv1_METHOD.
447 ctx.method = ssl.SSL.TLSv1_METHOD
449 # If True, verify certificates received from the peer and fail
450 # the handshake if verification fails. Otherwise, allow anonymous
451 # sessions and sessions with certificates which fail validation.
454 # Depth in certificate chain down to which to verify.
457 # If True, do not allow anonymous sessions.
458 ctx.requireCertification = True
460 # If True, do not re-verify the certificate on session resumption.
461 ctx.verifyOnce = True
463 # If True, generate a new key whenever ephemeral DH parameters are used
464 # to prevent small subgroup attacks.
465 ctx.enableSingleUseKeys = True
467 # If True, set a session ID on each context. This allows a shortened
468 # handshake to be used when a known client reconnects.
469 ctx.enableSessions = True
471 # If True, enable various non-spec protocol fixes for broken
472 # SSL implementations.
473 ctx.fixBrokenPeers = False
477 global_session = None
479 #===============================================================================
481 # Actions to take place on Session start
482 #===============================================================================
483 def sessionstart(reason, session):
484 global global_session
485 global_session = session
487 #===============================================================================
489 # Actions to take place after Network is up (startup the Webserver)
490 #===============================================================================
491 def networkstart(reason, **kwargs):
493 startWebserver(global_session)
495 elif reason is False:
496 stopWebserver(global_session)
498 def openconfig(session, **kwargs):
499 session.openWithCallback(configCB, WebIfConfigScreen)
501 def configCB(result, session):
503 print "[WebIf] config changed"
504 restartWebserver(session)
506 print "[WebIf] config not changed"
508 def Plugins(**kwargs):
509 return [PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart),
510 PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
511 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
512 where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]