Disable https until the fix is done
[enigma2-plugins.git] / webinterface / src / plugin.py
1 Version = '$Header$';
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
8
9 from twisted.internet import reactor, ssl
10 from twisted.web import server, http, util, static, resource
11
12 from zope.interface import Interface, implements
13 from socket import gethostname as socket_gethostname
14 from OpenSSL import SSL
15
16 from __init__ import _, __version__
17
18 #CONFIG INIT
19
20 #init the config
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.
28
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)
33
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)
38
39 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
40
41 global running_defered, waiting_shutdown, toplevel
42
43 running_defered = []
44 waiting_shutdown = 0
45 toplevel = None
46 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
47
48 #===============================================================================
49 # Helperclass to close running Instances of the Webinterface
50 #===============================================================================
51 class Closer:
52         counter = 0
53         def __init__(self, session, callback=None):
54                 self.callback = callback
55                 self.session = session
56
57 #===============================================================================
58 # Closes all running Instances of the Webinterface
59 #===============================================================================
60         def stop(self):
61                 global running_defered
62                 for d in running_defered:
63                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
64                         x = d.stopListening()
65                         try:
66                                 x.addCallback(self.isDown)
67                                 self.counter += 1
68                         except AttributeError:
69                                 pass
70                 running_defered = []
71                 if self.counter < 1:
72                         if self.callback is not None:
73                                 self.callback(self.session)
74
75 #===============================================================================
76 # #Is it already down?
77 #===============================================================================
78         def isDown(self, s):
79                 self.counter -= 1
80                 if self.counter < 1:
81                         if self.callback is not None:
82                                 self.callback(self.session)
83
84 #===============================================================================
85 # restart the Webinterface for all configured Interfaces
86 #===============================================================================
87 def restartWebserver(session):
88         try:
89                 del session.mediaplayer
90                 del session.messageboxanswer
91         except NameError:
92                 pass
93         except AttributeError:
94                 pass
95
96         global running_defered
97         if len(running_defered) > 0:
98                 Closer(session, startWebserver).stop()
99         else:
100                 startWebserver(session)
101
102 #===============================================================================
103 # start the Webinterface for all configured Interfaces
104 #===============================================================================
105 def startWebserver(session):
106         print "[Webinterface] startWebserver - iNetwork.ifaces: %s" %(iNetwork.ifaces)
107         
108         global running_defered
109         global toplevel
110         
111         session.mediaplayer = None
112         session.messageboxanswer = None
113         if toplevel is None:
114                 toplevel = getToplevel(session)
115         
116         errors = ""
117         
118         if config.plugins.Webinterface.enabled.value is not True:
119                 print "[Webinterface] is disabled!"
120         
121         else:
122                 for adaptername in iNetwork.ifaces:                             
123                         ip = '.'.join("%d" % d for d in iNetwork.ifaces[adaptername]['ip'])
124                                                 
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
126                         if ip != '0.0.0.0':
127                         #HTTP
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)
130                                         if ret == False:
131                                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.http.port.value)
132                         #HTTPS          
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)
135                                         if ret == False:
136                                                 errors = "%s%s:%i\n" %(errors, ip, config.plugins.Webinterface.https.port.value)
137         
138         #LOCAL HTTP Connections (Streamproxy)
139                 ret = startServerInstance(session, '127.0.0.1', 80, config.plugins.Webinterface.streamauth.value)                       
140                 if ret == False:
141                         errors = "%s%s:%i\n" %(errors, '127.0.0.1', 80)
142                 
143                 if errors != "":
144                         session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), MessageBox.TYPE_ERROR)
145                 
146 #===============================================================================
147 # stop the Webinterface for all configured Interfaces
148 #===============================================================================
149 def stopWebserver(session):
150         try:
151                 del session.mediaplayer
152                 del session.messageboxanswer
153         except NameError:
154                 pass
155         except AttributeError:
156                 pass
157
158         global running_defered
159         if len(running_defered) > 0:
160                 Closer(session).stop()
161
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):
168         try:
169                 if useauth:
170 # HTTPAuthResource handles the authentication for every Resource you want it to                 
171                         root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
172                         site = server.Site(root)                        
173                 else:
174                         site = server.Site(toplevel)
175         
176                 if usessl:
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)
179                 else:
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
183                 return True
184         
185         except Exception, e:
186                 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e         
187                 return False
188 #===============================================================================
189 # HTTPAuthResource
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)
195                 self.resource = res
196                 self.realm = realm
197                 self.authorized = False
198                 self.tries = 0
199                 self.unauthorizedResource = UnauthorizedResource(self.realm)            
200         
201         def unautorized(self, request):
202                 request.setResponseCode(http.UNAUTHORIZED)
203                 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
204
205                 return self.unauthorizedResource
206         
207         def isAuthenticated(self, request):
208                 # get the Session from the Request
209                 sessionNs = request.getSession().sessionNamespaces
210                 
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())
214                 
215                 #if the auth-information already is in the session                              
216                 else:
217                         if sessionNs['authenticated'] is False:
218                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
219                 
220                 #return the current authentication status                                               
221                 return sessionNs['authenticated']
222                                                                                                         
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)
229                 
230                 else:
231                         return self.unautorized(request).render(request)
232
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)
239                 
240                 else:
241                         return self.unautorized(request)
242
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)
250                 self.realm = realm
251                 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
252         
253         def getChild(self, path, request):
254                 return self.errorpage
255                 
256         def render(self, request):      
257                 return self.errorpage.render(request)
258
259 # Password verfication stuff
260
261 from hashlib import md5 as md5_new
262 from crypt import crypt
263
264 #===============================================================================
265 # getpwnam
266
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):
271         if not pwfile:
272                 pwfile = '/etc/passwd'
273
274         f = open(pwfile)
275         while 1:
276                 line = f.readline()
277                 if not line:
278                         f.close()
279                         raise KeyError, name
280                 entry = tuple(line.strip().split(':', 6))
281                 if entry[0] == name:
282                         f.close()
283                         return entry
284
285 #===============================================================================
286 # passcrypt
287 #
288 # Encrypt a password
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':
297                 return passwd
298
299 #===============================================================================
300 # check_passwd
301 #
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."""
307
308         if not pwfile or type(pwfile) == type(''):
309                 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
310         else:
311                 getuser = pwfile.get_passwd
312
313         try:
314                 enc_passwd = getuser(name)
315         except (KeyError, IOError):
316                 print "!!! EXCEPT"
317                 return False
318         if not enc_passwd:
319                 "!!! NOT ENC_PASSWD"
320                 return False
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')
324         else:
325                 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
326
327 def _to64(v, n):
328         DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
329         r = ''
330         while (n - 1 >= 0):
331                 r = r + DES_SALT[v & 0x3F]
332                 v = v >> 6
333                 n = n - 1
334         return r
335
336 #===============================================================================
337 # passcrypt_md5
338 # Encrypt a password via md5
339 #===============================================================================
340 def passcrypt_md5(passwd, salt=None, magic='$1$'):
341         if not salt:
342                 pass
343         elif salt[:len(magic)] == magic:
344                 # remove magic from salt if present
345                 salt = salt[len(magic):]
346
347         # salt only goes up to first '$'
348         salt = salt.split('$')[0]
349         # limit length of salt to 8
350         salt = salt[:8]
351
352         ctx = md5_new(passwd)
353         ctx.update(magic)
354         ctx.update(salt)
355
356         ctx1 = md5_new(passwd)
357         ctx1.update(salt)
358         ctx1.update(passwd)
359
360         final = ctx1.digest()
361
362         for i in range(len(passwd), 0 , -16):
363                 if i > 16:
364                         ctx.update(final)
365                 else:
366                         ctx.update(final[:i])
367
368         i = len(passwd)
369         while i:
370                 if i & 1:
371                         ctx.update('\0')
372                 else:
373                         ctx.update(passwd[:1])
374                 i = i >> 1
375         final = ctx.digest()
376
377         for i in range(1000):
378                 ctx1 = md5_new()
379                 if i & 1:
380                         ctx1.update(passwd)
381                 else:
382                         ctx1.update(final)
383                 if i % 3: ctx1.update(salt)
384                 if i % 7: ctx1.update(passwd)
385                 if i & 1:
386                         ctx1.update(final)
387                 else:
388                         ctx1.update(passwd)
389                 final = ctx1.digest()
390
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)
403         l = final[11]
404         rv = rv + _to64(l, 2)
405
406         return rv
407
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
415                         that signed this key
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
419          '''
420
421          # our goal in here is to make a SSLContext object to pass to connectSSL
422          # or listenSSL
423
424          # Why these functioins... Not sure...
425          fd = open(myKey, 'r')
426          ss = fd.read()
427          theCert = ssl.PrivateCertificate.loadPEM(ss)
428          fd.close()
429          fd = open(trustedCA, 'r')
430          theCA = ssl.Certificate.loadPEM(fd.read())
431          fd.close()
432          #ctx = theCert.options(theCA)
433          ctx = theCert.options()
434
435          # Now the options you can set look like Standard OpenSSL Library options
436
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
440
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.
444          ctx.verify = True
445
446          # Depth in certificate chain down to which to verify.
447          ctx.verifyDepth = 1
448
449          # If True, do not allow anonymous sessions.
450          ctx.requireCertification = True
451
452          # If True, do not re-verify the certificate on session resumption.
453          ctx.verifyOnce = True
454
455          # If True, generate a new key whenever ephemeral DH parameters are used
456          # to prevent small subgroup attacks.
457          ctx.enableSingleUseKeys = True
458
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
462
463          # If True, enable various non-spec protocol fixes for broken
464          # SSL implementations.
465          ctx.fixBrokenPeers = False
466
467          return ctx
468
469 global_session = None
470
471 #===============================================================================
472 # sessionstart
473 # Actions to take place on Session start 
474 #===============================================================================
475 def sessionstart(reason, session):
476         global global_session
477         global_session = session
478
479 #===============================================================================
480 # networkstart
481 # Actions to take place after Network is up (startup the Webserver)
482 #===============================================================================
483 def networkstart(reason, **kwargs):
484         if reason is True:
485                 startWebserver(global_session)
486
487         elif reason is False:
488                 stopWebserver(global_session)
489
490 def openconfig(session, **kwargs):
491         session.openWithCallback(configCB, WebIfConfigScreen)
492
493 def configCB(result, session):
494         if result is True:
495                 print "[WebIf] config changed"
496                 restartWebserver(session)
497         else:
498                 print "[WebIf] config not changed"
499
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)]