From e81fff37418c307a3164b6939201fba8f99d6310 Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Tue, 5 Apr 2022 08:37:45 -0700 Subject: [PATCH] Add getter methods for remote and local IP addresses and ports. Now, every `TCP.Engine`, `TCP.Listen.Engine`, and `TCP.Accept.Ticket` has getter methods allowing the caller to get the remote and/or local address and port number for that entity. --- manifest.savi | 3 +++ spec/TCP.Spec.savi | 45 ++++++++++++++++++++++++++++---------- src/TCP.Accept.Ticket.savi | 13 +++++++---- src/TCP.Engine.savi | 35 +++++++++++++++++++++++++++++ src/TCP.Listen.Engine.savi | 22 ++++++++++++++++--- src/_FFI.savi | 1 + src/_NetAddress.savi | 41 +++++++++++++++------------------- 7 files changed, 118 insertions(+), 42 deletions(-) diff --git a/manifest.savi b/manifest.savi index 1138482..985e1ce 100644 --- a/manifest.savi +++ b/manifest.savi @@ -12,6 +12,9 @@ :dependency OSError v0 :from "github:savi-lang/OSError" + :dependency IPAddress v0 + :from "github:savi-lang/IPAddress" + :manifest bin "spec" :copies TCP :sources "spec/*.savi" diff --git a/spec/TCP.Spec.savi b/spec/TCP.Spec.savi index 34a509a..5e55d8d 100644 --- a/spec/TCP.Spec.savi +++ b/spec/TCP.Spec.savi @@ -13,8 +13,13 @@ :fun ref io_react(action IO.Action) case action == ( | IO.Action.Opened | - TCP.Spec.EchoClient.new(@env, Inspect[@io.listen_port_number]) - @env.err.print("[Listener] Listening") + try ( + listen_address = @io.listen_address_with_port_number! + @env.err.print("[Listener] Listening on \(listen_address)") + TCP.Spec.EchoClient.new(@env, "\(listen_address.port_number)") + | + @env.err.print("[Listener] Failed to get listen address") + ) | IO.Action.OpenFailed | @env.err.print("[Listener] Not listening:") @env.err.print(@io.listen_error.name) @@ -33,18 +38,27 @@ :let listener TCP.Spec.Listener :let io TCP.Engine :new (@env, @listener, ticket) + remote_address = ticket.remote_address_with_port_number + @env.err.print("[Echoer] Accepting \(remote_address)") @io = TCP.Engine.accept(@, --ticket) - @env.err.print("[Echoer] Accepted") :fun ref io_react(action IO.Action) case action == ( + | IO.Action.Opened | + try ( + local_address = @io.local_address_with_port_number! + remote_address = @io.remote_address_with_port_number! + @env.err.print( + "[Echoer] Accepted from \(remote_address) on \(local_address)" + ) + | + @env.err.print("[Echoer] Failed to get local and/or remote address") + ) | IO.Action.Read | @io.pending_reads -> (bytes_available | - @io.read_stream.advance_to_end - bytes val = @io.read_stream.extract_token - @env.err.print("[Echoer] Received:") - @env.err.print(bytes.as_string) - @io.write_stream << bytes.clone // TODO: is clone still needed? + bytes val = @io.read_stream.extract_all + @env.err.print("[Echoer] Received: \(Inspect[bytes])") + @io.write_stream << bytes try @io.flush! // TODO: should we flush automatically on close below? @io.close ) @@ -74,7 +88,15 @@ :fun ref io_react(action IO.Action) case action == ( | IO.Action.Opened | - @env.err.print("[EchoClient] Connected") + try ( + local_address = @io.local_address_with_port_number! + remote_address = @io.remote_address_with_port_number! + @env.err.print( + "[EchoClient] Connected from \(local_address) to \(remote_address)" + ) + | + @env.err.print("[EchoClient] Failed to get local and/or remote address") + ) @io.write_stream << b"Hello, World!" try @io.flush! @@ -85,9 +107,8 @@ | IO.Action.Read | @io.pending_reads -> (bytes_available | if (bytes_available >= b"Hello, World!".size) ( - @io.read_stream.advance_to_end - @env.err.print("[EchoClient] Received:") - @env.err.print(@io.read_stream.extract_token.as_string) + bytes val = @io.read_stream.extract_all + @env.err.print("[EchoClient] Received: \(Inspect[bytes])") @io.close ) ) diff --git a/src/TCP.Accept.Ticket.savi b/src/TCP.Accept.Ticket.savi index ebd073c..03f370c 100644 --- a/src/TCP.Accept.Ticket.savi +++ b/src/TCP.Accept.Ticket.savi @@ -14,10 +14,15 @@ :let _fd U32 :new iso _new(@_listener, @_fd) - // TODO: This struct should allow inspecting information about the - // attempted connection, such as the remote IP address, for example. - // This would allow the ticket-holder to make an informed decision - // to either accept or reject the connection based on that information. + :: Get the `IPAddress` of the remote socket. + :fun remote_address: _NetAddress._for_fd_peer(@_fd).ip_address + + :: Get the port number of the remote socket. + :fun remote_port_number: _NetAddress._for_fd_peer(@_fd).port_number + + :: Get the `IPAddress.WithPortNumber` of the remote socket. + :fun remote_address_with_port_number + _NetAddress._for_fd_peer(@_fd).ip_address_with_port_number :: Reject this attempted connection instead of accepting it into an engine. :: diff --git a/src/TCP.Engine.savi b/src/TCP.Engine.savi index 93bdb78..7cb7ae2 100644 --- a/src/TCP.Engine.savi +++ b/src/TCP.Engine.savi @@ -27,6 +27,7 @@ actor IO.Actor(IO.Action) ticket TCP.Accept.Ticket ) + actor.io_deferred_action(IO.Action.Opened) @io = IO.CoreEngine.new( _FFI.pony_asio_event_create(actor, ticket._fd, @_asio_flags, 0, True) ) @@ -83,3 +84,37 @@ if (bytes_read > 0) (yield @read_stream.bytes_ahead_of_marker) ) ) + + :: Get the local `IPAddress` of this side of the connection. + :: Raises an error if the connection is not currently open. + :fun local_address!: @_local_netaddr!.ip_address + + :: Get the local port number of this side of the connection. + :: Raises an error if the connection is not currently open. + :fun local_port_number!: @_local_netaddr!.port_number + + :: Get the local `IPAddress.WithPortNumber` of this side of the connection. + :: Raises an error if the connection is not currently open. + :fun local_address_with_port_number! + @_local_netaddr!.ip_address_with_port_number + + :fun _local_netaddr! + error! if @io.event_id.is_null + _NetAddress._for_fd(_FFI.pony_asio_event_fd(@io.event_id)) + + :: Get the `IPAddress` of the remote socket. + :: Raises an error if the connection is not currently open. + :fun remote_address!: @_remote_netaddr!.ip_address + + :: Get the port number of the remote socket. + :: Raises an error if the connection is not currently open. + :fun remote_port_number!: @_remote_netaddr!.port_number + + :: Get the `IPAddress.WithPortNumber` of the remote socket. + :: Raises an error if the connection is not currently open. + :fun remote_address_with_port_number! + @_remote_netaddr!.ip_address_with_port_number + + :fun _remote_netaddr! + error! if @io.event_id.is_null + _NetAddress._for_fd_peer(_FFI.pony_asio_event_fd(@io.event_id)) diff --git a/src/TCP.Listen.Engine.savi b/src/TCP.Listen.Engine.savi index 73b2267..0d0160a 100644 --- a/src/TCP.Listen.Engine.savi +++ b/src/TCP.Listen.Engine.savi @@ -3,7 +3,7 @@ :let _actor IO.Actor(IO.Action) :var _fd U32: -1 - :var _event_id AsioEvent.ID: CPointer(AsioEvent.ID.Opaque).null // TODO: AsioEvent.ID.null + :var _event_id AsioEvent.ID: AsioEvent.ID.null :var _count USize: 0 :var _limit USize @@ -12,7 +12,23 @@ :var _paused Bool: False :var listen_error OSError: OSError.None - :fun listen_port_number: _NetAddress._for_fd(@_fd).port // TODO: what happens if @_fd is invalid (-1)? + + :: Get the local `IPAddress` of the listener. + :: Raises an error if the listener is not currently listening. + :fun listen_address!: @_listen_netaddr!.ip_address + + :: Get the local port number of the listener. + :: Raises an error if the listener is not currently listening. + :fun listen_port_number!: @_listen_netaddr!.port_number + + :: Get the local `IPAddress.WithPortNumber` of the listener. + :: Raises an error if the listener is not currently listening. + :fun listen_address_with_port_number! + @_listen_netaddr!.ip_address_with_port_number + + :fun _listen_netaddr! + error! if (@_fd == -1) + _NetAddress._for_fd(@_fd) :new (@_actor, ticket TCP.Listen.Ticket, @_limit = 0) event = _FFI.pony_os_listen_tcp( @@ -38,7 +54,7 @@ ) if event.is_disposable ( _FFI.pony_asio_event_destroy(@_event_id) - @_event_id = CPointer(AsioEvent.ID.Opaque).null // TODO: AsioEvent.ID.null + @_event_id = AsioEvent.ID.null yield IO.Action.Closed ) ) diff --git a/src/_FFI.savi b/src/_FFI.savi index 9f0c3dc..2fa4b6a 100644 --- a/src/_FFI.savi +++ b/src/_FFI.savi @@ -12,5 +12,6 @@ :ffi pony_os_socket_close(fd U32) None :ffi pony_os_errno OSError :ffi pony_os_sockname(fd U32, net_addr _NetAddress'ref) None + :ffi pony_os_peername(fd U32, net_addr _NetAddress'ref) None :ffi pony_os_ipv4(net_addr _NetAddress'box) Bool :ffi pony_os_ipv6(net_addr _NetAddress'box) Bool diff --git a/src/_NetAddress.savi b/src/_NetAddress.savi index 637d7a3..3d3fbbc 100644 --- a/src/_NetAddress.savi +++ b/src/_NetAddress.savi @@ -1,8 +1,4 @@ - - -:class val _NetAddress - :is Equatable(_NetAddress) - +:class val _NetAddress // TODO: make this a struct after patching the runtime to not assume a class :let _family U16: 0 :let _port U16: 0 :: Port number in network byte order. :let _ipv4 U32: 0 :: Bits for an IPv4 address in network byte order. @@ -12,28 +8,27 @@ :let _ipv6d U32: 0 :: Bits 97-128 of an IPv6 address in network byte order. :let _scope U32: 0 :: IPv6 scope (unicast, anycast, multicast, etc...). - :new _for_fd(fd): _FFI.pony_os_sockname(fd, @) + :new _for_fd(fd): _FFI.pony_os_sockname(fd, @) + :new _for_fd_peer(fd): _FFI.pony_os_peername(fd, @) :fun is_ipv4: _FFI.pony_os_ipv4(@) :fun is_ipv6: _FFI.pony_os_ipv6(@) - :fun port: @_port.be_to_native // (converted to host byte order) - :fun scope: @_scope.be_to_native // (converted to host byte order) - :fun ipv4_addr: @_ipv4.be_to_native // (converted to host byte order) - // TODO: ipv6_addr - // TODO: family + :fun port_number: @_port.be_to_native - :fun "=="(other _NetAddress'box) - @_family == other._family - && @_port == other._port - && ( - if @is_ipv4 ( - @_ipv4 == other._ipv4 - | - @_ipv6a == other._ipv6a - && @_ipv6b == other._ipv6b - && @_ipv6c == other._ipv6c - && @_ipv6d == other._ipv6d + :fun ip_address + if @is_ipv4 ( + IPAddress.new_v4_raw(@_ipv4.be_to_native) + | + IPAddress.new_v6_raw( + @_ipv6a.be_to_native.u64.bit_shl(32).bit_or( + @_ipv6b.be_to_native.u64 + ) + @_ipv6c.be_to_native.u64.bit_shl(32).bit_or( + @_ipv6d.be_to_native.u64 + ) ) ) - && @_scope == other._scope + + :fun ip_address_with_port_number + @ip_address.with_port_number(@port_number)