Skip to content

TLS Support #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
venv
.pytest_cache/
__pycache__/
build/


13 changes: 13 additions & 0 deletions sunspec2/modbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@

modbus_rtu_clients = {}

# Reference for SVP driver
MAPPED = 'Mapped SunSpec Device'
RTU = 'Modbus RTU'
TCP = 'Modbus TCP'

class SunSpecModbusClientError(Exception):
pass
Expand Down Expand Up @@ -247,8 +251,10 @@ def scan(self, progress=None, delay=None, connect=False):

class SunSpecModbusClientDeviceTCP(SunSpecModbusClientDevice):
def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None,
tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False,
max_count=modbus_client.REQ_COUNT_MAX, test=False, model_class=SunSpecModbusClientModel):
SunSpecModbusClientDevice.__init__(self, model_class=model_class)

self.slave_id = slave_id
self.ipaddr = ipaddr
self.ipport = ipport
Expand All @@ -257,9 +263,16 @@ def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx
self.socket = None
self.trace_func = trace_func
self.max_count = max_count
self.tls = tls
self.cafile = cafile
self.certfile = certfile
self.keyfile = keyfile
self.insecure_skip_tls_verify = insecure_skip_tls_verify

self.client = modbus_client.ModbusClientTCP(slave_id=slave_id, ipaddr=ipaddr, ipport=ipport, timeout=timeout,
ctx=ctx, trace_func=trace_func,
tls=tls, cafile=cafile, certfile=certfile, keyfile=keyfile,
insecure_skip_tls_verify=insecure_skip_tls_verify,
max_count=modbus_client.REQ_COUNT_MAX, test=test)
if self.client is None:
raise SunSpecModbusClientError('No modbus tcp client set for device')
Expand Down
51 changes: 51 additions & 0 deletions sunspec2/modbus/modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
import socket
import struct
import serial
try:
import ssl
except Exception as e:
print('Missing ssl python package: %s' % e)

PARITY_NONE = 'N'
PARITY_EVEN = 'E'
Expand Down Expand Up @@ -428,7 +432,41 @@ def write(self, slave_id, addr, data, trace_func=None, max_count=REQ_COUNT_MAX):


class ModbusClientTCP(object):
"""Provides access to a Modbus TCP device.

Parameters:
slave_id :
Modbus slave id.
ipaddr :
IP address string.
ipport :
IP port.
timeout :
Modbus request timeout in seconds. Fractional seconds are permitted such as .5.
ctx :
Context variable to be used by the object creator. Not used by the modbus module.
trace_func :
Trace function to use for detailed logging. No detailed logging is perform is a trace function is
not supplied.
tls :
Use TLS (Modbus/TCP Security). Defaults to `tls=False`.
cafile :
Path to certificate authority (CA) certificate to use for validating server certificates.
Only used if `tls=True`.
certfile :
Path to client TLS certificate to use for client authentication. Only used if `tls=True`.
keyfile :
Path to client TLS key to use for client authentication. Only used if `tls=True`.
insecure_skip_tls_verify :
Skip verification of server TLS certificate. Only used if `tls=True`.
max_count :
Maximum register count for a single Modbus request.
test :
Use test socket. If True use the fake socket module for network communications.
"""

def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None,
tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False,
max_count=REQ_COUNT_MAX, test=False):
self.slave_id = slave_id
self.ipaddr = ipaddr
Expand All @@ -437,6 +475,11 @@ def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx
self.ctx = ctx
self.socket = None
self.trace_func = trace_func
self.tls = tls
self.cafile = cafile
self.certfile = certfile
self.keyfile = keyfile
self.tls_verify = not insecure_skip_tls_verify
self.max_count = max_count

if ipport is None:
Expand Down Expand Up @@ -465,6 +508,14 @@ def connect(self, timeout=None):
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(timeout)

if self.tls:
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=self.cafile)
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
context.check_hostname = self.tls_verify

self.socket = context.wrap_socket(self.socket, server_side=False, server_hostname=self.ipaddr)

self.socket.connect((self.ipaddr, self.ipport))
except Exception as e:
raise ModbusClientError('Connection error: %s' % str(e))
Expand Down