3 from enigma import eConsoleAppContainer
4 from Plugins.Plugin import PluginDescriptor
5 from Components.config import config, ConfigBoolean, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText
6 from Components.Network import iNetwork
7 from Screens.MessageBox import MessageBox
8 from WebIfConfig import WebIfConfigScreen
9 from WebChilds.Toplevel import getToplevel
11 from Tools.Directories import copyfile, resolveFilename, SCOPE_PLUGINS, SCOPE_CONFIG
13 from twisted.internet import reactor, ssl
14 from twisted.web import server, http, util, static, resource
16 from zope.interface import Interface, implements
17 from socket import gethostname as socket_gethostname
18 from OpenSSL import SSL
20 from os.path import isfile as os_isfile
24 from __init__ import _, __version__
29 config.plugins.Webinterface = ConfigSubsection()
30 config.plugins.Webinterface.enabled = ConfigYesNo(default=True)
31 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
32 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
33 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
34 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=True)
35 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
37 config.plugins.Webinterface.http = ConfigSubsection()
38 config.plugins.Webinterface.http.enabled = ConfigYesNo(default=True)
39 config.plugins.Webinterface.http.port = ConfigInteger(default = 80, limits=(1, 65535) )
40 config.plugins.Webinterface.http.auth = ConfigYesNo(default=False)
42 config.plugins.Webinterface.https = ConfigSubsection()
43 config.plugins.Webinterface.https.enabled = ConfigYesNo(default=True)
44 config.plugins.Webinterface.https.port = ConfigInteger(default = 443, limits=(1, 65535) )
45 config.plugins.Webinterface.https.auth = ConfigYesNo(default=True)
47 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
49 global running_defered, waiting_shutdown, toplevel
54 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
56 #===============================================================================
57 # Helperclass to close running Instances of the Webinterface
58 #===============================================================================
61 def __init__(self, session, callback=None):
62 self.callback = callback
63 self.session = session
65 #===============================================================================
66 # Closes all running Instances of the Webinterface
67 #===============================================================================
69 global running_defered
70 for d in running_defered:
71 print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
74 x.addCallback(self.isDown)
76 except AttributeError:
80 if self.callback is not None:
81 self.callback(self.session)
83 #===============================================================================
84 # #Is it already down?
85 #===============================================================================
89 if self.callback is not None:
90 self.callback(self.session)
92 def checkCertificates():
93 print "[WebInterface] checking for SSL Certificates"
94 srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG)
95 cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)
97 # Check whether there are regular certificates, if not copy the default ones over
98 if not os_isfile(srvcert) or not os_isfile(cacert):
104 def installCertificates(session, callback = None):
105 print "[WebInterface] Installing SSL Certificates to %s" %resolveFilename(SCOPE_CONFIG)
107 srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG)
108 cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)
109 scope_webif = '%sExtensions/WebInterface/' %resolveFilename(SCOPE_PLUGINS)
111 source = '%setc/server.pem' %scope_webif
113 ret = copyfile(source, target)
116 source = '%setc/cacert.pem' %scope_webif
118 ret = copyfile(source, target)
120 if ret == 0 and callback != None:
124 config.plugins.Webinterface.https.enabled.value = False
125 config.plugins.Webinterface.https.enabled.save()
127 # Start without https
131 session.open(MessageBox, "Couldn't install SSL-Certifactes for https access\nHttps access is now disabled!", MessageBox.TYPE_ERROR)
133 #===============================================================================
134 # restart the Webinterface for all configured Interfaces
135 #===============================================================================
136 def restartWebserver(session):
138 del session.mediaplayer
139 del session.messageboxanswer
142 except AttributeError:
145 global running_defered
146 if len(running_defered) > 0:
147 Closer(session, startWebserver).stop()
149 startWebserver(session)
151 #===============================================================================
152 # start the Webinterface for all configured Interfaces
153 #===============================================================================
154 def startWebserver(session):
155 global running_defered
158 session.mediaplayer = None
159 session.messageboxanswer = None
161 toplevel = getToplevel(session)
165 if config.plugins.Webinterface.enabled.value is not True:
166 print "[Webinterface] is disabled!"
169 # IF SSL is enabled we need to check for the certs first
170 # If they're not there we'll exit via return here
171 # and get called after Certificates are installed properly
172 if config.plugins.Webinterface.https.enabled.value:
173 if not checkCertificates():
174 print "[Webinterface] Installing Webserver Certificates for SSL encryption"
175 installCertificates(session, startWebserver)
178 for adaptername in iNetwork.ifaces:
179 ip = '.'.join("%d" % d for d in iNetwork.ifaces[adaptername]['ip'])
181 #Network.py sets the IP of inactive Adapters to 0.0.0.0, we do not want to listen on 0.0.0.0
184 if config.plugins.Webinterface.http.enabled.value is True:
185 ret = startServerInstance(session, ip, config.plugins.Webinterface.http.port.value, config.plugins.Webinterface.http.auth.value)
187 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
189 if config.plugins.Webinterface.https.enabled.value is True:
190 ret = startServerInstance(session, ip, config.plugins.Webinterface.https.port.value, config.plugins.Webinterface.https.auth.value, True)
192 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
194 #LOCAL HTTP Connections (Streamproxy)
195 ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.streamauth.value)
197 errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
200 session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), MessageBox.TYPE_ERROR)
202 #===============================================================================
203 # stop the Webinterface for all configured Interfaces
204 #===============================================================================
205 def stopWebserver(session):
207 del session.mediaplayer
208 del session.messageboxanswer
211 except AttributeError:
214 global running_defered
215 if len(running_defered) > 0:
216 Closer(session).stop()
218 #===============================================================================
219 # startServerInstance
220 # Starts an Instance of the Webinterface
221 # on given ipaddress, port, w/o auth, w/o ssl
222 #===============================================================================
223 def startServerInstance(session, ipaddress, port, useauth=False, usessl=False):
226 # HTTPAuthResource handles the authentication for every Resource you want it to
227 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
228 site = server.Site(root)
230 site = server.Site(toplevel)
234 ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem', '/etc/enigma2/cacert.pem', sslmethod=SSL.SSLv23_METHOD)
235 d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
237 d = reactor.listenTCP(port, site, interface=ipaddress)
238 running_defered.append(d)
239 print "[Webinterface] started on %s:%i" % (ipaddress, port), "auth=", useauth, "ssl=", usessl
243 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
245 #===============================================================================
247 # Handles HTTP Authorization for a given Resource
248 #===============================================================================
249 class HTTPAuthResource(resource.Resource):
250 def __init__(self, res, realm):
251 resource.Resource.__init__(self)
254 self.authorized = False
256 self.unauthorizedResource = UnauthorizedResource(self.realm)
258 def unautorized(self, request):
259 request.setResponseCode(http.UNAUTHORIZED)
260 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
262 return self.unauthorizedResource
264 def isAuthenticated(self, request):
265 # get the Session from the Request
266 sessionNs = request.getSession().sessionNamespaces
268 # if the auth-information has not yet been stored to the session
269 if not sessionNs.has_key('authenticated'):
270 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
272 #if the auth-information already is in the session
274 if sessionNs['authenticated'] is False:
275 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
277 #return the current authentication status
278 return sessionNs['authenticated']
280 #===============================================================================
281 # Call render of self.resource (if authenticated)
282 #===============================================================================
283 def render(self, request):
284 if self.isAuthenticated(request) is True:
285 return self.resource.render(request)
288 return self.unautorized(request).render(request)
290 #===============================================================================
291 # Override to call getChildWithDefault of self.resource (if authenticated)
292 #===============================================================================
293 def getChildWithDefault(self, path, request):
294 if self.isAuthenticated(request) is True:
295 return self.resource.getChildWithDefault(path, request)
298 return self.unautorized(request)
300 #===============================================================================
301 # UnauthorizedResource
302 # Returns a simple html-ified "Access Denied"
303 #===============================================================================
304 class UnauthorizedResource(resource.Resource):
305 def __init__(self, realm):
306 resource.Resource.__init__(self)
308 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
310 def getChild(self, path, request):
311 return self.errorpage
313 def render(self, request):
314 return self.errorpage.render(request)
316 # Password verfication stuff
318 from hashlib import md5 as md5_new
319 from crypt import crypt
321 #===============================================================================
324 # Get a password database entry for the given user name
325 # Example from the Python Library Reference.
326 #===============================================================================
327 def getpwnam(name, pwfile=None):
329 pwfile = '/etc/passwd'
337 entry = tuple(line.strip().split(':', 6))
342 #===============================================================================
346 #===============================================================================
347 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
348 """Encrypt a string according to rules in crypt(3)."""
349 if method.lower() == 'des':
350 return crypt(passwd, salt)
351 elif method.lower() == 'md5':
352 return passcrypt_md5(passwd, salt, magic)
353 elif method.lower() == 'clear':
356 #===============================================================================
359 # Checks username and Password against a given Unix Password file
360 # The default path is '/etc/passwd'
361 #===============================================================================
362 def check_passwd(name, passwd, pwfile='/etc/passwd'):
363 """Validate given user, passwd pair against password database."""
365 if not pwfile or type(pwfile) == type(''):
366 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
368 getuser = pwfile.get_passwd
371 enc_passwd = getuser(name)
372 except (KeyError, IOError):
378 elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
379 salt = enc_passwd[3:enc_passwd.find('$', 3)]
380 return enc_passwd == passcrypt(passwd, salt, 'md5')
382 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
385 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
388 r = r + DES_SALT[v & 0x3F]
393 #===============================================================================
395 # Encrypt a password via md5
396 #===============================================================================
397 def passcrypt_md5(passwd, salt=None, magic='$1$'):
400 elif salt[:len(magic)] == magic:
401 # remove magic from salt if present
402 salt = salt[len(magic):]
404 # salt only goes up to first '$'
405 salt = salt.split('$')[0]
406 # limit length of salt to 8
409 ctx = md5_new(passwd)
413 ctx1 = md5_new(passwd)
417 final = ctx1.digest()
419 for i in range(len(passwd), 0 , -16):
423 ctx.update(final[:i])
430 ctx.update(passwd[:1])
434 for i in range(1000):
440 if i % 3: ctx1.update(salt)
441 if i % 7: ctx1.update(passwd)
446 final = ctx1.digest()
448 rv = magic + salt + '$'
449 final = map(ord, final)
450 l = (final[0] << 16) + (final[6] << 8) + final[12]
451 rv = rv + _to64(l, 4)
452 l = (final[1] << 16) + (final[7] << 8) + final[13]
453 rv = rv + _to64(l, 4)
454 l = (final[2] << 16) + (final[8] << 8) + final[14]
455 rv = rv + _to64(l, 4)
456 l = (final[3] << 16) + (final[9] << 8) + final[15]
457 rv = rv + _to64(l, 4)
458 l = (final[4] << 16) + (final[10] << 8) + final[5]
459 rv = rv + _to64(l, 4)
461 rv = rv + _to64(l, 2)
465 global_session = None
467 #===============================================================================
469 # Actions to take place on Session start
470 #===============================================================================
471 def sessionstart(reason, session):
472 global global_session
473 global_session = session
475 #===============================================================================
477 # Actions to take place after Network is up (startup the Webserver)
478 #===============================================================================
479 def networkstart(reason, **kwargs):
481 startWebserver(global_session)
483 elif reason is False:
484 stopWebserver(global_session)
486 def openconfig(session, **kwargs):
487 session.openWithCallback(configCB, WebIfConfigScreen)
489 def configCB(result, session):
491 print "[WebIf] config changed"
492 restartWebserver(session)
494 print "[WebIf] config not changed"
496 def Plugins(**kwargs):
497 return [PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart),
498 PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
499 PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
500 where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]