openrvdas.logger.readers.udp_reader

No module-level documentation available.
  1#!/usr/bin/env python3
  2
  3import logging
  4import socket
  5
  6from logger.readers.reader import Reader  # noqa: E402
  7
  8# The UDP header's `length` field sets a theoretical limit of 65,535 bytes
  9# (8-byte header + 65,527 bytes of data) for a UDP datagram.  Technically, IPV4
 10# or IPv6 headers use up some of that size, so actual maximum data sent per
 11# datagram is slightly less.
 12#
 13# UDP receivers should always request the max, though, because if you request
 14# less than what's on the wire, you get what you asked for and the rest gets
 15# tossed on the floor.  There's no built-in error detection/correction in UDP,
 16# so that would mess things up pretty good.
 17READ_BUFFER_SIZE = 65535
 18
 19# On the send side of things, we can (and do) detect when a write has failed
 20# because the user's `record` was too big.  This is usually because the user is
 21# trying to send huge datagrams after looking up the "theoretical max size of a
 22# datagram" on wikipedia.  Well, it's called "theoretical" for a reason.  It's
 23# really the maximum size of the data portion of a UDP datagram, but that
 24# doesn't take into account extra header for IPv4/IPv6 or seemingly random
 25# system-level caps (e.g., Mac's socket implementation set maximum udp send
 26# size to 9K).
 27#
 28# When UDPWriter detects this condition, it fragments the record into smaller
 29# records and appends each fragment with this FRAGMENT_MARKER.  Inside
 30# UDPReader.read(), we check to see if a received datagram ends with this
 31# marker, and if it does, we read another datagram and combine the results
 32# (over and over until we get a datagram that doesn't end with the marker).
 33FRAGMENT_MARKER = b'\xff\xffTOOBIG\xff\xff'
 34
 35
 36################################################################################
 37class UDPReader(Reader):
 38    """Read UDP packets from network."""
 39    ############################
 40    def __init__(self, interface=None, port=None, mc_group=None,
 41                 reuseaddr=False, reuseport=False, eol=None,
 42                 allow_empty=False, this_is_a_test=False, **kwargs):
 43        """
 44        ```
 45        interface    IP (or resolvable name) of interface to listen on.  None or ''
 46                     will listen on INADDR_ANY (all interfaces).  If joining a
 47                     multicast group and None or '' specified, this will default
 48                     to whatever the system's hostname resolves to.  This IP should
 49                     not be on the loopback network (OK for testing, but won't work
 50                     in the real world).
 51
 52        port         Port to listen to for packets.  REQUIRED
 53
 54        mc_group     If specified, IP address of multicast group id to subscribe to.
 55
 56        reuseaddr    Specifies wether we set SO_REUSEADDR on the created socket.  If
 57                     you don't know you need this, don't enable it.
 58
 59        reuseport    Specifies wether we set SO_REUSEPORT on the created socket.  If
 60                     you don't know you need this, don't enable it.
 61
 62        eol          split the record by the eol character if present.
 63
 64        allow_empty - If True, preserve and return empty records
 65
 66        this_is_a_test - If True, recognize that this is being called in a unittest, so
 67                don't output warnings about not using loopback addresses.
 68        ```
 69        """
 70        super().__init__(**kwargs)
 71
 72        if interface:
 73            # resolve once in constructor
 74            interface = socket.gethostbyname(interface)
 75        else:
 76            interface = ''
 77        self.interface = interface
 78
 79        # make sure user passed in `port`
 80        #
 81        # NOTE: We want the order of the arguments to consistently be (ip,
 82        #       port, ...) across all the network readers/writers... but we
 83        #       want `interface` to be optional.  All kwargs need to come after
 84        #       all regular args, so we've assigned a default value of None to
 85        #       `port`.  But don't be confused, it is REQUIRED.
 86        #
 87        if not port:
 88            raise TypeError('must specify `port`')
 89        # make sure port gets stored as an int, even if passed in as a string
 90        self.port = int(port)
 91
 92        # prep multicast parameters
 93        if mc_group:
 94            # resolve once in constructor
 95            mc_group = socket.gethostbyname(mc_group)
 96            if not interface:
 97                # multicast needs to specify interface to use, so let's pick a
 98                # sane default
 99                #
