Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions examples/certificates/pymodbus.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDKjCCAhICCQCA+XBz2frVMDANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJF
UzEQMA4GA1UECAwHR3JhbmFkYTEQMA4GA1UEBwwHR3JhbmFkYTERMA8GA1UECgwI
cHltb2RidXMxETAPBgNVBAMMCHB5bW9kYnVzMB4XDTIyMDkwODE1NDgzNVoXDTMy
MDkwNTE1NDgzNVowVzELMAkGA1UEBhMCRVMxEDAOBgNVBAgMB0dyYW5hZGExEDAO
BgNVBAcMB0dyYW5hZGExETAPBgNVBAoMCHB5bW9kYnVzMREwDwYDVQQDDAhweW1v
ZGJ1czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJMRjbCj1S64SgyR
tMMMLWHf80xSYjCPcw9JXkAjAPp76fCV1b5dxc2BRvgwRm7xbfPHFUNLC8stQres
hBBDUBbaXdmfwg5M4IwKeEekvOcFKbtBe2ZHFiwmSY4SR77jK7zVn/1WIwlSqIiI
La9t4Ij79W/X0RWqTa3ExRHhB0JKNmn2REbNbhQ1RsLYM5fRsbUM51aChvpuHsS2
myhPlXqk/37/M8VNLxJuhOz+S6iwSUsOdAwlnXJFm4CM9fWgq+jx5O4RPYMkYWIy
UKVvUlKaPieZkrMd3AfDFcYZ/6rAwykVkl7ghCfuw33g1CSOxJQrNrxRX25omD6d
nw+K158CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAj5R0dBtHDnxyuAwgFgUVyMoT
uf+lnO9tfeZ6GdcUPJSahzopDk7LBLLM5HHVDUoCMtEBdZ3p01IGSvVG11dfFatZ
Ej7oYJGvm8P9z24n2LLve4IbEk42mb+hD8zY4DBD4WGMeleq8Zcbq7Y91s1+u9n8
rMILX5WrmZkcvCzfSSvqdLN/XKsFGM9X0dxGuF/eXplyg7mb6U7CLWwG0wo84HnQ
rIHw20Ss/BQklUpgihn4DHMOtuuTzNY9r1sYlDSFg8syMreNv25F303+mEOWMyBj
r8eUiDlSJg6vsZ+CzbAeNhEj5muIXVjXy0Vj1toe5dVqdtCnRtz0bZCwsrn5Sg==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions examples/certificates/pymodbus.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCTEY2wo9UuuEoM
kbTDDC1h3/NMUmIwj3MPSV5AIwD6e+nwldW+XcXNgUb4MEZu8W3zxxVDSwvLLUK3
rIQQQ1AW2l3Zn8IOTOCMCnhHpLznBSm7QXtmRxYsJkmOEke+4yu81Z/9ViMJUqiI
iC2vbeCI+/Vv19EVqk2txMUR4QdCSjZp9kRGzW4UNUbC2DOX0bG1DOdWgob6bh7E
tpsoT5V6pP9+/zPFTS8SboTs/kuosElLDnQMJZ1yRZuAjPX1oKvo8eTuET2DJGFi
MlClb1JSmj4nmZKzHdwHwxXGGf+qwMMpFZJe4IQn7sN94NQkjsSUKza8UV9uaJg+
nZ8PitefAgMBAAECggEAJRU/AfQ9k9s6KLMwviG667v5cBwx0AbLv2YDku6Al6hZ
E6XkUGz6rFfVmk0p5V2RGO/xB5fLsH6IHSIt5p/iePC0y2QCHXL7B7S4IXCDhUkC
/cmEIQT5rUY2M9GeL56+b436Gnn2uaoS/uI/isWM4V2OxJXJjcn0bkV4X4q7NtZ3
yZfjHIqMfOu7/nIsc27K1mF3WoWViUHVRMbc7mxU9J30xwNRfdtxuTVHnAa+knwe
e99XDgtx+XF/MaUERYQwbv1JBKArIJgjS4+jZ3Bh7l7qv6htsVeNZCu6/xjkgivA
I1Dy407JF8kVvxZSStKg955dfEjQmSKO2Iletq4OQQKBgQDCyTx2LXPfjpUFUJAb
I0nLVZWtFae6xOhQ0YKNmbjFcOam38unG5+tZm5hQ22/H5zo6ybH9PeEEfqs9+dX
C5TZ+1ybadmkilIfCSWpOGSVkS0irLunUhV3qpQNy40XREjqs8E04VwkmEMyBZMh
1QSObKKvgw3kbLTxRT2aky6UYQKBgQDBSWOFX9uT88isvS5pc6jYtOHUdSvJjAXB
8zOH3/Z7v6Xw0AeXfR/rSLbd0jWWih5QZyB3QgmtilB3ybF5IyQwcKWpaU4kStSF
zaQOArnRjJoX5HiNTTcSlnRejIPGGcfDJ/5mEZdz7IZJejkx9bxgeU8Jd7itRyYH
EUp29/Dr/wKBgQCE8osk8hMDhDLsRLeG/kfw08JsN7qavKj/+G/OLBy0DkIvpdI/
hZgv5xjxo+81IuObCl0W33lNGGRrSG0KuWnoeisUHGQjbRFuA3VEEax9dXBEGXef
VWQ3oCKbY9IyDjZikzwM1sBobdB3RNzOm7EXcMh9WMrTnOrHPoY0ib6iIQKBgGo7
n8tKaDDlKEJop/2laAPTRtuymqJnpzJ2LLhdS+ev6dB1Rfbo+oYirP817eYTe53N
UHa7gP40qw1reXOO8PD+uM5n5l7kEfKIl8ZrkR2vHXJMTEW9TquUrdjZegODX9vP
O29187vvH896sbzXxvVvNxWJC6ORG0F/K2I/29aZAoGAEqflT9Tlg5NF06xoBXhK
siiR9D9elgzUy/j3TVJbmPMniNGlHMj9JEVP8wnnMXoO8FBAF60YtetNWWsJVYCW
c59f39IrIF/1vbE4YwTuEa3ldCQMF2wx6yFJr8nCrp/4KYJMvlpFWTgyueo7WD5u
Uz5MX3quC+VnzfR5mwDPm1s=
-----END PRIVATE KEY-----
24 changes: 16 additions & 8 deletions examples/client_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
python3 server_sync.py
"""
import argparse
import os
import asyncio
import logging

Expand Down Expand Up @@ -49,6 +50,13 @@ def setup_async_client(args=None):
if args.comm != "serial" and args.port:
args.port = int(args.port)
_logger.info("### Create client object")
cwd = os.getcwd().split("/")[-1]
if cwd == "examples":
path = "."
elif cwd == "test":
path = "../examples"
else:
path = "examples"

if args.comm == "tcp":
client = AsyncModbusTcpClient(
Expand Down Expand Up @@ -97,7 +105,7 @@ def setup_async_client(args=None):
)
elif args.comm == "tls":
client = AsyncModbusTlsClient(
"localhost",
"127.0.0.1",
port=args.port,
# Common optional paramers:
framer=args.framer,
Expand All @@ -107,11 +115,11 @@ def setup_async_client(args=None):
# close_comm_on_error=False,
# strict=True,
# TLS setup parameters
# sslctx=None,
# certfile=None,
# keyfile=None,
# password=None,
# server_hostname="localhost",
# sslctx=sslctx,
certfile=f"{path}/certificates/pymodbus.crt",
keyfile=f"{path}/certificates/pymodbus.key",
# password="none",
server_hostname="localhost",
)
return client

Expand Down Expand Up @@ -191,5 +199,5 @@ def get_commandline():

if __name__ == "__main__":
# Connect/disconnect no calls.
testclient = setup_async_client()
asyncio.run(run_async_client(testclient))
testclient = setup_async_client(".")
asyncio.run(run_async_client(testclient), debug=True)
14 changes: 11 additions & 3 deletions examples/client_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
python3 server_sync.py
"""
import argparse
import os
import logging

