# encoding: ascii-8bit # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it # under the terms of the GNU Affero General Public License # as published by the Free Software Foundation; version 3 with # attribution addendums as found in the LICENSE.txt # # 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 Affero General Public License for more details. # Modified by OpenC3, Inc. # All changes Copyright 2022, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. require 'socket' require 'ipaddr' require 'timeout' # for Timeout::Error # Define needed constants for Windows Socket::IP_MULTICAST_IF = 9 unless Socket.const_defined?('IP_MULTICAST_IF') Socket::IP_MULTICAST_TTL = 10 unless Socket.const_defined?('IP_MULTICAST_TTL') module OpenC3 class UdpReadWriteSocket HOST_0_0_0_0 = '0.0.0.0' # @param bind_port [Integer[ Port to write data out from and receive data on (0 = randomly assigned) # @param bind_address [String] Local address to bind to (0.0.0.0 = All local addresses) # @param external_port [Integer] External port to write to # @param external_address [String] External host to send data to # @param multicast_interface_address [String] Local outgoing interface to send multicast packets from # @param ttl [Integer] Time To Live for outgoing multicast packets # @param read_multicast [Boolean] Whether or not to try to read from the external address as multicast # @param write_multicast [Boolean] Whether or not to write to the external address as multicast def initialize( bind_port = 0, bind_address = HOST_0_0_0_0, external_port = nil, external_address = nil, multicast_interface_address = nil, ttl = 1, read_multicast = true, write_multicast = true ) @socket = UDPSocket.new # Basic setup to reuse address @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) # Bind to local address and port - This sets recv port, write_src port, recv_address, and write_src_address @socket.bind(bind_address, bind_port) if bind_address and bind_port # Default send to the specified address and port @socket.connect(external_address, external_port) if external_address and external_port # Handle multicast if UdpReadWriteSocket.multicast?(external_address, external_port) if write_multicast # Basic setup set time to live @socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl.to_i) # Set outgoing interface @socket.setsockopt( Socket::IPPROTO_IP, Socket::IP_MULTICAST_IF, IPAddr.new(multicast_interface_address).hton ) if multicast_interface_address end # Receive messages sent to the multicast address if read_multicast multicast_interface_address = HOST_0_0_0_0 unless multicast_interface_address membership = IPAddr.new(external_address).hton + IPAddr.new(multicast_interface_address).hton @socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership) end end end # @param data [String] Binary data to send # @param write_timeout [Float] Time in seconds to wait for the data to send def write(data, write_timeout = 10.0) num_bytes_to_send = data.length total_bytes_sent = 0 bytes_sent = 0 data_to_send = data loop do begin bytes_sent = @socket.write_nonblock(data_to_send) rescue Errno::EAGAIN, Errno::EWOULDBLOCK result = IO.fast_select(nil, [@socket], nil, write_timeout) if result retry else raise Timeout::Error, "Write Timeout" end end total_bytes_sent += bytes_sent break if total_bytes_sent >= num_bytes_to_send data_to_send = data[total_bytes_sent..-1] end end # @param read_timeout [Float] Time in seconds to wait for the read to # complete def read(read_timeout = nil) data = nil begin data, _ = @socket.recvfrom_nonblock(65536) rescue Errno::EAGAIN, Errno::EWOULDBLOCK result = IO.fast_select([@socket], nil, nil, read_timeout) if result retry else raise Timeout::Error, "Read Timeout" end end data end # Defer all methods to the UDPSocket def method_missing(method, *args, &block) @socket.__send__(method, *args, &block) end # @param host [String] Machine name or IP address # @param port [String] Port # @return [Boolean] Whether the hostname is multicast def self.multicast?(host, port) return false if host.nil? || port.nil? Addrinfo.udp(host, port).ipv4_multicast? end end # Creates a UDPSocket and implements a non-blocking write. class UdpWriteSocket < UdpReadWriteSocket # @param dest_address [String] Host to send data to # @param dest_port [Integer] Port to send data to # @param src_port [Integer[ Port to send data out from # @param multicast_interface_address [String] Local outgoing interface to send multicast packets from # @param ttl [Integer] Time To Live for outgoing packets # @param bind_address [String] Local address to bind to (0.0.0.0 = All local addresses) def initialize( dest_address, dest_port, src_port = nil, multicast_interface_address = nil, ttl = 1, bind_address = HOST_0_0_0_0 ) super( src_port, bind_address, dest_port, dest_address, multicast_interface_address, ttl, false, true) end end # Creates a UDPSocket and implements a non-blocking read. class UdpReadSocket < UdpReadWriteSocket # @param recv_port [Integer] Port to receive data on # @param multicast_address [String] Address to add multicast # @param multicast_interface_address [String] Local incoming interface to receive multicast packets on # @param bind_address [String] Local address to bind to (0.0.0.0 = All local addresses) def initialize( recv_port = 0, multicast_address = nil, multicast_interface_address = nil, bind_address = HOST_0_0_0_0 ) super( recv_port, bind_address, nil, multicast_address, multicast_interface_address, 1, true, false) end end end