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