beremiz

Merge
py2compat
15 months ago, Tomaz Orac
06cd935c5b65
Merge
--- 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 @@
port = 3000
webport = 8009
PSKpath = None
+KeyStore = None
+ConfDir = None
wampconf = None
servicename = None
autostart = False
@@ -151,9 +154,28 @@
elif o == "-w":
webport = None if a == "off" else int(a)
elif o == "-c":
- wampconf = None if a == "off" else a
+ if a == "off":
+ wampconf = None
+ elif os.path.isdir(a):
+ ConfDir = a
+ _PSKpath = os.path.join(a, "wampconf.json")
+ if os.path.isfile(_PSKpath):
+ wampconf = _PSKpath
+ elif os.path.isfile(a):
+ wampconf = a
+ ConfDir = os.path.dirname(a)
elif o == "-s":
- PSKpath = None if a == "off" else a
+ if a == "off":
+ PSKpath = None
+ elif os.path.isdir(a):
+ KeyStore = a
+ _PSKpath = os.path.join(a, "psk.txt")
+ if os.path.isfile(_PSKpath):
+ PSKpath = _PSKpath
+ elif os.path.isfile(a) or os.path.isdir(os.path.dirname(a)):
+ PSKpath = a
+ KeyStore = os.path.dirname(a)
+
elif o == "-e":
fnameanddirname = list(os.path.split(os.path.realpath(a)))
fnameanddirname.reverse()
@@ -548,13 +570,12 @@
try:
website = NS.RegisterWebsite(interface, webport)
pyruntimevars["website"] = website
- statuschange.append(NS.website_statuslistener_factory(website))
except Exception:
LogMessageAndException(_("Nevow Web service failed. "))
if havewamp:
try:
- WC.RegisterWampClient(wampconf, PSKpath)
+ WC.RegisterWampClient(wampconf, PSKpath, ConfDir, KeyStore)
WC.RegisterWebSettings(NS)
except Exception:
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)
if c_cbs is 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)
+
return res
"""
--- 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 @@
WorkingDir = None
-class PLCHMI(athena.LiveElement):
-
- initialised = False
-
- 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.invisible[
- 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.pcl_state = False
- self.HMI = None
- self.resetPLCStartedHMI()
-
- def setPLCState(self, state):
- self.pcl_state = state
- if self.HMI is not None:
- self.callRemote('updateHMI')
-
- def setPLCStartedHMI(self, hmi):
- self.PLCStartedHMIClass = hmi
-
- def resetPLCStartedHMI(self):
- self.PLCStartedHMIClass = DefaultPLCStartedHMI
-
- def getHMI(self):
- return self.HMI
-
- def HMIexec(self, function, *args, **kwargs):
- if self.HMI is not None:
- getattr(self.HMI, function, lambda: None)(*args, **kwargs)
- athena.expose(HMIexec)
-
- def resetHMI(self):
- self.HMI = None
-
- def PLCElement(self, ctx, data):
- return self.getPLCElement()
- renderer(PLCElement)
-
- def getPLCElement(self):
- self.detachFragmentChildren()
- if self.pcl_state:
- f = self.PLCStartedHMIClass()
- else:
- f = PLCStoppedHMI()
- f.setFragmentParent(self)
- self.HMI = f
- return f
- athena.expose(getPLCElement)
-
- def detachFragmentChildren(self):
- for child in self.liveFragmentChildren[:]:
- child.detach()
-
-
class ConfigurableBindings(configurable.Configurable):
def __init__(self):
@@ -380,83 +297,23 @@
return res
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',
- type='text/css',
- href=url.here.child("webform_css"))
- ],
- tags.body[
- tags.div[
- tags.div(
- render=tags.directive(
- "MainPage")),
- ]]]])
- MainPage = MainPage()
- PLCHMI = PLCHMI
+class LandingPage(rend.Page):
+ addSlash = True
+ docFactory = loaders.stan(
+ tags.html[
+ tags.head[tags.title[PAGE_TITLE]],
+ tags.body[
+ tags.a(href="settings")["Access Settings"]
+ ]
+ ]
+ )
def child_settings(self, context):
return SettingsPage()
- 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)
-
- def getHMI(self):
- 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)
-
- def UnLoadHMI(self):
- self.MainPage.resetPLCStartedHMI()
-
- def PLCStarted(self):
- self.plcState = True
- self.MainPage.setPLCState(True)
-
- def PLCStopped(self):
- self.plcState = False
- 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 = self.MainPage
- f.setFragmentParent(self)
- return ctx.tag[f]
-
- def child_(self, ctx):
- 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 reason
- # 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 @@
return website
-class statuslistener(object):
-
- def __init__(self, site):
- self.oldstate = None
- self.site = site
-
- def listen(self, state):
- if state != self.oldstate:
- action = {'Started': self.site.PLCStarted,
- 'Stopped': self.site.PLCStopped}.get(state, None)
- if action is not None:
- action()
- self.oldstate = state
-
-
-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 @@
import json
import os
import re
+import shutil
+import six
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
import formless
@@ -50,6 +55,7 @@
# Find pre-existing project WAMP config file
_WampConf = None
_WampSecret = None
+_WampTrust = None
ExposedCalls = [
("StartPLC", {}),
@@ -81,7 +87,8 @@
"protocolOptions": {
"autoPingInterval": 10,
"autoPingTimeout": 5
- }
+ },
+ "verifyHostname": True
}
# Those two lists are meant to be filled by customized runtime
@@ -223,7 +230,7 @@
WampClientConf = None
if os.path.exists(_WampConf):
- try:
+ try:
WampClientConf = json.load(open(_WampConf))
UpdateWithDefault(WampClientConf, defaultWampConfig)
except ValueError:
@@ -243,11 +250,6 @@
return WampClientConf
-def SetWampSecret(wampSecret):
- with open(os.path.realpath(_WampSecret), 'w') as f:
- f.write(wampSecret)
-
-
def SetConfiguration(WampClientConf):
global lastKnownConfig
@@ -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")
+
+ if _WampTrust is None:
+ _WampTrust = os.path.join(KeyStore, "wampTrustStore.crt")
# set config file path only if not already set
if _WampConf is None:
@@ -326,13 +334,28 @@
# start the client from a Twisted endpoint
if _transportFactory:
- connectWS(_transportFactory)
+ contextFactory=None
+ if _transportFactory.isSecure:
+ contextFactory = MakeSecureContextFactory(WampClientConf["verifyHostname"])
+
+ connectWS(_transportFactory, contextFactory)
print(_("WAMP client connecting to :"), WampClientConf["url"])
return True
else:
print(_("WAMP client can not connect to :"), WampClientConf["url"])
return False
+def MakeSecureContextFactory(verifyHostname):
+ if not verifyHostname:
+ return None
+ trustRoot=None
+ if os.path.exists(_WampTrust):
+ cert = crypto.load_certificate(
+ crypto.FILETYPE_PEM,
+ 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 = [
'active', 'url', 'ID',
"clientFactoryOptions.maxDelay",
"protocolOptions.autoPingInterval",
- "protocolOptions.autoPingTimeout"
+ "protocolOptions.autoPingTimeout",
+ "verifyHostname"
]
@@ -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()
- SetWampSecret(secret)
+ with open(os.path.realpath(_WampSecret), 'w') as destfd:
+ secretfile.seek(0)
+ 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):
+ pass
+
+
+class FileUploadDeleteRenderer(webform.FileUploadRenderer):
+
+ def input(self, context, slot, data, name, value):
+ # pylint: disable=expression-not-assigned
+ slot[_("Upload:")]
+ slot = webform.FileUploadRenderer.input(
+ self, context, slot, data, name, value)
+ file_exists = data.typedValue.getAttribute('file_exists')
+ if file_exists and file_exists():
+ unique = str(id(self))
+ file_delete = data.typedValue.getAttribute('file_delete')
+ slot = slot[
+ 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")
+ ]
+ return slot
+
+
+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)
+
+
webFormInterface = [
("status",
annotate.String(label=_("Current status"),
@@ -476,7 +547,14 @@
default=wampConfigDefault)),
("protocolOptions.autoPingTimeout",
annotate.Integer(label=_("Auto ping timeout (s)"),
- default=wampConfigDefault))
+ default=wampConfigDefault)),
+ ("trustStore",
+ FileUploadDelete(label=_("File containing server certificate"),
+ file_exists=getTrustStorePresence,
+ file_delete=getTrustStoreDeleteUrl)),
+ ("verifyHostname",
+ 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):
+ os.remove(_WampTrust)
+ return static.Data("TrustStore deleted", 'text/html'), ()
def RegisterWebSettings(NS):
@@ -502,4 +584,5 @@
wampConfig)
WebSettings.addCustomURL(WAMP_SECRET_URL, deliverWampSecret)
+ WebSettings.addCustomURL(WAMP_DELETE_TRUSTSTORE_URL, deleteTrustStore)