Fix the ugly "missing SSL Certifactes" issue on fresh installed images.
[enigma2-plugins.git] / webinterface / src / plugin.py
1 Version = '$Header$';
2
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
10
11 from Tools.Directories import copyfile, resolveFilename, SCOPE_PLUGINS, SCOPE_CONFIG
12
13 from twisted.internet import reactor, ssl
14 from twisted.web import server, http, util, static, resource
15
16 from zope.interface import Interface, implements
17 from socket import gethostname as socket_gethostname
18 from OpenSSL import SSL
19
20 from os.path import isfile as os_isfile
21
22
23
24 from __init__ import _, __version__
25
26 #CONFIG INIT
27
28 #init the config
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.
36
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)
41
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)
46
47 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
48
49 global running_defered, waiting_shutdown, toplevel
50
51 running_defered = []
52 waiting_shutdown = 0
53 toplevel = None
54 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
55
56 #===============================================================================
57 # Helperclass to close running Instances of the Webinterface
58 #===============================================================================
59 class Closer:
60         counter = 0
61         def __init__(self, session, callback=None):
62                 self.callback = callback
63                 self.session = session
64
65 #===============================================================================
66 # Closes all running Instances of the Webinterface
67 #===============================================================================
68         def stop(self):
69                 global running_defered
70                 for d in running_defered:
71                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
72                         x = d.stopListening()
73                         try:
74                                 x.addCallback(self.isDown)
75                                 self.counter += 1
76                         except AttributeError:
77                                 pass
78                 running_defered = []
79                 if self.counter < 1:
80                         if self.callback is not None:
81                                 self.callback(self.session)
82
83 #===============================================================================
84 # #Is it already down?
85 #===============================================================================
86         def isDown(self, s):
87                 self.counter -= 1
88                 if self.counter < 1:
89                         if self.callback is not None:
90                                 self.callback(self.session)
91
92 def checkCertificates():
93         print "[WebInterface] checking for SSL Certificates"
94         srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG) 
95         cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)
96
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):
99                 return False
100         
101         else:
102                 return True
103                 
104 def installCertificates(session, callback = None):
105         print "[WebInterface] Installing SSL Certificates to %s" %resolveFilename(SCOPE_CONFIG)
106         
107         srvcert = '%sserver.pem' %resolveFilename(SCOPE_CONFIG) 
108         cacert = '%scacert.pem' %resolveFilename(SCOPE_CONFIG)  
109         scope_webif = '%sExtensions/WebInterface/' %resolveFilename(SCOPE_PLUGINS)
110         
111         source = '%setc/server.pem' %scope_webif
112         target = srvcert
113         ret = copyfile(source, target)
114         
115         if ret == 0:
116                 source = '%setc/cacert.pem' %scope_webif
117                 target = cacert
118                 ret = copyfile(source, target)
119                 
120                 if ret == 0 and callback != None:
121                         callback(session)
122         
123         if ret < 0:
124                 config.plugins.Webinterface.https.enabled.value = False
125                 config.plugins.Webinterface.https.enabled.save()
126                 
127                 # Start without https
128                 callback(session)
129                 
130                 #Inform the user
131                 session.open(MessageBox, "Couldn't install SSL-Certifactes for https access\nHttps access is now disabled!", MessageBox.TYPE_ERROR)
132         
133 #===============================================================================
134 # restart the Webinterface for all configured Interfaces
135 #===============================================================================
136 def restartWebserver(session):
137         try:
138                 del session.mediaplayer
139                 del session.messageboxanswer
140         except NameError:
141                 pass
142         except AttributeError:
143                 pass
144
145         global running_defered
146         if len(running_defered) > 0:
147                 Closer(session, startWebserver).stop()
148         else:
149                 startWebserver(session)
150         
151 #===============================================================================
152 # start the Webinterface for all configured Interfaces
153 #===============================================================================
154 def startWebserver(session):
155         global running_defered
156         global toplevel
157         
158         session.mediaplayer = None
159         session.messageboxanswer = None
160         if toplevel is None:
161                 toplevel = getToplevel(session)
162         
163         errors = ""
164         
165         if config.plugins.Webinterface.enabled.value is not True:
166                 print "[Webinterface] is disabled!"
167         
168         else:
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)
176                                 return
177                                 
178                 for adaptername in iNetwork.ifaces:                             
179                         ip = '.'.join("%d" % d for d in iNetwork.ifaces[adaptername]['ip'])
180                                                 
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
182                         if ip != '0.0.0.0':
183                         #HTTP
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)
186                                         if ret == False:
187                                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
188                         #HTTPS          
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)
191                                         if ret == False:
192                                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
193         
194         #LOCAL HTTP Connections (Streamproxy)
195                 ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.streamauth.value)                       
196                 if ret == False:
197                         errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
198                 
199                 if errors != "":
200                         session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), MessageBox.TYPE_ERROR)
201                 
202 #===============================================================================
203 # stop the Webinterface for all configured Interfaces
204 #===============================================================================
205 def stopWebserver(session):
206         try:
207                 del session.mediaplayer
208                 del session.messageboxanswer
209         except NameError:
210                 pass
211         except AttributeError:
212                 pass
213
214         global running_defered
215         if len(running_defered) > 0:
216                 Closer(session).stop()
217
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):
224         try:
225                 if useauth:
226 # HTTPAuthResource handles the authentication for every Resource you want it to                 
227                         root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
228                         site = server.Site(root)                        
229                 else:
230                         site = server.Site(toplevel)
231         
232                 if usessl:
233                         
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)
236                 else:
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
240                 return True
241         
242         except Exception, e:
243                 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e         
244                 return False
245 #===============================================================================
246 # HTTPAuthResource
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)
252                 self.resource = res
253                 self.realm = realm
254                 self.authorized = False
255                 self.tries = 0
256                 self.unauthorizedResource = UnauthorizedResource(self.realm)            
257         
258         def unautorized(self, request):
259                 request.setResponseCode(http.UNAUTHORIZED)
260                 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
261
262                 return self.unauthorizedResource
263         
264         def isAuthenticated(self, request):
265                 # get the Session from the Request
266                 sessionNs = request.getSession().sessionNamespaces
267                 
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())
271                 
272                 #if the auth-information already is in the session                              
273                 else:
274                         if sessionNs['authenticated'] is False:
275                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
276                 
277                 #return the current authentication status                                               
278                 return sessionNs['authenticated']
279                                                                                                         
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)
286                 
287                 else:
288                         return self.unautorized(request).render(request)
289
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)
296                 
297                 else:
298                         return self.unautorized(request)
299
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)
307                 self.realm = realm
308                 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
309         
310         def getChild(self, path, request):
311                 return self.errorpage
312                 
313         def render(self, request):      
314                 return self.errorpage.render(request)
315
316 # Password verfication stuff
317
318 from hashlib import md5 as md5_new
319 from crypt import crypt
320
321 #===============================================================================
322 # getpwnam
323
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):
328         if not pwfile:
329                 pwfile = '/etc/passwd'
330
331         f = open(pwfile)
332         while 1:
333                 line = f.readline()
334                 if not line:
335                         f.close()
336                         raise KeyError, name
337                 entry = tuple(line.strip().split(':', 6))
338                 if entry[0] == name:
339                         f.close()
340                         return entry
341
342 #===============================================================================
343 # passcrypt
344 #
345 # Encrypt a password
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':
354                 return passwd
355
356 #===============================================================================
357 # check_passwd
358 #
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."""
364
365         if not pwfile or type(pwfile) == type(''):
366                 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
367         else:
368                 getuser = pwfile.get_passwd
369
370         try:
371                 enc_passwd = getuser(name)
372         except (KeyError, IOError):
373                 print "!!! EXCEPT"
374                 return False
375         if not enc_passwd:
376                 "!!! NOT ENC_PASSWD"
377                 return False
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')
381         else:
382                 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
383
384 def _to64(v, n):
385         DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
386         r = ''
387         while (n - 1 >= 0):
388                 r = r + DES_SALT[v & 0x3F]
389                 v = v >> 6
390                 n = n - 1
391         return r
392
393 #===============================================================================
394 # passcrypt_md5
395 # Encrypt a password via md5
396 #===============================================================================
397 def passcrypt_md5(passwd, salt=None, magic='$1$'):
398         if not salt:
399                 pass
400         elif salt[:len(magic)] == magic:
401                 # remove magic from salt if present
402                 salt = salt[len(magic):]
403
404         # salt only goes up to first '$'
405         salt = salt.split('$')[0]
406         # limit length of salt to 8
407         salt = salt[:8]
408
409         ctx = md5_new(passwd)
410         ctx.update(magic)
411         ctx.update(salt)
412
413         ctx1 = md5_new(passwd)
414         ctx1.update(salt)
415         ctx1.update(passwd)
416
417         final = ctx1.digest()
418
419         for i in range(len(passwd), 0 , -16):
420                 if i > 16:
421                         ctx.update(final)
422                 else:
423                         ctx.update(final[:i])
424
425         i = len(passwd)
426         while i:
427                 if i & 1:
428                         ctx.update('\0')
429                 else:
430                         ctx.update(passwd[:1])
431                 i = i >> 1
432         final = ctx.digest()
433
434         for i in range(1000):
435                 ctx1 = md5_new()
436                 if i & 1:
437                         ctx1.update(passwd)
438                 else:
439                         ctx1.update(final)
440                 if i % 3: ctx1.update(salt)
441                 if i % 7: ctx1.update(passwd)
442                 if i & 1:
443                         ctx1.update(final)
444                 else:
445                         ctx1.update(passwd)
446                 final = ctx1.digest()
447
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)
460         l = final[11]
461         rv = rv + _to64(l, 2)
462
463         return rv
464
465 global_session = None
466
467 #===============================================================================
468 # sessionstart
469 # Actions to take place on Session start 
470 #===============================================================================
471 def sessionstart(reason, session):
472         global global_session
473         global_session = session
474
475 #===============================================================================
476 # networkstart
477 # Actions to take place after Network is up (startup the Webserver)
478 #===============================================================================
479 def networkstart(reason, **kwargs):
480         if reason is True:
481                 startWebserver(global_session)
482
483         elif reason is False:
484                 stopWebserver(global_session)
485
486 def openconfig(session, **kwargs):
487         session.openWithCallback(configCB, WebIfConfigScreen)
488
489 def configCB(result, session):
490         if result is True:
491                 print "[WebIf] config changed"
492                 restartWebserver(session)
493         else:
494                 print "[WebIf] config not changed"
495
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)]