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=False)
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!"
122 for adaptername in iNetwork.ifaces:
123 ip = '.'.join("%d" % d for d in iNetwork.ifaces[adaptername]['ip'])
125 #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 if config.plugins.Webinterface.http.enabled.value is True:
129 ret = startServerInstance(session, ip, config.plugins.Webinterface.http.port.value, config.plugins.Webinterface.http.auth.value)
131 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
133 if config.plugins.Webinterface.https.enabled.value is True:
134 ret = startServerInstance(session, ip, config.plugins.Webinterface.https.port.value, config.plugins.Webinterface.https.auth.value, True)
136 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
138 #LOCAL HTTP Connections (Streamproxy)
139 ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.streamauth.value)
141 errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
144 session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), MessageBox.TYPE_ERROR)
146 #===============================================================================
147 # stop the Webinterface for all configured Interfaces
148 #===============================================================================
149 def stopWebserver(session):
151 del session.mediaplayer
152 del session.messageboxanswer
155 except AttributeError:
158 global running_defered
159 if len(running_defered) > 0:
160 Closer(session).stop()
162 #===============================================================================
163 # startServerInstance
164 # Starts an Instance of the Webinterface
165 # on given ipaddress, port, w/o auth, w/o ssl
166 #===============================================================================
167 def startServerInstance(session, ipaddress, port, useauth=False, usessl=False):
170 # HTTPAuthResource handles the authentication for every Resource you want it to
171 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
172 site = server.Site(root)
174 site = server.Site(toplevel)
177 ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem', '/etc/enigma2/cacert.pem', sslmethod=SSL.SSLv23_METHOD)
178 d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
180 d = reactor.listenTCP(port, site, interface=ipaddress)
181 running_defered.append(d)
182 print "[Webinterface] started on %s:%i" % (ipaddress, port), "auth=", useauth, "ssl=", usessl
186 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
188 #===============================================================================
190 # Handles HTTP Authorization for a given Resource
191 #===============================================================================
192 class HTTPAuthResource(resource.Resource):
193 def __init__(self, res, realm):
194 resource.Resource.__init__(self)
197 self.authorized = False
199 self.unauthorizedResource = UnauthorizedResource(self.realm)
201 def unautorized(self, request):
202 request.setResponseCode(http.UNAUTHORIZED)
203 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
205 return self.unauthorizedResource
207 def isAuthenticated(self, request):
208 # get the Session from the Request
209 sessionNs = request.getSession().sessionNamespaces
211 # if the auth-information has not yet been stored to the session
212 if not sessionNs.has_key('authenticated'):
213 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
215 #if the auth-information already is in the session
217 if sessionNs['authenticated'] is False:
218 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
220 #return the current authentication status
221 return sessionNs['authenticated']
223 #===============================================================================
224 # Call render of self.resource (if authenticated)
225 #===============================================================================
226 def render(self, request):
227 if self.isAuthenticated(request) is True:
228 return self.resource.render(request)
231 return self.unautorized(request).render(request)
233 #===============================================================================
234 # Override to call getChildWithDefault of self.resource (if authenticated)
235 #===============================================================================
236 def getChildWithDefault(self, path, request):
237 if self.isAuthenticated(request) is True:
238 return self.resource.getChildWithDefault(path, request)
241 return self.unautorized(request)
243 #===============================================================================
244 # UnauthorizedResource
245 # Returns a simple html-ified "Access Denied"
246 #===============================================================================
247 class UnauthorizedResource(resource.Resource):
248 def __init__(self, realm):
249 resource.Resource.__init__(self)
251 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
253 def getChild(self, path, request):
254 return self.errorpage
256 def render(self, request):
257 return self.errorpage.render(request)
259 # Password verfication stuff
261 from hashlib import md5 as md5_new
262 from crypt import crypt
264 #===============================================================================
267 # Get a password database entry for the given user name
268 # Example from the Python Library Reference.
269 #===============================================================================
270 def getpwnam(name, pwfile=None):
272 pwfile = '/etc/passwd'
280 entry = tuple(line.strip().split(':', 6))
285 #===============================================================================
289 #===============================================================================
290 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
291 """Encrypt a string according to rules in crypt(3)."""
292 if method.lower() == 'des':
293 return crypt(passwd, salt)
294 elif method.lower() == 'md5':
295 return passcrypt_md5(passwd, salt, magic)
296 elif method.lower() == 'clear':
299 #===============================================================================
302 # Checks username and Password against a given Unix Password file
303 # The default path is '/etc/passwd'
304 #===============================================================================
305 def check_passwd(name, passwd, pwfile='/etc/passwd'):
306 """Validate given user, passwd pair against password database."""
308 if not pwfile or type(pwfile) == type(''):
309 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
311 getuser = pwfile.get_passwd
314 enc_passwd = getuser(name)
315 except (KeyError, IOError):
321 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
322 salt = enc_passwd[3:enc_passwd.find('$', 3)]
323 return enc_passwd == passcrypt(passwd, salt, 'md5')
325 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
328 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
331 r = r + DES_SALT[v & 0x3F]
336 #===============================================================================
338 # Encrypt a password via md5
339 #===============================================================================
340 def passcrypt_md5(passwd, salt=None, magic='$1$'):
343 elif salt[:len(magic)] == magic:
344 # remove magic from salt if present
345 salt = salt[len(magic):]
347 # salt only goes up to first '$'
348 salt = salt.split('$')[0]
349 # limit length of salt to 8
352 ctx = md5_new(passwd)
356 ctx1 = md5_new(passwd)
360 final = ctx1.digest()
362 for i in range(len(passwd), 0 , -16):
366 ctx.update(final[:i])
373 ctx.update(passwd[:1])
377 for i in range(1000):
383 if i % 3: ctx1.update(salt)
384 if i % 7: ctx1.update(passwd)
389 final = ctx1.digest()
391 rv = magic + salt + '$'
392 final = map(ord, final)
393 l = (final[0] << 16) + (final[6] << 8) + final[12]
394 rv = rv + _to64(l, 4)
395 l = (final[1] << 16) + (final[7] << 8) + final[13]
396 rv = rv + _to64(l, 4)
397 l = (final[2] << 16) + (final[8] << 8) + final[14]
398 rv = rv + _to64(l, 4)
399 l = (final[3] << 16) + (final[9] << 8) + final[15]
400 rv = rv + _to64(l, 4)
401 l = (final[4] << 16) + (final[10] << 8) + final[5]
402 rv = rv + _to64(l, 4)
404 rv = rv + _to64(l, 2)
408 #===============================================================================
409 # Creates an SSL Context to use with twisted.web
410 #===============================================================================
411 def makeSSLContext(myKey, trustedCA):
412 '''Returns an ssl Context Object
413 @param myKey a pem formated key and certifcate with for my current host
414 the other end of this connection must have the cert from the CA
416 @param trustedCA a pem formated certificat from a CA you trust
417 you will only allow connections from clients signed by this CA
418 and you will only allow connections to a server signed by this CA
421 # our goal in here is to make a SSLContext object to pass to connectSSL
424 # Why these functioins... Not sure...
425 fd = open(myKey, 'r')
427 theCert = ssl.PrivateCertificate.loadPEM(ss)
429 fd = open(trustedCA, 'r')
430 theCA = ssl.Certificate.loadPEM(fd.read())
432 #ctx = theCert.options(theCA)
433 ctx = theCert.options()
435 # Now the options you can set look like Standard OpenSSL Library options
437 # The SSL protocol to use, one of SSLv23_METHOD, SSLv2_METHOD,
438 # SSLv3_METHOD, TLSv1_METHOD. Defaults to TLSv1_METHOD.
439 ctx.method = ssl.SSL.TLSv1_METHOD
441 # If True, verify certificates received from the peer and fail
442 # the handshake if verification fails. Otherwise, allow anonymous
443 # sessions and sessions with certificates which fail validation.
446 # Depth in certificate chain down to which to verify.
449 # If True, do not allow anonymous sessions.
450 ctx.requireCertification = True
452 # If True, do not re-verify the certificate on session resumption.
453 ctx.verifyOnce = True
455 # If True, generate a new key whenever ephemeral DH parameters are used
456 # to prevent small subgroup attacks.
457 ctx.enableSingleUseKeys = True
459 # If True, set a session ID on each context. This allows a shortened
460 # handshake to be used when a known client reconnects.
461 ctx.enableSessions = True
463 # If True, enable various non-spec protocol fixes for broken
464 # SSL implementations.
465 ctx.fixBrokenPeers = False
469 global_session = None
471 #===============================================================================
473 # Actions to take place on Session start
474 #===============================================================================
475 def sessionstart(reason, session):
476 global global_session
477 global_session = session
479 #===============================================================================
481 # Actions to take place after Network is up (startup the Webserver)
482 #===============================================================================
483 def networkstart(reason, **kwargs):
485 startWebserver(global_session)
487 elif reason is False:
488 stopWebserver(global_session)
490 def openconfig(session, **kwargs):
491 session.openWithCallback(configCB, WebIfConfigScreen)
493 def configCB(result, session):
495 print "[WebIf] config changed"
496 restartWebserver(session)
498 print "[WebIf] config not changed"
500 def Plugins(**kwargs):
501 return [PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart),
502 PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
503 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
504 where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]