# --------------------------------------------------------------------------- #
Expand All @@ -47,6 +48,13 @@ def setup_sync_client(args=None):
args = get_commandline()
if args.comm != "serial" and args.port:
args.port = int(args.port)
cwd = os.getcwd().split("/")[-1]
if cwd == "examples":
path = "."
elif cwd == "test":
path = "../examples"
else:
path = "examples"
_logger.info("### Create client object")
if args.comm == "tcp":
client = ModbusTcpClient(
Expand Down Expand Up @@ -106,10 +114,10 @@ def setup_sync_client(args=None):
# strict=True,
# TLS setup parameters
# sslctx=None,
# certfile=None,
# keyfile=None,
certfile=f"{path}/certificates/pymodbus.crt",
keyfile=f"{path}/certificates/pymodbus.key",
# password=None,
# server_hostname="localhost",
server_hostname="localhost",
)
return client

Expand Down
18 changes: 13 additions & 5 deletions examples/server_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
python3 client_sync.py
"""
import argparse
import os
import asyncio
import logging

Expand Down Expand Up @@ -145,6 +146,13 @@ def setup_async_server(args):
async def run_async_server(args=None):
"""Run server."""
server_id, port, store, identity, framer = setup_async_server(args)
cwd = os.getcwd().split("/")[-1]
if cwd == "examples":
path = "."
elif cwd == "test":
path = "../examples"
else:
path = "examples"

txt = f"### start ASYNC server on port {port}"
_logger.info(txt)
Expand Down Expand Up @@ -215,10 +223,10 @@ async def run_async_server(args=None):
framer=framer, # The framer strategy to use
# handler=None, # handler for each session
allow_reuse_address=True, # allow the reuse of an address
# certfile=None, # The cert file path for TLS (used if sslctx is None)
# sslctx=None, # The SSLContext to use for TLS (default None and auto create)
# keyfile=None, # The key file path for TLS (used if sslctx is None)
# password=None, # The password for for decrypting the private key file
certfile=f"{path}/certificates/pymodbus.crt", # The cert file path for TLS (used if sslctx is None)
# sslctx=sslctx, # The SSLContext to use for TLS (default None and auto create)
keyfile=f"{path}/certificates/pymodbus.key", # The key file path for TLS (used if sslctx is None)
# password="none", # The password for for decrypting the private key file
# reqclicert=False, # Force the sever request client"s certificate
# ignore_missing_slaves=True, # ignore request to a missing slave
# broadcast_enable=False, # treat unit_id 0 as broadcast address,
Expand Down Expand Up @@ -305,4 +313,4 @@ def get_commandline():


if __name__ == "__main__":
asyncio.run(run_async_server())
asyncio.run(run_async_server("."), debug=True)
16 changes: 12 additions & 4 deletions examples/server_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
python3 client_sync.py
"""
import argparse
import os
import logging

