diff --git a/include/tinycsocket.h b/include/tinycsocket.h index ece087d..92f7e64 100644 --- a/include/tinycsocket.h +++ b/include/tinycsocket.h @@ -29,7 +29,7 @@ #ifndef TINYCSOCKET_INTERNAL_H_ #define TINYCSOCKET_INTERNAL_H_ -static const char* const TCS_VERSION_TXT = "v0.3.86"; +static const char* const TCS_VERSION_TXT = "v0.3.87"; extern const char* const TCS_LICENSE_TXT; /* @@ -569,7 +569,7 @@ TcsResult tcs_socket(TcsSocket* out_socket, TcsFamily family, TcsSocketType type * * Creates an IPv4 or IPv6 TCP socket based on the address family of the provided address(es). * If @p local_address is not NULL, SO_REUSEADDR is set and the socket is bound to it. -* If @p remote_address is not NULL, the socket connects to it. +* If @p remote_address is not NULL, the socket connects to it, so it must have a port. * At least one of @p local_address or @p remote_address must be non-NULL. * If both are provided, they must have the same address family. * On failure, *out_socket is always set back to #TCS_SOCKET_INVALID. @@ -606,7 +606,7 @@ TcsResult tcs_socket(TcsSocket* out_socket, TcsFamily family, TcsSocketType type * @param[in] timeout_ms maximum time in milliseconds to wait for connection, or #TCS_WAIT_INF for OS default timeout. Ignored if @p remote_address is NULL. * * @return #TCS_SUCCESS if successful, otherwise the error code. -* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, or if both are provided with different address families. +* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, if both are provided with different address families, or if @p remote_address has port 0. * @retval #TCS_ERROR_CONNECTION_REFUSED if the remote server refused the connection. * @retval #TCS_ERROR_TIMED_OUT if the connection attempt timed out. * @@ -623,7 +623,9 @@ TcsResult tcs_socket_tcp(TcsSocket* out_socket, * @brief Create a TCP socket from string addresses, optionally bind and/or connect. * * Resolves the address strings with ::tcs_address_resolve() and delegates to ::tcs_socket_tcp(). -* Addresses must include a port, e.g. "127.0.0.1:8080" or "[::1]:8080". +* Addresses are given as "host:port", e.g. "localhost:8080", "127.0.0.1:8080" or "[::1]:8080". +* The first resolved address is used. @p remote_address must include a port. +* @p local_address without a port binds to a port chosen by the operating system. * At least one of @p local_address or @p remote_address must be non-NULL. * On failure, *out_socket is always set back to #TCS_SOCKET_INVALID. * @@ -654,7 +656,8 @@ TcsResult tcs_socket_tcp(TcsSocket* out_socket, * @param[in] timeout_ms maximum time in milliseconds to wait for connection, or #TCS_WAIT_INF for OS default timeout. Ignored if @p remote_address is NULL. * * @return #TCS_SUCCESS if successful, otherwise the error code. -* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, or if both addresses are NULL. +* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, if an address string is malformed, or if @p remote_address has no port. +* @retval #TCS_ERROR_ADDRESS_LOOKUP_FAILED if an address string could not be resolved. * @retval #TCS_ERROR_CONNECTION_REFUSED if the remote server refused the connection. * @retval #TCS_ERROR_TIMED_OUT if the connection attempt timed out. * @@ -677,6 +680,7 @@ TcsResult tcs_socket_tcp_str(TcsSocket* out_socket, * When only @p remote_address is given (no @p local_address), the socket is also connected, * allowing use of ::tcs_send(). When both are given, the socket is not connected to avoid * filtering out multicast traffic — use ::tcs_send_to() instead. +* Whenever the socket is connected, @p remote_address must have a port. * At least one of @p local_address or @p remote_address must be non-NULL. * If both are provided, they must have the same address family. * On failure, *out_socket is always set back to #TCS_SOCKET_INVALID. @@ -712,7 +716,7 @@ TcsResult tcs_socket_tcp_str(TcsSocket* out_socket, * @param[in] remote_address address to connect to, or NULL to skip connecting. * * @return #TCS_SUCCESS if successful, otherwise the error code. -* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, or if both are provided with different address families. +* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, if both are provided with different address families, or if @p remote_address is connected to but has port 0. * * @see tcs_socket_udp_str() * @see tcs_close() @@ -724,8 +728,10 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, /** * @brief Create a UDP socket from string addresses, optionally bind and/or connect. * -* Parses the address strings with ::tcs_address_resolve() and delegates to ::tcs_socket_udp(). -* Addresses must include a port, e.g. "127.0.0.1:8080" or "[::1]:8080". +* Resolves the address strings with ::tcs_address_resolve() and delegates to ::tcs_socket_udp(). +* Addresses are given as "host:port", e.g. "localhost:8080", "127.0.0.1:8080" or "[::1]:8080". +* The first resolved address is used. A @p remote_address that the socket connects to must include a port. +* @p local_address without a port binds to a port chosen by the operating system. * At least one of @p local_address or @p remote_address must be non-NULL. * On failure, *out_socket is always set back to #TCS_SOCKET_INVALID. * @@ -755,7 +761,8 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, * @param[in] remote_address address string to connect to, or NULL to skip connecting. * * @return #TCS_SUCCESS if successful, otherwise the error code. -* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, or if both addresses are NULL. +* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, if an address string is malformed, or if @p remote_address is connected to but has no port. +* @retval #TCS_ERROR_ADDRESS_LOOKUP_FAILED if an address string could not be resolved. * * @see tcs_socket_udp() * @see tcs_close() @@ -1831,7 +1838,17 @@ TcsResult tcs_interface_list(struct TcsInterface out_interfaces[], size_t interf /** * @brief Resolve a hostname to one or more addresses. * -* @param[in] hostname hostname or IP string to resolve. +* The string may include a port, which is then set on every returned address: +* @verbatim +* localhost +* localhost:1212 +* 192.168.0.1:1212 +* [::1]:1212 +* @endverbatim +* Numeric addresses are parsed with ::tcs_address_parse() without any lookup. +* Without a port, the port of the returned addresses is 0. +* +* @param[in] hostname hostname or IP string to resolve, optionally with a port. * @param[in] address_family address family filter, or ::TCS_FAMILY_ANY for all. * @param[out] out_addresses array to receive resolved addresses, or NULL to only count. * @param[in] addresses_length number of elements in the @p out_addresses array. @@ -4022,6 +4039,35 @@ TcsResult tcs_address_resolve(const char* hostname, if (address_family.native == -1) // sentinel for unsupported families (e.g. TCS_FAMILY_PACKET on non-Linux) return TCS_ERROR_NOT_SUPPORTED; + // getaddrinfo() does not understand "hostname:port". A string with exactly one ':' carries a port, which is + // split off here and applied to the results. Brackets belong to numeric IPv6 only, handled by the fast path. + if (strpbrk(hostname, "[]") != NULL) + return TCS_ERROR_INVALID_ARGUMENT; + char host[256]; // Hostnames are at most 253 characters + uint16_t port = 0; + const char* colon = strchr(hostname, ':'); + bool has_port = colon != NULL && strchr(colon + 1, ':') == NULL; + size_t host_size = has_port ? (size_t)(colon - hostname) : strlen(hostname); + if (host_size == 0 || host_size >= sizeof host) + return TCS_ERROR_INVALID_ARGUMENT; + if (has_port) + { + if (colon[1] == '\0') + return TCS_ERROR_INVALID_ARGUMENT; + unsigned long port_value = 0; + for (const char* c = colon + 1; *c != '\0'; ++c) + { + if (*c < '0' || *c > '9') + return TCS_ERROR_INVALID_ARGUMENT; + port_value = port_value * 10 + (unsigned long)(*c - '0'); + if (port_value > 65535) + return TCS_ERROR_INVALID_ARGUMENT; + } + port = (uint16_t)port_value; + } + memcpy(host, hostname, host_size); + host[host_size] = '\0'; + struct addrinfo native_hints; memset(&native_hints, 0, sizeof native_hints); native_hints.ai_family = address_family.native; @@ -4030,7 +4076,7 @@ TcsResult tcs_address_resolve(const char* hostname, native_hints.ai_protocol = IPPROTO_UDP; struct addrinfo* native_addrinfo_list = NULL; - int sts = getaddrinfo(hostname, NULL, &native_hints, &native_addrinfo_list); + int sts = getaddrinfo(host, NULL, &native_hints, &native_addrinfo_list); // TODO: Move error codes to errno2retcode() if (sts == EAI_SYSTEM) @@ -4069,6 +4115,10 @@ TcsResult tcs_address_resolve(const char* hostname, TcsResult convert_address_status = native2sockaddr(iter->ai_addr, &out_addresses[i]); if (convert_address_status != TCS_SUCCESS) continue; + if (out_addresses[i].family.native == TCS_FAMILY_IPV4.native) + out_addresses[i].data.ipv4.port = port; + else if (out_addresses[i].family.native == TCS_FAMILY_IPV6.native) + out_addresses[i].data.ipv6.port = port; i++; } } @@ -6923,12 +6973,41 @@ TcsResult tcs_address_resolve(const char* hostname, if (address_family.native == TCS_FAMILY_PACKET.native) // Only the numeric fast path above can give a MAC address return TCS_ERROR_NOT_SUPPORTED; + // getaddrinfo() does not understand "hostname:port". A string with exactly one ':' carries a port, which is + // split off here and applied to the results. Brackets belong to numeric IPv6 only, handled by the fast path. + if (strpbrk(hostname, "[]") != NULL) + return TCS_ERROR_INVALID_ARGUMENT; + char host[256]; // Hostnames are at most 253 characters + uint16_t port = 0; + const char* colon = strchr(hostname, ':'); + bool has_port = colon != NULL && strchr(colon + 1, ':') == NULL; + size_t host_size = has_port ? (size_t)(colon - hostname) : strlen(hostname); + if (host_size == 0 || host_size >= sizeof host) + return TCS_ERROR_INVALID_ARGUMENT; + if (has_port) + { + if (colon[1] == '\0') + return TCS_ERROR_INVALID_ARGUMENT; + unsigned long port_value = 0; + for (const char* c = colon + 1; *c != '\0'; ++c) + { + if (*c < '0' || *c > '9') + return TCS_ERROR_INVALID_ARGUMENT; + port_value = port_value * 10 + (unsigned long)(*c - '0'); + if (port_value > 65535) + return TCS_ERROR_INVALID_ARGUMENT; + } + port = (uint16_t)port_value; + } + memcpy(host, hostname, host_size); + host[host_size] = '\0'; + ADDRINFOA native_hints; memset(&native_hints, 0, sizeof native_hints); native_hints.ai_family = address_family.native; PADDRINFOA native_addrinfo_list = NULL; - int getaddrinfo_status = getaddrinfo(hostname, NULL, &native_hints, &native_addrinfo_list); + int getaddrinfo_status = getaddrinfo(host, NULL, &native_hints, &native_addrinfo_list); if (getaddrinfo_status != 0) return TCS_ERROR_ADDRESS_LOOKUP_FAILED; @@ -6950,6 +7029,10 @@ TcsResult tcs_address_resolve(const char* hostname, TcsResult address_convert_status = native2sockaddr(iter->ai_addr, &out_addresses[i]); if (address_convert_status != TCS_SUCCESS) continue; + if (out_addresses[i].family.native == TCS_FAMILY_IPV4.native) + out_addresses[i].data.ipv4.port = port; + else if (out_addresses[i].family.native == TCS_FAMILY_IPV6.native) + out_addresses[i].data.ipv6.port = port; i++; } } @@ -7292,6 +7375,13 @@ TcsResult tcs_socket_tcp(TcsSocket* out_socket, if (local_address != NULL && remote_address != NULL && local_address->family.native != remote_address->family.native) return TCS_ERROR_INVALID_ARGUMENT; + // A remote address without a port can never be connected to + if (remote_address != NULL && remote_address->family.native == TCS_FAMILY_IPV4.native && + remote_address->data.ipv4.port == 0) + return TCS_ERROR_INVALID_ARGUMENT; + if (remote_address != NULL && remote_address->family.native == TCS_FAMILY_IPV6.native && + remote_address->data.ipv6.port == 0) + return TCS_ERROR_INVALID_ARGUMENT; if (timeout_ms < 0 && timeout_ms != TCS_WAIT_INF) return TCS_ERROR_INVALID_ARGUMENT; @@ -7443,6 +7533,15 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, local_address->family.native != remote_address->family.native) return TCS_ERROR_INVALID_ARGUMENT; + // A multicast group joined on a bound socket is the only remote address the socket is not connected to + bool is_multicast = remote_address != NULL && tcs_address_is_multicast(remote_address); + bool do_connect = remote_address != NULL && (!is_multicast || local_address == NULL); + // A remote address without a port can never be connected to + if (do_connect && remote_address->family.native == TCS_FAMILY_IPV4.native && remote_address->data.ipv4.port == 0) + return TCS_ERROR_INVALID_ARGUMENT; + if (do_connect && remote_address->family.native == TCS_FAMILY_IPV6.native && remote_address->data.ipv6.port == 0) + return TCS_ERROR_INVALID_ARGUMENT; + TcsFamily family = local_address != NULL ? local_address->family : remote_address->family; TcsResult res = tcs_socket(out_socket, family, TCS_SOCKET_DGRAM, TCS_PROTOCOL_IP_UDP); @@ -7467,8 +7566,6 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, if (remote_address != NULL) { - bool is_multicast = tcs_address_is_multicast(remote_address); - if (is_multicast) { res = tcs_opt_membership_add(*out_socket, remote_address); @@ -7479,7 +7576,7 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, } } - if (!is_multicast || local_address == NULL) + if (do_connect) { res = tcs_connect(*out_socket, remote_address); if (res != TCS_SUCCESS) diff --git a/src/tinycsocket_common.c b/src/tinycsocket_common.c index 087755c..422eed3 100644 --- a/src/tinycsocket_common.c +++ b/src/tinycsocket_common.c @@ -130,6 +130,13 @@ TcsResult tcs_socket_tcp(TcsSocket* out_socket, if (local_address != NULL && remote_address != NULL && local_address->family.native != remote_address->family.native) return TCS_ERROR_INVALID_ARGUMENT; + // A remote address without a port can never be connected to + if (remote_address != NULL && remote_address->family.native == TCS_FAMILY_IPV4.native && + remote_address->data.ipv4.port == 0) + return TCS_ERROR_INVALID_ARGUMENT; + if (remote_address != NULL && remote_address->family.native == TCS_FAMILY_IPV6.native && + remote_address->data.ipv6.port == 0) + return TCS_ERROR_INVALID_ARGUMENT; if (timeout_ms < 0 && timeout_ms != TCS_WAIT_INF) return TCS_ERROR_INVALID_ARGUMENT; @@ -281,6 +288,15 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, local_address->family.native != remote_address->family.native) return TCS_ERROR_INVALID_ARGUMENT; + // A multicast group joined on a bound socket is the only remote address the socket is not connected to + bool is_multicast = remote_address != NULL && tcs_address_is_multicast(remote_address); + bool do_connect = remote_address != NULL && (!is_multicast || local_address == NULL); + // A remote address without a port can never be connected to + if (do_connect && remote_address->family.native == TCS_FAMILY_IPV4.native && remote_address->data.ipv4.port == 0) + return TCS_ERROR_INVALID_ARGUMENT; + if (do_connect && remote_address->family.native == TCS_FAMILY_IPV6.native && remote_address->data.ipv6.port == 0) + return TCS_ERROR_INVALID_ARGUMENT; + TcsFamily family = local_address != NULL ? local_address->family : remote_address->family; TcsResult res = tcs_socket(out_socket, family, TCS_SOCKET_DGRAM, TCS_PROTOCOL_IP_UDP); @@ -305,8 +321,6 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, if (remote_address != NULL) { - bool is_multicast = tcs_address_is_multicast(remote_address); - if (is_multicast) { res = tcs_opt_membership_add(*out_socket, remote_address); @@ -317,7 +331,7 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, } } - if (!is_multicast || local_address == NULL) + if (do_connect) { res = tcs_connect(*out_socket, remote_address); if (res != TCS_SUCCESS) diff --git a/src/tinycsocket_internal.h b/src/tinycsocket_internal.h index a045373..8a02469 100644 --- a/src/tinycsocket_internal.h +++ b/src/tinycsocket_internal.h @@ -23,7 +23,7 @@ #ifndef TINYCSOCKET_INTERNAL_H_ #define TINYCSOCKET_INTERNAL_H_ -static const char* const TCS_VERSION_TXT = "v0.3.86"; +static const char* const TCS_VERSION_TXT = "v0.3.87"; extern const char* const TCS_LICENSE_TXT; /* @@ -563,7 +563,7 @@ TcsResult tcs_socket(TcsSocket* out_socket, TcsFamily family, TcsSocketType type * * Creates an IPv4 or IPv6 TCP socket based on the address family of the provided address(es). * If @p local_address is not NULL, SO_REUSEADDR is set and the socket is bound to it. -* If @p remote_address is not NULL, the socket connects to it. +* If @p remote_address is not NULL, the socket connects to it, so it must have a port. * At least one of @p local_address or @p remote_address must be non-NULL. * If both are provided, they must have the same address family. * On failure, *out_socket is always set back to #TCS_SOCKET_INVALID. @@ -600,7 +600,7 @@ TcsResult tcs_socket(TcsSocket* out_socket, TcsFamily family, TcsSocketType type * @param[in] timeout_ms maximum time in milliseconds to wait for connection, or #TCS_WAIT_INF for OS default timeout. Ignored if @p remote_address is NULL. * * @return #TCS_SUCCESS if successful, otherwise the error code. -* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, or if both are provided with different address families. +* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, if both are provided with different address families, or if @p remote_address has port 0. * @retval #TCS_ERROR_CONNECTION_REFUSED if the remote server refused the connection. * @retval #TCS_ERROR_TIMED_OUT if the connection attempt timed out. * @@ -617,7 +617,9 @@ TcsResult tcs_socket_tcp(TcsSocket* out_socket, * @brief Create a TCP socket from string addresses, optionally bind and/or connect. * * Resolves the address strings with ::tcs_address_resolve() and delegates to ::tcs_socket_tcp(). -* Addresses must include a port, e.g. "127.0.0.1:8080" or "[::1]:8080". +* Addresses are given as "host:port", e.g. "localhost:8080", "127.0.0.1:8080" or "[::1]:8080". +* The first resolved address is used. @p remote_address must include a port. +* @p local_address without a port binds to a port chosen by the operating system. * At least one of @p local_address or @p remote_address must be non-NULL. * On failure, *out_socket is always set back to #TCS_SOCKET_INVALID. * @@ -648,7 +650,8 @@ TcsResult tcs_socket_tcp(TcsSocket* out_socket, * @param[in] timeout_ms maximum time in milliseconds to wait for connection, or #TCS_WAIT_INF for OS default timeout. Ignored if @p remote_address is NULL. * * @return #TCS_SUCCESS if successful, otherwise the error code. -* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, or if both addresses are NULL. +* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, if an address string is malformed, or if @p remote_address has no port. +* @retval #TCS_ERROR_ADDRESS_LOOKUP_FAILED if an address string could not be resolved. * @retval #TCS_ERROR_CONNECTION_REFUSED if the remote server refused the connection. * @retval #TCS_ERROR_TIMED_OUT if the connection attempt timed out. * @@ -671,6 +674,7 @@ TcsResult tcs_socket_tcp_str(TcsSocket* out_socket, * When only @p remote_address is given (no @p local_address), the socket is also connected, * allowing use of ::tcs_send(). When both are given, the socket is not connected to avoid * filtering out multicast traffic — use ::tcs_send_to() instead. +* Whenever the socket is connected, @p remote_address must have a port. * At least one of @p local_address or @p remote_address must be non-NULL. * If both are provided, they must have the same address family. * On failure, *out_socket is always set back to #TCS_SOCKET_INVALID. @@ -706,7 +710,7 @@ TcsResult tcs_socket_tcp_str(TcsSocket* out_socket, * @param[in] remote_address address to connect to, or NULL to skip connecting. * * @return #TCS_SUCCESS if successful, otherwise the error code. -* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, or if both are provided with different address families. +* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, if both are provided with different address families, or if @p remote_address is connected to but has port 0. * * @see tcs_socket_udp_str() * @see tcs_close() @@ -718,8 +722,10 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, /** * @brief Create a UDP socket from string addresses, optionally bind and/or connect. * -* Parses the address strings with ::tcs_address_resolve() and delegates to ::tcs_socket_udp(). -* Addresses must include a port, e.g. "127.0.0.1:8080" or "[::1]:8080". +* Resolves the address strings with ::tcs_address_resolve() and delegates to ::tcs_socket_udp(). +* Addresses are given as "host:port", e.g. "localhost:8080", "127.0.0.1:8080" or "[::1]:8080". +* The first resolved address is used. A @p remote_address that the socket connects to must include a port. +* @p local_address without a port binds to a port chosen by the operating system. * At least one of @p local_address or @p remote_address must be non-NULL. * On failure, *out_socket is always set back to #TCS_SOCKET_INVALID. * @@ -749,7 +755,8 @@ TcsResult tcs_socket_udp(TcsSocket* out_socket, * @param[in] remote_address address string to connect to, or NULL to skip connecting. * * @return #TCS_SUCCESS if successful, otherwise the error code. -* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, or if both addresses are NULL. +* @retval #TCS_ERROR_INVALID_ARGUMENT if @p out_socket is NULL, if *out_socket is not #TCS_SOCKET_INVALID, if both addresses are NULL, if an address string is malformed, or if @p remote_address is connected to but has no port. +* @retval #TCS_ERROR_ADDRESS_LOOKUP_FAILED if an address string could not be resolved. * * @see tcs_socket_udp() * @see tcs_close() @@ -1825,7 +1832,17 @@ TcsResult tcs_interface_list(struct TcsInterface out_interfaces[], size_t interf /** * @brief Resolve a hostname to one or more addresses. * -* @param[in] hostname hostname or IP string to resolve. +* The string may include a port, which is then set on every returned address: +* @verbatim +* localhost +* localhost:1212 +* 192.168.0.1:1212 +* [::1]:1212 +* @endverbatim +* Numeric addresses are parsed with ::tcs_address_parse() without any lookup. +* Without a port, the port of the returned addresses is 0. +* +* @param[in] hostname hostname or IP string to resolve, optionally with a port. * @param[in] address_family address family filter, or ::TCS_FAMILY_ANY for all. * @param[out] out_addresses array to receive resolved addresses, or NULL to only count. * @param[in] addresses_length number of elements in the @p out_addresses array. diff --git a/src/tinycsocket_posix.c b/src/tinycsocket_posix.c index c6b089d..e34e7a0 100644 --- a/src/tinycsocket_posix.c +++ b/src/tinycsocket_posix.c @@ -1707,6 +1707,35 @@ TcsResult tcs_address_resolve(const char* hostname, if (address_family.native == -1) // sentinel for unsupported families (e.g. TCS_FAMILY_PACKET on non-Linux) return TCS_ERROR_NOT_SUPPORTED; + // getaddrinfo() does not understand "hostname:port". A string with exactly one ':' carries a port, which is + // split off here and applied to the results. Brackets belong to numeric IPv6 only, handled by the fast path. + if (strpbrk(hostname, "[]") != NULL) + return TCS_ERROR_INVALID_ARGUMENT; + char host[256]; // Hostnames are at most 253 characters + uint16_t port = 0; + const char* colon = strchr(hostname, ':'); + bool has_port = colon != NULL && strchr(colon + 1, ':') == NULL; + size_t host_size = has_port ? (size_t)(colon - hostname) : strlen(hostname); + if (host_size == 0 || host_size >= sizeof host) + return TCS_ERROR_INVALID_ARGUMENT; + if (has_port) + { + if (colon[1] == '\0') + return TCS_ERROR_INVALID_ARGUMENT; + unsigned long port_value = 0; + for (const char* c = colon + 1; *c != '\0'; ++c) + { + if (*c < '0' || *c > '9') + return TCS_ERROR_INVALID_ARGUMENT; + port_value = port_value * 10 + (unsigned long)(*c - '0'); + if (port_value > 65535) + return TCS_ERROR_INVALID_ARGUMENT; + } + port = (uint16_t)port_value; + } + memcpy(host, hostname, host_size); + host[host_size] = '\0'; + struct addrinfo native_hints; memset(&native_hints, 0, sizeof native_hints); native_hints.ai_family = address_family.native; @@ -1715,7 +1744,7 @@ TcsResult tcs_address_resolve(const char* hostname, native_hints.ai_protocol = IPPROTO_UDP; struct addrinfo* native_addrinfo_list = NULL; - int sts = getaddrinfo(hostname, NULL, &native_hints, &native_addrinfo_list); + int sts = getaddrinfo(host, NULL, &native_hints, &native_addrinfo_list); // TODO: Move error codes to errno2retcode() if (sts == EAI_SYSTEM) @@ -1754,6 +1783,10 @@ TcsResult tcs_address_resolve(const char* hostname, TcsResult convert_address_status = native2sockaddr(iter->ai_addr, &out_addresses[i]); if (convert_address_status != TCS_SUCCESS) continue; + if (out_addresses[i].family.native == TCS_FAMILY_IPV4.native) + out_addresses[i].data.ipv4.port = port; + else if (out_addresses[i].family.native == TCS_FAMILY_IPV6.native) + out_addresses[i].data.ipv6.port = port; i++; } } diff --git a/src/tinycsocket_win32.c b/src/tinycsocket_win32.c index 80cb7de..d9a8480 100644 --- a/src/tinycsocket_win32.c +++ b/src/tinycsocket_win32.c @@ -2545,12 +2545,41 @@ TcsResult tcs_address_resolve(const char* hostname, if (address_family.native == TCS_FAMILY_PACKET.native) // Only the numeric fast path above can give a MAC address return TCS_ERROR_NOT_SUPPORTED; + // getaddrinfo() does not understand "hostname:port". A string with exactly one ':' carries a port, which is + // split off here and applied to the results. Brackets belong to numeric IPv6 only, handled by the fast path. + if (strpbrk(hostname, "[]") != NULL) + return TCS_ERROR_INVALID_ARGUMENT; + char host[256]; // Hostnames are at most 253 characters + uint16_t port = 0; + const char* colon = strchr(hostname, ':'); + bool has_port = colon != NULL && strchr(colon + 1, ':') == NULL; + size_t host_size = has_port ? (size_t)(colon - hostname) : strlen(hostname); + if (host_size == 0 || host_size >= sizeof host) + return TCS_ERROR_INVALID_ARGUMENT; + if (has_port) + { + if (colon[1] == '\0') + return TCS_ERROR_INVALID_ARGUMENT; + unsigned long port_value = 0; + for (const char* c = colon + 1; *c != '\0'; ++c) + { + if (*c < '0' || *c > '9') + return TCS_ERROR_INVALID_ARGUMENT; + port_value = port_value * 10 + (unsigned long)(*c - '0'); + if (port_value > 65535) + return TCS_ERROR_INVALID_ARGUMENT; + } + port = (uint16_t)port_value; + } + memcpy(host, hostname, host_size); + host[host_size] = '\0'; + ADDRINFOA native_hints; memset(&native_hints, 0, sizeof native_hints); native_hints.ai_family = address_family.native; PADDRINFOA native_addrinfo_list = NULL; - int getaddrinfo_status = getaddrinfo(hostname, NULL, &native_hints, &native_addrinfo_list); + int getaddrinfo_status = getaddrinfo(host, NULL, &native_hints, &native_addrinfo_list); if (getaddrinfo_status != 0) return TCS_ERROR_ADDRESS_LOOKUP_FAILED; @@ -2572,6 +2601,10 @@ TcsResult tcs_address_resolve(const char* hostname, TcsResult address_convert_status = native2sockaddr(iter->ai_addr, &out_addresses[i]); if (address_convert_status != TCS_SUCCESS) continue; + if (out_addresses[i].family.native == TCS_FAMILY_IPV4.native) + out_addresses[i].data.ipv4.port = port; + else if (out_addresses[i].family.native == TCS_FAMILY_IPV6.native) + out_addresses[i].data.ipv6.port = port; i++; } } diff --git a/tests/tests.cpp b/tests/tests.cpp index 1564812..dab38dc 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -784,6 +784,180 @@ TEST_CASE("tcs_socket_tcp_str bind and connect") REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); } +TEST_CASE("tcs_socket_tcp_str hostname with port") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + // Given + // Both ends are given as "hostname:port" (as in the examples), so they resolve to the same address family. + TcsSocket listen_socket = TCS_SOCKET_INVALID; + TcsSocket accept_socket = TCS_SOCKET_INVALID; + TcsSocket client_socket = TCS_SOCKET_INVALID; + + TcsResult listen_res = tcs_socket_tcp_str(&listen_socket, "localhost:1475", NULL, 0); + CHECK(listen_res == TCS_SUCCESS); + if (listen_res == TCS_SUCCESS) + { + // The port from the string must have been used when binding + struct TcsAddress bound_address = TCS_ADDRESS_NONE; + CHECK(tcs_address_socket_local(listen_socket, &bound_address) == TCS_SUCCESS); + if (bound_address.family.native == TCS_FAMILY_IPV6.native) + CHECK(bound_address.data.ipv6.port == 1475); + else + CHECK(bound_address.data.ipv4.port == 1475); + + REQUIRE(tcs_listen(listen_socket, TCS_BACKLOG_MAX) == TCS_SUCCESS); + + // When + TcsResult connect_res = tcs_socket_tcp_str(&client_socket, NULL, "localhost:1475", 5000); + CHECK(connect_res == TCS_SUCCESS); + if (connect_res == TCS_SUCCESS) // Guard: never block in accept if the connect failed + { + CHECK(tcs_accept(listen_socket, &accept_socket, NULL) == TCS_SUCCESS); + + const uint8_t* send_buffer = (const uint8_t*)"hostname"; + uint8_t recv_buffer[8] = {0}; + CHECK(tcs_send(client_socket, send_buffer, 8, TCS_MSG_SENDALL, NULL) == TCS_SUCCESS); + CHECK(tcs_receive(accept_socket, recv_buffer, 8, TCS_MSG_WAITALL, NULL) == TCS_SUCCESS); + + // Then + CHECK(memcmp(recv_buffer, send_buffer, 8) == 0); + } + else + { + CHECK(client_socket == TCS_SOCKET_INVALID); + } + } + else + { + CHECK(listen_socket == TCS_SOCKET_INVALID); + } + + // Clean up + if (client_socket != TCS_SOCKET_INVALID) + CHECK(tcs_close(&client_socket) == TCS_SUCCESS); + if (accept_socket != TCS_SOCKET_INVALID) + CHECK(tcs_close(&accept_socket) == TCS_SUCCESS); + if (listen_socket != TCS_SOCKET_INVALID) + CHECK(tcs_close(&listen_socket) == TCS_SUCCESS); + REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); +} + +TEST_CASE("tcs_socket_tcp_str IPv6 bracket notation bind and connect") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + // Given + TcsSocket listen_socket = TCS_SOCKET_INVALID; + TcsSocket accept_socket = TCS_SOCKET_INVALID; + TcsSocket client_socket = TCS_SOCKET_INVALID; + + REQUIRE(tcs_socket_tcp_str(&listen_socket, "[::1]:1476", NULL, 0) == TCS_SUCCESS); + REQUIRE(tcs_listen(listen_socket, TCS_BACKLOG_MAX) == TCS_SUCCESS); + + // When + TcsResult connect_res = tcs_socket_tcp_str(&client_socket, NULL, "[::1]:1476", 5000); + CHECK(connect_res == TCS_SUCCESS); + if (connect_res == TCS_SUCCESS) // Guard: never block in accept if the connect failed + { + struct TcsAddress peer_address = TCS_ADDRESS_NONE; + CHECK(tcs_accept(listen_socket, &accept_socket, &peer_address) == TCS_SUCCESS); + CHECK(peer_address.family.native == TCS_FAMILY_IPV6.native); + CHECK(tcs_address_is_loopback(&peer_address)); + + const uint8_t* send_buffer = (const uint8_t*)"ipv6"; + uint8_t recv_buffer[8] = {0}; + CHECK(tcs_send(client_socket, send_buffer, 4, TCS_MSG_SENDALL, NULL) == TCS_SUCCESS); + CHECK(tcs_receive(accept_socket, recv_buffer, 4, TCS_MSG_WAITALL, NULL) == TCS_SUCCESS); + + // Then + CHECK(memcmp(recv_buffer, send_buffer, 4) == 0); + } + else + { + CHECK(client_socket == TCS_SOCKET_INVALID); + } + + // Clean up + if (client_socket != TCS_SOCKET_INVALID) + CHECK(tcs_close(&client_socket) == TCS_SUCCESS); + if (accept_socket != TCS_SOCKET_INVALID) + CHECK(tcs_close(&accept_socket) == TCS_SUCCESS); + CHECK(tcs_close(&listen_socket) == TCS_SUCCESS); + REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); +} + +TEST_CASE("tcs_socket_tcp_str invalid arguments") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + // NULL out_socket + CHECK(tcs_socket_tcp_str(NULL, "127.0.0.1:1477", NULL, 0) == TCS_ERROR_INVALID_ARGUMENT); + + // Both addresses NULL + TcsSocket socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_tcp_str(&socket, NULL, NULL, 0) == TCS_ERROR_INVALID_ARGUMENT); + CHECK(socket == TCS_SOCKET_INVALID); + + // out_socket already holds a socket, it must be left untouched + TcsSocket existing = TCS_SOCKET_INVALID; + REQUIRE(tcs_socket(&existing, TCS_FAMILY_IPV4, TCS_SOCKET_STREAM, TCS_PROTOCOL_IP_TCP) == TCS_SUCCESS); + const TcsSocket existing_copy = existing; + CHECK(tcs_socket_tcp_str(&existing, "127.0.0.1:1477", NULL, 0) == TCS_ERROR_INVALID_ARGUMENT); + CHECK(existing == existing_copy); + CHECK(tcs_close(&existing) == TCS_SUCCESS); + + // Negative timeout other than TCS_WAIT_INF + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_tcp_str(&socket, NULL, "127.0.0.1:1477", -2) == TCS_ERROR_INVALID_ARGUMENT); + CHECK(socket == TCS_SOCKET_INVALID); + + // Address that can neither be parsed nor resolved + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_tcp_str(&socket, NULL, "not an address", 100) != TCS_SUCCESS); + CHECK(socket == TCS_SOCKET_INVALID); + + // Port out of range or not numeric + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_tcp_str(&socket, "localhost:99999", NULL, 0) != TCS_SUCCESS); + CHECK(socket == TCS_SOCKET_INVALID); + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_tcp_str(&socket, "localhost:14ab", NULL, 0) != TCS_SUCCESS); + CHECK(socket == TCS_SOCKET_INVALID); + + // Family mismatch between local and remote + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_tcp_str(&socket, "127.0.0.1:1477", "[::1]:1477", 100) != TCS_SUCCESS); + CHECK(socket == TCS_SOCKET_INVALID); + + // Clean up + REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); +} + +TEST_CASE("tcs_socket_tcp_str connect failure leaves socket invalid") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + // Connect to a port nobody listens on + TcsSocket socket = TCS_SOCKET_INVALID; + TcsResult res = tcs_socket_tcp_str(&socket, NULL, "127.0.0.1:1478", 100); + + if (res != TCS_ERROR_TIMED_OUT && res != TCS_ERROR_CONNECTION_REFUSED) + { + WARN(res == TCS_ERROR_TIMED_OUT); + WARN(res == TCS_ERROR_CONNECTION_REFUSED); + CHECK((res == TCS_ERROR_TIMED_OUT || res == TCS_ERROR_CONNECTION_REFUSED)); + } + CHECK(socket == TCS_SOCKET_INVALID); + + // Clean up + REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); +} + TEST_CASE("tcs_socket_udp bind and send_to") { // Setup @@ -888,6 +1062,169 @@ TEST_CASE("tcs_socket_udp_str bind and send_to") REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); } +TEST_CASE("tcs_socket_udp_str hostname with port") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + // Given + // Both ends are given as "hostname:port" (as in the examples), so they resolve to the same address family. + TcsSocket socket_recv = TCS_SOCKET_INVALID; + TcsSocket socket_send = TCS_SOCKET_INVALID; + + TcsResult bind_res = tcs_socket_udp_str(&socket_recv, "localhost:1479", NULL); + CHECK(bind_res == TCS_SUCCESS); + if (bind_res == TCS_SUCCESS) + { + // The port from the string must have been used when binding + struct TcsAddress bound_address = TCS_ADDRESS_NONE; + CHECK(tcs_address_socket_local(socket_recv, &bound_address) == TCS_SUCCESS); + if (bound_address.family.native == TCS_FAMILY_IPV6.native) + CHECK(bound_address.data.ipv6.port == 1479); + else + CHECK(bound_address.data.ipv4.port == 1479); + + CHECK(tcs_opt_receive_timeout_set(socket_recv, 5000) == TCS_SUCCESS); + + // When + TcsResult connect_res = tcs_socket_udp_str(&socket_send, NULL, "localhost:1479"); + CHECK(connect_res == TCS_SUCCESS); + if (connect_res == TCS_SUCCESS) + { + const uint8_t* send_buffer = (const uint8_t*)"udp"; + uint8_t recv_buffer[8] = {0}; + size_t received = 0; + struct TcsAddress source_address = TCS_ADDRESS_NONE; + CHECK(tcs_send(socket_send, send_buffer, 3, TCS_FLAG_NONE, NULL) == TCS_SUCCESS); + CHECK(tcs_receive_from(socket_recv, recv_buffer, 8, TCS_FLAG_NONE, &source_address, &received) == + TCS_SUCCESS); + + // Then + CHECK(received == 3); + CHECK(memcmp(recv_buffer, send_buffer, 3) == 0); + CHECK(source_address.family.native == bound_address.family.native); + CHECK(tcs_address_is_loopback(&source_address)); + } + else + { + CHECK(socket_send == TCS_SOCKET_INVALID); + } + } + else + { + CHECK(socket_recv == TCS_SOCKET_INVALID); + } + + // Clean up + if (socket_send != TCS_SOCKET_INVALID) + CHECK(tcs_close(&socket_send) == TCS_SUCCESS); + if (socket_recv != TCS_SOCKET_INVALID) + CHECK(tcs_close(&socket_recv) == TCS_SUCCESS); + REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); +} + +TEST_CASE("tcs_socket_udp_str invalid arguments") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + // NULL out_socket + CHECK(tcs_socket_udp_str(NULL, "127.0.0.1:1480", NULL) == TCS_ERROR_INVALID_ARGUMENT); + + // Both addresses NULL + TcsSocket socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_udp_str(&socket, NULL, NULL) == TCS_ERROR_INVALID_ARGUMENT); + CHECK(socket == TCS_SOCKET_INVALID); + + // out_socket already holds a socket, it must be left untouched + TcsSocket existing = TCS_SOCKET_INVALID; + REQUIRE(tcs_socket(&existing, TCS_FAMILY_IPV4, TCS_SOCKET_DGRAM, TCS_PROTOCOL_IP_UDP) == TCS_SUCCESS); + const TcsSocket existing_copy = existing; + CHECK(tcs_socket_udp_str(&existing, "127.0.0.1:1480", NULL) == TCS_ERROR_INVALID_ARGUMENT); + CHECK(existing == existing_copy); + CHECK(tcs_close(&existing) == TCS_SUCCESS); + + // Address that can neither be parsed nor resolved + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_udp_str(&socket, "not an address", NULL) != TCS_SUCCESS); + CHECK(socket == TCS_SOCKET_INVALID); + + // Port out of range or not numeric + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_udp_str(&socket, "localhost:99999", NULL) != TCS_SUCCESS); + CHECK(socket == TCS_SOCKET_INVALID); + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_udp_str(&socket, "localhost:14ab", NULL) != TCS_SUCCESS); + CHECK(socket == TCS_SOCKET_INVALID); + + // Family mismatch between local and remote + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_udp_str(&socket, "127.0.0.1:1480", "[::1]:1480") != TCS_SUCCESS); + CHECK(socket == TCS_SOCKET_INVALID); + + // Clean up + REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); +} + +TEST_CASE("Socket presets: remote address needs a port, local address does not") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + // A remote address with port 0 can never be meaningful, reject it up front + struct TcsAddress remote_no_port = TCS_ADDRESS_NONE; + remote_no_port.family = TCS_FAMILY_IPV4; + remote_no_port.data.ipv4.address = TCS_ADDRESS_IPV4_LOOPBACK; + remote_no_port.data.ipv4.port = 0; + + TcsSocket socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_tcp(&socket, NULL, &remote_no_port, 100) == TCS_ERROR_INVALID_ARGUMENT); + CHECK(socket == TCS_SOCKET_INVALID); + if (socket != TCS_SOCKET_INVALID) // Do not leak the socket while this test is red + tcs_close(&socket); + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_udp(&socket, NULL, &remote_no_port) == TCS_ERROR_INVALID_ARGUMENT); + CHECK(socket == TCS_SOCKET_INVALID); + if (socket != TCS_SOCKET_INVALID) // Do not leak the socket while this test is red + tcs_close(&socket); + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_tcp_str(&socket, NULL, "localhost", 100) == TCS_ERROR_INVALID_ARGUMENT); + CHECK(socket == TCS_SOCKET_INVALID); + if (socket != TCS_SOCKET_INVALID) // Do not leak the socket while this test is red + tcs_close(&socket); + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_udp_str(&socket, NULL, "127.0.0.1") == TCS_ERROR_INVALID_ARGUMENT); + CHECK(socket == TCS_SOCKET_INVALID); + if (socket != TCS_SOCKET_INVALID) // Do not leak the socket while this test is red + tcs_close(&socket); + + // A local address without port means "any free port", and the chosen port must be readable afterwards + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_tcp_str(&socket, "localhost", NULL, 0) == TCS_SUCCESS); + if (socket != TCS_SOCKET_INVALID) + { + struct TcsAddress bound_address = TCS_ADDRESS_NONE; + CHECK(tcs_address_socket_local(socket, &bound_address) == TCS_SUCCESS); + if (bound_address.family.native == TCS_FAMILY_IPV6.native) + CHECK(bound_address.data.ipv6.port != 0); + else + CHECK(bound_address.data.ipv4.port != 0); + CHECK(tcs_close(&socket) == TCS_SUCCESS); + } + socket = TCS_SOCKET_INVALID; + CHECK(tcs_socket_udp_str(&socket, "127.0.0.1", NULL) == TCS_SUCCESS); + if (socket != TCS_SOCKET_INVALID) + { + struct TcsAddress bound_address = TCS_ADDRESS_NONE; + CHECK(tcs_address_socket_local(socket, &bound_address) == TCS_SUCCESS); + CHECK(bound_address.data.ipv4.port != 0); + CHECK(tcs_close(&socket) == TCS_SUCCESS); + } + + // Clean up + REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); +} + TEST_CASE("tcs_poll simple memory check") { // Setup @@ -1134,6 +1471,74 @@ TEST_CASE("Address information count") // Clean up REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); } +TEST_CASE("tcs_address_resolve hostname with port") +{ + // Setup + REQUIRE(tcs_lib_init() == TCS_SUCCESS); + + struct TcsAddress addresses[4]; + size_t count = 0; + + // "hostname:port" must resolve and carry the port on every returned address + CHECK(tcs_address_resolve("localhost:1212", TCS_FAMILY_ANY, addresses, 4, &count) == TCS_SUCCESS); + CHECK(count > 0); + for (size_t i = 0; i < count && i < 4; ++i) + { + CHECK(tcs_address_is_loopback(&addresses[i])); + if (addresses[i].family.native == TCS_FAMILY_IPV6.native) + CHECK(addresses[i].data.ipv6.port == 1212); + else + CHECK(addresses[i].data.ipv4.port == 1212); + } + + // Family filter still applies + count = 0; + CHECK(tcs_address_resolve("localhost:1213", TCS_FAMILY_IPV4, addresses, 4, &count) == TCS_SUCCESS); + CHECK(count > 0); + CHECK(addresses[0].family.native == TCS_FAMILY_IPV4.native); + CHECK(addresses[0].data.ipv4.address == TCS_ADDRESS_IPV4_LOOPBACK); + CHECK(addresses[0].data.ipv4.port == 1213); + + // Counting only (no output array) must work with a port too + count = 0; + CHECK(tcs_address_resolve("localhost:1212", TCS_FAMILY_ANY, NULL, 0, &count) == TCS_SUCCESS); + CHECK(count > 0); + + // Without port the port is 0 + count = 0; + CHECK(tcs_address_resolve("localhost", TCS_FAMILY_IPV4, addresses, 4, &count) == TCS_SUCCESS); + CHECK(count > 0); + CHECK(addresses[0].data.ipv4.port == 0); + + // Numeric forms are unchanged: port when given, IPv6 without brackets has no port + count = 0; + CHECK(tcs_address_resolve("127.0.0.1:80", TCS_FAMILY_ANY, addresses, 4, &count) == TCS_SUCCESS); + CHECK(count == 1); + CHECK(addresses[0].family.native == TCS_FAMILY_IPV4.native); + CHECK(addresses[0].data.ipv4.port == 80); + count = 0; + CHECK(tcs_address_resolve("[::1]:443", TCS_FAMILY_ANY, addresses, 4, &count) == TCS_SUCCESS); + CHECK(count == 1); + CHECK(addresses[0].family.native == TCS_FAMILY_IPV6.native); + CHECK(addresses[0].data.ipv6.port == 443); + count = 0; + CHECK(tcs_address_resolve("::1", TCS_FAMILY_ANY, addresses, 4, &count) == TCS_SUCCESS); + CHECK(count == 1); + CHECK(addresses[0].family.native == TCS_FAMILY_IPV6.native); + CHECK(addresses[0].data.ipv6.port == 0); + + // Bad ports are rejected + count = 0; + CHECK(tcs_address_resolve("localhost:99999", TCS_FAMILY_ANY, addresses, 4, &count) != TCS_SUCCESS); + count = 0; + CHECK(tcs_address_resolve("localhost:14ab", TCS_FAMILY_ANY, addresses, 4, &count) != TCS_SUCCESS); + count = 0; + CHECK(tcs_address_resolve("localhost:", TCS_FAMILY_ANY, addresses, 4, &count) != TCS_SUCCESS); + + // Clean up + REQUIRE(tcs_lib_cleanup() == TCS_SUCCESS); +} + TEST_CASE("Interface list") { // Setup