100                # NOTE: This means hostname cannot be an alias to localhost, or
101                #       you won't be able to send IGMP packets correctly.
102                #
103                self.interface = socket.gethostbyname(socket.gethostname())
104
105        self.mc_group = mc_group
106
107        self.reuseaddr = reuseaddr
108        self.reuseport = reuseport
109
110        self.eol = eol
111        self.allow_empty = allow_empty
112
113        self.this_is_a_test = this_is_a_test
114
115        # socket gets initialized on-demand in read()
116        self.socket = None
117
118    ############################
119    def __del__(self):
120        try:
121            if self.socket:
122                self.socket.close()
123        except AttributeError:
124            pass
125
126    ############################
127    def _open_socket(self):
128        """Do socket prep so we're ready to read().  Returns socket object or None on
129        failure.
130        """
131        sock = socket.socket(family=socket.AF_INET,
132                             type=socket.SOCK_DGRAM,
133                             proto=socket.IPPROTO_UDP)
134        if self.reuseaddr:
135            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
136        if self.reuseport:
137            try:  # Raspbian doesn't recognize SO_REUSEPORT
138                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, True)
139            except AttributeError:
140                logging.warning('Unable to set socket REUSEPORT; may be unsupported.')
141
142        # If mc_group is specified, subscribe to it as a multicast group
143        if self.mc_group:
144            # set outgoing multicast interface
145            #
146            # NOTE: Can't use loopback device for this, otherwise IGMP packets
147            #       never leave the system, and you never actually join the
148            #       group.
149            #
150            if self.interface.startswith('127.') and not self.this_is_a_test:
151                logging.warning("Can't use loopback device for joining multicast groups.  Make "
152                                "sure your system's hostname does NOT resolve to something in "
153                                "the 127.0.0.0/8 address block (e.g., localhost, 127.0.0.1), or "
154                                "specify the interface to use by passing its IP address as the "
155                                "`interface` parameter.  (You can ignore this message if you're "
156                                "actually just doing loopback testing.)")
157            sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
158                            socket.inet_aton(self.interface))
159
160            # join the group via IGMP
161            #
162            # NOTE: Since these are both already encoded as binary by
163            #       inet_aton(), we can just concatenate them.  Alternatively,
164            #       could use struct.pack("4s4s", ...) to create a struct to
165            #       pass into setsockopt()
166            #
167            sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
168                            socket.inet_aton(self.mc_group) + socket.inet_aton(self.interface))
169
170            # bind to mc_group:port
171            sock.bind((self.mc_group, self.port))
172
173        else:
174            # broadcast or unicast, bind to specificed interface
175            sock.bind((self.interface, self.port))
176
177        return sock
178
179    ############################
180    def read(self):
181        """
182        Read the next UDP packet.
183        """
184        # If socket isn't ready, set it up.  If something fails, return w/out reading.
185        if not self.socket:
186            self.socket = self._open_socket()
187        if not self.socket:
188            logging.error('UDPReader.read: unable to open UDP socket')
189            return
190
191        # Read datagrams until we get one that doesn't end with a FRAGMENT_MARKER
192        record_buffer = b''
193        while True:
194            try:
195                record = self.socket.recv(READ_BUFFER_SIZE)
196            except OSError as e:
197                logging.error('UDPReader error: %s', str(e))
198                return None
199            logging.debug('UDPReader.read: received %d bytes', len(record))
200
201            if record.endswith(FRAGMENT_MARKER):
202                # UDPWriter fragmented this record because it was too large to
203                # send as a single datagram
204                logging.info('UDPrader.read: detected fragmented packet')
205                record_buffer += record.rsplit(FRAGMENT_MARKER, maxsplit=1)[0]
206                logging.debug('record_buffer: %s', record_buffer)
207            else:
208                record_buffer += record
209                break
210
211        # we've got a whole record in our record_buffer, decode it
212
213        # if eol == None, return the record as is
214        if not self.eol:
215            return self._decode_bytes(record_buffer, self.allow_empty)
216
217        # otherwise split the record by the eol
218        decoded_records = self._decode_bytes(record_buffer,
219                                             self.allow_empty).rstrip(self.eol).split(self.eol)
220
221        # if there was only one record, return just the first element in the
222        # list, otherwise return the whole list.
223        return decoded_records[0] if len(decoded_records) == 1 else decoded_records
READ_BUFFER_SIZE = 65535
FRAGMENT_MARKER = b'\xff\xffTOOBIG\xff\xff'
class UDPReader(logger.readers.reader.Reader):
 38class UDPReader(Reader):
 39    """Read UDP packets from network."""
 40    ############################
 41    def __init__(self, interface=None, port=None, mc_group=None,
 42                 reuseaddr=False, reuseport=False, eol=None,
 43                 allow_empty=False, this_is_a_test=False, **kwargs):
 44        """
 45        ```
 46        interface    IP (or resolvable name) of interface to listen on.  None or ''
 47                     will listen on INADDR_ANY (all interfaces).  If joining a
 48                     multicast group and None or '' specified, this will default
 49                     to whatever the system's hostname resolves to.  This IP should
 50                     not be on the loopback network (OK for testing, but won't work
 51                     in the real world).
 52
 53        port         Port to listen to for packets.  REQUIRED
 54
 55        mc_group     If specified, IP address of multicast group id to subscribe to.
 56
 57        reuseaddr    Specifies wether we set SO_REUSEADDR on the created socket.  If
 58                     you don't know you need this, don't enable it.
 59
 60        reuseport    Specifies wether we set SO_REUSEPORT on the created socket.  If
 61                     you don't know you need this, don't enable it.
 62
 63        eol          split the record by the eol character if present.
 64
 65        allow_empty - If True, preserve and return empty records
 66
 67        this_is_a_test - If True, recognize that this is being called in a unittest, so
 68                don't output warnings about not using loopback addresses.
 69        ```
 70        """
 71        super().__init__(**kwargs)
 72
 73        if interface:
 74            # resolve once in constructor
 75            interface = socket.gethostbyname(interface)
 76        else:
 77            interface = ''
 78        self.interface = interface
 79
 80        # make sure user passed in `port`
 81        #
 82        # NOTE: We want the order of the arguments to consistently be (ip,
 83        #       port, ...) across all the network readers/writers... but we
 84        #       want `interface` to be optional.  All kwargs need to come after
 85        #       all regular args, so we've assigned a default value of None to
 86        #       `port`.  But don't be confused, it is REQUIRED.
 87        #
 88        if not port:
 89            raise TypeError('must specify `port`')
 90        # make sure port gets stored as an int, even if passed in as a string
 91        self.port = int(port)
 92
 93        # prep multicast parameters
 94        if mc_group:
 95            # resolve once in constructor
 96            mc_group = socket.gethostbyname(mc_group)
 97            if not interface:
 98                # multicast needs to specify interface to use, so let's pick a
 99                # sane default
