From a3a305ef95322a533fbb14e6c8c2124d847465d9 Mon Sep 17 00:00:00 2001 From: Sebastian Majewski Date: Sun, 8 Sep 2024 22:52:52 -0500 Subject: [PATCH] Example client code refactor --- examples/lib/client.py | 158 ++++++++++++++++++ examples/lib/service.py | 34 ++-- examples/lib/tcp_service.py | 2 +- examples/lib/udp_service.py | 2 +- examples/tcp_daytime_service.py | 8 +- examples/tcp_discard_service.py | 9 +- examples/tcp_echo_client.py | 89 +++------- examples/tcp_echo_service.py | 8 +- examples/udp_daytime_service.py | 8 +- examples/udp_discard_service.py | 8 +- examples/udp_echo_client.py | 105 +++--------- examples/udp_echo_service.py | 8 +- .../icmp6/icmp6__packet_handler_rx.py | 5 + 13 files changed, 249 insertions(+), 195 deletions(-) create mode 100755 examples/lib/client.py diff --git a/examples/lib/client.py b/examples/lib/client.py new file mode 100755 index 00000000..2c7127c8 --- /dev/null +++ b/examples/lib/client.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +################################################################################ +## ## +## PyTCP - Python TCP/IP stack ## +## Copyright (C) 2020-present Sebastian Majewski ## +## ## +## This program is free software: you can redistribute it and/or modify ## +## it under the terms of the GNU General Public License as published by ## +## the Free Software Foundation, either version 3 of the License, or ## +## (at your option) any later version. ## +## ## +## This program is distributed in the hope that it will be useful, ## +## but WITHOUT ANY WARRANTY; without even the implied warranty of ## +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## +## GNU General Public License for more details. ## +## ## +## You should have received a copy of the GNU General Public License ## +## along with this program. If not, see . ## +## ## +## Author's email: ccie18643@gmail.com ## +## Github repository: https://github.com/ccie18643/PyTCP ## +## ## +################################################################################ + + +""" +The 'user space' generic client base class used in examples. + +examples/lib/client.py + +ver 3.0.2 +""" + + +from __future__ import annotations + +import threading +import time +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +import click + +from pytcp.lib import socket +from pytcp.lib.net_addr.ip_address import IpAddress + +if TYPE_CHECKING: + from pytcp.lib.socket import Socket + + +class Client(ABC): + """ + Generic client support class. + """ + + _protocol_name: str + _client_name: str + _local_ip_address: IpAddress + _local_port: int + _remote_ip_address: IpAddress + _remote_port: int + _run_thread: bool + + def start(self) -> None: + """ + Start the service thread. + """ + + click.echo( + f"Starting the {self._protocol_name} {self._client_name} service." + ) + self._run_thread = True + threading.Thread(target=self._thread__client).start() + time.sleep(0.1) + + def stop(self) -> None: + """ + Stop the service thread. + """ + + click.echo( + f"Stopping the {self._protocol_name} {self._client_name} service." + ) + self._run_thread = False + time.sleep(0.1) + + def _get_client_socket(self) -> Socket | None: + """ + Create and bind the client socket. + """ + + match ( + self._local_ip_address.version, + self._remote_ip_address.version, + self._protocol_name, + ): + case 6, 6, "TCP": + client_socket = socket.socket( + family=socket.AF_INET6, type=socket.SOCK_STREAM + ) + case 4, 4, "TCP": + client_socket = socket.socket( + family=socket.AF_INET4, type=socket.SOCK_STREAM + ) + case 6, 6, "UDP": + client_socket = socket.socket( + family=socket.AF_INET6, type=socket.SOCK_DGRAM + ) + case 4, 4, "UDP": + client_socket = socket.socket( + family=socket.AF_INET4, type=socket.SOCK_DGRAM + ) + case _: + raise ValueError("Invalid IP version or protocol.") + + click.echo( + f"Client {self._protocol_name} {self._client_name}: " + f"Created socket [{client_socket}]." + ) + + try: + client_socket.bind((str(self._local_ip_address), self._local_port)) + click.echo( + f"Client {self._protocol_name} {self._client_name}: Bound socket " + f"to {self._local_ip_address}, port {self._local_port}." + ) + except OSError as error: + click.echo( + f"Client {self._protocol_name} {self._client_name}: Unable to bind socket " + f"to {self._local_ip_address}, port {self._local_port} - {error!r}.", + ) + return None + + try: + client_socket.connect( + (str(self._remote_ip_address), self._remote_port) + ) + click.echo( + f"Client {self._protocol_name} {self._client_name}: Connection opened " + f"to {self._remote_ip_address}, port {self._remote_port}." + ) + except OSError as error: + click.echo( + f"Client {self._protocol_name} {self._client_name}: Connection to " + f"{self._remote_ip_address}, port {self._remote_port} failed - {error!r}." + ) + return None + + return client_socket + + @abstractmethod + def _thread__client(self) -> None: + """ + Client thread. + """ + + raise NotImplementedError diff --git a/examples/lib/service.py b/examples/lib/service.py index 6b4bbb0c..7c2d47ec 100755 --- a/examples/lib/service.py +++ b/examples/lib/service.py @@ -55,18 +55,10 @@ class Service(ABC): """ _protocol_name: str - - def __init__( - self, *, service_name: str, local_ip_address: IpAddress, local_port: int - ) -> None: - """ - Initialize the Service object. - """ - - self._service_name = service_name - self._local_ip_address = local_ip_address - self._local_port = local_port - self._run_thread = False + _service_name: str + _local_ip_address: IpAddress + _local_port: int + _run_thread: bool def start(self) -> None: """ @@ -91,33 +83,31 @@ def stop(self) -> None: self._run_thread = False time.sleep(0.1) - def _get_listening_socket(self) -> Socket | None: + def _get_service_socket(self) -> Socket | None: """ - Create and bind the listening socket. + Create and bind the service socket. """ match self._local_ip_address.version, self._protocol_name: case 6, "TCP": - listening_socket = socket.socket( + service_socket = socket.socket( family=socket.AF_INET6, type=socket.SOCK_STREAM ) case 4, "TCP": - listening_socket = socket.socket( + service_socket = socket.socket( family=socket.AF_INET4, type=socket.SOCK_STREAM ) case 6, "UDP": - listening_socket = socket.socket( + service_socket = socket.socket( family=socket.AF_INET6, type=socket.SOCK_DGRAM ) case 4, "UDP": - listening_socket = socket.socket( + service_socket = socket.socket( family=socket.AF_INET4, type=socket.SOCK_DGRAM ) try: - listening_socket.bind( - (str(self._local_ip_address), self._local_port) - ) + service_socket.bind((str(self._local_ip_address), self._local_port)) click.echo( f"Service {self._protocol_name} {self._service_name}: Socket created, " f"bound to {self._local_ip_address}, port {self._local_port}." @@ -130,7 +120,7 @@ def _get_listening_socket(self) -> Socket | None: ) return None - return listening_socket + return service_socket @abstractmethod def _thread__service(self) -> None: diff --git a/examples/lib/tcp_service.py b/examples/lib/tcp_service.py index 4738e0ad..7cb8b84a 100755 --- a/examples/lib/tcp_service.py +++ b/examples/lib/tcp_service.py @@ -59,7 +59,7 @@ def _thread__service(self) -> None: Service thread. """ - if listening_socket := self._get_listening_socket(): + if listening_socket := self._get_service_socket(): listening_socket.listen() click.echo( f"Service {self._protocol_name} {self._service_name}: Socket " diff --git a/examples/lib/udp_service.py b/examples/lib/udp_service.py index e7932f05..58db3aad 100755 --- a/examples/lib/udp_service.py +++ b/examples/lib/udp_service.py @@ -53,5 +53,5 @@ def _thread__service(self) -> None: Service initialization. """ - if listening_socket := self._get_listening_socket(): + if listening_socket := self._get_service_socket(): self._service(socket=listening_socket) diff --git a/examples/tcp_daytime_service.py b/examples/tcp_daytime_service.py index 5d88613a..8fcb3136 100755 --- a/examples/tcp_daytime_service.py +++ b/examples/tcp_daytime_service.py @@ -78,11 +78,9 @@ def __init__( Class constructor. """ - super().__init__( - service_name="Daytime", - local_ip_address=local_ip_address, - local_port=local_port, - ) + self._service_name = "Daytime" + self._local_ip_address = local_ip_address + self._local_port = local_port self._message_count = message_count self._message_delay = message_delay diff --git a/examples/tcp_discard_service.py b/examples/tcp_discard_service.py index 8d398f88..aac864ee 100755 --- a/examples/tcp_discard_service.py +++ b/examples/tcp_discard_service.py @@ -69,11 +69,10 @@ def __init__(self, *, local_ip_address: IpAddress, local_port: int): """ Class constructor. """ - super().__init__( - service_name="Discard", - local_ip_address=local_ip_address, - local_port=local_port, - ) + + self._service_name = "Discard" + self._local_ip_address = local_ip_address + self._local_port = local_port @override def _service(self, *, socket: Socket) -> None: diff --git a/examples/tcp_echo_client.py b/examples/tcp_echo_client.py index 412f9b6d..c22ecbf7 100755 --- a/examples/tcp_echo_client.py +++ b/examples/tcp_echo_client.py @@ -41,8 +41,8 @@ import click +from examples.lib.client import Client from pytcp import TcpIpStack, initialize_interface -from pytcp.lib import socket from pytcp.lib.net_addr import ( ClickTypeIp4Address, ClickTypeIp4Host, @@ -59,11 +59,14 @@ ) -class TcpEchoClient: +class TcpEchoClient(Client): """ TCP Echo client support class. """ + _protocol_name = "TCP" + _client_name = "Echo" + def __init__( self, *, @@ -112,72 +115,32 @@ def _thread__client(self) -> None: Client thread. """ - match self._local_ip_address.version: - case 6: - client_socket = socket.socket( - family=socket.AF_INET6, type=socket.SOCK_STREAM - ) - case 4: - client_socket = socket.socket( - family=socket.AF_INET4, type=socket.SOCK_STREAM + if client_socket := self._get_client_socket(): + message_count = self._message_count + while message_count: + message = "[------START------] " + for i in range(self._message_size - 2): + message += f"[------{i + 1:05}------] " + message += "[-------END-------]\n" + + try: + client_socket.send(bytes(message, "utf-8")) + except OSError as error: + click.echo(f"Client TCP Echo: send() error - {error!r}.") + break + + click.echo( + f"Client TCP Echo: Sent {len(message)} bytes of data to " + f"{self._remote_ip_address}, port {self._remote_port}." ) + time.sleep(self._message_delay) + message_count = min(message_count, message_count - 1) - click.echo(f"Client TCP Echo: Created socket [{client_socket}].") - - try: - client_socket.bind((str(self._local_ip_address), self._local_port)) - click.echo( - "Client TCP Echo: Bound socket to " - f"{self._local_ip_address}, port {self._local_port}." - ) - except OSError as error: - click.echo( - "Client TCP Echo: Unable to bind socket to " - f"{self._local_ip_address}, port {self._local_port} - " - f"{error!r}.", - ) - return - - try: - client_socket.connect( - (str(self._remote_ip_address), self._remote_port) - ) - click.echo( - "Client TCP Echo: Connection opened to " - f"{self._remote_ip_address}, port {self._remote_port}." - ) - except OSError as error: - click.echo( - f"Client TCP Echo: Connection to {self._remote_ip_address}, " - f"port {self._remote_port} failed - {error!r}." - ) - return - - message_count = self._message_count - while message_count: - message = "[------START------] " - for i in range(self._message_size - 2): - message += f"[------{i + 1:05}------] " - message += "[-------END-------]\n" - - try: - client_socket.send(bytes(message, "utf-8")) - except OSError as error: - click.echo(f"Client TCP Echo: send() error - {error!r}.") - break - + client_socket.close() click.echo( - f"Client TCP Echo: Sent {len(message)} bytes of data to " + "Client TCP Echo: Closed connection to " f"{self._remote_ip_address}, port {self._remote_port}." ) - time.sleep(self._message_delay) - message_count = min(message_count, message_count - 1) - - client_socket.close() - click.echo( - "Client TCP Echo: Closed connection to " - f"{self._remote_ip_address}, port {self._remote_port}." - ) @click.command() diff --git a/examples/tcp_echo_service.py b/examples/tcp_echo_service.py index c598fc3f..cbf63467 100755 --- a/examples/tcp_echo_service.py +++ b/examples/tcp_echo_service.py @@ -71,11 +71,9 @@ def __init__(self, *, local_ip_address: IpAddress, local_port: int): Class constructor. """ - super().__init__( - service_name="Echo", - local_ip_address=local_ip_address, - local_port=local_port, - ) + self._service_name = "Echo" + self._local_ip_address = local_ip_address + self._local_port = local_port @override def _service(self, *, socket: Socket) -> None: diff --git a/examples/udp_daytime_service.py b/examples/udp_daytime_service.py index ac98f6aa..0163c5e0 100755 --- a/examples/udp_daytime_service.py +++ b/examples/udp_daytime_service.py @@ -71,11 +71,9 @@ def __init__(self, *, local_ip_address: IpAddress, local_port: int): Class constructor. """ - super().__init__( - service_name="Echo", - local_ip_address=local_ip_address, - local_port=local_port, - ) + self._service_name = "Echo" + self._local_ip_address = local_ip_address + self._local_port = local_port @override def _service(self, *, socket: Socket) -> None: diff --git a/examples/udp_discard_service.py b/examples/udp_discard_service.py index c17af7d9..bec06a2a 100755 --- a/examples/udp_discard_service.py +++ b/examples/udp_discard_service.py @@ -70,11 +70,9 @@ def __init__(self, *, local_ip_address: IpAddress, local_port: int): Class constructor. """ - super().__init__( - service_name="Discard", - local_ip_address=local_ip_address, - local_port=local_port, - ) + self._service_name = "Discard" + self._local_ip_address = local_ip_address + self._local_port = local_port @override def _service(self, *, socket: Socket) -> None: diff --git a/examples/udp_echo_client.py b/examples/udp_echo_client.py index 041d63b1..1ab772aa 100755 --- a/examples/udp_echo_client.py +++ b/examples/udp_echo_client.py @@ -36,13 +36,12 @@ from __future__ import annotations -import threading import time import click +from examples.lib.client import Client from pytcp import TcpIpStack, initialize_interface -from pytcp.lib import socket from pytcp.lib.net_addr import ( ClickTypeIp4Address, ClickTypeIp4Host, @@ -59,11 +58,14 @@ ) -class UdpEchoClient: +class UdpEchoClient(Client): """ UDP Echo client support class. """ + _protocol_name = "UDP" + _client_name = "Echo" + def __init__( self, *, @@ -88,91 +90,38 @@ def __init__( self._message_size = message_size self._run_thread = False - def start(self) -> None: + def _thread__client(self) -> None: """ - Start the service thread. + Client thread. """ - click.echo("Starting the UDP Echo client.") - self._run_thread = True - threading.Thread(target=self._thread__client).start() - time.sleep(0.1) + if client_socket := self._get_client_socket(): + message_count = self._message_count - def stop(self) -> None: - """ - Stop the service thread. - """ + while self._run_thread and message_count: + message = "[------START------] " + for i in range(self._message_size - 2): + message += f"[------{i + 1:05}------] " + message += "[-------END-------]\n" - click.echo("Stopinging the UDP Echo client.") - self._run_thread = False - time.sleep(0.1) + try: + client_socket.send(bytes(message, "utf-8")) + except OSError as error: + click.echo(f"Client UDP Echo: send() error - {error!r}.") + break - def _thread__client(self) -> None: - match self._local_ip_address.version: - case 6: - client_socket = socket.socket( - family=socket.AF_INET6, type=socket.SOCK_DGRAM + click.echo( + f"Client UDP Echo: Sent {len(message)} bytes of data to " + f"{self._remote_ip_address}, port {self._remote_port}." ) - case 4: - client_socket = socket.socket( - family=socket.AF_INET4, type=socket.SOCK_DGRAM - ) - - click.echo(f"Client UDP Echo: Created socket [{client_socket}].") - - try: - client_socket.bind((str(self._local_ip_address), self._local_port)) - click.echo( - "Client UDP Echo: Bound socket to " - f"{self._local_ip_address}, port {self._local_port}." - ) - except OSError as error: - click.echo( - "Client UDP Echo: Unable to bind socket to " - f"{self._local_ip_address}, port {self._local_port} - {error!r}.", - ) - return - - try: - client_socket.connect( - (str(self._remote_ip_address), self._remote_port) - ) - click.echo( - f"Client UDP Echo: Connection opened to " - f"{self._remote_ip_address}, port {self._remote_port}." - ) - except OSError as error: - click.echo( - f"Client UDP Echo: Connection to {self._remote_ip_address}, " - f"port {self._remote_port} failed - {error!r}." - ) - return - - message_count = self._message_count - while self._run_thread and message_count: - message = "[------START------] " - for i in range(self._message_size - 2): - message += f"[------{i + 1:05}------] " - message += "[-------END-------]\n" - - try: - client_socket.send(bytes(message, "utf-8")) - except OSError as error: - click.echo(f"Client UDP Echo: send() error - {error!r}.") - break + time.sleep(self._message_delay) + message_count = min(message_count, message_count - 1) + client_socket.close() click.echo( - f"Client UDP Echo: Sent {len(message)} bytes of data to " - f"{self._remote_ip_address}, port {self._remote_port}." + "Client UDP Echo: Closed connection to " + f"{self._remote_ip_address}, port {self._remote_port}.", ) - time.sleep(self._message_delay) - message_count = min(message_count, message_count - 1) - - client_socket.close() - click.echo( - "Client UDP Echo: Closed connection to " - f"{self._remote_ip_address}, port {self._remote_port}.", - ) @click.command() diff --git a/examples/udp_echo_service.py b/examples/udp_echo_service.py index b58afe75..a0f85877 100755 --- a/examples/udp_echo_service.py +++ b/examples/udp_echo_service.py @@ -71,11 +71,9 @@ def __init__(self, *, local_ip_address: IpAddress, local_port: int): Class constructor. """ - super().__init__( - service_name="Echo", - local_ip_address=local_ip_address, - local_port=local_port, - ) + self._service_name = "Echo" + self._local_ip_address = local_ip_address + self._local_port = local_port @override def _service(self, *, socket: Socket) -> None: diff --git a/pytcp/protocols/icmp6/icmp6__packet_handler_rx.py b/pytcp/protocols/icmp6/icmp6__packet_handler_rx.py index 560f6d9b..3ae5b80f 100755 --- a/pytcp/protocols/icmp6/icmp6__packet_handler_rx.py +++ b/pytcp/protocols/icmp6/icmp6__packet_handler_rx.py @@ -208,6 +208,11 @@ def __phrx_icmp6__destination_unreachable( socket.notify_unreachable() return + # TODO: Need to add here handler for situation where Destination Unreachable + # message is received as response to TCP SYN packet. + # Way to reproduce: 'examples/tcp_echo_client.py --remote-ip-address 2600::' + # Similar handler shlould be added to ICMPv4 as well. + __debug__ and log( "icmp6", f"{packet_rx.tracker} - Unreachable data doesn't match "