Merge branch 'master' of ssh://drmichael@scm.schwerkraft.elitedvb.net/scmrepos/git...
[enigma2-plugins.git] / webinterface / src / plugin.py
1 Version = '$Header$';
2
3 from enigma import eConsoleAppContainer, eTPM
4 from Plugins.Plugin import PluginDescriptor
5
6 from Components.config import config, ConfigBoolean, ConfigSubsection, ConfigInteger, ConfigYesNo, ConfigText, ConfigEnableDisable
7 from Screens.MessageBox import MessageBox
8 from WebIfConfig import WebIfConfigScreen
9 from WebChilds.Toplevel import getToplevel
10 from Tools.HardwareInfo import HardwareInfo
11
12 from Tools.Directories import copyfile, resolveFilename, SCOPE_PLUGINS, SCOPE_CONFIG
13 from Tools.IO import saveFile
14
15 from twisted.internet import reactor, ssl
16 from twisted.internet.error import CannotListenError
17 from twisted.web import server, http, util, static, resource
18
19 from zope.interface import Interface, implements
20 from socket import gethostname as socket_gethostname
21 from OpenSSL import SSL, crypto
22 from time import gmtime
23 from os.path import isfile as os_isfile, exists as os_exists
24
25 from __init__ import _, __version__, decrypt_block
26 from webif import get_random, validate_certificate
27
28 import random, uuid, time, hashlib
29
30 tpm = eTPM()
31 rootkey = ['\x9f', '|', '\xe4', 'G', '\xc9', '\xb4', '\xf4', '#', '&', '\xce', '\xb3', '\xfe', '\xda', '\xc9', 'U', '`', '\xd8', '\x8c', 's', 'o', '\x90', '\x9b', '\\', 'b', '\xc0', '\x89', '\xd1', '\x8c', '\x9e', 'J', 'T', '\xc5', 'X', '\xa1', '\xb8', '\x13', '5', 'E', '\x02', '\xc9', '\xb2', '\xe6', 't', '\x89', '\xde', '\xcd', '\x9d', '\x11', '\xdd', '\xc7', '\xf4', '\xe4', '\xe4', '\xbc', '\xdb', '\x9c', '\xea', '}', '\xad', '\xda', 't', 'r', '\x9b', '\xdc', '\xbc', '\x18', '3', '\xe7', '\xaf', '|', '\xae', '\x0c', '\xe3', '\xb5', '\x84', '\x8d', '\r', '\x8d', '\x9d', '2', '\xd0', '\xce', '\xd5', 'q', '\t', '\x84', 'c', '\xa8', ')', '\x99', '\xdc', '<', '"', 'x', '\xe8', '\x87', '\x8f', '\x02', ';', 'S', 'm', '\xd5', '\xf0', '\xa3', '_', '\xb7', 'T', '\t', '\xde', '\xa7', '\xf1', '\xc9', '\xae', '\x8a', '\xd7', '\xd2', '\xcf', '\xb2', '.', '\x13', '\xfb', '\xac', 'j', '\xdf', '\xb1', '\x1d', ':', '?']
32 hw = HardwareInfo()
33 #CONFIG INIT
34
35 #init the config
36 config.plugins.Webinterface = ConfigSubsection()
37 config.plugins.Webinterface.enabled = ConfigYesNo(default=True)
38 config.plugins.Webinterface.show_in_extensionsmenu = ConfigYesNo(default = False)
39 config.plugins.Webinterface.allowzapping = ConfigYesNo(default=True)
40 config.plugins.Webinterface.includemedia = ConfigYesNo(default=False)
41 config.plugins.Webinterface.autowritetimer = ConfigYesNo(default=False)
42 config.plugins.Webinterface.loadmovielength = ConfigYesNo(default=True)
43 config.plugins.Webinterface.version = ConfigText(__version__) # used to make the versioninfo accessible enigma2-wide, not confgurable in GUI.
44
45 config.plugins.Webinterface.http = ConfigSubsection()
46 config.plugins.Webinterface.http.enabled = ConfigYesNo(default=True)
47 config.plugins.Webinterface.http.port = ConfigInteger(default = 80, limits=(1, 65535) )
48 config.plugins.Webinterface.http.auth = ConfigYesNo(default=False)
49
50 config.plugins.Webinterface.https = ConfigSubsection()
51 config.plugins.Webinterface.https.enabled = ConfigYesNo(default=True)
52 config.plugins.Webinterface.https.port = ConfigInteger(default = 443, limits=(1, 65535) )
53 config.plugins.Webinterface.https.auth = ConfigYesNo(default=True)
54
55 config.plugins.Webinterface.streamauth = ConfigYesNo(default=False)
56
57 config.plugins.Webinterface.anti_hijack = ConfigEnableDisable(default=False)
58 config.plugins.Webinterface.extended_security = ConfigEnableDisable(default=False)
59
60 global running_defered, waiting_shutdown, toplevel
61
62 running_defered = []
63 waiting_shutdown = 0
64 toplevel = None
65 server.VERSION = "Enigma2 WebInterface Server $Revision$".replace("$Revi", "").replace("sion: ", "").replace("$", "")
66
67 KEY_FILE = resolveFilename(SCOPE_CONFIG, "key.pem")
68 CERT_FILE = resolveFilename(SCOPE_CONFIG, "cert.pem")
69
70 #===============================================================================
71 # Helperclass to close running Instances of the Webinterface
72 #===============================================================================
73 class Closer:
74         counter = 0
75         def __init__(self, session, callback=None, l2k=None):
76                 self.callback = callback
77                 self.session = session
78                 self.l2k = l2k
79 #===============================================================================
80 # Closes all running Instances of the Webinterface
81 #===============================================================================
82         def stop(self):
83                 global running_defered
84                 for d in running_defered:
85                         print "[Webinterface] stopping interface on ", d.interface, " with port", d.port
86                         x = d.stopListening()
87
88                         try:
89                                 x.addCallback(self.isDown)
90                                 self.counter += 1
91                         except AttributeError:
92                                 pass
93                 running_defered = []
94                 if self.counter < 1:
95                         if self.callback is not None:
96                                 self.callback(self.session, self.l2k)
97
98 #===============================================================================
99 # #Is it already down?
100 #===============================================================================
101         def isDown(self, s):
102                 self.counter -= 1
103                 if self.counter < 1:
104                         if self.callback is not None:
105                                 self.callback(self.session, self.l2k)
106
107 def installCertificates(session):
108         if not os_exists(CERT_FILE) \
109                         or not os_exists(KEY_FILE):
110                 print "[Webinterface].installCertificates :: Generating SSL key pair and CACert"
111                 # create a key pair
112                 k = crypto.PKey()
113                 k.generate_key(crypto.TYPE_RSA, 1024)
114
115                 # create a self-signed cert
116                 cert = crypto.X509()
117                 cert.get_subject().C = "DE"
118                 cert.get_subject().ST = "Home"
119                 cert.get_subject().L = "Home"
120                 cert.get_subject().O = "Dreambox"
121                 cert.get_subject().OU = "STB"
122                 cert.get_subject().CN = socket_gethostname()
123                 cert.set_serial_number(random.randint(1000000,1000000000))
124                 cert.set_notBefore("20120101000000Z");
125                 cert.set_notAfter("20301231235900Z")
126                 cert.set_issuer(cert.get_subject())
127                 cert.set_pubkey(k)
128                 print "[Webinterface].installCertificates :: Signing SSL key pair with new CACert"
129                 cert.sign(k, 'sha1')
130
131                 try:
132                         print "[Webinterface].installCertificates ::  Installing newly generated certificate and key pair"
133                         saveFile(CERT_FILE, crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
134                         saveFile(KEY_FILE, crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
135                 except IOError, e:
136                         #Disable https
137                         config.plugins.Webinterface.https.enabled.value = False
138                         config.plugins.Webinterface.https.enabled.save()
139                         #Inform the user
140                         session.open(MessageBox, "Couldn't install generated SSL-Certifactes for https access\nHttps access is disabled!", MessageBox.TYPE_ERROR)
141
142
143 #===============================================================================
144 # restart the Webinterface for all configured Interfaces
145 #===============================================================================
146 def restartWebserver(session, l2k):
147         try:
148                 del session.mediaplayer
149                 del session.messageboxanswer
150         except NameError:
151                 pass
152         except AttributeError:
153                 pass
154
155         global running_defered
156         if len(running_defered) > 0:
157                 Closer(session, startWebserver, l2k).stop()
158         else:
159                 startWebserver(session, l2k)
160
161 #===============================================================================
162 # start the Webinterface for all configured Interfaces
163 #===============================================================================
164 def startWebserver(session, l2k):
165         global running_defered
166         global toplevel
167
168         session.mediaplayer = None
169         session.messageboxanswer = None
170         if toplevel is None:
171                 toplevel = getToplevel(session)
172
173         errors = ""
174
175         if config.plugins.Webinterface.enabled.value is not True:
176                 print "[Webinterface] is disabled!"
177
178         else:
179                 # IF SSL is enabled we need to check for the certs first
180                 # If they're not there we'll exit via return here
181                 # and get called after Certificates are installed properly
182                 if config.plugins.Webinterface.https.enabled.value:
183                         installCertificates(session)
184
185                 # Listen on all Interfaces
186
187                 #HTTP
188                 port = config.plugins.Webinterface.http.port.value
189                 auth = config.plugins.Webinterface.http.auth.value
190                 if config.plugins.Webinterface.http.enabled.value is True:
191                         ret = startServerInstance(session, port, useauth=auth, l2k=l2k)
192                         if not ret:
193                                 errors = "%s port %i\n" %(errors, port)
194                         else:
195                                 registerBonjourService('http', port)
196
197                 #Streaming requires listening on localhost:80 no matter what, ensure it its available
198                 if config.plugins.Webinterface.http.port.value != 80 or not config.plugins.Webinterface.http.enabled.value:
199                         #LOCAL HTTP Connections (Streamproxy)
200                         local4 = "127.0.0.1"
201                         local4mapped = "::ffff:127.0.0.1"
202                         local6 = "::1"
203
204                         ret = startServerInstance(session, 80, useauth=auth, l2k=l2k, ipaddress=local4)
205                         if not ret:
206                                 errors = "%s%s:%i\n" %(errors, local4, 80)
207                         ret = startServerInstance(session, 80, useauth=auth, l2k=l2k, ipaddress=local4mapped, ipaddress2=local6)
208                         #ip6 is optional
209 #                       if not ret:
210 #                               errors = "%s%s/%s:%i\n" %(errors, local4mapped, local6, 80)
211
212                 #HTTPS
213                 if config.plugins.Webinterface.https.enabled.value is True:
214                         sport = config.plugins.Webinterface.https.port.value
215                         sauth = config.plugins.Webinterface.https.auth.value
216
217                         ret = startServerInstance(session, sport, useauth=sauth, l2k=l2k, usessl=True)
218                         if not ret:
219                                 errors = "%s%s:%i\n" %(errors, "0.0.0.0 / ::", sport)
220                         else:
221                                 registerBonjourService('https', sport)
222
223                 if errors:
224                         session.open(MessageBox, "Webinterface - Couldn't listen on:\n %s" % (errors), type=MessageBox.TYPE_ERROR, timeout=30)
225
226 #===============================================================================
227 # stop the Webinterface for all configured Interfaces
228 #===============================================================================
229 def stopWebserver(session):
230         try:
231                 del session.mediaplayer
232                 del session.messageboxanswer
233         except NameError:
234                 pass
235         except AttributeError:
236                 pass
237
238         global running_defered
239         if len(running_defered) > 0:
240                 Closer(session).stop()
241
242 #===============================================================================
243 # startServerInstance
244 # Starts an Instance of the Webinterface
245 # on given ipaddress, port, w/o auth, w/o ssl
246 #===============================================================================
247 def startServerInstance(session, port, useauth=False, l2k=None, usessl=False, ipaddress="::", ipaddress2=None):
248         l3k = None
249         l3c = tpm.getData(eTPM.DT_LEVEL3_CERT)
250
251         if l3c is None:
252                 return False
253
254         l3k = validate_certificate(l3c, l2k)
255         if l3k is None:
256                 return False
257
258         random = get_random()
259         if random is None:
260                 return False
261
262         value = tpm.computeSignature(random)
263         result = decrypt_block(value, l3k)
264
265         if result is None:
266                 return False
267         else:
268                 if result [80:88] != random:
269                         return False
270
271         if useauth:
272 # HTTPAuthResource handles the authentication for every Resource you want it to
273                 root = HTTPAuthResource(toplevel, "Enigma2 WebInterface")
274                 site = server.Site(root)
275         else:
276                 root = HTTPRootResource(toplevel)
277                 site = server.Site(root)
278
279         result = False
280
281         def logFail(addr, exception=None):
282                 print "[Webinterface] FAILED to listen on %s:%i auth=%s ssl=%s" % (addr, port, useauth, usessl)
283                 if exception:
284                         print exception
285
286         if usessl:
287                 ctx = ChainedOpenSSLContextFactory(KEY_FILE, CERT_FILE)
288                 try:
289                         d = reactor.listenSSL(port, site, ctx, interface=ipaddress)
290                         result = True
291                         running_defered.append(d)
292                 except CannotListenError as e:
293                         logFail(ipaddress, e)
294                 if ipaddress2:
295                         try:
296                                 d = reactor.listenSSL(port, site, ctx, interface=ipaddress2)
297                                 result = True
298                                 running_defered.append(d)
299                         except CannotListenError as e:
300                                 logFail(ipaddress2, e)
301         else:
302                 try:
303                         d = reactor.listenTCP(port, site, interface=ipaddress)
304                         result = True
305                         running_defered.append(d)
306                 except CannotListenError as e:
307                         logFail(ipaddress, e)
308                 if ipaddress2:
309                         try:
310                                 d = reactor.listenTCP(port, site, interface=ipaddress2)
311                                 result = True
312                                 running_defered.append(d)
313                         except CannotListenError as e:
314                                 logFail(ipaddress2, e)
315         
316         print "[Webinterface] started on %s:%i auth=%s ssl=%s" % (ipaddress, port, useauth, usessl)
317         return result
318
319         #except Exception, e:
320                 #print "[Webinterface] starting FAILED on %s:%i!" % (ipaddress, port), e
321                 #return False
322
323 class ChainedOpenSSLContextFactory(ssl.DefaultOpenSSLContextFactory):
324         def __init__(self, privateKeyFileName, certificateChainFileName, sslmethod=SSL.SSLv23_METHOD):
325                 self.privateKeyFileName = privateKeyFileName
326                 self.certificateChainFileName = certificateChainFileName
327                 self.sslmethod = sslmethod
328                 self.cacheContext()
329
330         def cacheContext(self):
331                 ctx = SSL.Context(self.sslmethod)
332                 ctx.use_certificate_chain_file(self.certificateChainFileName)
333                 ctx.use_privatekey_file(self.privateKeyFileName)
334                 self._context = ctx
335
336 class SimpleSession(object):
337         def __init__(self, expires=0):
338                 self._id = "0"
339                 self._expires = time.time() + expires if expires > 0 else 0
340
341         def _generateId(self):
342                 if config.plugins.Webinterface.extended_security.value:
343                         self._id = str ( uuid.uuid4() )
344                 else:
345                         self._id = "0"
346
347         def _getId(self):
348                 if self.expired():
349                         self._generateId()
350                 return self._id
351
352         def expired(self):
353                 expired = False
354                 if config.plugins.Webinterface.extended_security.value:
355                         expired = self._expires > 0 and self._expires < time.time()
356                         expired = expired or self._id == "0"
357                 else:
358                         expired = self._id != "0"
359                 return expired
360
361         id = property(_getId)
362
363 #Every request made will pass this Resource (as it is the root resource)
364 #Any "global" checks should be done here
365 class HTTPRootResource(resource.Resource):
366         SESSION_PROTECTED_PATHS = ['/web/', '/opkg', '/ipkg']
367         SESSION_EXCEPTIONS = [
368                 '/web/epgsearch.rss', '/web/movielist.m3u', '/web/movielist.rss', '/web/services.m3u', '/web/session',
369                 '/web/stream.m3u', '/web/stream', '/web/streamcurrent.m3u', '/web/strings.js', '/web/ts.m3u']
370
371         def __init__(self, res):
372                 print "[HTTPRootResource}.__init__"
373                 resource.Resource.__init__(self)
374                 self.resource = res
375                 self.sessionInvalidResource = resource.ErrorPage(http.PRECONDITION_FAILED, "Precondition failed!", "sessionid is missing, invalid or expired!")
376                 self._sessions = {}
377
378         def getClientToken(self, request):
379                 ip = request.getClientIP()
380                 ua = request.getHeader("User-Agent") or "Default UA"
381                 return hashlib.sha1("%s/%s" %(ip, ua)).hexdigest()
382
383         def isSessionValid(self, request):
384                 session = self._sessions.get( self.getClientToken(request), None )
385                 if session is None or session.expired():
386                         session = SimpleSession()
387                         key = self.getClientToken(request)
388                         print "[HTTPRootResource].isSessionValid :: created session with id '%s' for client with token '%s'" %(session.id, key)
389                         self._sessions[ key ] = session
390
391                 request.enigma2_session = session
392
393                 if config.plugins.Webinterface.extended_security.value and not request.path in self.SESSION_EXCEPTIONS:
394                         protected = False
395                         for path in self.SESSION_PROTECTED_PATHS:
396                                 if request.path.startswith(path):
397                                         protected = True
398
399                         if protected:
400                                 rsid = request.args.get('sessionid', None)
401                                 if rsid:
402                                         rsid = rsid[0]
403                                 return session and session.id == rsid
404
405                 return True
406
407         def render(self, request):
408                 #enable SAMEORIGIN policy for iframes
409                 if config.plugins.Webinterface.anti_hijack.value:
410                         request.setHeader("X-Frame-Options", "SAMEORIGIN")
411
412                 if self.isSessionValid(request):
413                         return self.resource.render(request)
414                 else:
415                         return self.sessionInvalidResource.render(request)
416
417         def getChildWithDefault(self, path, request):
418                 #enable SAMEORIGIN policy for iframes
419                 if config.plugins.Webinterface.anti_hijack.value:
420                         request.setHeader("X-Frame-Options", "SAMEORIGIN")
421
422                 if self.isSessionValid(request):
423                         return self.resource.getChildWithDefault(path, request)
424                 else:
425                         print "[Webinterface.HTTPRootResource.render] !!! session invalid !!!"
426                         return self.sessionInvalidResource
427
428 #===============================================================================
429 # HTTPAuthResource
430 # Handles HTTP Authorization for a given Resource
431 #===============================================================================
432 class HTTPAuthResource(HTTPRootResource):
433         def __init__(self, res, realm):
434                 HTTPRootResource.__init__(self, res)
435                 self.realm = realm
436                 self.authorized = False
437                 self.unauthorizedResource = resource.ErrorPage(http.UNAUTHORIZED, "Access denied", "Authentication credentials invalid!")
438
439         def unauthorized(self, request):
440                 request.setHeader('WWW-authenticate', 'Basic realm="%s"' % self.realm)
441                 request.setResponseCode(http.UNAUTHORIZED)
442                 return self.unauthorizedResource
443
444         def isAuthenticated(self, request):
445                 host = request.getHost().host
446                 #If streamauth is disabled allow all acces from localhost
447                 if not config.plugins.Webinterface.streamauth.value:
448                         if( host == "127.0.0.1" or host == "localhost" ):
449                                 print "[WebInterface.plugin.isAuthenticated] Streaming auth is disabled bypassing authcheck because host is '%s'" %host
450                                 return True
451
452                 # get the Session from the Request
453                 http_session = request.getSession().sessionNamespaces
454
455                 # if the auth-information has not yet been stored to the http_session
456                 if not http_session.has_key('authenticated'):
457                         if request.getUser() != '':
458                                 http_session['authenticated'] = check_passwd(request.getUser(), request.getPassword())
459                         else:
460                                 http_session['authenticated'] = False
461
462                 #if the auth-information already is in the http_session
463                 else:
464                         if http_session['authenticated'] is False:
465                                 http_session['authenticated'] = check_passwd(request.getUser(), request.getPassword() )
466
467                 #return the current authentication status
468                 return http_session['authenticated']
469
470 #===============================================================================
471 # Call render of self.resource (if authenticated)
472 #===============================================================================
473         def render(self, request):
474                 if self.isAuthenticated(request) is True:
475                         return HTTPRootResource.render(self, request)
476                 else:
477                         print "[Webinterface.HTTPAuthResource.render] !!! unauthorized !!!"
478                         return self.unauthorized(request).render(request)
479
480 #===============================================================================
481 # Override to call getChildWithDefault of self.resource (if authenticated)
482 #===============================================================================
483         def getChildWithDefault(self, path, request):
484                 if self.isAuthenticated(request) is True:
485                         return HTTPRootResource.getChildWithDefault(self, path, request)
486                 else:
487                         print "[Webinterface.HTTPAuthResource.getChildWithDefault] !!! unauthorized !!!"
488                         return self.unauthorized(request)
489
490 # Password verfication stuff
491 from crypt import crypt
492 from pwd import getpwnam
493 from spwd import getspnam
494
495
496 def check_passwd(name, passwd):
497         cryptedpass = None
498         try:
499                 cryptedpass = getpwnam(name)[1]
500         except:
501                 return False
502
503         #shadowed or not, that's the questions here
504         if cryptedpass == 'x' or cryptedpass == '*':
505                 try:
506                         cryptedpass = getspnam(name)[1]
507                 except:
508                         return False
509
510         if cryptedpass == '':
511                 return True
512
513         return crypt(passwd, cryptedpass) == cryptedpass
514
515 global_session = None
516
517 #===============================================================================
518 # sessionstart
519 # Actions to take place on Session start
520 #===============================================================================
521 def sessionstart(reason, session):
522         global global_session
523         global_session = session
524         networkstart(True, session)
525
526
527 def registerBonjourService(protocol, port):
528         try:
529                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
530
531                 service = bonjour.buildService(protocol, port)
532                 bonjour.registerService(service, True)
533                 print "[WebInterface.registerBonjourService] Service for protocol '%s' with port '%i' registered!" %(protocol, port)
534                 return True
535
536         except ImportError, e:
537                 print "[WebInterface.registerBonjourService] %s" %e
538                 return False
539
540 def unregisterBonjourService(protocol):
541         try:
542                 from Plugins.Extensions.Bonjour.Bonjour import bonjour
543
544                 bonjour.unregisterService(protocol)
545                 print "[WebInterface.unregisterBonjourService] Service for protocol '%s' unregistered!" %(protocol)
546                 return True
547
548         except ImportError, e:
549                 print "[WebInterface.unregisterBonjourService] %s" %e
550                 return False
551
552 def checkBonjour():
553         if ( not config.plugins.Webinterface.http.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
554                 unregisterBonjourService('http')
555         if ( not config.plugins.Webinterface.https.enabled.value ) or ( not config.plugins.Webinterface.enabled.value ):
556                 unregisterBonjourService('https')
557
558 #===============================================================================
559 # networkstart
560 # Actions to take place after Network is up (startup the Webserver)
561 #===============================================================================
562 #def networkstart(reason, **kwargs):
563 def networkstart(reason, session):
564         l2r = False
565         l2k = None
566         l2c = tpm.getData(eTPM.DT_LEVEL2_CERT)
567
568         if l2c is None:
569                 return
570
571         l2k = validate_certificate(l2c, rootkey)
572         if l2k is None:
573                 return
574
575         if reason is True:
576                 startWebserver(session, l2k)
577                 checkBonjour()
578
579         elif reason is False:
580                 stopWebserver(session)
581                 checkBonjour()
582
583 def openconfig(session, **kwargs):
584         session.openWithCallback(configCB, WebIfConfigScreen)
585
586 def configCB(result, session):
587         l2r = False
588         l2k = None
589         l2c = tpm.getData(eTPM.DT_LEVEL2_CERT)
590
591         if l2c is None:
592                 return
593
594         l2k = validate_certificate(l2c, rootkey)
595         if l2k is None:
596                 return
597
598         if result:
599                 print "[WebIf] config changed"
600                 restartWebserver(session, l2k)
601                 checkBonjour()
602         else:
603                 print "[WebIf] config not changed"
604
605 def Plugins(**kwargs):
606         p = PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart)
607         p.weight = 100 #webif should start as last plugin
608         list = [p,
609 #                       PluginDescriptor(where=[PluginDescriptor.WHERE_NETWORKCONFIG_READ], fnc=networkstart),
610                         PluginDescriptor(name=_("Webinterface"), description=_("Configuration for the Webinterface"),
611                                                         where=[PluginDescriptor.WHERE_PLUGINMENU], icon="plugin.png", fnc=openconfig)]
612         if config.plugins.Webinterface.show_in_extensionsmenu.value:
613                 list.append(PluginDescriptor(name="Webinterface", description=_("Configuration for the Webinterface"),
614                         where = PluginDescriptor.WHERE_EXTENSIONSMENU, icon="plugin.png", fnc=openconfig))
615         return list