100                #
101                # NOTE: This means hostname cannot be an alias to localhost, or
102                #       you won't be able to send IGMP packets correctly.
103                #
104                self.interface = socket.gethostbyname(socket.gethostname())
105
106        self.mc_group = mc_group
107
108        self.reuseaddr = reuseaddr
109        self.reuseport = reuseport
110
111        self.eol = eol
112        self.allow_empty = allow_empty
113
114        self.this_is_a_test = this_is_a_test
115
116        # socket gets initialized on-demand in read()
117        self.socket = None
118
119    ############################
120    def __del__(self):
121        try:
122            if self.socket:
123                self.socket.close()
124        except AttributeError:
125            pass
126
127    ############################
128    def _open_socket(self):
129        """Do socket prep so we're ready to read().  Returns socket object or None on
130        failure.
131        """
132        sock = socket.socket(family=socket.AF_INET,
133                             type=socket.SOCK_DGRAM,
134                             proto=socket.IPPROTO_UDP)
135        if self.reuseaddr:
136            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
137        if self.reuseport:
138            try:  # Raspbian doesn't recognize SO_REUSEPORT
139                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, True)
140            except AttributeError:
141                logging.warning('Unable to set socket REUSEPORT; may be unsupported.')
142
143        # If mc_group is specified, subscribe to it as a multicast group
144        if self.mc_group:
145            # set outgoing multicast interface
146            #
147            # NOTE: Can't use loopback device for this, otherwise IGMP packets
148            #       never leave the system, and you never actually join the
149            #       group.
150            #
151            if self.interface.startswith('127.') and not self.this_is_a_test:
152                logging.warning("Can't use loopback device for joining multicast groups.  Make "
153                                "sure your system's hostname does NOT resolve to something in "
154                                "the 127.0.0.0/8 address block (e.g., localhost, 127.0.0.1), or "
155                                "specify the interface to use by passing its IP address as the "
156                                "`interface` parameter.  (You can ignore this message if you're "
157                                "actually just doing loopback testing.)")
158            sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
159                            socket.inet_aton(self.interface))
160
161            # join the group via IGMP
162            #
163            # NOTE: Since these are both already encoded as binary by
164            #       inet_aton(), we can just concatenate them.  Alternatively,
165            #       could use struct.pack("4s4s", ...) to create a struct to
166            #       pass into setsockopt()
167            #
168            sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
169                            socket.inet_aton(self.mc_group) + socket.inet_aton(self.interface))
170
171            # bind to mc_group:port
172            sock.bind((self.mc_group, self.port))
173
174        else:
175            # broadcast or unicast, bind to specificed interface
176            sock.bind((self.interface, self.port))
177
178        return sock
179
180    ############################
181    def read(self):
182        """
183        Read the next UDP packet.
184        """
185        # If socket isn't ready, set it up.  If something fails, return w/out reading.
186        if not self.socket:
187            self.socket = self._open_socket()
188        if not self.socket:
189            logging.error('UDPReader.read: unable to open UDP socket')
190            return
191
192        # Read datagrams until we get one that doesn't end with a FRAGMENT_MARKER
193        record_buffer = b''
194        while True:
195            try:
196                record = self.socket.recv(READ_BUFFER_SIZE)
197            except OSError as e:
198                logging.error('UDPReader error: %s', str(e))
199                return None
200            logging.debug('UDPReader.read: received %d bytes', len(record))
201
202            if record.endswith(FRAGMENT_MARKER):
203                # UDPWriter fragmented this record because it was too large to
204                # send as a single datagram
205                logging.info('UDPrader.read: detected fragmented packet')
206                record_buffer += record.rsplit(FRAGMENT_MARKER, maxsplit=1)[0]
207                logging.debug('record_buffer: %s', record_buffer)
208            else:
209                record_buffer += record
210                break
211
212        # we've got a whole record in our record_buffer, decode it
213
214        # if eol == None, return the record as is
215        if not self.eol:
216            return self._decode_bytes(record_buffer, self.allow_empty)
217
218        # otherwise split the record by the eol
219        decoded_records = self._decode_bytes(record_buffer,
220                                             self.allow_empty).rstrip(self.eol).split(self.eol)
221
222        # if there was only one record, return just the first element in the
223        # list, otherwise return the whole list.
224        return decoded_records[0] if len(decoded_records) == 1 else decoded_records