from pymodbus.datastore import (
Expand Down Expand Up @@ -144,6 +145,13 @@ def setup_sync_server(args):
def run_sync_server(args=None):
"""Run server."""
server_id, port, store, identity, framer = setup_sync_server(args)
cwd = os.getcwd().split("/")[-1]
if cwd == "examples":
path = "."
elif cwd == "test":
path = "../examples"
else:
path = "examples"
txt = f"### start server, listening on {port} - {server_id}"
_logger.info(txt)
if server_id == "tcp":
Expand Down Expand Up @@ -214,10 +222,10 @@ def run_sync_server(args=None):
address=None, # listen address
framer=framer, # The framer strategy to use
# handler=None, # handler for each session
# allow_reuse_address=True, # allow the reuse of an address
# certfile=None, # The cert file path for TLS (used if sslctx is None)
allow_reuse_address=True, # allow the reuse of an address
certfile=f"{path}/certificates/pymodbus.crt", # The cert file path for TLS (used if sslctx is None)
# sslctx=None, # The SSLContext to use for TLS (default None and auto create)
# keyfile=None, # The key file path for TLS (used if sslctx is None)
keyfile=f"{path}/certificates/pymodbus.key", # The key file path for TLS (used if sslctx is None)
# password=None, # The password for for decrypting the private key file
# reqclicert=False, # Force the sever request client"s certificate
# ignore_missing_slaves=True, # ignore request to a missing slave
Expand Down Expand Up @@ -305,5 +313,5 @@ def get_commandline():


if __name__ == "__main__":
server = run_sync_server()
server = run_sync_server(".")
server.shutdown()
54 changes: 15 additions & 39 deletions pymodbus/client/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
import ssl

from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient
from pymodbus.client.base import ModbusClientProtocol
from pymodbus.constants import Defaults
from pymodbus.framer import ModbusFramer
from pymodbus.framer.tls_framer import ModbusTlsFramer
from pymodbus.transaction import FifoTransactionManager

_logger = logging.getLogger(__name__)

Expand All @@ -28,18 +26,20 @@ def sslctx_provider(
:param keyfile: The optional client"s key file path for TLS server request
:param password: The password for for decrypting client"s private key file
"""
if sslctx is None:
# According to MODBUS/TCP Security Protocol Specification, it is
# TLSv2 at least
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
sslctx.verify_mode = ssl.CERT_REQUIRED
sslctx.check_hostname = True

if certfile and keyfile:
sslctx.load_cert_chain(
certfile=certfile, keyfile=keyfile, password=password
)

if sslctx:
return sslctx

sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
sslctx.check_hostname = False
sslctx.verify_mode = ssl.CERT_NONE
sslctx.options |= ssl.OP_NO_TLSv1_1
sslctx.options |= ssl.OP_NO_TLSv1
sslctx.options |= ssl.OP_NO_SSLv3
sslctx.options |= ssl.OP_NO_SSLv2
if certfile and keyfile:
sslctx.load_cert_chain(
certfile=certfile, keyfile=keyfile, password=password
)
return sslctx


Expand Down Expand Up @@ -89,28 +89,11 @@ def __init__(
self.params.keyfile = keyfile
self.params.password = password
self.params.server_hostname = server_hostname

if not sslctx:
self.params.sslctx = ssl.create_default_context()
# According to MODBUS/TCP Security Protocol Specification, it is
# TLSv2 at least
self.sslctx.options |= ssl.OP_NO_TLSv1_1
self.sslctx.options |= ssl.OP_NO_TLSv1
self.sslctx.options |= ssl.OP_NO_SSLv3
self.sslctx.options |= ssl.OP_NO_SSLv2
else:
self.sslctx = sslctx
AsyncModbusTcpClient.__init__(self, host, port=port, framer=framer, **kwargs)

async def connect(self):
"""Initiate connection to start client."""
# get current loop, if there are no loop a RuntimeError will be raised
self.loop = asyncio.get_running_loop()
return await AsyncModbusTcpClient.connect(self)

async def _connect(self):
"""Connect to server."""
_logger.debug("Connecting.")
_logger.debug("Connecting tls.")
try:
return await self.loop.create_connection(
self._create_protocol,
Expand All @@ -128,13 +111,6 @@ async def _connect(self):
_logger.info(txt)
self.reset_delay()

def _create_protocol(self):
"""Create initialized protocol instance with Factory function."""
protocol = ModbusClientProtocol(framer=self.params.framer, **self.params.kwargs)
protocol.transaction = FifoTransactionManager(self)
protocol.factory = self
return protocol


class ModbusTlsClient(ModbusTcpClient):
"""**ModbusTlsClient**.
Expand Down
24 changes: 18 additions & 6 deletions pymodbus/server/async_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ def sslctx_provider(
if sslctx is None:
# According to MODBUS/TCP Security Protocol Specification, it is
# TLSv2 at least
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
sslctx.verify_mode = ssl.CERT_NONE
sslctx.check_hostname = False
sslctx.options |= ssl.OP_NO_TLSv1_1
sslctx.options |= ssl.OP_NO_TLSv1
sslctx.options |= ssl.OP_NO_SSLv3
sslctx.options |= ssl.OP_NO_SSLv2
sslctx.load_cert_chain(certfile=certfile, keyfile=keyfile, password=password)

if reqclicert:
Expand Down Expand Up @@ -542,7 +548,10 @@ async def serve_forever(self):
**self.factory_parms,
)
self.serving.set_result(True)
await self.server.serve_forever()
try:
await self.server.serve_forever()
except asyncio.exceptions.CancelledError:
pass
else:
raise RuntimeError(
"Can't call serve_forever on an already running server object"
Expand Down Expand Up @@ -710,10 +719,13 @@ def __init__(
async def serve_forever(self):
"""Start endless loop."""
if self.protocol is None:
self.protocol, self.endpoint = await self.loop.create_datagram_endpoint(
lambda: self.handler(self),
**self.factory_parms,
)
try:
self.protocol, self.endpoint = await self.loop.create_datagram_endpoint(
lambda: self.handler(self),
**self.factory_parms,
)
except asyncio.exceptions.CancelledError:
pass
self.serving.set_result(True)
await self.stop_serving
else:
Expand Down
5 changes: 4 additions & 1 deletion pymodbus/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,10 @@ def getTransaction(self, tid):
"""
txt = f"Getting transaction {tid}"
_logger.debug(txt)

if not tid:
if self.transactions:
return self.transactions.popitem()[1]
return None
return self.transactions.pop(tid, None)

def delTransaction(self, tid):
Expand Down
Loading