From 37c533507c44d87072658fb6de1020233fcf6d25 Mon Sep 17 00:00:00 2001 From: twystd Date: Fri, 27 Dec 2024 15:09:41 -0800 Subject: [PATCH] emulator: added support for multiple requests on an established TCP/IP connection --- Rev.0/cli/Makefile | 2 +- Rev.0/cli/commands.py | 22 ++++- Rev.0/cli/main.py | 19 ++++- Rev.0/cli/pool.py | 141 ++++++++++++++++++++++++++++++++ Rev.0/cli/tls.py | 4 +- Rev.0/emulator/go/UT0311/tcp.go | 82 ++++++++++++------- TODO.md | 2 +- 7 files changed, 234 insertions(+), 38 deletions(-) create mode 100644 Rev.0/cli/pool.py diff --git a/Rev.0/cli/Makefile b/Rev.0/cli/Makefile index e7a3665..a7d2208 100644 --- a/Rev.0/cli/Makefile +++ b/Rev.0/cli/Makefile @@ -8,7 +8,7 @@ build: format python3 -m compileall . debug: - python3 main.py get-status + python3 main.py --destination $(ADDRESS) --protocol tcp::pool debug get-controller: python3 main.py get-controller diff --git a/Rev.0/cli/commands.py b/Rev.0/cli/commands.py index 7dd51d5..c6cbfef 100644 --- a/Rev.0/cli/commands.py +++ b/Rev.0/cli/commands.py @@ -10,6 +10,7 @@ from uhppoted import decode import tls +import pool CONTROLLER = 405419896 DOOR = 3 @@ -97,11 +98,22 @@ def get_controller(u, dest, timeout, args, protocol='udp'): request = encode.get_controller_request(CONTROLLER) bind = '0.0.0.0' - reply = _send(request, bind, dest, timeout, True) + reply = _tls(request, bind, dest, timeout, True) if reply != None: return decode.get_controller_response(reply) else: return None + + elif protocol == 'tcp::pool': + request = encode.get_controller_request(CONTROLLER) + bind = '0.0.0.0' + + reply = _pool(request, bind, dest, timeout, True) + if reply != None: + return decode.get_controller_response(reply) + else: + return None + else: controller = (CONTROLLER, dest, protocol) @@ -405,7 +417,13 @@ def onEvent(event): # INTERNAL: TLS handler -def _send(request, bind, dest, timeout, debug): +def _tls(request, bind, dest, timeout, debug): transport = tls.TLS(bind, debug) return transport.send(request, dest, timeout) + +# INTERNAL: pooled TCP handler +def _pool(request, bind, dest, timeout, debug): + transport = pool.Pool(bind, debug) + + return transport.send(request, dest, timeout) diff --git a/Rev.0/cli/main.py b/Rev.0/cli/main.py index 1087d27..63275cc 100644 --- a/Rev.0/cli/main.py +++ b/Rev.0/cli/main.py @@ -2,6 +2,7 @@ import argparse import sys +import time import traceback from trace import Trace @@ -14,7 +15,7 @@ def main(): usage() return -1 - parser = argparse.ArgumentParser(description='uhppoted-codegen example') + parser = argparse.ArgumentParser(description='uhppoted-breakout CLI') parser.add_argument('command', type=str, help='command') @@ -47,7 +48,7 @@ def main(): default=2.5, help='(optional) operation timeout (in seconds). Defaults to 2.5.') - parser.add_argument('--protocol', choices=['udp', 'tcp', 'tls'], default='udp', help='transport protocol') + parser.add_argument('--protocol', choices=['udp', 'tcp', 'tcp::pool', 'tls'], default='udp', help='transport protocol') args = parser.parse_args() cmd = args.command @@ -62,6 +63,20 @@ def main(): print() print(f'*** ERROR {cmd}: {x}') print() + elif cmd == 'debug': + try: + for i in range(3): + exec(commands()['get-controller'], args) + time.sleep(5) + except Exception as x: + print() + print(f'*** ERROR {cmd}: {x}') + print() + if debug: + print(traceback.format_exc()) + + sys.exit(1) + elif cmd in commands(): try: exec(commands()[cmd], args) diff --git a/Rev.0/cli/pool.py b/Rev.0/cli/pool.py new file mode 100644 index 0000000..c76b3dd --- /dev/null +++ b/Rev.0/cli/pool.py @@ -0,0 +1,141 @@ +''' +UHPPOTE TCP communications wrapper. + +Implements the functionality to send and receive 64 byte TCP packets to/from a UHPPOTE +access controller using a 'pooled' TCP/IP connection. +''' + +import socket +import ssl +import struct +import re +import time +import ipaddress + +from uhppoted import net + + +class Pool: + pool = {} + + def __init__(self, bind='0.0.0.0', debug=False): + ''' + Initialises a TCP/IP communications wrapper with the bind address. + + Parameters: + bind (string) The IPv4 address:port to which to bind when sending a request. + debug (bool) Dumps the sent and received packets to the console if enabled. + + Returns: + Initialised TCPPool object. + + Raises: + Exception If any of the supplied IPv4 values cannot be translated to a valid IPv4 + address:port combination. + ''' + self._bind = (bind, 0) + self._debug = debug + + def send(self, request, dest_addr, timeout=2.5): + ''' + Binds to the bind address from the constructor and connects to the access controller after which it sends + the request and waits 'timeout' seconds for the reply (if any). + + Parameters: + request (bytearray) 64 byte request packet. + dest_addr (string) Optional IPv4 address:port of the controller. Defaults to port 60000 + if dest_addr does not include a port. + timeout (float) Optional operation timeout (in seconds). Defaults to 2.5s. + + Returns: + Received response packet (if any) or None (for set-ip request). + + Raises: + Error For any socket related errors. + ''' + self.dump(request) + + addr = net.resolve(f'{dest_addr}') + pool = self.pool + key = f'{addr[0]}:{addr[1]}' + sock = None + + if key in pool: + s = pool[key] + if s.fileno() != -1: + sock = s + + if not sock: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if not is_INADDR_ANY(self._bind): + sock.bind(self._bind) + + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, net.WRITE_TIMEOUT) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, net.READ_TIMEOUT) + + sock.connect(addr) + + pool[key] = sock + + sock.sendall(request) + + if request[1] == 0x96: + return None + else: + return _read(sock, timeout=timeout, debug=self._debug) + + def dump(self, packet): + ''' + Prints a packet to the console as a formatted hexadecimal string if debug was enabled in the + constructor. + + Parameters: + packet (bytearray) 64 byte UDP packet. + + Returns: + None. + ''' + if self._debug: + net.dump(packet) + + +def is_INADDR_ANY(addr): + if addr == None: + return True + + if f'{addr}' == '': + return True + + if addr == (('0.0.0.0', 0)): + return True + + return False + + +# TODO convert to asyncio +def _read(sock, timeout=2.5, debug=False): + ''' + Waits 2.5 seconds for a single 64 byte packet to be received on the socket. Prints the packet to the console + if debug is True. + + Parameters: + sock (socket) Initialised and open UDP socket. + timeout (float) Optional operation timeout (in seconds). Defaults to 2.5s. + debug (bool) Enables dumping the received packet to the console. + + Returns: + Received 64 byte UDP packet (or None). + ''' + time_limit = net.timeout_to_seconds(timeout) + + sock.settimeout(time_limit) + + while True: + reply = sock.recv(1024) + if len(reply) == 64: + if debug: + net.dump(reply) + return reply + + return None diff --git a/Rev.0/cli/tls.py b/Rev.0/cli/tls.py index 1da1376..32268c0 100644 --- a/Rev.0/cli/tls.py +++ b/Rev.0/cli/tls.py @@ -1,8 +1,8 @@ ''' -UHPPOTE TCP communications wrapper. +UHPPOTE TLS communications wrapper. Implements the functionality to send and receive 64 byte TCP packets to/from a UHPPOTE -access controller. +access controller over TLS. ''' import socket diff --git a/Rev.0/emulator/go/UT0311/tcp.go b/Rev.0/emulator/go/UT0311/tcp.go index a2775be..8dc5b30 100644 --- a/Rev.0/emulator/go/UT0311/tcp.go +++ b/Rev.0/emulator/go/UT0311/tcp.go @@ -1,8 +1,12 @@ package UT0311 import ( + "errors" + "io" "net" "net/netip" + "os" + "time" codec "github.com/uhppoted/uhppote-core/encoding/UTO311-L0x" "github.com/uhppoted/uhppote-core/messages" @@ -11,6 +15,8 @@ import ( type TCP struct { } +const READ_TIMEOUT = 30000 * time.Millisecond + func (tcp TCP) listen(received func(any) (any, error)) error { bind := netip.MustParseAddrPort("0.0.0.0:60000") @@ -28,41 +34,57 @@ func (tcp TCP) listen(received func(any) (any, error)) error { infof("TCP incoming") go func() { - buffer := make([]byte, 2048) - - if N, err := client.Read(buffer); err != nil { + if err := tcp.read(client, received); err != nil { warnf("TCP read error (%v)", err) - } else { - debugf("TCP received %v bytes from %v", N, client.RemoteAddr()) - - if request, err := messages.UnmarshalRequest(buffer[0:N]); err != nil { - warnf("TCP %v", err) - } else { - reply, err := received(request) - - if err != nil { - warnf("TCP %v", err) - } - - if !isnil(reply) { - if packet, err := codec.Marshal(reply); err != nil { - warnf("TCP %v", err) - } else if packet == nil { - warnf("TCP invalid reply packet (%v)", packet) - } else if N, err := client.Write(packet); err != nil { - warnf("TCP %v", err) - } else { - debugf("TCP sent %v bytes to %v", N, client.RemoteAddr()) - } - } - } } + }() + } + } + } +} + +func (tcp TCP) read(socket net.Conn, received func(any) (any, error)) error { + defer socket.Close() - if err := client.Close(); err != nil { - warnf("TCP close error (%v)", err) + for { + buffer := make([]byte, 2048) + deadline := time.Now().Add(READ_TIMEOUT) + + socket.SetReadDeadline(deadline) + + if N, err := socket.Read(buffer); err != nil && errors.Is(err, io.EOF) { + return nil + } else if err != nil && errors.Is(err, os.ErrDeadlineExceeded) { + warnf("TCP closing connection to %v (idle)", socket.RemoteAddr()) + return nil + } else if err != nil { + return err + } else { + debugf("TCP received %v bytes from %v", N, socket.RemoteAddr()) + + if request, err := messages.UnmarshalRequest(buffer[0:N]); err != nil { + warnf("TCP %v", err) + } else { + reply, err := received(request) + + if err != nil { + warnf("TCP %v", err) + } + + if !isnil(reply) { + if packet, err := codec.Marshal(reply); err != nil { + warnf("TCP %v", err) + } else if packet == nil { + warnf("TCP invalid reply packet (%v)", packet) + } else if N, err := socket.Write(packet); err != nil { + warnf("TCP %v", err) + } else { + debugf("TCP sent %v bytes to %v", N, socket.RemoteAddr()) } - }() + } } } } + + return nil } diff --git a/TODO.md b/TODO.md index dcc7338..ba1979e 100644 --- a/TODO.md +++ b/TODO.md @@ -65,7 +65,7 @@ On some boards, the XOSC can take longer than usual to stabilize. On such boards - [ ] API - [x] UDP - [x] TCP/IP - - [ ] allow multiple requests + - [x] allow multiple requests - [ ] TLS - [x] CLI TLS - [x] mutual auth