Read UDP packets from network.

UDPReader( interface=None, port=None, mc_group=None, reuseaddr=False, reuseport=False, eol=None, allow_empty=False, this_is_a_test=False, **kwargs)
 41    def __init__(self, interface=None, port=None, mc_group=None,
 42                 reuseaddr=False, reuseport=False, eol=None,
 43                 allow_empty=False, this_is_a_test=False, **kwargs):
 44        """
 45        ```
 46        interface    IP (or resolvable name) of interface to listen on.  None or ''
 47                     will listen on INADDR_ANY (all interfaces).  If joining a
 48                     multicast group and None or '' specified, this will default
 49                     to whatever the system's hostname resolves to.  This IP should
 50                     not be on the loopback network (OK for testing, but won't work
 51                     in the real world).
 52
 53        port         Port to listen to for packets.  REQUIRED
 54
 55        mc_group     If specified, IP address of multicast group id to subscribe to.
 56
 57        reuseaddr    Specifies wether we set SO_REUSEADDR on the created socket.  If
 58                     you don't know you need this, don't enable it.
 59
 60        reuseport    Specifies wether we set SO_REUSEPORT on the created socket.  If
 61                     you don't know you need this, don't enable it.
 62
 63        eol          split the record by the eol character if present.
 64
 65        allow_empty - If True, preserve and return empty records
 66
 67        this_is_a_test - If True, recognize that this is being called in a unittest, so
 68                don't output warnings about not using loopback addresses.
 69        ```
 70        """
 71        super().__init__(**kwargs)
 72
 73        if interface:
 74            # resolve once in constructor
 75            interface = socket.gethostbyname(interface)
 76        else:
 77            interface = ''
 78        self.interface = interface
 79
 80        # make sure user passed in `port`
 81        #
 82        # NOTE: We want the order of the arguments to consistently be (ip,
 83        #       port, ...) across all the network readers/writers... but we
 84        #       want `interface` to be optional.  All kwargs need to come after
 85        #       all regular args, so we've assigned a default value of None to
 86        #       `port`.  But don't be confused, it is REQUIRED.
 87        #
 88        if not port:
 89            raise TypeError('must specify `port`')
 90        # make sure port gets stored as an int, even if passed in as a string
 91        self.port = int(port)
 92
 93        # prep multicast parameters
 94        if mc_group:
 95            # resolve once in constructor
 96            mc_group = socket.gethostbyname(mc_group)
 97            if not interface:
 98                # multicast needs to specify interface to use, so let's pick a
 99                # sane default
