Merge changes to switch from twisted.web2 to twisted.web
[vuplus_dvbapp-plugin] / webinterface / src / plugin.py
1 Version = '$Header$';
2 from Plugins.Plugin import PluginDescriptor
3 from Components.config import config, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText, ConfigSubList
4 from Screens.MessageBox import MessageBox
5 from WebIfConfig import WebIfConfigScreen, initConfig, updateConfig
6 from WebChilds.Toplevel import getToplevel
7
8 from twisted.internet import reactor, ssl
9 from twisted.web import server, http, util, static, resource
10 from twisted.python.log import startLogging
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 DEBUG_TO_FILE = False # PLEASE DONT ENABLE LOGGING BY DEFAULT (OR COMMIT TO PLUGIN CVS)
19
20 DEBUGFILE = "/tmp/twisted.log"
21
22 #CONFIG INIT
23
24 #init the config
25 config.plugins.Webinterface = ConfigSubsection()
26 config.plugins.Webinterface.enable = ConfigYesNo(default=True)
27 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
28 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
29 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
30 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=False)
31 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
32 config.plugins.Webinterface.interfacecount = ConfigInteger(0)
33 config.plugins.Webinterface.interfaces = ConfigSubList()
34 initConfig()
35 config.plugins.Webinterface.warningsslsend = ConfigYesNo(default=False)
36
37 global running_defered, waiting_shutdown
38 running_defered = []
39 waiting_shutdown = 0
40 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
41
42 class Closer:
43         counter = 0
44         def __init__(self, session, callback=None):
45                 self.callback = callback
46                 self.session = session
47
48         def stop(self):
49                 global running_defered
50                 for d in running_defered:
51                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
52                         x = d.stopListening()
53                         try:
54                                 x.addCallback(self.isDown)
55                                 self.counter += 1
56                         except AttributeError:
57                                 pass
58                 running_defered = []
59                 if self.counter < 1:
60                         if self.callback is not None:
61                                 self.callback(self.session)
62
63         def isDown(self, s):
64                 self.counter -= 1
65                 if self.counter < 1:
66                         if self.callback is not None:
67                                 self.callback(self.session)
68
69 def restartWebserver(session):
70         try:
71                 del session.mediaplayer
72                 del session.messageboxanswer
73         except NameError:
74                 pass
75         except AttributeError:
76                 pass
77
78         global running_defered
79         if len(running_defered) > 0:
80                 Closer(session, startWebserver).stop()
81         else:
82                 startWebserver(session)
83
84 def startWebserver(session):
85         global running_defered
86         session.mediaplayer = None
87         session.messageboxanswer = None
88
89         if config.plugins.Webinterface.enable.value is not True:
90                 print "[Webinterface] is disabled!"
91                 return False
92         if DEBUG_TO_FILE:
93                 print "[Webinterface] start twisted logfile, writing to %s" % DEBUGFILE
94                 startLogging(open(DEBUGFILE, 'w'))
95
96         for c in config.plugins.Webinterface.interfaces:
97                 if c.disabled.value is False:
98                         startServerInstance(session, c.address.value, c.port.value, c.useauth.value, c.usessl.value)
99                 else:
100                         print "[Webinterface] not starting disabled interface on %s:%i" % (c.address.value, c.port.value)
101
102 def stopWebserver(session):
103         try:
104                 del session.mediaplayer
105                 del session.messageboxanswer
106         except NameError:
107                 pass
108         except AttributeError:
109                 pass
110
111         global running_defered
112         if len(running_defered) > 0:
113                 Closer(session).stop()
114
115 def startServerInstance(session, ipaddress, port, useauth=False, usessl=False):
116         try:
117                 toplevel = getToplevel(session)
118                 if useauth:
119                         root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
120                         site = server.Site(root)                        
121                 else:
122                         site = server.Site(toplevel)
123         
124                 if usessl:
125                         ctx = ssl.DefaultOpenSSLContextFactory('/etc/enigma2/server.pem', '/etc/enigma2/cacert.pem', sslmethod=SSL.SSLv23_METHOD)
126                         d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
127                 else:
128                         d = reactor.listenTCP(port, site, interface=ipaddress)
129                 running_defered.append(d)
130                 print "[Webinterface] started on %s:%i" % (ipaddress, port), "auth=", useauth, "ssl=", usessl
131         
132         except Exception, e:
133                 print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
134                 session.open(MessageBox, 'starting FAILED on %s:%i!\n\n%s' % (ipaddress, port, str(e)), MessageBox.TYPE_ERROR)
135         
136 class HTTPAuthResource(resource.Resource):
137         def __init__(self, res, realm):
138                 resource.Resource.__init__(self)
139                 self.resource = res
140                 self.realm = realm
141                 self.authorized = False
142                 self.tries = 0
143                 self.unauthorizedResource = UnauthorizedResource(self.realm)            
144         
145         def unautorized(self, request):
146                 print "[Webinterface] Unauthorized!"
147                 request.setResponseCode(http.UNAUTHORIZED)
148                 request.setHeader('WWW-authenticate', 'basic realm="%s"' % self.realm)
149
150                 return self.unauthorizedResource
151         
152         def isAuthenticated(self, request):
153                 # get the Session from the Request
154                 sessionNs = request.getSession().sessionNamespaces
155                 
156                 # if the auth-information has not yet been stored to the session
157                 if not sessionNs.has_key('authenticated'):
158                         sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword())
159                 
160                 #if the auth-information already is in the session                              
161                 else:
162                         if sessionNs['authenticated'] is False:
163                                 sessionNs['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
164                 
165                 #return the current authentication status                                               
166                 return sessionNs['authenticated']
167                                                                                                         
168                                                                                                         
169         def render(self, request):                      
170                 if self.isAuthenticated(request) is True:       
171                         return self.resource.render(request)
172                 
173                 else:
174                         return self.unautorized(request)
175         
176         
177         def getChildWithDefault(self, path, request):
178                 if self.isAuthenticated(request) is True:
179                         return self.resource.getChildWithDefault(path, request)
180                 
181                 else:
182                         return self.unautorized(request)
183
184 class UnauthorizedResource(resource.Resource):
185         def __init__(self, realm):
186                 resource.Resource.__init__(self)
187                 self.realm = realm
188                 self.errorpage = static.Data('<html><body>Access Denied.</body></html>', 'text/html')
189                 
190         def render(self, request):      
191                 return self.errorpage.render(request)
192
193 from hashlib import md5 as md5_new
194 from crypt import crypt
195
196 DES_SALT = list('./0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
197 def getpwnam(name, pwfile=None):
198         """Return pasword database entry for the given user name.
199
200         Example from the Python Library Reference.
201         """
202
203         if not pwfile:
204                 pwfile = '/etc/passwd'
205
206         f = open(pwfile)
207         while 1:
208                 line = f.readline()
209                 if not line:
210                         f.close()
211                         raise KeyError, name
212                 entry = tuple(line.strip().split(':', 6))
213                 if entry[0] == name:
214                         f.close()
215                         return entry
216
217 def passcrypt(passwd, salt=None, method='des', magic='$1$'):
218         """Encrypt a string according to rules in crypt(3)."""
219         if method.lower() == 'des':
220                 return crypt(passwd, salt)
221         elif method.lower() == 'md5':
222                 return passcrypt_md5(passwd, salt, magic)
223         elif method.lower() == 'clear':
224                 return passwd
225
226 def check_passwd(name, passwd, pwfile='/etc/passwd'):
227         """Validate given user, passwd pair against password database."""
228
229         if not pwfile or type(pwfile) == type(''):
230                 getuser = lambda x, pwfile = pwfile: getpwnam(x, pwfile)[1]
231         else:
232                 getuser = pwfile.get_passwd
233
234         try:
235                 enc_passwd = getuser(name)
236         except (KeyError, IOError):
237                 print "!!! EXCEPT"
238                 return False
239         if not enc_passwd:
240                 "!!! NOT ENC_PASSWD"
241                 return False
242         elif len(enc_passwd) >= 3 and enc_passwd[:3] == '$1$':
243                 salt = enc_passwd[3:enc_passwd.find('$', 3)]
244                 return enc_passwd == passcrypt(passwd, salt, 'md5')
245         else:
246                 return enc_passwd == passcrypt(passwd, enc_passwd[:2])
247
248 def _to64(v, n):
249         r = ''
250         while (n - 1 >= 0):
251                 r = r + DES_SALT[v & 0x3F]
252                 v = v >> 6
253                 n = n - 1
254         return r
255
256 def passcrypt_md5(passwd, salt=None, magic='$1$'):
257         """Encrypt passwd with MD5 algorithm."""
258
259         if not salt:
260                 pass
261         elif salt[:len(magic)] == magic:
262                 # remove magic from salt if present
263                 salt = salt[len(magic):]
264
265         # salt only goes up to first '$'
266         salt = salt.split('$')[0]
267         # limit length of salt to 8
268         salt = salt[:8]
269
270         ctx = md5_new(passwd)
271         ctx.update(magic)
272         ctx.update(salt)
273
274         ctx1 = md5_new(passwd)
275         ctx1.update(salt)
276         ctx1.update(passwd)
277
278         final = ctx1.digest()
279
280         for i in range(len(passwd), 0 , -16):
281                 if i > 16:
282                         ctx.update(final)
283                 else:
284                         ctx.update(final[:i])
285
286         i = len(passwd)
287         while i:
288                 if i & 1:
289                         ctx.update('\0')
290                 else:
291                         ctx.update(passwd[:1])
292                 i = i >> 1
293         final = ctx.digest()
294
295         for i in range(1000):
296                 ctx1 = md5_new()
297                 if i & 1:
298                         ctx1.update(passwd)
299                 else:
300                         ctx1.update(final)
301                 if i % 3: ctx1.update(salt)
302                 if i % 7: ctx1.update(passwd)
303                 if i & 1:
304                         ctx1.update(final)
305                 else:
306                         ctx1.update(passwd)
307                 final = ctx1.digest()
308
309         rv = magic + salt + '$'
310         final = map(ord, final)
311         l = (final[0] << 16) + (final[6] << 8) + final[12]
312         rv = rv + _to64(l, 4)
313         l = (final[1] << 16) + (final[7] << 8) + final[13]
314         rv = rv + _to64(l, 4)
315         l = (final[2] << 16) + (final[8] << 8) + final[14]
316         rv = rv + _to64(l, 4)
317         l = (final[3] << 16) + (final[9] << 8) + final[15]
318         rv = rv + _to64(l, 4)
319         l = (final[4] << 16) + (final[10] << 8) + final[5]
320         rv = rv + _to64(l, 4)
321         l = final[11]
322         rv = rv + _to64(l, 2)
323
324         return rv
325
326 #### stuff for SSL Support
327 def makeSSLContext(myKey, trustedCA):
328          '''Returns an ssl Context Object
329         @param myKey a pem formated key and certifcate with for my current host
330                         the other end of this connection must have the cert from the CA
331                         that signed this key
332         @param trustedCA a pem formated certificat from a CA you trust
333                         you will only allow connections from clients signed by this CA
334                         and you will only allow connections to a server signed by this CA
335          '''
336
337          # our goal in here is to make a SSLContext object to pass to connectSSL
338          # or listenSSL
339
340          # Why these functioins... Not sure...
341          fd = open(myKey, 'r')
342          ss = fd.read()
343          theCert = ssl.PrivateCertificate.loadPEM(ss)
344          fd.close()
345          fd = open(trustedCA, 'r')
346          theCA = ssl.Certificate.loadPEM(fd.read())
347          fd.close()
348          #ctx = theCert.options(theCA)
349          ctx = theCert.options()
350
351          # Now the options you can set look like Standard OpenSSL Library options
352
353          # The SSL protocol to use, one of SSLv23_METHOD, SSLv2_METHOD,
354          # SSLv3_METHOD, TLSv1_METHOD. Defaults to TLSv1_METHOD.
355          ctx.method = ssl.SSL.TLSv1_METHOD
356
357          # If True, verify certificates received from the peer and fail
358          # the handshake if verification fails. Otherwise, allow anonymous
359          # sessions and sessions with certificates which fail validation.
360          ctx.verify = True
361
362          # Depth in certificate chain down to which to verify.
363          ctx.verifyDepth = 1
364
365          # If True, do not allow anonymous sessions.
366          ctx.requireCertification = True
367
368          # If True, do not re-verify the certificate on session resumption.
369          ctx.verifyOnce = True
370
371          # If True, generate a new key whenever ephemeral DH parameters are used
372          # to prevent small subgroup attacks.
373          ctx.enableSingleUseKeys = True
374
375          # If True, set a session ID on each context. This allows a shortened
376          # handshake to be used when a known client reconnects.
377          ctx.enableSessions = True
378
379          # If True, enable various non-spec protocol fixes for broken
380          # SSL implementations.
381          ctx.fixBrokenPeers = False
382
383          return ctx
384
385 global_session = None
386
387 def sessionstart(reason, session):
388         global global_session
389         global_session = session
390
391 def autostart(reason, **kwargs):
392         if reason is True:
393 #               try:
394                 updateConfig()
395                 startWebserver(global_session)
396 #               except ImportError, e:
397 #                       print "[Webinterface] twisted not available, not starting web services", e
398         elif reason is False:
399                 stopWebserver(global_session)
400
401 def openconfig(session, **kwargs):
402         session.openWithCallback(configCB, WebIfConfigScreen)
403
404 def configCB(result, session):
405         if result is True:
406                 print "[WebIf] config changed"
407                 restartWebserver(session)
408         else:
409                 print "[WebIf] config not changed"
410
411 def Plugins(**kwargs):
412         return [PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart),
413                         PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=autostart),
414                         PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
415                                                         where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]