--- a/Beremiz_service.py Tue Feb 18 08:58:54 2025 +0100
+++ b/Beremiz_service.py Tue Feb 25 13:19:06 2025 +0100
@@ -78,8 +78,9 @@
-x enable/disable wxTaskbarIcon (0:disable 1:enable) (default:1)
-t enable/disable Twisted web interface (0:disable 1:enable) (default:1)
-w web server port or "off" to disable web server (default:8009)
- -c WAMP client config file (can be overriden by wampconf.json in project)
- -s PSK secret path (default:PSK disabled)
+ -c WAMP client config file or Configuration directory containing "wamconf.json" (default:Wamp disabled, config dir is working_dir) + Note: Wamp config is overriden by wampconf.json given in project files. + -s PSK secret file or existing KeyStore directory containing "psk.txt" (default:PSK disabled, KeyStore is working_dir) -e python extension (absolute path .py)
working_dir - directory where are stored PLC files
@@ -99,6 +100,8 @@
@@ -151,9 +154,28 @@
webport = None if a == "off" else int(a)
- wampconf = None if a == "off" else a
+ _PSKpath = os.path.join(a, "wampconf.json") + if os.path.isfile(_PSKpath): + elif os.path.isfile(a): + ConfDir = os.path.dirname(a) - PSKpath = None if a == "off" else a
+ _PSKpath = os.path.join(a, "psk.txt") + if os.path.isfile(_PSKpath): + elif os.path.isfile(a) or os.path.isdir(os.path.dirname(a)): + KeyStore = os.path.dirname(a) fnameanddirname = list(os.path.split(os.path.realpath(a)))
fnameanddirname.reverse()
@@ -548,13 +570,12 @@
website = NS.RegisterWebsite(interface, webport)
pyruntimevars["website"] = website
- statuschange.append(NS.website_statuslistener_factory(website))
LogMessageAndException(_("Nevow Web service failed. "))
- WC.RegisterWampClient(wampconf, PSKpath)
+ WC.RegisterWampClient(wampconf, PSKpath, ConfDir, KeyStore) WC.RegisterWebSettings(NS)
LogMessageAndException(_("WAMP client startup failed. "))
--- a/mqtt/library.py Tue Feb 18 08:58:54 2025 +0100
+++ b/mqtt/library.py Tue Feb 25 13:19:06 2025 +0100
@@ -65,9 +65,6 @@
def MQTT_subscribe(clientname, topic, cb, QoS = 1):
global MQTT_client_cbs, MQTT_subscribers_cbs
- MQTT_subscribers_cbs.setdefault(clientname, {})[topic] = (cb, QoS)
- res = _MQTT_subscribe(clientname, topic, QoS)
c_cbs = MQTT_client_cbs.get(clientname, None)
cb_onmsg, cb_resub = mqtt_per_client_cb_factory(clientname)
@@ -78,6 +75,9 @@
register_c_function.argtypes = [mqtt_c_cb_onmsg_type, mqtt_c_cb_resub_type]
register_c_function(*c_cbs)
+ MQTT_subscribers_cbs.setdefault(clientname, {})[topic] = (cb, QoS) + res = _MQTT_subscribe(clientname, topic, QoS) --- a/runtime/NevowServer.py Tue Feb 18 08:58:54 2025 +0100
+++ b/runtime/NevowServer.py Tue Feb 25 13:19:06 2025 +0100
@@ -51,89 +51,6 @@
-class PLCHMI(athena.LiveElement):
- def HMIinitialised(self, result):
- self.initialised = True
- def HMIinitialisation(self):
- self.HMIinitialised(None)
-class DefaultPLCStartedHMI(PLCHMI):
- docFactory = loaders.stan(
- tags.div(render=tags.directive('liveElement'))[
- tags.h1["PLC IS NOW STARTED"],
-class PLCStoppedHMI(PLCHMI):
- docFactory = loaders.stan(
- tags.div(render=tags.directive('liveElement'))[
- tags.h1["PLC IS STOPPED"],
-class MainPage(athena.LiveElement):
- jsClass = u"WebInterface.PLC"
- docFactory = loaders.stan(
- tags.div(render=tags.directive('liveElement'))[
- tags.div(id='content')[
- tags.div(render=tags.directive('PLCElement'))]
- tags.a(href='settings')['Settings']])
- def __init__(self, *a, **kw):
- athena.LiveElement.__init__(self, *a, **kw)
- self.resetPLCStartedHMI()
- def setPLCState(self, state):
- if self.HMI is not None:
- self.callRemote('updateHMI')
- def setPLCStartedHMI(self, hmi):
- self.PLCStartedHMIClass = hmi
- def resetPLCStartedHMI(self):
- self.PLCStartedHMIClass = DefaultPLCStartedHMI
- def HMIexec(self, function, *args, **kwargs):
- if self.HMI is not None:
- getattr(self.HMI, function, lambda: None)(*args, **kwargs)
- def PLCElement(self, ctx, data):
- return self.getPLCElement()
- def getPLCElement(self):
- self.detachFragmentChildren()
- f = self.PLCStartedHMIClass()
- f.setFragmentParent(self)
- athena.expose(getPLCElement)
- def detachFragmentChildren(self):
- for child in self.liveFragmentChildren[:]:
class ConfigurableBindings(configurable.Configurable):
@@ -380,83 +297,23 @@
return super(ExtensionSettingsPage, self).locateChild(ctx, segments)
-class WebInterface(athena.LivePage):
- docFactory = loaders.stan([tags.raw(xhtml_header),
- tags.html(xmlns="http://www.w3.org/1999/xhtml")[
- tags.head(render=tags.directive('liveglue'))[
- tags.title[PAGE_TITLE],
- tags.link(rel='stylesheet',
- href=url.here.child("webform_css"))
+class LandingPage(rend.Page): + docFactory = loaders.stan( + tags.head[tags.title[PAGE_TITLE]], + tags.a(href="settings")["Access Settings"] def child_settings(self, context):
- def __init__(self, plcState=False, *a, **kw):
- super(WebInterface, self).__init__(*a, **kw)
- self.jsModules.mapping[u'WebInterface'] = paths.AbsNeighbourFile(
- __file__, 'webinterface.js')
- self.plcState = plcState
- self.MainPage.setPLCState(plcState)
- return self.MainPage.getHMI()
- def LoadHMI(self, hmi, jsmodules):
- for name, path in jsmodules.iteritems():
- self.jsModules.mapping[name] = os.path.join(WorkingDir, path)
- self.MainPage.setPLCStartedHMI(hmi)
- self.MainPage.resetPLCStartedHMI()
- self.MainPage.setPLCState(True)
- self.MainPage.setPLCState(False)
- def renderHTTP(self, ctx):
- Force content type to fit with SVG
- req = ctx.locate(inevow.IRequest)
- req.setHeader('Content-type', 'application/xhtml+xml')
- return super(WebInterface, self).renderHTTP(ctx)
- def render_MainPage(self, ctx, data):
- f.setFragmentParent(self)
- self.MainPage.detachFragmentChildren()
- return WebInterface(plcState=self.plcState)
- def beforeRender(self, ctx):
- d = self.notifyOnDisconnect()
- d.addErrback(self.disconnected)
- def disconnected(self, reason):
- self.MainPage.resetHMI()
- # print "We will be called back when the client disconnects"
def RegisterWebsite(iface, port):
- website = WebInterface()
+ website = LandingPage() site = appserver.NevowSite(website)
reactor.listenTCP(port, site, interface=iface)
@@ -464,22 +321,3 @@
-class statuslistener(object):
- def __init__(self, site):
- def listen(self, state):
- if state != self.oldstate:
- action = {'Started': self.site.PLCStarted,
- 'Stopped': self.site.PLCStopped}.get(state, None)
-def website_statuslistener_factory(site):
- return statuslistener(site).listen
--- a/runtime/WampClient.py Tue Feb 18 08:58:54 2025 +0100
+++ b/runtime/WampClient.py Tue Feb 25 13:19:06 2025 +0100
@@ -28,6 +28,8 @@
from six import text_type as text
from autobahn.twisted import wamp
from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS
@@ -35,6 +37,9 @@
from autobahn.wamp.serializer import MsgPackSerializer
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.python.components import registerAdapter
+from twisted.internet.ssl import optionsForClientTLS, CertificateOptions +from twisted.internet._sslverify import OpenSSLCertificateAuthorities +from OpenSSL import crypto from formless import annotate, webform
@@ -50,6 +55,7 @@
# Find pre-existing project WAMP config file
@@ -81,7 +87,8 @@
# Those two lists are meant to be filled by customized runtime
@@ -223,7 +230,7 @@
if os.path.exists(_WampConf):
WampClientConf = json.load(open(_WampConf))
UpdateWithDefault(WampClientConf, defaultWampConfig)
@@ -243,11 +250,6 @@
-def SetWampSecret(wampSecret):
- with open(os.path.realpath(_WampSecret), 'w') as f:
def SetConfiguration(WampClientConf):
@@ -275,10 +277,16 @@
return re.match(r'wss?://[^\s?:#-]+(:[0-9]+)?(/[^\s]*)?$', uri) is not None
-def RegisterWampClient(wampconf=None, wampsecret=None):
- global _WampConf, _WampSecret
- _WampConfDefault = os.path.join(WorkingDir, "wampconf.json")
- _WampSecretDefault = os.path.join(WorkingDir, "wamp.secret")
+def RegisterWampClient(wampconf=None, wampsecret=None, ConfDir=None, KeyStore=None): + global _WampConf, _WampSecret, _WampTrust + ConfDir = ConfDir if ConfDir else WorkingDir + KeyStore = KeyStore if KeyStore else WorkingDir + _WampConfDefault = os.path.join(ConfDir, "wampconf.json") + _WampSecretDefault = os.path.join(KeyStore, "wamp.secret") + _WampTrust = os.path.join(KeyStore, "wampTrustStore.crt") # set config file path only if not already set
@@ -326,13 +334,28 @@
# start the client from a Twisted endpoint
- connectWS(_transportFactory)
+ if _transportFactory.isSecure: + contextFactory = MakeSecureContextFactory(WampClientConf["verifyHostname"]) + connectWS(_transportFactory, contextFactory) print(_("WAMP client connecting to :"), WampClientConf["url"])
print(_("WAMP client can not connect to :"), WampClientConf["url"])
+def MakeSecureContextFactory(verifyHostname): + if os.path.exists(_WampTrust): + cert = crypto.load_certificate( + six.u(open(_WampTrust, 'r').read()) + trustRoot=OpenSSLCertificateAuthorities([cert]) + return optionsForClientTLS(_transportFactory.host, trustRoot=trustRoot) def StopReconnectWampClient():
if _transportFactory is not None:
@@ -380,11 +403,13 @@
# WEB CONFIGURATION INTERFACE
WAMP_SECRET_URL = "secret"
+WAMP_DELETE_TRUSTSTORE_URL = "delete_truststore" webExposedConfigItems = [
"clientFactoryOptions.maxDelay",
"protocolOptions.autoPingInterval",
- "protocolOptions.autoPingTimeout"
+ "protocolOptions.autoPingTimeout", @@ -406,8 +431,17 @@
if secretfile_field is not None:
secretfile = getattr(secretfile_field, "file", None)
if secretfile is not None:
- secret = secretfile_field.file.read()
+ with open(os.path.realpath(_WampSecret), 'w') as destfd: + shutil.copyfileobj(secretfile,destfd) + trustStore_field = kwargs["trustStore"] + if trustStore_field is not None: + trustStore_file = getattr(trustStore_field, "file", None) + if trustStore_file is not None: + with open(os.path.realpath(_WampTrust), 'w') as destfd: + trustStore_file.seek(0) + shutil.copyfileobj(trustStore_file,destfd) newConfig = lastKnownConfig.copy()
for argname in webExposedConfigItems:
@@ -451,6 +485,43 @@
child(lastKnownConfig["ID"] + ".secret")
+class FileUploadDelete(annotate.FileUpload): +class FileUploadDeleteRenderer(webform.FileUploadRenderer): + def input(self, context, slot, data, name, value): + # pylint: disable=expression-not-assigned + slot = webform.FileUploadRenderer.input( + self, context, slot, data, name, value) + file_exists = data.typedValue.getAttribute('file_exists') + if file_exists and file_exists(): + file_delete = data.typedValue.getAttribute('file_delete') + tags.a(href=file_delete, target=unique)[_("Delete")], + tags.iframe(srcdoc="File exists", name=unique, + height="20", width="150", + marginheight="5", marginwidth="5", + scrolling="no", frameborder="0") +registerAdapter(FileUploadDeleteRenderer, FileUploadDelete, + formless.iformless.ITypedRenderer) +def getTrustStorePresence(): + return os.path.exists(_WampTrust) +def getTrustStoreDeleteUrl(ctx, argument): + return url.URL.fromContext(ctx).child(WAMP_DELETE_TRUSTSTORE_URL) annotate.String(label=_("Current status"),
@@ -476,7 +547,14 @@
default=wampConfigDefault)),
("protocolOptions.autoPingTimeout",
annotate.Integer(label=_("Auto ping timeout (s)"),
- default=wampConfigDefault))
+ default=wampConfigDefault)), + FileUploadDelete(label=_("File containing server certificate"), + file_exists=getTrustStorePresence, + file_delete=getTrustStoreDeleteUrl)), + annotate.Boolean(label=_("Verify hostname matches certificate hostname"), + default=wampConfigDefault)), def deliverWampSecret(ctx, segments):
@@ -490,6 +568,10 @@
secret = LoadWampSecret(_WampSecret)
return static.Data(secret, 'application/octet-stream'), ()
+def deleteTrustStore(ctx, segments): + if os.path.exists(_WampTrust): + return static.Data("TrustStore deleted", 'text/html'), () def RegisterWebSettings(NS):
@@ -502,4 +584,5 @@
WebSettings.addCustomURL(WAMP_SECRET_URL, deliverWampSecret)
+ WebSettings.addCustomURL(WAMP_DELETE_TRUSTSTORE_URL, deleteTrustStore)