100                #
101                # NOTE: This means hostname cannot be an alias to localhost, or
102                #       you won't be able to send IGMP packets correctly.
103                #
104                self.interface = socket.gethostbyname(socket.gethostname())
105
106        self.mc_group = mc_group
107
108        self.reuseaddr = reuseaddr
109        self.reuseport = reuseport
110
111        self.eol = eol
112        self.allow_empty = allow_empty
113
114        self.this_is_a_test = this_is_a_test
115
116        # socket gets initialized on-demand in read()
117        self.socket = None
interface    IP (or resolvable name) of interface to listen on.  None or ''
             will listen on INADDR_ANY (all interfaces).  If joining a
             multicast group and None or '' specified, this will default
             to whatever the system's hostname resolves to.  This IP should
             not be on the loopback network (OK for testing, but won't work
             in the real world).

port         Port to listen to for packets.  REQUIRED

mc_group     If specified, IP address of multicast group id to subscribe to.

reuseaddr    Specifies wether we set SO_REUSEADDR on the created socket.  If
             you don't know you need this, don't enable it.

reuseport    Specifies wether we set SO_REUSEPORT on the created socket.  If
             you don't know you need this, don't enable it.

eol          split the record by the eol character if present.

allow_empty - If True, preserve and return empty records

this_is_a_test - If True, recognize that this is being called in a unittest, so
        don't output warnings about not using loopback addresses.
interface
port
mc_group
reuseaddr
reuseport
eol
allow_empty
this_is_a_test
socket
def read(self):
181    def read(self):
182        """
183        Read the next UDP packet.
184        """
185        # If socket isn't ready, set it up.  If something fails, return w/out reading.
186        if not self.socket:
187            self.socket = self._open_socket()
188        if not self.socket:
189            logging.error('UDPReader.read: unable to open UDP socket')
190            return
191
192        # Read datagrams until we get one that doesn't end with a FRAGMENT_MARKER
193        record_buffer = b''
194        while True:
195            try:
196                record = self.socket.recv(READ_BUFFER_SIZE)
197            except OSError as e:
198                logging.error('UDPReader error: %s', str(e))
199                return None
200            logging.debug('UDPReader.read: received %d bytes', len(record))
201
202            if record.endswith(FRAGMENT_MARKER):
203                # UDPWriter fragmented this record because it was too large to
204                # send as a single datagram
205                logging.info('UDPrader.read: detected fragmented packet')
206                record_buffer += record.rsplit(FRAGMENT_MARKER, maxsplit=1)[0]
207                logging.debug('record_buffer: %s', record_buffer)
208            else:
209                record_buffer += record
210                break
211
212        # we've got a whole record in our record_buffer, decode it
213
214        # if eol == None, return the record as is
215        if not self.eol:
216            return self._decode_bytes(record_buffer, self.allow_empty)
217
218        # otherwise split the record by the eol
219        decoded_records = self._decode_bytes(record_buffer,
220                                             self.allow_empty).rstrip(self.eol).split(self.eol)
221
222        # if there was only one record, return just the first element in the
223        # list, otherwise return the whole list.
224        return decoded_records[0] if len(decoded_records) == 1 else decoded_records

Read the next UDP packet.