From d73df1454eebfd2095db92e2f2393de3375d7b65 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 5 Jan 2020 20:52:42 +0100 Subject: [PATCH] Added TCP and UDP plugins - closes #106 --- docs/source/platypush/plugins/tcp.rst | 5 ++ docs/source/platypush/plugins/udp.rst | 5 ++ docs/source/plugins.rst | 2 + platypush/plugins/tcp.py | 119 ++++++++++++++++++++++++++ platypush/plugins/udp.py | 58 +++++++++++++ 5 files changed, 189 insertions(+) create mode 100644 docs/source/platypush/plugins/tcp.rst create mode 100644 docs/source/platypush/plugins/udp.rst create mode 100644 platypush/plugins/tcp.py create mode 100644 platypush/plugins/udp.py diff --git a/docs/source/platypush/plugins/tcp.rst b/docs/source/platypush/plugins/tcp.rst new file mode 100644 index 00000000..85676e00 --- /dev/null +++ b/docs/source/platypush/plugins/tcp.rst @@ -0,0 +1,5 @@ +``platypush.plugins.tcp`` +========================= + +.. automodule:: platypush.plugins.tcp + :members: diff --git a/docs/source/platypush/plugins/udp.rst b/docs/source/platypush/plugins/udp.rst new file mode 100644 index 00000000..0ae4d10a --- /dev/null +++ b/docs/source/platypush/plugins/udp.rst @@ -0,0 +1,5 @@ +``platypush.plugins.udp`` +========================= + +.. automodule:: platypush.plugins.udp + :members: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index d24237ab..183c024b 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -85,11 +85,13 @@ Plugins platypush/plugins/switch.rst platypush/plugins/switch.switchbot.rst platypush/plugins/switch.wemo.rst + platypush/plugins/tcp.rst platypush/plugins/todoist.rst platypush/plugins/torrent.rst platypush/plugins/trello.rst platypush/plugins/tts.rst platypush/plugins/tts.google.rst + platypush/plugins/udp.rst platypush/plugins/user.rst platypush/plugins/utils.rst platypush/plugins/variable.rst diff --git a/platypush/plugins/tcp.py b/platypush/plugins/tcp.py new file mode 100644 index 00000000..79fa5cf1 --- /dev/null +++ b/platypush/plugins/tcp.py @@ -0,0 +1,119 @@ +import base64 +import json +import socket + +from typing import Optional, Union + +from platypush.plugins import Plugin, action + + +class TcpPlugin(Plugin): + """ + Plugin for raw TCP communications. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._sockets = {} + + def _connect(self, host: str, port: int, timeout: Optional[float] = None) -> socket.socket: + sd = self._sockets.get((host, port)) + if sd: + return sd + + sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if timeout: + sd.settimeout(timeout) + sd.connect((host, port)) + self._sockets[(host, port)] = sd + return sd + + @action + def connect(self, host: str, port: int, timeout: Optional[float] = None): + """ + Open a TCP connection. + + :param host: Host IP/name. + :param port: TCP port. + :param timeout: Connection timeout in seconds (default: None). + """ + self._connect(host, port, timeout) + + @action + def close(self, host: str, port: int): + """ + Close an active TCP connection. + + :param host: Host IP/name. + :param port: TCP port. + """ + sd = self._sockets.get((host, port)) + if not sd: + self.logger.warning('Not connected to ({}, {})'.format(host, port)) + return + + sd.close() + + @action + def send(self, data: Union[bytes, str], host: str, port: int, binary: bool = False, + timeout: Optional[float] = None, recv_response: bool = False, **recv_opts): + """ + Send data over a TCP connection. If the connection isn't active it will be created. + + :param data: Data to be sent, as bytes or string. + :param host: Host IP/name. + :param port: TCP port. + :param binary: If set to True and ``data`` is a string then will be treated as base64-encoded binary input. + :param timeout: Connection timeout in seconds (default: None). + :param recv_response: If True then the action will wait for a response from the server before closing the + connection. Note that ``recv_opts`` must be specified in this case - at least ``length``. + """ + if isinstance(data, list) or isinstance(data, dict): + data = json.dumps(data) + if isinstance(data, str): + data = data.encode() + if binary: + data = base64.decodebytes(data) + + sd = self._connect(host, port, timeout) + + try: + sd.send(data) + if recv_response: + recv_opts.update({ + 'host': host, + 'port': port, + 'timeout': timeout, + 'binary': binary, + }) + + return self.recv(**recv_opts) + finally: + self.close(host, port) + + @action + def recv(self, length: int, host: str, port: int, binary: bool = False, timeout: Optional[float] = None) -> str: + """ + Receive data from a TCP connection. If the connection isn't active it will be created. + + :param length: Maximum number of bytes to be received. + :param host: Host IP/name. + :param port: TCP port. + :param binary: If set to True then the output will be base64-encoded, otherwise decoded as string. + :param timeout: Connection timeout in seconds (default: None). + """ + sd = self._connect(host, port, timeout) + + try: + data = sd.recv(length) + if binary: + data = base64.encodebytes(data).decode() + else: + data = data.decode() + + return data + finally: + self.close(host, port) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/udp.py b/platypush/plugins/udp.py new file mode 100644 index 00000000..755458d3 --- /dev/null +++ b/platypush/plugins/udp.py @@ -0,0 +1,58 @@ +import base64 +import json +import socket + +from typing import Optional, Union + +from platypush.plugins import Plugin, action + + +class UdpPlugin(Plugin): + """ + Plugin for raw UDP communications. + """ + + @action + def send(self, data: Union[bytes, str], host: str, port: int, binary: bool = False, + timeout: Optional[float] = None, recv_response: bool = False, **recv_opts): + """ + Send data over a UDP connection. + + :param data: Data to be sent, as bytes or string. + :param host: Host IP/name. + :param port: TCP port. + :param binary: If set to True and ``data`` is a string then will be treated as base64-encoded binary input. + :param timeout: Connection timeout in seconds (default: None). + :param recv_response: If True then the action will wait for a response from the server before closing the + connection. Note that ``recv_opts`` must be specified in this case - at least ``length``. + """ + if isinstance(data, list) or isinstance(data, dict): + data = json.dumps(data) + if isinstance(data, str): + data = data.encode() + if binary: + data = base64.decodebytes(data) + + sd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + if timeout: + sd.settimeout(timeout) + + sd.sendto(data, (host, port)) + if not recv_response: + return + + recv_opts.update({ + 'host': host, + 'port': port, + 'timeout': timeout, + 'binary': binary, + }) + + data = sd.recvfrom(**recv_opts) + if binary: + data = base64.encodebytes(data) + data = data.decode() + return data + + +# vim:sw=4:ts=4:et: