Skip to content

Commit ce19705

Browse files
authored
Add TLS feature for Modbus asynchronous (#470)
* Add TLS feature for Modbus asynchronous client Since we have Modbus TLS client in synchronous mode, we can also implement Modbus TLS client in asynchronous mode with ASYNC_IO. * Add TLS feature for Modbus asynchronous server Since we have Modbus TLS server in synchronous mode, we can also implement Modbus TLS server in asynchronous mode with ASYNC_IO.
1 parent 635d8ab commit ce19705

File tree

8 files changed

+464
-4
lines changed

8 files changed

+464
-4
lines changed

examples/common/asyncio_server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# --------------------------------------------------------------------------- #
1414
import asyncio
1515
from pymodbus.server.asyncio import StartTcpServer
16+
from pymodbus.server.asyncio import StartTlsServer
1617
from pymodbus.server.asyncio import StartUdpServer
1718
from pymodbus.server.asyncio import StartSerialServer
1819

@@ -127,6 +128,12 @@ async def run_server():
127128
# StartTcpServer(context, identity=identity,
128129
# framer=ModbusRtuFramer, address=("0.0.0.0", 5020))
129130

131+
# Tls:
132+
# await StartTlsServer(context, identity=identity, address=("localhost", 8020),
133+
# certfile="server.crt", keyfile="server.key",
134+
# allow_reuse_address=True, allow_reuse_port=True,
135+
# defer_start=False)
136+
130137
# Udp:
131138
# server = await StartUdpServer(context, identity=identity, address=("0.0.0.0", 5020),
132139
# allow_reuse_address=True, defer_start=True)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python
2+
"""
3+
Simple Asynchronous Modbus TCP over TLS client
4+
---------------------------------------------------------------------------
5+
6+
This is a simple example of writing a asynchronous modbus TCP over TLS client
7+
that uses Python builtin module ssl - TLS/SSL wrapper for socket objects for
8+
the TLS feature and asyncio.
9+
"""
10+
# -------------------------------------------------------------------------- #
11+
# import neccessary libraries
12+
# -------------------------------------------------------------------------- #
13+
import ssl
14+
from pymodbus.client.asynchronous.tls import AsyncModbusTLSClient
15+
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
16+
17+
# -------------------------------------------------------------------------- #
18+
# the TLS detail security can be set in SSLContext which is the context here
19+
# -------------------------------------------------------------------------- #
20+
context = ssl.create_default_context()
21+
context.options |= ssl.OP_NO_SSLv2
22+
context.options |= ssl.OP_NO_SSLv3
23+
context.options |= ssl.OP_NO_TLSv1
24+
context.options |= ssl.OP_NO_TLSv1_1
25+
26+
async def start_async_test(client):
27+
result = await client.read_coils(1, 8)
28+
print(result.bits)
29+
await client.write_coils(1, [False]*3)
30+
result = await client.read_coils(1, 8)
31+
print(result.bits)
32+
33+
if __name__ == '__main__':
34+
# -------------------------------------------------------------------------- #
35+
# pass SSLContext which is the context here to ModbusTcpClient()
36+
# -------------------------------------------------------------------------- #
37+
loop, client = AsyncModbusTLSClient(ASYNC_IO, 'test.host.com', 8020,
38+
sslctx=context)
39+
loop.run_until_complete(start_async_test(client.protocol))
40+
loop.close()

pymodbus/client/asynchronous/asyncio/__init__.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
import socket
55
import asyncio
66
import functools
7+
import ssl
78
from pymodbus.exceptions import ConnectionException
89
from pymodbus.client.asynchronous.mixins import AsyncModbusClientMixin
910
from pymodbus.compat import byte2int
11+
from pymodbus.transaction import FifoTransactionManager
1012
import logging
1113

1214
_logger = logging.getLogger(__name__)
1315

1416
DGRAM_TYPE = socket.SocketKind.SOCK_DGRAM
1517

16-
1718
class BaseModbusAsyncClientProtocol(AsyncModbusClientMixin):
1819
"""
1920
Asyncio specific implementation of asynchronous modbus client protocol.
@@ -423,6 +424,66 @@ def protocol_lost_connection(self, protocol):
423424
' callback called while not connected.')
424425

425426

427+
class ReconnectingAsyncioModbusTlsClient(ReconnectingAsyncioModbusTcpClient):
428+
"""
429+
Client to connect to modbus device repeatedly over TLS."
430+
"""
431+
def __init__(self, protocol_class=None, loop=None, framer=None):
432+
"""
433+
Initialize ReconnectingAsyncioModbusTcpClient
434+
:param protocol_class: Protocol used to talk to modbus device.
435+
:param loop: Event loop to use
436+
"""
437+
self.framer = framer
438+
ReconnectingAsyncioModbusTcpClient.__init__(self, protocol_class, loop)
439+
440+
@asyncio.coroutine
441+
def start(self, host, port=802, sslctx=None, server_hostname=None):
442+
"""
443+
Initiates connection to start client
444+
:param host:
445+
:param port:
446+
:param sslctx:
447+
:param server_hostname:
448+
:return:
449+
"""
450+
self.sslctx = sslctx
451+
if self.sslctx is None:
452+
self.sslctx = ssl.create_default_context()
453+
# According to MODBUS/TCP Security Protocol Specification, it is
454+
# TLSv2 at least
455+
self.sslctx.options |= ssl.OP_NO_TLSv1_1
456+
self.sslctx.options |= ssl.OP_NO_TLSv1
457+
self.sslctx.options |= ssl.OP_NO_SSLv3
458+
self.sslctx.options |= ssl.OP_NO_SSLv2
459+
self.server_hostname = server_hostname
460+
yield from ReconnectingAsyncioModbusTcpClient.start(self, host, port)
461+
462+
@asyncio.coroutine
463+
def _connect(self):
464+
_logger.debug('Connecting.')
465+
try:
466+
yield from self.loop.create_connection(self._create_protocol,
467+
self.host,
468+
self.port,
469+
ssl=self.sslctx,
470+
server_hostname=self.server_hostname)
471+
except Exception as ex:
472+
_logger.warning('Failed to connect: %s' % ex)
473+
asyncio.ensure_future(self._reconnect(), loop=self.loop)
474+
else:
475+
_logger.info('Connected to %s:%s.' % (self.host, self.port))
476+
self.reset_delay()
477+
478+
def _create_protocol(self):
479+
"""
480+
Factory function to create initialized protocol instance.
481+
"""
482+
protocol = self.protocol_class(framer=self.framer)
483+
protocol.transaction = FifoTransactionManager(self)
484+
protocol.factory = self
485+
return protocol
486+
426487
class ReconnectingAsyncioModbusUdpClient(object):
427488
"""
428489
Client to connect to modbus device repeatedly over UDP.
@@ -774,6 +835,27 @@ def init_tcp_client(proto_cls, loop, host, port, **kwargs):
774835
return client
775836

776837

838+
@asyncio.coroutine
839+
def init_tls_client(proto_cls, loop, host, port, sslctx=None,
840+
server_hostname=None, framer=None, **kwargs):
841+
"""
842+
Helper function to initialize tcp client
843+
:param proto_cls:
844+
:param loop:
845+
:param host:
846+
:param port:
847+
:param sslctx:
848+
:param server_hostname:
849+
:param framer:
850+
:param kwargs:
851+
:return:
852+
"""
853+
client = ReconnectingAsyncioModbusTlsClient(protocol_class=proto_cls,
854+
loop=loop, framer=framer)
855+
yield from client.start(host, port, sslctx, server_hostname)
856+
return client
857+
858+
777859
@asyncio.coroutine
778860
def init_udp_client(proto_cls, loop, host, port, **kwargs):
779861
"""
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Factory to create asynchronous tls clients based on asyncio
3+
"""
4+
from __future__ import unicode_literals
5+
from __future__ import absolute_import
6+
7+
import logging
8+
9+
from pymodbus.client.asynchronous import schedulers
10+
from pymodbus.client.asynchronous.thread import EventLoopThread
11+
from pymodbus.constants import Defaults
12+
13+
LOGGER = logging.getLogger(__name__)
14+
15+
def async_io_factory(host="127.0.0.1", port=Defaults.TLSPort, sslctx=None,
16+
server_hostname=None, framer=None, source_address=None,
17+
timeout=None, **kwargs):
18+
"""
19+
Factory to create asyncio based asynchronous tls clients
20+
:param host: Host IP address
21+
:param port: Port
22+
:param sslctx: The SSLContext to use for TLS (default None and auto create)
23+
:param server_hostname: Target server's name matched for certificate
24+
:param framer: Modbus Framer
25+
:param source_address: Bind address
26+
:param timeout: Timeout in seconds
27+
:param kwargs:
28+
:return: asyncio event loop and tcp client
29+
"""
30+
import asyncio
31+
from pymodbus.client.asynchronous.asyncio import init_tls_client
32+
loop = kwargs.get("loop") or asyncio.new_event_loop()
33+
proto_cls = kwargs.get("proto_cls", None)
34+
if not loop.is_running():
35+
asyncio.set_event_loop(loop)
36+
cor = init_tls_client(proto_cls, loop, host, port, sslctx, server_hostname,
37+
framer)
38+
client = loop.run_until_complete(asyncio.gather(cor))[0]
39+
else:
40+
cor = init_tls_client(proto_cls, loop, host, port, sslctx, server_hostname,
41+
framer)
42+
future = asyncio.run_coroutine_threadsafe(cor, loop=loop)
43+
client = future.result()
44+
45+
return loop, client
46+
47+
48+
def get_factory(scheduler):
49+
"""
50+
Gets protocol factory based on the backend scheduler being used
51+
:param scheduler: ASYNC_IO
52+
:return
53+
"""
54+
if scheduler == schedulers.ASYNC_IO:
55+
return async_io_factory
56+
else:
57+
LOGGER.warning("Allowed Schedulers: {}".format(
58+
schedulers.ASYNC_IO
59+
))
60+
raise Exception("Invalid Scheduler '{}'".format(scheduler))
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import unicode_literals
2+
from __future__ import absolute_import
3+
4+
import logging
5+
from pymodbus.client.asynchronous.factory.tls import get_factory
6+
from pymodbus.constants import Defaults
7+
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
8+
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
9+
from pymodbus.factory import ClientDecoder
10+
from pymodbus.transaction import ModbusTlsFramer
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class AsyncModbusTLSClient(object):
16+
"""
17+
Actual Async TLS Client to be used.
18+
19+
To use do::
20+
21+
from pymodbus.client.asynchronous.tls import AsyncModbusTLSClient
22+
"""
23+
def __new__(cls, scheduler, host="127.0.0.1", port=Defaults.TLSPort,
24+
framer=None, sslctx=None, server_hostname=None,
25+
source_address=None, timeout=None, **kwargs):
26+
"""
27+
Scheduler to use:
28+
- async_io (asyncio)
29+
:param scheduler: Backend to use
30+
:param host: Host IP address
31+
:param port: Port
32+
:param framer: Modbus Framer to use
33+
:param sslctx: The SSLContext to use for TLS (default None and auto create)
34+
:param server_hostname: Target server's name matched for certificate
35+
:param source_address: source address specific to underlying backend
36+
:param timeout: Time out in seconds
37+
:param kwargs: Other extra args specific to Backend being used
38+
:return:
39+
"""
40+
if (not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4))
41+
and scheduler == ASYNC_IO):
42+
logger.critical("ASYNCIO is supported only on python3")
43+
import sys
44+
sys.exit(1)
45+
framer = framer or ModbusTlsFramer(ClientDecoder())
46+
factory_class = get_factory(scheduler)
47+
yieldable = factory_class(host=host, port=port, sslctx=sslctx,
48+
server_hostname=server_hostname,
49+
framer=framer, source_address=source_address,
50+
timeout=timeout, **kwargs)
51+
return yieldable
52+

0 commit comments

Comments
 (0)