diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index a452163988..c11d88495b 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -82,7 +82,7 @@ int bsd_sendmmsg(LIBUS_SOCKET_DESCRIPTOR fd, struct udp_sendbuf* sendbuf, int fl while (1) { int ret = sendmsg_x(fd, sendbuf->msgvec, sendbuf->num, flags); if (ret >= 0) return ret; - // If we receive EMMSGSIZE, we should use the fallback code. + // If we receive EMSGSIZE, we should use the fallback code. if (errno == EMSGSIZE) break; if (errno != EINTR) return ret; } @@ -199,8 +199,8 @@ int bsd_udp_setup_sendbuf(struct udp_sendbuf *buf, size_t bufsize, void** payloa struct sockaddr *addr = (struct sockaddr *)addresses[i]; socklen_t addr_len = 0; if (addr) { - addr_len = addr->sa_family == AF_INET ? sizeof(struct sockaddr_in) - : addr->sa_family == AF_INET6 ? sizeof(struct sockaddr_in6) + addr_len = addr->sa_family == AF_INET ? sizeof(struct sockaddr_in) + : addr->sa_family == AF_INET6 ? sizeof(struct sockaddr_in6) : 0; if (addr_len > 0) { buf->has_addresses = 1; @@ -283,7 +283,7 @@ LIBUS_SOCKET_DESCRIPTOR apple_no_sigpipe(LIBUS_SOCKET_DESCRIPTOR fd) { #ifdef __APPLE__ if (fd != LIBUS_SOCKET_ERROR) { int no_sigpipe = 1; - setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, (void *) &no_sigpipe, sizeof(int)); + setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(int)); } #endif return fd; @@ -309,7 +309,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_set_nonblocking(LIBUS_SOCKET_DESCRIPTOR fd) { if (LIKELY(fd != LIBUS_SOCKET_ERROR)) { int flags = fcntl(fd, F_GETFL, 0); - // F_GETFL supports O_NONBLCOK + // F_GETFL supports O_NONBLOCK fcntl(fd, F_SETFL, flags | O_NONBLOCK); flags = fcntl(fd, F_GETFD, 0); @@ -322,8 +322,167 @@ LIBUS_SOCKET_DESCRIPTOR bsd_set_nonblocking(LIBUS_SOCKET_DESCRIPTOR fd) { return fd; } +static int setsockopt_6_or_4(LIBUS_SOCKET_DESCRIPTOR fd, int option4, int option6, const void *option_value, socklen_t option_len) { + int res = setsockopt(fd, IPPROTO_IPV6, option6, option_value, option_len); + + if (res == 0) { + return 0; + } + +#ifdef _WIN32 + const int err = WSAGetLastError(); + if (err == WSAENOPROTOOPT || err == WSAEINVAL) { +#else + if (errno == ENOPROTOOPT || errno == EINVAL) { +#endif + return setsockopt(fd, IPPROTO_IP, option4, option_value, option_len); + } + + return res; +} + void bsd_socket_nodelay(LIBUS_SOCKET_DESCRIPTOR fd, int enabled) { - setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void *) &enabled, sizeof(enabled)); + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &enabled, sizeof(enabled)); +} + +int bsd_socket_broadcast(LIBUS_SOCKET_DESCRIPTOR fd, int enabled) { + return setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &enabled, sizeof(enabled)); +} + +int bsd_socket_multicast_loopback(LIBUS_SOCKET_DESCRIPTOR fd, int enabled) { + return setsockopt_6_or_4(fd, IP_MULTICAST_LOOP, IPV6_MULTICAST_LOOP, &enabled, sizeof(enabled)); +} + +int bsd_socket_multicast_interface(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_storage *addr) { +#ifdef _WIN32 + if (fd == SOCKET_ERROR){ + WSASetLastError(WSAEBADF); + errno = EBADF; + return -1; + } +#endif + + if (addr->ss_family == AF_INET) { + const struct sockaddr_in *addr4 = (const struct sockaddr_in*) addr; + int first_octet = ntohl(addr4->sin_addr.s_addr) >> 24; + // 224.0.0.0 through 239.255.255.255 (224.0.0.0/4) are multicast addresses + // and thus not valid interface addresses. + if (!(224 <= first_octet && first_octet <= 239)) { + return setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &addr4->sin_addr, sizeof(addr4->sin_addr)); + } + } + + if (addr->ss_family == AF_INET6) { + const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6*) addr; + return setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &addr6->sin6_scope_id, sizeof(addr6->sin6_scope_id)); + } + +#ifdef _WIN32 + WSASetLastError(WSAEINVAL); +#endif + errno = EINVAL; + return -1; +} + +static int bsd_socket_set_membership4(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_in *addr, const struct sockaddr_in *iface, int drop) { + struct ip_mreq mreq; + memset(&mreq, 0, sizeof(mreq)); + mreq.imr_multiaddr.s_addr = addr->sin_addr.s_addr; + mreq.imr_interface.s_addr = iface->sin_addr.s_addr; + int option = drop ? IP_DROP_MEMBERSHIP : IP_ADD_MEMBERSHIP; + return setsockopt(fd, IPPROTO_IP, option, &mreq, sizeof(mreq)); +} + +static int bsd_socket_set_membership6(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_in6 *addr, const struct sockaddr_in6 *iface, int drop) { + struct ipv6_mreq mreq; + memset(&mreq, 0, sizeof(mreq)); + mreq.ipv6mr_multiaddr = addr->sin6_addr; + mreq.ipv6mr_interface = iface->sin6_scope_id; + int option = drop ? IPV6_LEAVE_GROUP : IPV6_JOIN_GROUP; + return setsockopt(fd, IPPROTO_IP, option, &mreq, sizeof(mreq)); +} + +int bsd_socket_set_membership(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_storage *addr, const struct sockaddr_storage *iface, int drop) { + if (addr->ss_family != iface->ss_family) { + errno = EINVAL; + return -1; + } + + if (addr->ss_family == AF_INET6) { + return bsd_socket_set_membership6(fd, (const struct sockaddr_in6*) addr, (const struct sockaddr_in6*) iface, drop); + } else { + return bsd_socket_set_membership4(fd, (const struct sockaddr_in*) addr, (const struct sockaddr_in*) iface, drop); + } +} + +static int bsd_socket_set_source_specific_membership4(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_in *source, const struct sockaddr_in *group, const struct sockaddr_in *iface, int drop) { + struct ip_mreq_source mreq; + memset(&mreq, 0, sizeof(mreq)); + + if (iface != NULL) { + mreq.imr_interface.s_addr = iface->sin_addr.s_addr; + } else { + mreq.imr_interface.s_addr = htonl(INADDR_ANY); + } + + mreq.imr_sourceaddr.s_addr = source->sin_addr.s_addr; + mreq.imr_multiaddr.s_addr = group->sin_addr.s_addr; + + int option = drop? IP_ADD_SOURCE_MEMBERSHIP : IP_DROP_SOURCE_MEMBERSHIP; + + return setsockopt(fd, IPPROTO_IP, option, &mreq, sizeof(mreq)); +} + +static int bsd_socket_set_source_specific_membership6(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_in6 *source, const struct sockaddr_in6 *group, const struct sockaddr_in6 *iface, int drop) { + struct group_source_req mreq; + memset(&mreq, 0, sizeof(mreq)); + + if (iface != NULL) { + mreq.gsr_interface = iface->sin6_scope_id; + } + + memcpy(&mreq.gsr_source, source, sizeof(mreq.gsr_source)); + memcpy(&mreq.gsr_group, group, sizeof(mreq.gsr_group)); + + int option = drop? MCAST_JOIN_SOURCE_GROUP : MCAST_LEAVE_SOURCE_GROUP; + + return setsockopt(fd, IPPROTO_IPV6, option, &mreq, sizeof(mreq)); +} + +int bsd_socket_set_source_specific_membership(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_storage *source, const struct sockaddr_storage *group, const struct sockaddr_storage *iface, int drop) { + if (source->ss_family == group->ss_family && group->ss_family == iface->ss_family) { + if (source->ss_family == AF_INET) { + return bsd_socket_set_source_specific_membership4(fd, (const struct sockaddr_in*) source, (const struct sockaddr_in*) group, (const struct sockaddr_in*) iface, drop); + } else if (source->ss_family == AF_INET6) { + return bsd_socket_set_source_specific_membership6(fd, (const struct sockaddr_in6*) source, (const struct sockaddr_in6*) group, (const struct sockaddr_in6*) iface, drop); + } + } + +#ifdef _WIN32 + WSASetLastError(WSAEINVAL); +#endif + errno = EINVAL; + return -1; +} + +static int bsd_socket_ttl_any(LIBUS_SOCKET_DESCRIPTOR fd, int ttl, int ipv4, int ipv6) { + if (ttl < 1 || ttl > 255) { +#ifdef _WIN32 + WSASetLastError(WSAEINVAL); +#endif + errno = EINVAL; + return -1; + } + + return setsockopt_6_or_4(fd, ipv4, ipv6, &ttl, sizeof(ttl)); +} + +int bsd_socket_ttl_unicast(LIBUS_SOCKET_DESCRIPTOR fd, int ttl) { + return bsd_socket_ttl_any(fd, ttl, IP_TTL, IPV6_UNICAST_HOPS); +} + +int bsd_socket_ttl_multicast(LIBUS_SOCKET_DESCRIPTOR fd, int ttl) { + return bsd_socket_ttl_any(fd, ttl, IP_MULTICAST_TTL, IPV6_MULTICAST_HOPS); } int bsd_socket_keepalive(LIBUS_SOCKET_DESCRIPTOR fd, int on, unsigned int delay) { @@ -398,11 +557,15 @@ void bsd_socket_flush(LIBUS_SOCKET_DESCRIPTOR fd) { // Linux TCP_CORK has the same underlying corking mechanism as with MSG_MORE #ifdef TCP_CORK int enabled = 0; - setsockopt(fd, IPPROTO_TCP, TCP_CORK, (void *) &enabled, sizeof(int)); + setsockopt(fd, IPPROTO_TCP, TCP_CORK, &enabled, sizeof(int)); #endif } -LIBUS_SOCKET_DESCRIPTOR bsd_create_socket(int domain, int type, int protocol) { +LIBUS_SOCKET_DESCRIPTOR bsd_create_socket(int domain, int type, int protocol, int *err) { + if (err != NULL) { + *err = 0; + } + LIBUS_SOCKET_DESCRIPTOR created_fd; #if defined(SOCK_CLOEXEC) && defined(SOCK_NONBLOCK) const int flags = SOCK_CLOEXEC | SOCK_NONBLOCK; @@ -411,6 +574,9 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_socket(int domain, int type, int protocol) { } while (IS_EINTR(created_fd)); if (UNLIKELY(created_fd == -1)) { + if (err != NULL) { + *err = errno; + } return LIBUS_SOCKET_ERROR; } @@ -421,6 +587,9 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_socket(int domain, int type, int protocol) { } while (IS_EINTR(created_fd)); if (UNLIKELY(created_fd == -1)) { + if (err != NULL) { + *err = errno; + } return LIBUS_SOCKET_ERROR; } @@ -615,7 +784,7 @@ int bsd_would_block() { static int us_internal_bind_and_listen(LIBUS_SOCKET_DESCRIPTOR listenFd, struct sockaddr *listenAddr, socklen_t listenAddrLength, int backlog, int* error) { int result; - do + do result = bind(listenFd, listenAddr, listenAddrLength); while (IS_EINTR(result)); @@ -624,7 +793,7 @@ static int us_internal_bind_and_listen(LIBUS_SOCKET_DESCRIPTOR listenFd, struct return -1; } - do + do result = listen(listenFd, backlog); while (IS_EINTR(result)); *error = LIBUS_ERR; @@ -640,18 +809,20 @@ inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd int* error ) { - if ((options & LIBUS_LISTEN_EXCLUSIVE_PORT)) { + if ((options & LIBUS_LISTEN_EXCLUSIVE_PORT)) { #if _WIN32 int optval2 = 1; - setsockopt(listenFd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (void *) &optval2, sizeof(optval2)); + setsockopt(listenFd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &optval2, sizeof(optval2)); #endif } else { -#if defined(SO_REUSEPORT) if((options & LIBUS_LISTEN_REUSE_PORT)) { int optval2 = 1; - setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, (void *) &optval2, sizeof(optval2)); - } +#if defined(SO_REUSEPORT) + setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, &optval2, sizeof(optval2)); +#else + setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &optval2, sizeof(optval2)); #endif + } } #if defined(SO_REUSEADDR) @@ -661,10 +832,10 @@ inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd // allow binding to addresses that are in use by sockets in TIME_WAIT, it // effectively allows 'stealing' a port which is in use by another application. // See libuv issue #1360. - - + + int optval3 = 1; - setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, (void *) &optval3, sizeof(optval3)); + setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &optval3, sizeof(optval3)); #endif #endif @@ -672,10 +843,10 @@ inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd // TODO: revise support to match node.js // if (listenAddr->ai_family == AF_INET6) { // int disabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0; - // setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, (void *) &disabled, sizeof(disabled)); + // setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &disabled, sizeof(disabled)); // } int disabled = 0; - setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, (void *) &disabled, sizeof(disabled)); + setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &disabled, sizeof(disabled)); #endif if (us_internal_bind_and_listen(listenFd, listenAddr->ai_addr, (socklen_t) listenAddr->ai_addrlen, 512, error)) { @@ -706,7 +877,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int struct addrinfo *listenAddr; for (struct addrinfo *a = result; a != NULL; a = a->ai_next) { if (a->ai_family == AF_INET6) { - listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol); + listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, NULL); if (listenFd == LIBUS_SOCKET_ERROR) { continue; } @@ -723,7 +894,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int for (struct addrinfo *a = result; a != NULL; a = a->ai_next) { if (a->ai_family == AF_INET) { - listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol); + listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, NULL); if (listenFd == LIBUS_SOCKET_ERROR) { continue; } @@ -822,11 +993,11 @@ static LIBUS_SOCKET_DESCRIPTOR bsd_create_unix_socket_address(const char *path, if (path_len >= sizeof(server_address->sun_path)) { #if defined(_WIN32) // simulate ENAMETOOLONG - SetLastError(ERROR_FILENAME_EXCED_RANGE); + SetLastError(ERROR_FILENAME_EXCED_RANGE); #else errno = ENAMETOOLONG; #endif - + return LIBUS_SOCKET_ERROR; } @@ -837,7 +1008,7 @@ static LIBUS_SOCKET_DESCRIPTOR bsd_create_unix_socket_address(const char *path, static LIBUS_SOCKET_DESCRIPTOR internal_bsd_create_listen_socket_unix(const char* path, int options, struct sockaddr_un* server_address, size_t addrlen, int* error) { LIBUS_SOCKET_DESCRIPTOR listenFd = LIBUS_SOCKET_ERROR; - listenFd = bsd_create_socket(AF_UNIX, SOCK_STREAM, 0); + listenFd = bsd_create_socket(AF_UNIX, SOCK_STREAM, 0, NULL); if (listenFd == LIBUS_SOCKET_ERROR) { return LIBUS_SOCKET_ERROR; @@ -884,7 +1055,11 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket_unix(const char *path, size_t l return listenFd; } -LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port) { +LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int options, int *err) { + if (err != NULL) { + *err = 0; + } + struct addrinfo hints, *result; memset(&hints, 0, sizeof(struct addrinfo)); @@ -895,7 +1070,11 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port) { char port_string[16]; snprintf(port_string, 16, "%d", port); - if (getaddrinfo(host, port_string, &hints, &result)) { + int gai_result = getaddrinfo(host, port_string, &hints, &result); + if (gai_result != 0) { + if (err != NULL) { + *err = -gai_result; + } return LIBUS_SOCKET_ERROR; } @@ -903,14 +1082,14 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port) { struct addrinfo *listenAddr = NULL; for (struct addrinfo *a = result; a && listenFd == LIBUS_SOCKET_ERROR; a = a->ai_next) { if (a->ai_family == AF_INET6) { - listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol); + listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, err); listenAddr = a; } } for (struct addrinfo *a = result; a && listenFd == LIBUS_SOCKET_ERROR; a = a->ai_next) { if (a->ai_family == AF_INET) { - listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol); + listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, err); listenAddr = a; } } @@ -923,12 +1102,30 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port) { if (port != 0) { /* Should this also go for UDP? */ int enabled = 1; - setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, (void *) &enabled, sizeof(enabled)); + setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)); } - + + if ((options & LIBUS_LISTEN_EXCLUSIVE_PORT)) { +#if _WIN32 + int optval2 = 1; + setsockopt(listenFd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &optval2, sizeof(optval2)); +#endif + } else { + if((options & LIBUS_LISTEN_REUSE_PORT)) { + int optval2 = 1; +#if defined(SO_REUSEPORT) + setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, &optval2, sizeof(optval2)); +#else + setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &optval2, sizeof(optval2)); +#endif + } + } + #ifdef IPV6_V6ONLY - int disabled = 0; - setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, (void *) &disabled, sizeof(disabled)); + if (listenAddr->ai_family == AF_INET6) { + int disabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0; + setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &disabled, sizeof(disabled)); + } #endif /* We need destination address for udp packets in both ipv6 and ipv4 */ @@ -939,9 +1136,9 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port) { #endif int enabled = 1; - if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_RECVPKTINFO, (void *) &enabled, sizeof(enabled)) == -1) { + if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &enabled, sizeof(enabled)) == -1) { if (errno == 92) { - if (setsockopt(listenFd, IPPROTO_IP, IP_PKTINFO, (void *) &enabled, sizeof(enabled)) != 0) { + if (setsockopt(listenFd, IPPROTO_IP, IP_PKTINFO, &enabled, sizeof(enabled)) != 0) { //printf("Error setting IPv4 pktinfo!\n"); } } else { @@ -950,9 +1147,9 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port) { } /* These are used for getting the ECN */ - if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_RECVTCLASS, (void *) &enabled, sizeof(enabled)) == -1) { + if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_RECVTCLASS, &enabled, sizeof(enabled)) == -1) { if (errno == 92) { - if (setsockopt(listenFd, IPPROTO_IP, IP_RECVTOS, (void *) &enabled, sizeof(enabled)) != 0) { + if (setsockopt(listenFd, IPPROTO_IP, IP_RECVTOS, &enabled, sizeof(enabled)) != 0) { //printf("Error setting IPv4 ECN!\n"); } } else { @@ -962,12 +1159,22 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port) { /* We bind here as well */ if (bind(listenFd, listenAddr->ai_addr, (socklen_t) listenAddr->ai_addrlen)) { + if (err != NULL) { +#ifdef _WIN32 + *err = WSAGetLastError(); +#else + *err = errno; +#endif + } bsd_close_socket(listenFd); freeaddrinfo(result); return LIBUS_SOCKET_ERROR; } freeaddrinfo(result); + if (err != NULL) { + *err = 0; + } return listenFd; } @@ -1012,7 +1219,7 @@ int bsd_disconnect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd) { int res = connect(fd, &addr, sizeof(addr)); // EAFNOSUPPORT is harmless in this case - we just want to disconnect - if (res == 0 || + if (res == 0 || #ifdef _WIN32 WSAGetLastError() == WSAEAFNOSUPPORT #else @@ -1080,14 +1287,14 @@ static int bsd_do_connect_raw(LIBUS_SOCKET_DESCRIPTOR fd, struct sockaddr *addr, } } - + #else int r; do { errno = 0; r = connect(fd, (struct sockaddr *)addr, namelen); } while (IS_EINTR(r)); - + // connect() can return -1 with an errno of 0. // the errno is the correct one in that case. if (r == -1 && errno != 0) { @@ -1097,7 +1304,7 @@ static int bsd_do_connect_raw(LIBUS_SOCKET_DESCRIPTOR fd, struct sockaddr *addr, return errno; } - + return 0; #endif } @@ -1122,7 +1329,7 @@ static int convert_null_addr(const struct sockaddr_storage *addr, struct sockadd } } return 0; -} +} static int is_loopback(struct sockaddr_storage *sockaddr) { if (sockaddr->ss_family == AF_INET) { @@ -1138,7 +1345,7 @@ static int is_loopback(struct sockaddr_storage *sockaddr) { #endif LIBUS_SOCKET_DESCRIPTOR bsd_create_connect_socket(struct sockaddr_storage *addr, int options) { - LIBUS_SOCKET_DESCRIPTOR fd = bsd_create_socket(addr->ss_family, SOCK_STREAM, 0); + LIBUS_SOCKET_DESCRIPTOR fd = bsd_create_socket(addr->ss_family, SOCK_STREAM, 0, NULL); if (fd == LIBUS_SOCKET_ERROR) { return LIBUS_SOCKET_ERROR; } @@ -1146,7 +1353,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_connect_socket(struct sockaddr_storage *addr, #ifdef _WIN32 win32_set_nonblocking(fd); - // On windows we can't connect to the null address directly. + // On windows we can't connect to the null address directly. // To match POSIX behavior, we need to connect to localhost instead. struct sockaddr_storage converted; if (convert_null_addr(addr, &converted)) { @@ -1185,7 +1392,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_connect_socket(struct sockaddr_storage *addr, } static LIBUS_SOCKET_DESCRIPTOR internal_bsd_create_connect_socket_unix(const char *server_path, size_t len, int options, struct sockaddr_un* server_address, const size_t addrlen) { - LIBUS_SOCKET_DESCRIPTOR fd = bsd_create_socket(AF_UNIX, SOCK_STREAM, 0); + LIBUS_SOCKET_DESCRIPTOR fd = bsd_create_socket(AF_UNIX, SOCK_STREAM, 0, NULL); if (fd == LIBUS_SOCKET_ERROR) { return LIBUS_SOCKET_ERROR; diff --git a/packages/bun-usockets/src/internal/networking/bsd.h b/packages/bun-usockets/src/internal/networking/bsd.h index e100e12bf6..56e958508e 100644 --- a/packages/bun-usockets/src/internal/networking/bsd.h +++ b/packages/bun-usockets/src/internal/networking/bsd.h @@ -26,7 +26,7 @@ #include "libusockets.h" -#ifdef _WIN32 +#ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif @@ -178,9 +178,16 @@ int bsd_udp_packet_buffer_local_ip(struct udp_recvbuf *msgvec, int index, char * LIBUS_SOCKET_DESCRIPTOR apple_no_sigpipe(LIBUS_SOCKET_DESCRIPTOR fd); LIBUS_SOCKET_DESCRIPTOR bsd_set_nonblocking(LIBUS_SOCKET_DESCRIPTOR fd); void bsd_socket_nodelay(LIBUS_SOCKET_DESCRIPTOR fd, int enabled); +int bsd_socket_broadcast(LIBUS_SOCKET_DESCRIPTOR fd, int enabled); +int bsd_socket_ttl_unicast(LIBUS_SOCKET_DESCRIPTOR fd, int ttl); +int bsd_socket_ttl_multicast(LIBUS_SOCKET_DESCRIPTOR fd, int ttl); +int bsd_socket_multicast_loopback(LIBUS_SOCKET_DESCRIPTOR fd, int enabled); +int bsd_socket_multicast_interface(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_storage *addr); +int bsd_socket_set_membership(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_storage *addr, const struct sockaddr_storage *iface, int drop); +int bsd_socket_set_source_specific_membership(LIBUS_SOCKET_DESCRIPTOR fd, const struct sockaddr_storage *source, const struct sockaddr_storage *group, const struct sockaddr_storage *iface, int drop); int bsd_socket_keepalive(LIBUS_SOCKET_DESCRIPTOR fd, int on, unsigned int delay); void bsd_socket_flush(LIBUS_SOCKET_DESCRIPTOR fd); -LIBUS_SOCKET_DESCRIPTOR bsd_create_socket(int domain, int type, int protocol); +LIBUS_SOCKET_DESCRIPTOR bsd_create_socket(int domain, int type, int protocol, int *err); void bsd_close_socket(LIBUS_SOCKET_DESCRIPTOR fd); void bsd_shutdown_socket(LIBUS_SOCKET_DESCRIPTOR fd); @@ -211,7 +218,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket_unix(const char *path, size_t pathlen, int options, int* error); /* Creates an UDP socket bound to the hostname and port */ -LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port); +LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int options, int *err); int bsd_connect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd, const char *host, int port); int bsd_disconnect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd); diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index cf34618f85..dd27d70eee 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -42,8 +42,8 @@ #endif #ifdef BUN_DEBUG -#define nonnull_fn_decl -#else +#define nonnull_fn_decl +#else #ifndef nonnull_fn_decl #define nonnull_fn_decl __attribute__((nonnull)) #endif @@ -98,7 +98,7 @@ enum { LIBUS_SOCKET_ALLOW_HALF_OPEN = 2, /* Setting reusePort allows multiple sockets on the same host to bind to the same port. Incoming connections are distributed by the operating system to listening sockets. This option is available only on some platforms, such as Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+*/ LIBUS_LISTEN_REUSE_PORT = 4, - /* etting ipv6Only will disable dual-stack support, i.e., binding to host :: won't make 0.0.0.0 be bound.*/ + /* Setting ipv6Only will disable dual-stack support, i.e., binding to host :: won't make 0.0.0.0 be bound.*/ LIBUS_SOCKET_IPV6_ONLY = 8, }; @@ -153,10 +153,12 @@ struct us_udp_packet_buffer_t *us_create_udp_packet_buffer(); //struct us_udp_socket_t *us_create_udp_socket(us_loop_r loop, void (*data_cb)(struct us_udp_socket_t *, struct us_udp_packet_buffer_t *, int), void (*drain_cb)(struct us_udp_socket_t *), char *host, unsigned short port); -struct us_udp_socket_t *us_create_udp_socket(us_loop_r loop, void (*data_cb)(struct us_udp_socket_t *, void *, int), void (*drain_cb)(struct us_udp_socket_t *), void (*close_cb)(struct us_udp_socket_t *), const char *host, unsigned short port, void *user); +struct us_udp_socket_t *us_create_udp_socket(us_loop_r loop, void (*data_cb)(struct us_udp_socket_t *, void *, int), void (*drain_cb)(struct us_udp_socket_t *), void (*close_cb)(struct us_udp_socket_t *), const char *host, unsigned short port, int flags, int *err, void *user); void us_udp_socket_close(struct us_udp_socket_t *s); +int us_udp_socket_set_broadcast(struct us_udp_socket_t *s, int enabled); + /* This one is ugly, should be ext! not user */ void *us_udp_socket_user(struct us_udp_socket_t *s); @@ -223,11 +225,11 @@ struct us_bun_socket_context_options_t { const char *ssl_ciphers; int ssl_prefer_low_memory_usage; /* Todo: rename to prefer_low_memory_usage and apply for TCP as well */ const char **key; - unsigned int key_count; + unsigned int key_count; const char **cert; - unsigned int cert_count; + unsigned int cert_count; const char **ca; - unsigned int ca_count; + unsigned int ca_count; unsigned int secure_options; int reject_unauthorized; int request_cert; @@ -310,7 +312,7 @@ struct us_listen_socket_t *us_socket_context_listen_unix(int ssl, us_socket_cont void us_listen_socket_close(int ssl, struct us_listen_socket_t *ls) nonnull_fn_decl; /* - Returns one of + Returns one of - struct us_socket_t * - indicated by the value at on_connecting being set to 1 This is the fast path where the DNS result is available immediately and only a single remote address is available diff --git a/packages/bun-usockets/src/quic.c b/packages/bun-usockets/src/quic.c index 5290746324..d4d94c7041 100644 --- a/packages/bun-usockets/src/quic.c +++ b/packages/bun-usockets/src/quic.c @@ -102,7 +102,7 @@ void on_udp_socket_writable(struct us_udp_socket_t *s) { // we need two differetn handlers to know to put it in client or servcer context void on_udp_socket_data_client(struct us_udp_socket_t *s, struct us_udp_packet_buffer_t *buf, int packets) { - + int fd = us_poll_fd((struct us_poll_t *) s); //printf("Reading on fd: %d\n", fd); @@ -113,7 +113,7 @@ void on_udp_socket_data_client(struct us_udp_socket_t *s, struct us_udp_packet_b // do we have udp socket contexts? or do we just have user data? us_quic_socket_context_t *context = us_udp_socket_user(s); - + /* We just shove it to lsquic */ for (int i = 0; i < packets; i++) { char *payload = us_udp_packet_buffer_payload(buf, i); @@ -155,7 +155,7 @@ void on_udp_socket_data_client(struct us_udp_socket_t *s, struct us_udp_packet_b int ret = lsquic_engine_packet_in(context->client_engine, payload, length, (struct sockaddr *) &local_addr, peer_addr, (void *) s, 0); //printf("Engine returned: %d\n", ret); - + } lsquic_engine_process_conns(context->client_engine); @@ -163,7 +163,7 @@ void on_udp_socket_data_client(struct us_udp_socket_t *s, struct us_udp_packet_b } void on_udp_socket_data(struct us_udp_socket_t *s, struct us_udp_packet_buffer_t *buf, int packets) { - + //printf("UDP socket got data: %p\n", s); @@ -175,7 +175,7 @@ void on_udp_socket_data(struct us_udp_socket_t *s, struct us_udp_packet_buffer_t // process conns now? to accept new connections? lsquic_engine_process_conns(context->engine); - + /* We just shove it to lsquic */ for (int i = 0; i < packets; i++) { char *payload = us_udp_packet_buffer_payload(buf, i); @@ -218,7 +218,7 @@ void on_udp_socket_data(struct us_udp_socket_t *s, struct us_udp_packet_buffer_t int ret = lsquic_engine_packet_in(context->engine, payload, length, (struct sockaddr *) &local_addr, peer_addr, (void *) s, 0); //printf("Engine returned: %d\n", ret); - + } lsquic_engine_process_conns(context->engine); @@ -653,9 +653,9 @@ struct ssl_ctx_st *get_ssl_ctx(void *peer_ctx, const struct sockaddr *local) { SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION); SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION); - + //SSL_CTX_set_default_verify_paths(ctx); - + // probably cannot use this when http is in use? // alpn is needed SSL_CTX_set_alpn_select_cb(ctx, select_alpn, NULL); @@ -950,7 +950,7 @@ us_quic_socket_context_t *us_create_quic_socket_context(struct us_loop_t *loop, .ea_stream_if_ctx = context, .ea_get_ssl_ctx = get_ssl_ctx, - + // lookup certificate .ea_lookup_cert = sni_lookup, .ea_cert_lu_ctx = 0, @@ -980,7 +980,7 @@ us_quic_socket_context_t *us_create_quic_socket_context(struct us_loop_t *loop, .ea_stream_if_ctx = context, //.ea_get_ssl_ctx = get_ssl_ctx, // for client? - + // lookup certificate //.ea_lookup_cert = sni_lookup, // for client? //.ea_cert_lu_ctx = 13, // for client? @@ -1008,7 +1008,7 @@ us_quic_socket_context_t *us_create_quic_socket_context(struct us_loop_t *loop, us_quic_listen_socket_t *us_quic_socket_context_listen(us_quic_socket_context_t *context, const char *host, int port, int ext_size) { /* We literally do create a listen socket */ - return (us_quic_listen_socket_t *) us_create_udp_socket(context->loop, /*context->recv_buf*/ NULL, on_udp_socket_data, on_udp_socket_writable, host, port, context); + return (us_quic_listen_socket_t *) us_create_udp_socket(context->loop, /*context->recv_buf*/ NULL, on_udp_socket_data, on_udp_socket_writable, host, port, 0, context); //return NULL; } @@ -1030,7 +1030,7 @@ us_quic_socket_t *us_quic_socket_context_connect(us_quic_socket_context_t *conte addr->sin6_family = AF_INET6; // Create the UDP socket binding to ephemeral port - struct us_udp_socket_t *udp_socket = us_create_udp_socket(context->loop, /*context->recv_buf*/ NULL, on_udp_socket_data_client, on_udp_socket_writable, 0, 0, context); + struct us_udp_socket_t *udp_socket = us_create_udp_socket(context->loop, /*context->recv_buf*/ NULL, on_udp_socket_data_client, on_udp_socket_writable, 0, 0, 0, context); // Determine what port we got, creating the local sockaddr int ephemeral = us_udp_socket_bound_port(udp_socket); diff --git a/packages/bun-usockets/src/udp.c b/packages/bun-usockets/src/udp.c index af447414c4..aa4069976d 100644 --- a/packages/bun-usockets/src/udp.c +++ b/packages/bun-usockets/src/udp.c @@ -55,7 +55,7 @@ int us_udp_socket_send(struct us_udp_socket_t *s, void** payloads, size_t* lengt num -= count; // TODO nohang flag? int sent = bsd_sendmmsg(fd, buf, MSG_DONTWAIT); - if (sent < 0) { + if (sent < 0) { return sent; } total_sent += sent; @@ -91,9 +91,7 @@ void us_udp_socket_remote_ip(struct us_udp_socket_t *s, char *buf, int *length) } } -void *us_udp_socket_user(struct us_udp_socket_t *s) { - struct us_udp_socket_t *udp = (struct us_udp_socket_t *) s; - +void *us_udp_socket_user(struct us_udp_socket_t *udp) { return udp->user; } @@ -108,6 +106,18 @@ void us_udp_socket_close(struct us_udp_socket_t *s) { s->on_close(s); } +int us_udp_socket_set_broadcast(struct us_udp_socket_t *s, int enabled) { + return bsd_socket_broadcast(us_poll_fd(&s->p), enabled); +} + +int us_udp_socket_set_ttl_unicast(struct us_udp_socket_t *s, int ttl) { + return bsd_socket_ttl_unicast(us_poll_fd(&s->p), ttl); +} + +int us_udp_socket_set_ttl_multicast(struct us_udp_socket_t *s, int ttl) { + return bsd_socket_ttl_multicast(us_poll_fd(&s->p), ttl); +} + int us_udp_socket_connect(struct us_udp_socket_t *s, const char* host, unsigned short port) { return bsd_connect_udp_socket(us_poll_fd((struct us_poll_t *)s), host, port); } @@ -116,17 +126,35 @@ int us_udp_socket_disconnect(struct us_udp_socket_t *s) { return bsd_disconnect_udp_socket(us_poll_fd((struct us_poll_t *)s)); } +int us_udp_socket_set_multicast_loopback(struct us_udp_socket_t *s, int enabled) { + return bsd_socket_multicast_loopback(us_poll_fd(&s->p), enabled); +} + +int us_udp_socket_set_multicast_interface(struct us_udp_socket_t *s, const struct sockaddr_storage *addr) { + return bsd_socket_multicast_interface(us_poll_fd(&s->p), addr); +} + +int us_udp_socket_set_membership(struct us_udp_socket_t *s, const struct sockaddr_storage *addr, const struct sockaddr_storage *iface, int drop) { + return bsd_socket_set_membership(us_poll_fd(&s->p), addr, iface, drop); +} + +int us_udp_socket_set_source_specific_membership(struct us_udp_socket_t *s, const struct sockaddr_storage *source, const struct sockaddr_storage *group, const struct sockaddr_storage *iface, int drop) { + return bsd_socket_set_source_specific_membership(us_poll_fd(&s->p), source, group, iface, drop); +} + struct us_udp_socket_t *us_create_udp_socket( - struct us_loop_t *loop, - void (*data_cb)(struct us_udp_socket_t *, void *, int), - void (*drain_cb)(struct us_udp_socket_t *), + struct us_loop_t *loop, + void (*data_cb)(struct us_udp_socket_t *, void *, int), + void (*drain_cb)(struct us_udp_socket_t *), void (*close_cb)(struct us_udp_socket_t *), - const char *host, - unsigned short port, + const char *host, + unsigned short port, + int flags, + int *err, void *user ) { - LIBUS_SOCKET_DESCRIPTOR fd = bsd_create_udp_socket(host, port); + LIBUS_SOCKET_DESCRIPTOR fd = bsd_create_udp_socket(host, port, flags, err); if (fd == LIBUS_SOCKET_ERROR) { return 0; } @@ -157,6 +185,6 @@ struct us_udp_socket_t *us_create_udp_socket( udp->next = NULL; us_poll_start((struct us_poll_t *) udp, udp->loop, LIBUS_SOCKET_READABLE | LIBUS_SOCKET_WRITABLE); - + return (struct us_udp_socket_t *) udp; } \ No newline at end of file diff --git a/src/bun.js/api/bun/udp_socket.zig b/src/bun.js/api/bun/udp_socket.zig index 055679d60a..4786b5228f 100644 --- a/src/bun.js/api/bun/udp_socket.zig +++ b/src/bun.js/api/bun/udp_socket.zig @@ -63,6 +63,7 @@ fn onData(socket: *uws.udp.Socket, buf: *uws.udp.PacketBuffer, packets: c_int) c var addr_buf: [INET6_ADDRSTRLEN + 1:0]u8 = undefined; var hostname: ?[*:0]const u8 = null; var port: u16 = 0; + var scope_id: ?u32 = null; switch (peer.family) { std.posix.AF.INET => { @@ -74,6 +75,8 @@ fn onData(socket: *uws.udp.Socket, buf: *uws.udp.PacketBuffer, packets: c_int) c const peer6: *std.posix.sockaddr.in6 = @ptrCast(peer); hostname = inet_ntop(peer.family, &peer6.addr, &addr_buf, addr_buf.len); port = ntohs(peer6.port); + if (peer6.scope_id != 0) + scope_id = peer6.scope_id; }, else => continue, } @@ -90,11 +93,27 @@ fn onData(socket: *uws.udp.Socket, buf: *uws.udp.PacketBuffer, packets: c_int) c _ = udpSocket.js_refcount.fetchAdd(1, .monotonic); defer _ = udpSocket.js_refcount.fetchSub(1, .monotonic); + const span = std.mem.span(hostname.?); + var hostname_string = if (scope_id) |id| blk: { + if (comptime !bun.Environment.isWindows) { + const net_if = @cImport({ + @cInclude("net/if.h"); + }); + + var buffer = std.mem.zeroes([net_if.IF_NAMESIZE:0]u8); + if (net_if.if_indextoname(id, &buffer) != null) { + break :blk bun.String.createFormat("{s}%{s}", .{ span, std.mem.span(@as([*:0]u8, &buffer)) }) catch bun.outOfMemory(); + } + } + + break :blk bun.String.createFormat("{s}%{d}", .{ span, id }) catch bun.outOfMemory(); + } else bun.String.init(span); + _ = callback.call(globalThis, udpSocket.thisValue, &.{ udpSocket.thisValue, udpSocket.config.binary_type.toJS(slice, globalThis), JSC.jsNumber(port), - JSC.ZigString.init(std.mem.span(hostname.?)).toJS(globalThis), + hostname_string.transferToJS(globalThis), }) catch |err| { _ = udpSocket.callErrorHandler(.zero, &.{udpSocket.globalThis.takeException(err)}); }; @@ -117,6 +136,7 @@ pub const UDPSocketConfig = struct { hostname: [:0]u8, connect: ?ConnectConfig = null, port: u16, + flags: i32, binary_type: JSC.BinaryType = .Buffer, on_data: JSValue = .zero, on_drain: JSValue = .zero, @@ -153,9 +173,15 @@ pub const UDPSocketConfig = struct { } }; + const flags: i32 = if (try options.getTruthy(globalThis, "flags")) |value| + try bun.validators.validateInt32(globalThis, value, "flags", .{}, null, null) + else + 0; + var config = This{ .hostname = hostname, .port = port, + .flags = flags, }; if (try options.getTruthy(globalThis, "socket")) |socket| { @@ -290,6 +316,8 @@ pub const UDPSocket = struct { .vm = vm, }); + var err: i32 = 0; + if (uws.udp.Socket.create( this.loop, onData, @@ -297,12 +325,25 @@ pub const UDPSocket = struct { onClose, config.hostname, config.port, + config.flags, + &err, this, )) |socket| { this.socket = socket; } else { this.closed = true; - this.deinit(); + defer this.deinit(); + if (err != 0) { + const code = @tagName(bun.C.SystemErrno.init(@as(c_int, @intCast(err))).?); + const sys_err = JSC.SystemError{ + .errno = err, + .code = bun.String.static(code), + .message = bun.String.createFormat("bind {s} {s}", .{ code, config.hostname }) catch bun.outOfMemory(), + }; + const error_value = sys_err.toErrorInstance(globalThis); + error_value.put(globalThis, "address", bun.String.createUTF8ForJS(globalThis, config.hostname)); + return globalThis.throwValue(error_value); + } return globalThis.throw("Failed to bind socket", .{}); } @@ -314,12 +355,12 @@ pub const UDPSocket = struct { if (config.connect) |connect| { const ret = this.socket.connect(connect.address, connect.port); if (ret != 0) { - if (JSC.Maybe(void).errnoSys(ret, .connect)) |err| { - return globalThis.throwValue(err.toJS(globalThis)); + if (JSC.Maybe(void).errnoSys(ret, .connect)) |sys_err| { + return globalThis.throwValue(sys_err.toJS(globalThis)); } - if (bun.c_ares.Error.initEAI(ret)) |err| { - return globalThis.throwValue(err.toJS(globalThis)); + if (bun.c_ares.Error.initEAI(ret)) |eai_err| { + return globalThis.throwValue(eai_err.toJS(globalThis)); } } this.connect_info = .{ .port = connect.port }; @@ -353,6 +394,209 @@ pub const UDPSocket = struct { return true; } + pub fn setBroadcast(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + if (this.closed) { + return globalThis.throwValue(bun.JSC.Maybe(void).errnoSys(@as(i32, @intCast(@intFromEnum(std.posix.E.BADF))), .setsockopt).?.toJS(globalThis)); + } + + const arguments = callframe.arguments(); + if (arguments.len < 1) { + return globalThis.throwInvalidArguments("Expected 1 argument, got {}", .{arguments.len}); + } + + const enabled = arguments[0].toBoolean(); + const res = this.socket.setBroadcast(enabled); + + if (getUSError(res, .setsockopt, true)) |err| { + return globalThis.throwValue(err.toJS(globalThis)); + } + + return arguments[0]; + } + + pub fn setMulticastLoopback(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + if (this.closed) { + return globalThis.throwValue(bun.JSC.Maybe(void).errnoSys(@as(i32, @intCast(@intFromEnum(std.posix.E.BADF))), .setsockopt).?.toJS(globalThis)); + } + + const arguments = callframe.arguments(); + if (arguments.len < 1) { + return globalThis.throwInvalidArguments("Expected 1 argument, got {}", .{arguments.len}); + } + + const enabled = arguments[0].toBoolean(); + const res = this.socket.setMulticastLoopback(enabled); + + if (getUSError(res, .setsockopt, true)) |err| { + return globalThis.throwValue(err.toJS(globalThis)); + } + + return arguments[0]; + } + + fn setMembership(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame, drop: bool) bun.JSError!JSValue { + if (this.closed) { + return globalThis.throwValue(bun.JSC.Maybe(void).errnoSys(@as(i32, @intCast(@intFromEnum(std.posix.E.BADF))), .setsockopt).?.toJS(globalThis)); + } + + const arguments = callframe.arguments(); + if (arguments.len < 1) { + return globalThis.throwInvalidArguments("Expected 1 argument, got {}", .{arguments.len}); + } + + var addr: std.posix.sockaddr.storage = undefined; + if (!parseAddr(this, globalThis, JSC.jsNumber(0), arguments[0], &addr)) { + return globalThis.throwValue(bun.JSC.Maybe(void).errnoSys(@as(i32, @intCast(@intFromEnum(std.posix.E.INVAL))), .setsockopt).?.toJS(globalThis)); + } + + var interface: std.posix.sockaddr.storage = undefined; + + const res = if (arguments.len > 1 and parseAddr(this, globalThis, JSC.jsNumber(0), arguments[1], &interface)) blk: { + if (addr.family != interface.family) { + return globalThis.throwInvalidArguments("Family mismatch between address and interface", .{}); + } + break :blk this.socket.setMembership(&addr, &interface, drop); + } else this.socket.setMembership(&addr, null, drop); + + if (getUSError(res, .setsockopt, true)) |err| { + return globalThis.throwValue(err.toJS(globalThis)); + } + + return .true; + } + + pub fn addMembership(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + return this.setMembership(globalThis, callframe, false); + } + + pub fn dropMembership(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + return this.setMembership(globalThis, callframe, true); + } + + fn setSourceSpecificMembership(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame, drop: bool) bun.JSError!JSValue { + if (this.closed) { + return globalThis.throwValue(bun.JSC.Maybe(void).errnoSys(@as(i32, @intCast(@intFromEnum(std.posix.E.BADF))), .setsockopt).?.toJS(globalThis)); + } + + const arguments = callframe.arguments(); + if (arguments.len < 2) { + return globalThis.throwInvalidArguments("Expected 2 arguments, got {}", .{arguments.len}); + } + + var source_addr: std.posix.sockaddr.storage = undefined; + if (!parseAddr(this, globalThis, JSC.jsNumber(0), arguments[0], &source_addr)) { + return globalThis.throwValue(bun.JSC.Maybe(void).errnoSys(@as(i32, @intCast(@intFromEnum(std.posix.E.INVAL))), .setsockopt).?.toJS(globalThis)); + } + + var group_addr: std.posix.sockaddr.storage = undefined; + if (!parseAddr(this, globalThis, JSC.jsNumber(0), arguments[1], &group_addr)) { + return globalThis.throwValue(bun.JSC.Maybe(void).errnoSys(@as(i32, @intCast(@intFromEnum(std.posix.E.INVAL))), .setsockopt).?.toJS(globalThis)); + } + + if (source_addr.family != group_addr.family) { + return globalThis.throwInvalidArguments("Family mismatch between source and group addresses", .{}); + } + + var interface: std.posix.sockaddr.storage = undefined; + + const res = if (arguments.len > 2 and parseAddr(this, globalThis, JSC.jsNumber(0), arguments[2], &interface)) blk: { + if (source_addr.family != interface.family) { + return globalThis.throwInvalidArguments("Family mismatch among source, group and interface addresses", .{}); + } + break :blk this.socket.setSourceSpecificMembership(&source_addr, &group_addr, &interface, drop); + } else this.socket.setSourceSpecificMembership(&source_addr, &group_addr, null, drop); + + if (getUSError(res, .setsockopt, true)) |err| { + return globalThis.throwValue(err.toJS(globalThis)); + } + + return .true; + } + + pub fn addSourceSpecificMembership(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + return this.setSourceSpecificMembership(globalThis, callframe, false); + } + + pub fn dropSourceSpecificMembership(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + return this.setSourceSpecificMembership(globalThis, callframe, true); + } + + pub fn setMulticastInterface(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + if (this.closed) { + return globalThis.throwValue(bun.JSC.Maybe(void).errnoSys(@as(i32, @intCast(@intFromEnum(std.posix.E.BADF))), .setsockopt).?.toJS(globalThis)); + } + + const arguments = callframe.arguments(); + if (arguments.len < 1) { + return globalThis.throwInvalidArguments("Expected 1 argument, got {}", .{arguments.len}); + } + + var addr: std.posix.sockaddr.storage = undefined; + + if (!parseAddr(this, globalThis, JSC.jsNumber(0), arguments[0], &addr)) { + return .false; + } + + const res = this.socket.setMulticastInterface(&addr); + + if (getUSError(res, .setsockopt, true)) |err| { + return globalThis.throwValue(err.toJS(globalThis)); + } + + return .true; + } + + pub fn setTTL(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + return setAnyTTL(this, globalThis, callframe, uws.udp.Socket.setUnicastTTL); + } + + pub fn setMulticastTTL(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + return setAnyTTL(this, globalThis, callframe, uws.udp.Socket.setMulticastTTL); + } + + fn getUSError(res: c_int, tag: bun.sys.Tag, comptime use_wsa: bool) ?bun.JSC.Maybe(void) { + if (comptime bun.Environment.isWindows) { + // setsockopt returns 0 on success, but errnoSys considers 0 to be failure on Windows. + // This applies to some other usockets functions too. + if (res >= 0) { + return null; + } + + if (comptime use_wsa) { + if (bun.windows.WSAGetLastError()) |wsa| { + if (wsa != .SUCCESS) { + std.os.windows.ws2_32.WSASetLastError(0); + return bun.JSC.Maybe(void).errno(wsa.toE(), tag); + } + } + } + + return bun.JSC.Maybe(void).errno(@as(bun.C.E, @enumFromInt(std.c._errno().*)), tag); + } else { + return bun.JSC.Maybe(void).errnoSys(res, tag); + } + } + + fn setAnyTTL(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame, comptime function: fn (*uws.udp.Socket, i32) c_int) bun.JSError!JSValue { + if (this.closed) { + return globalThis.throwValue(bun.JSC.Maybe(void).errnoSys(@as(i32, @intCast(@intFromEnum(std.posix.E.BADF))), .setsockopt).?.toJS(globalThis)); + } + + const arguments = callframe.arguments(); + if (arguments.len < 1) { + return globalThis.throwInvalidArguments("Expected 1 argument, got {}", .{arguments.len}); + } + + const ttl = arguments[0].coerceToInt32(globalThis); + const res = function(this.socket, ttl); + + if (getUSError(res, .setsockopt, true)) |err| { + return globalThis.throwValue(err.toJS(globalThis)); + } + + return JSValue.jsNumber(ttl); + } + pub fn sendMany(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { if (this.closed) { return globalThis.throw("Socket is closed", .{}); @@ -424,7 +668,7 @@ pub const UDPSocket = struct { return globalThis.throwInvalidArguments("Mismatch between array length property and number of items", .{}); } const res = this.socket.send(payloads, lens, addr_ptrs); - if (bun.JSC.Maybe(void).errnoSys(res, .send)) |err| { + if (getUSError(res, .send, true)) |err| { return globalThis.throwValue(err.toJS(globalThis)); } return JSValue.jsNumber(res); @@ -482,7 +726,7 @@ pub const UDPSocket = struct { }; const res = this.socket.send(&.{payload.ptr}, &.{payload.len}, &.{addr_ptr}); - if (bun.JSC.Maybe(void).errnoSys(res, .send)) |err| { + if (getUSError(res, .send, true)) |err| { return globalThis.throwValue(err.toJS(globalThis)); } return JSValue.jsBoolean(res > 0); @@ -510,6 +754,35 @@ pub const UDPSocket = struct { addr4.family = std.posix.AF.INET; } else { var addr6: *std.posix.sockaddr.in6 = @ptrCast(storage); + addr6.scope_id = 0; + + if (str.indexOfAsciiChar('%')) |percent| { + if (percent + 1 < str.length()) { + const iface_id: u32 = blk: { + if (comptime bun.Environment.isWindows) { + if (str.substring(percent + 1).toInt32()) |signed| { + if (std.math.cast(u32, signed)) |id| { + break :blk id; + } + } + } else { + const index = std.c.if_nametoindex(address_slice[percent + 1 .. :0]); + if (index > 0) { + if (std.math.cast(u32, index)) |id| { + break :blk id; + } + } + } + // "an invalid Scope gets turned into #0 (default selection)" + // (test-dgram-multicast-set-interface.js) + break :blk 0; + }; + + address_slice[percent] = '\x00'; + addr6.scope_id = iface_id; + } + } + if (inet_pton(std.posix.AF.INET6, address_slice.ptr, &addr6.addr) == 1) { addr6.port = htons(@truncate(port)); addr6.family = std.posix.AF.INET6; diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index e982a9e88a..931740d6e1 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -331,6 +331,42 @@ export default [ closed: { getter: "getClosed", }, + setBroadcast: { + fn: "setBroadcast", + length: 1, + }, + setTTL: { + fn: "setTTL", + length: 1, + }, + setMulticastTTL: { + fn: "setMulticastTTL", + length: 1, + }, + setMulticastLoopback: { + fn: "setMulticastLoopback", + length: 1, + }, + setMulticastInterface: { + fn: "setMulticastInterface", + length: 1, + }, + addMembership: { + fn: "addMembership", + length: 2, + }, + dropMembership: { + fn: "dropMembership", + length: 2, + }, + addSourceSpecificMembership: { + fn: "addSourceSpecificMembership", + length: 3, + }, + dropSourceSpecificMembership: { + fn: "dropSourceSpecificMembership", + length: 3, + }, }, klass: {}, }), diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index c82651d73a..702290f825 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -1044,7 +1044,7 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject case ErrorCode::ERR_SOCKET_DGRAM_NOT_CONNECTED: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_DGRAM_NOT_CONNECTED, "Not connected"_s)); case ErrorCode::ERR_SOCKET_DGRAM_NOT_RUNNING: - return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_DGRAM_NOT_RUNNING, "Not running"_s)); + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_DGRAM_NOT_RUNNING, "Socket is not running"_s)); case ErrorCode::ERR_INVALID_CURSOR_POS: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_CURSOR_POS, "Cannot set cursor row without setting its column"_s)); case ErrorCode::ERR_MULTIPLE_CALLBACK: diff --git a/src/bun.js/bindings/JSBuffer.cpp b/src/bun.js/bindings/JSBuffer.cpp index 927ee60966..b8346a3ec3 100644 --- a/src/bun.js/bindings/JSBuffer.cpp +++ b/src/bun.js/bindings/JSBuffer.cpp @@ -813,26 +813,25 @@ static inline JSC::EncodedJSValue jsBufferConstructorFunction_concatBody(JSC::JS } for (unsigned i = 0; i < arrayLength; i++) { - auto element = array->getIndex(lexicalGlobalObject, i); + JSValue element = array->getIndex(lexicalGlobalObject, i); RETURN_IF_EXCEPTION(throwScope, {}); - auto* typedArray = JSC::jsDynamicCast(element); - if (!typedArray) { - throwTypeError(lexicalGlobalObject, throwScope, "Buffer.concat expects Uint8Array"_s); + if (auto* bufferView = JSC::jsDynamicCast(element)) { + if (UNLIKELY(bufferView->isDetached())) { + throwVMTypeError(lexicalGlobalObject, throwScope, "ArrayBufferView is detached"_s); + return {}; + } + + auto length = bufferView->byteLength(); + + if (length > 0) + args.append(element); + + byteLength += length; + } else { + throwTypeError(lexicalGlobalObject, throwScope, "Buffer.concat expects Buffer or Uint8Array"_s); return {}; } - - if (UNLIKELY(typedArray->isDetached())) { - throwVMTypeError(lexicalGlobalObject, throwScope, "Uint8Array is detached"_s); - return {}; - } - - auto length = typedArray->length(); - - if (length > 0) - args.append(element); - - byteLength += length; } size_t availableLength = byteLength; @@ -869,12 +868,12 @@ static inline JSC::EncodedJSValue jsBufferConstructorFunction_concatBody(JSC::JS auto* head = outBuffer->typedVector(); const int arrayLengthI = arrayLength; for (int i = 0; i < arrayLengthI && remain > 0; i++) { - auto* typedArray = JSC::jsCast(args.at(i)); - size_t length = std::min(remain, typedArray->length()); + auto* bufferView = JSC::jsCast(args.at(i)); + size_t length = std::min(remain, bufferView->byteLength()); ASSERT_WITH_MESSAGE(length > 0, "length should be greater than 0. This should be checked before appending to the MarkedArgumentBuffer."); - auto* source = typedArray->typedVector(); + auto* source = bufferView->vector(); ASSERT(source); memcpy(head, source, length); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 27ca054ab1..a10b72db92 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1780,6 +1780,7 @@ pub const SystemError = extern struct { this.message.ref(); this.syscall.ref(); this.hostname.ref(); + this.dest.ref(); } pub fn toErrorInstance(this: *const SystemError, global: *JSGlobalObject) JSValue { @@ -1808,13 +1809,7 @@ pub const SystemError = extern struct { /// implementing follows this convention. It is exclusively used /// to match the error code that `node:os` throws. pub fn toErrorInstanceWithInfoObject(this: *const SystemError, global: *JSGlobalObject) JSValue { - defer { - this.path.deref(); - this.code.deref(); - this.message.deref(); - this.syscall.deref(); - this.dest.deref(); - } + defer this.deref(); return SystemError__toErrorInstanceWithInfoObject(this, global); } diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 3555261098..30b90a7404 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -4495,8 +4495,8 @@ pub const udp = struct { pub const Socket = opaque { const This = @This(); - pub fn create(loop: *Loop, data_cb: *const fn (*This, *PacketBuffer, c_int) callconv(.C) void, drain_cb: *const fn (*This) callconv(.C) void, close_cb: *const fn (*This) callconv(.C) void, host: [*c]const u8, port: c_ushort, user_data: ?*anyopaque) ?*This { - return us_create_udp_socket(loop, data_cb, drain_cb, close_cb, host, port, user_data); + pub fn create(loop: *Loop, data_cb: *const fn (*This, *PacketBuffer, c_int) callconv(.C) void, drain_cb: *const fn (*This) callconv(.C) void, close_cb: *const fn (*This) callconv(.C) void, host: [*c]const u8, port: c_ushort, options: c_int, err: ?*c_int, user_data: ?*anyopaque) ?*This { + return us_create_udp_socket(loop, data_cb, drain_cb, close_cb, host, port, options, err, user_data); } pub fn send(this: *This, payloads: []const [*]const u8, lengths: []const usize, addresses: []const ?*const anyopaque) c_int { @@ -4535,9 +4535,37 @@ pub const udp = struct { pub fn disconnect(this: *This) c_int { return us_udp_socket_disconnect(this); } + + pub fn setBroadcast(this: *This, enabled: bool) c_int { + return us_udp_socket_set_broadcast(this, @intCast(@intFromBool(enabled))); + } + + pub fn setUnicastTTL(this: *This, ttl: i32) c_int { + return us_udp_socket_set_ttl_unicast(this, @intCast(ttl)); + } + + pub fn setMulticastTTL(this: *This, ttl: i32) c_int { + return us_udp_socket_set_ttl_multicast(this, @intCast(ttl)); + } + + pub fn setMulticastLoopback(this: *This, enabled: bool) c_int { + return us_udp_socket_set_multicast_loopback(this, @intCast(@intFromBool(enabled))); + } + + pub fn setMulticastInterface(this: *This, iface: *const std.posix.sockaddr.storage) c_int { + return us_udp_socket_set_multicast_interface(this, iface); + } + + pub fn setMembership(this: *This, address: *const std.posix.sockaddr.storage, iface: ?*const std.posix.sockaddr.storage, drop: bool) c_int { + return us_udp_socket_set_membership(this, address, iface, @intFromBool(drop)); + } + + pub fn setSourceSpecificMembership(this: *This, source: *const std.posix.sockaddr.storage, group: *const std.posix.sockaddr.storage, iface: ?*const std.posix.sockaddr.storage, drop: bool) c_int { + return us_udp_socket_set_source_specific_membership(this, source, group, iface, @intFromBool(drop)); + } }; - extern fn us_create_udp_socket(loop: ?*Loop, data_cb: *const fn (*udp.Socket, *PacketBuffer, c_int) callconv(.C) void, drain_cb: *const fn (*udp.Socket) callconv(.C) void, close_cb: *const fn (*udp.Socket) callconv(.C) void, host: [*c]const u8, port: c_ushort, user_data: ?*anyopaque) ?*udp.Socket; + extern fn us_create_udp_socket(loop: ?*Loop, data_cb: *const fn (*udp.Socket, *PacketBuffer, c_int) callconv(.C) void, drain_cb: *const fn (*udp.Socket) callconv(.C) void, close_cb: *const fn (*udp.Socket) callconv(.C) void, host: [*c]const u8, port: c_ushort, options: c_int, err: ?*c_int, user_data: ?*anyopaque) ?*udp.Socket; extern fn us_udp_socket_connect(socket: ?*udp.Socket, hostname: [*c]const u8, port: c_uint) c_int; extern fn us_udp_socket_disconnect(socket: ?*udp.Socket) c_int; extern fn us_udp_socket_send(socket: ?*udp.Socket, [*c]const [*c]const u8, [*c]const usize, [*c]const ?*const anyopaque, c_int) c_int; @@ -4547,6 +4575,13 @@ pub const udp = struct { extern fn us_udp_socket_bound_ip(socket: ?*udp.Socket, buf: [*c]u8, length: [*c]i32) void; extern fn us_udp_socket_remote_ip(socket: ?*udp.Socket, buf: [*c]u8, length: [*c]i32) void; extern fn us_udp_socket_close(socket: ?*udp.Socket) void; + extern fn us_udp_socket_set_broadcast(socket: ?*udp.Socket, enabled: c_int) c_int; + extern fn us_udp_socket_set_ttl_unicast(socket: ?*udp.Socket, ttl: c_int) c_int; + extern fn us_udp_socket_set_ttl_multicast(socket: ?*udp.Socket, ttl: c_int) c_int; + extern fn us_udp_socket_set_multicast_loopback(socket: ?*udp.Socket, enabled: c_int) c_int; + extern fn us_udp_socket_set_multicast_interface(socket: ?*udp.Socket, iface: *const std.posix.sockaddr.storage) c_int; + extern fn us_udp_socket_set_membership(socket: ?*udp.Socket, address: *const std.posix.sockaddr.storage, iface: ?*const std.posix.sockaddr.storage, drop: c_int) c_int; + extern fn us_udp_socket_set_source_specific_membership(socket: ?*udp.Socket, source: *const std.posix.sockaddr.storage, group: *const std.posix.sockaddr.storage, iface: ?*const std.posix.sockaddr.storage, drop: c_int) c_int; pub const PacketBuffer = opaque { const This = @This(); diff --git a/src/js/node/dgram.ts b/src/js/node/dgram.ts index 0ed940d47f..e68678c2ea 100644 --- a/src/js/node/dgram.ts +++ b/src/js/node/dgram.ts @@ -30,7 +30,14 @@ const CONNECT_STATE_CONNECTED = 2; const RECV_BUFFER = true; const SEND_BUFFER = false; +const LIBUS_LISTEN_DEFAULT = 0; +const LIBUS_LISTEN_EXCLUSIVE_PORT = 1; +const LIBUS_SOCKET_ALLOW_HALF_OPEN = 2; +const LIBUS_SOCKET_REUSE_PORT = 4; +const LIBUS_SOCKET_IPV6_ONLY = 8; + const kStateSymbol = Symbol("state symbol"); +const kOwnerSymbol = Symbol("owner symbol"); const async_id_symbol = Symbol("async_id_symbol"); const { hideFromStack, throwNotImplemented } = require("internal/shared"); @@ -42,11 +49,16 @@ const { validateAbortSignal, } = require("internal/validators"); +const { isIP } = require("./net"); + const EventEmitter = require("node:events"); +const { deprecate } = require("node:util"); + const SymbolDispose = Symbol.dispose; const SymbolAsyncDispose = Symbol.asyncDispose; const ObjectSetPrototypeOf = Object.setPrototypeOf; +const ObjectDefineProperty = Object.defineProperty; const FunctionPrototypeBind = Function.prototype.bind; class ERR_SOCKET_BUFFER_SIZE extends Error { @@ -73,6 +85,13 @@ function lookup6(lookup, address, callback) { return lookup(address || "::1", 6, callback); } +function EINVAL(syscall) { + throw Object.assign(new Error(`${syscall} EINVAL`), { + code: "EINVAL", + syscall, + }); +} + let dns; function newHandle(type, lookup) { @@ -95,9 +114,26 @@ function newHandle(type, lookup) { throw $ERR_SOCKET_BAD_TYPE(); } + handle.onmessage = onMessage; + return handle; } +function onMessage(nread, handle, buf, rinfo) { + const self = handle[kOwnerSymbol]; + if (nread < 0) { + return self.emit( + "error", + Object.assign(new Error("recvmsg"), { + syscall: "recvmsg", + errno: nread, + }), + ); + } + rinfo.size = buf.length; // compatibility + self.emit("message", buf, rinfo); +} + let udpSocketChannel; function Socket(type, listener) { @@ -116,6 +152,7 @@ function Socket(type, listener) { } const handle = newHandle(type, lookup); + handle[kOwnerSymbol] = this; // this[async_id_symbol] = handle.getAsyncId(); this.type = type; @@ -129,6 +166,7 @@ function Socket(type, listener) { connectState: CONNECT_STATE_DISCONNECTED, queue: undefined, reuseAddr: options && options.reuseAddr, // Use UV_UDP_REUSEADDR if true. + reusePort: options && options.reusePort, ipv6Only: options && options.ipv6Only, recvBufferSize, sendBufferSize, @@ -182,7 +220,10 @@ Socket.prototype.bind = function (port_, address_ /* , callback */) { const state = this[kStateSymbol]; - if (state.bindState !== BIND_STATE_UNBOUND) throw $ERR_SOCKET_ALREADY_BOUND(); + if (state.bindState !== BIND_STATE_UNBOUND) { + this.emit("error", $ERR_SOCKET_ALREADY_BOUND()); + return; + } state.bindState = BIND_STATE_BINDING; @@ -261,45 +302,64 @@ Socket.prototype.bind = function (port_, address_ /* , callback */) { } let flags = 0; - if (state.reuseAddr) flags |= 0; //UV_UDP_REUSEADDR; - if (state.ipv6Only) flags |= 0; //UV_UDP_IPV6ONLY; + + if (state.reuseAddr) { + flags |= 0; //UV_UDP_REUSEADDR; + } + + if (state.ipv6Only) { + flags |= LIBUS_SOCKET_IPV6_ONLY; + } + + if (state.reusePort) { + exclusive = true; // TODO: cluster support + flags |= LIBUS_SOCKET_REUSE_PORT; + } else { + flags |= LIBUS_LISTEN_EXCLUSIVE_PORT; + } // TODO flags const family = this.type === "udp4" ? "IPv4" : "IPv6"; - Bun.udpSocket({ - hostname: ip, - port: port || 0, - socket: { - data: (_socket, data, port, address) => { - this.emit("message", data, { - port: port, - address: address, - size: data.length, - // TODO check if this is correct - family, - }); + try { + Bun.udpSocket({ + hostname: ip, + port: port || 0, + flags, + socket: { + data: (_socket, data, port, address) => { + this.emit("message", data, { + port: port, + address: address, + size: data.length, + // TODO check if this is correct + family, + }); + }, + error: error => { + this.emit("error", error); + }, }, - error: error => { - this.emit("error", error); - }, - }, - }).$then( - socket => { - if (state.unrefOnBind) { - socket.unref(); - state.unrefOnBind = false; - } - state.handle.socket = socket; - state.receiving = true; - state.bindState = BIND_STATE_BOUND; + }).$then( + socket => { + if (state.unrefOnBind) { + socket.unref(); + state.unrefOnBind = false; + } + state.handle.socket = socket; + state.receiving = true; + state.bindState = BIND_STATE_BOUND; - this.emit("listening"); - }, - err => { - state.bindState = BIND_STATE_UNBOUND; - this.emit("error", err); - }, - ); + this.emit("listening"); + }, + err => { + state.bindState = BIND_STATE_UNBOUND; + this.emit("error", err); + }, + ); + } catch (err) { + state.bindState = BIND_STATE_UNBOUND; + this.emit("error", err); + } }); return this; @@ -580,9 +640,12 @@ function doSend(ex, self, ip, list, address, port, callback) { // TODO check if this makes sense if (callback) { if (err) { + err.address = ip; + err.port = port; + err.message = `send ${err.code} ${ip}:${port}`; process.nextTick(callback, err); } else { - const sent = success ? data.length : 0; + const sent = success ? data.byteLength : 0; process.nextTick(callback, null, sent); } } @@ -644,7 +707,7 @@ Socket.prototype.close = function (callback) { state.receiving = false; state.handle.socket?.close(); - state.handle.socket = undefined; + state.handle = null; defaultTriggerAsyncIdScope(this[async_id_symbol], process.nextTick, socketCloseNT, this); return this; @@ -690,127 +753,133 @@ Socket.prototype.remoteAddress = function () { }; Socket.prototype.setBroadcast = function (arg) { - throwNotImplemented("setBroadcast", 10381); - /* - const err = this[kStateSymbol].handle.setBroadcast(arg ? 1 : 0); - if (err) { - throw new ErrnoException(err, 'setBroadcast'); + const handle = this[kStateSymbol].handle; + if (!handle?.socket) { + throw new Error("setBroadcast EBADF"); } - */ + return handle.socket.setBroadcast(arg); }; Socket.prototype.setTTL = function (ttl) { - throwNotImplemented("setTTL", 10381); - /* - validateNumber(ttl, 'ttl'); - - const err = this[kStateSymbol].handle.setTTL(ttl); - if (err) { - throw new ErrnoException(err, 'setTTL'); + if (typeof ttl !== "number") { + throw $ERR_INVALID_ARG_TYPE("ttl", "number", ttl); } - return ttl; - */ + const handle = this[kStateSymbol].handle; + if (!handle?.socket) { + throw new Error("setTTL EBADF"); + } + return handle.socket.setTTL(ttl); }; Socket.prototype.setMulticastTTL = function (ttl) { - throwNotImplemented("setMulticastTTL", 10381); - /* - validateNumber(ttl, 'ttl'); - - const err = this[kStateSymbol].handle.setMulticastTTL(ttl); - if (err) { - throw new ErrnoException(err, 'setMulticastTTL'); + if (typeof ttl !== "number") { + throw $ERR_INVALID_ARG_TYPE("ttl", "number", ttl); } - return ttl; - */ + const handle = this[kStateSymbol].handle; + if (!handle?.socket) { + throw new Error("setMulticastTTL EBADF"); + } + return handle.socket.setMulticastTTL(ttl); }; Socket.prototype.setMulticastLoopback = function (arg) { - throwNotImplemented("setMulticastLoopback", 10381); - /* - const err = this[kStateSymbol].handle.setMulticastLoopback(arg ? 1 : 0); - if (err) { - throw new ErrnoException(err, 'setMulticastLoopback'); + const handle = this[kStateSymbol].handle; + if (!handle?.socket) { + throw new Error("setMulticastLoopback EBADF"); } - - return arg; // 0.4 compatibility - */ + return handle.socket.setMulticastLoopback(arg); }; Socket.prototype.setMulticastInterface = function (interfaceAddress) { - throwNotImplemented("setMulticastInterface", 10381); - /* - validateString(interfaceAddress, 'interfaceAddress'); - - const err = this[kStateSymbol].handle.setMulticastInterface(interfaceAddress); - if (err) { - throw new ErrnoException(err, 'setMulticastInterface'); + validateString(interfaceAddress, "interfaceAddress"); + const handle = this[kStateSymbol].handle; + if (!handle?.socket) { + throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); + } + if (!handle.socket.setMulticastInterface(interfaceAddress)) { + throw EINVAL("setMulticastInterface"); } - */ }; Socket.prototype.addMembership = function (multicastAddress, interfaceAddress) { - throwNotImplemented("addMembership", 10381); - /* if (!multicastAddress) { - throw $ERR_MISSING_ARGS('multicastAddress'); + throw $ERR_MISSING_ARGS("multicastAddress"); } - - const { handle } = this[kStateSymbol]; - const err = handle.addMembership(multicastAddress, interfaceAddress); - if (err) { - throw new ErrnoException(err, 'addMembership'); + validateString(multicastAddress, "multicastAddress"); + if (typeof interfaceAddress !== "undefined") { + validateString(interfaceAddress, "interfaceAddress"); } - */ + const { handle, bindState } = this[kStateSymbol]; + if (!handle?.socket) { + if (!isIP(multicastAddress)) { + throw EINVAL("addMembership"); + } + throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); + } + if (bindState === BIND_STATE_UNBOUND) { + this.bind({ port: 0, exclusive: true }, null); + } + return handle.socket.addMembership(multicastAddress, interfaceAddress); }; Socket.prototype.dropMembership = function (multicastAddress, interfaceAddress) { - throwNotImplemented("dropMembership", 10381); - /* if (!multicastAddress) { - throw $ERR_MISSING_ARGS('multicastAddress'); + throw $ERR_MISSING_ARGS("multicastAddress"); + } + validateString(multicastAddress, "multicastAddress"); + if (typeof interfaceAddress !== "undefined") { + validateString(interfaceAddress, "interfaceAddress"); } - const { handle } = this[kStateSymbol]; - const err = handle.dropMembership(multicastAddress, interfaceAddress); - if (err) { - throw new ErrnoException(err, 'dropMembership'); + if (!handle?.socket) { + if (!isIP(multicastAddress)) { + throw EINVAL("dropMembership"); + } + throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); } - */ + return handle.socket.dropMembership(multicastAddress, interfaceAddress); }; Socket.prototype.addSourceSpecificMembership = function (sourceAddress, groupAddress, interfaceAddress) { - throwNotImplemented("addSourceSpecificMembership", 10381); - /* - validateString(sourceAddress, 'sourceAddress'); - validateString(groupAddress, 'groupAddress'); - - const err = - this[kStateSymbol].handle.addSourceSpecificMembership(sourceAddress, - groupAddress, - interfaceAddress); - if (err) { - throw new ErrnoException(err, 'addSourceSpecificMembership'); + validateString(sourceAddress, "sourceAddress"); + validateString(groupAddress, "groupAddress"); + if (typeof interfaceAddress !== "undefined") { + validateString(interfaceAddress, "interfaceAddress"); } - */ + + const { handle, bindState } = this[kStateSymbol]; + if (!handle?.socket) { + if (!isIP(sourceAddress) || !isIP(groupAddress)) { + throw EINVAL("addSourceSpecificMembership"); + } + throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); + } + if (bindState === BIND_STATE_UNBOUND) { + this.bind(0); + } + return handle.socket.addSourceSpecificMembership(sourceAddress, groupAddress, interfaceAddress); }; Socket.prototype.dropSourceSpecificMembership = function (sourceAddress, groupAddress, interfaceAddress) { - throwNotImplemented("dropSourceSpecificMembership", 10381); - /* - validateString(sourceAddress, 'sourceAddress'); - validateString(groupAddress, 'groupAddress'); - - const err = - this[kStateSymbol].handle.dropSourceSpecificMembership(sourceAddress, - groupAddress, - interfaceAddress); - if (err) { - throw new ErrnoException(err, 'dropSourceSpecificMembership'); + validateString(sourceAddress, "sourceAddress"); + validateString(groupAddress, "groupAddress"); + if (typeof interfaceAddress !== "undefined") { + validateString(interfaceAddress, "interfaceAddress"); } - */ + + const { handle, bindState } = this[kStateSymbol]; + if (!handle?.socket) { + if (!isIP(sourceAddress) || !isIP(groupAddress)) { + throw EINVAL("dropSourceSpecificMembership"); + } + throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); + } + if (bindState === BIND_STATE_UNBOUND) { + this.bind(0); + } + return handle.socket.dropSourceSpecificMembership(sourceAddress, groupAddress, interfaceAddress); }; Socket.prototype.ref = function () { @@ -860,71 +929,123 @@ Socket.prototype.getSendQueueCount = function () { }; // Deprecated private APIs. +ObjectDefineProperty(Socket.prototype, "_handle", { + get: deprecate( + function () { + return this[kStateSymbol].handle; + }, + "Socket.prototype._handle is deprecated", + "DEP0112", + ), + set: deprecate( + function (val) { + this[kStateSymbol].handle = val; + }, + "Socket.prototype._handle is deprecated", + "DEP0112", + ), +}); + +ObjectDefineProperty(Socket.prototype, "_receiving", { + get: deprecate( + function () { + return this[kStateSymbol].receiving; + }, + "Socket.prototype._receiving is deprecated", + "DEP0112", + ), + set: deprecate( + function (val) { + this[kStateSymbol].receiving = val; + }, + "Socket.prototype._receiving is deprecated", + "DEP0112", + ), +}); + +ObjectDefineProperty(Socket.prototype, "_bindState", { + get: deprecate( + function () { + return this[kStateSymbol].bindState; + }, + "Socket.prototype._bindState is deprecated", + "DEP0112", + ), + set: deprecate( + function (val) { + this[kStateSymbol].bindState = val; + }, + "Socket.prototype._bindState is deprecated", + "DEP0112", + ), +}); + +ObjectDefineProperty(Socket.prototype, "_queue", { + get: deprecate( + function () { + return this[kStateSymbol].queue; + }, + "Socket.prototype._queue is deprecated", + "DEP0112", + ), + set: deprecate( + function (val) { + this[kStateSymbol].queue = val; + }, + "Socket.prototype._queue is deprecated", + "DEP0112", + ), +}); + +ObjectDefineProperty(Socket.prototype, "_reuseAddr", { + get: deprecate( + function () { + return this[kStateSymbol].reuseAddr; + }, + "Socket.prototype._reuseAddr is deprecated", + "DEP0112", + ), + set: deprecate( + function (val) { + this[kStateSymbol].reuseAddr = val; + }, + "Socket.prototype._reuseAddr is deprecated", + "DEP0112", + ), +}); + +function healthCheck(socket) { + if (!socket[kStateSymbol].handle) { + throw $ERR_SOCKET_DGRAM_NOT_RUNNING(); + } +} + +Socket.prototype._healthCheck = deprecate( + function () { + healthCheck(this); + }, + "Socket.prototype._healthCheck() is deprecated", + "DEP0112", +); + +function stopReceiving(socket) { + const state = socket[kStateSymbol]; + + if (!state.receiving) return; + + // state.handle.recvStop(); + state.receiving = false; +} + +Socket.prototype._stopReceiving = deprecate( + function () { + stopReceiving(this); + }, + "Socket.prototype._stopReceiving() is deprecated", + "DEP0112", +); + /* -ObjectDefineProperty(Socket.prototype, '_handle', { - __proto__: null, - get: deprecate(function() { - return this[kStateSymbol].handle; - }, 'Socket.prototype._handle is deprecated', 'DEP0112'), - set: deprecate(function(val) { - this[kStateSymbol].handle = val; - }, 'Socket.prototype._handle is deprecated', 'DEP0112'), -}); - - -ObjectDefineProperty(Socket.prototype, '_receiving', { - __proto__: null, - get: deprecate(function() { - return this[kStateSymbol].receiving; - }, 'Socket.prototype._receiving is deprecated', 'DEP0112'), - set: deprecate(function(val) { - this[kStateSymbol].receiving = val; - }, 'Socket.prototype._receiving is deprecated', 'DEP0112'), -}); - - -ObjectDefineProperty(Socket.prototype, '_bindState', { - __proto__: null, - get: deprecate(function() { - return this[kStateSymbol].bindState; - }, 'Socket.prototype._bindState is deprecated', 'DEP0112'), - set: deprecate(function(val) { - this[kStateSymbol].bindState = val; - }, 'Socket.prototype._bindState is deprecated', 'DEP0112'), -}); - - -ObjectDefineProperty(Socket.prototype, '_queue', { - __proto__: null, - get: deprecate(function() { - return this[kStateSymbol].queue; - }, 'Socket.prototype._queue is deprecated', 'DEP0112'), - set: deprecate(function(val) { - this[kStateSymbol].queue = val; - }, 'Socket.prototype._queue is deprecated', 'DEP0112'), -}); - - -ObjectDefineProperty(Socket.prototype, '_reuseAddr', { - __proto__: null, - get: deprecate(function() { - return this[kStateSymbol].reuseAddr; - }, 'Socket.prototype._reuseAddr is deprecated', 'DEP0112'), - set: deprecate(function(val) { - this[kStateSymbol].reuseAddr = val; - }, 'Socket.prototype._reuseAddr is deprecated', 'DEP0112'), -}); - - -Socket.prototype._healthCheck = deprecate(function() { - healthCheck(this); -}, 'Socket.prototype._healthCheck() is deprecated', 'DEP0112'); - - -Socket.prototype._stopReceiving = deprecate(function() { - stopReceiving(this); -}, 'Socket.prototype._stopReceiving() is deprecated', 'DEP0112'); - function _createSocketHandle(address, port, addressType, fd, flags) { const handle = newHandle(addressType); let err; @@ -953,8 +1074,8 @@ function _createSocketHandle(address, port, addressType, fd, flags) { // want to runtime-deprecate it at some point. There's no hurry, though. ObjectDefineProperty(UDP.prototype, 'owner', { __proto__: null, - get() { return this[owner_symbol]; }, - set(v) { return this[owner_symbol] = v; }, + get() { return this[kOwnerSymbol]; }, + set(v) { return this[kOwnerSymbol] = v; }, }); */ diff --git a/src/js/node/util.ts b/src/js/node/util.ts index 26b3e3a927..16eb2479ab 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -31,36 +31,42 @@ const format = utl.format; const stripVTControlCharacters = utl.stripVTControlCharacters; const codesWarned = new Set(); + +function getDeprecationWarningEmitter(code, msg, deprecated, shouldEmitWarning = () => true) { + let warned = false; + return function () { + if (!warned && shouldEmitWarning()) { + warned = true; + if (code !== undefined) { + if (!codesWarned.has(code)) { + process.emitWarning(msg, "DeprecationWarning", code, deprecated); + codesWarned.add(code); + } + } else { + process.emitWarning(msg, "DeprecationWarning", deprecated); + } + } + }; +} + function deprecate(fn, msg, code) { - if (process.noDeprecation === true) { - return fn; - } + // Lazy-load to avoid a circular dependency. if (code !== undefined) validateString(code, "code"); - var warned = false; - function deprecated() { - if (!warned) { - if (process.throwDeprecation) { - var err = new Error(msg); - if (code) err.code = code; - throw err; - } else if (process.traceDeprecation) { - console.trace(msg); - } else { - if (code !== undefined) { - // only warn for each code once - if (codesWarned.has(code)) { - process.emitWarning(msg, "DeprecationWarning", code); - } - codesWarned.add(code); - } else { - process.emitWarning(msg, "DeprecationWarning"); - } - } - warned = true; + const emitDeprecationWarning = getDeprecationWarningEmitter(code, msg, deprecated); + + function deprecated(...args) { + if (!process.noDeprecation) { + emitDeprecationWarning(); } - return fn.$apply(this, arguments); + if (new.target) { + return Reflect.construct(fn, args, new.target); + } + return fn.$apply(this, args); } + + // The wrapper will keep the same prototype as fn to maintain prototype chain + Object.setPrototypeOf(deprecated, fn); return deprecated; } diff --git a/test/js/node/test/common/udp.js b/test/js/node/test/common/udp.js new file mode 100644 index 0000000000..a94d76fc09 --- /dev/null +++ b/test/js/node/test/common/udp.js @@ -0,0 +1,24 @@ +'use strict'; +const dgram = require('dgram'); + +const options = { type: 'udp4', reusePort: true }; + +function checkSupportReusePort() { + return new Promise((resolve, reject) => { + const socket = dgram.createSocket(options); + socket.bind(0); + socket.on('listening', () => { + socket.close(resolve); + }); + socket.on('error', (err) => { + console.log('The `reusePort` option is not supported:', err.message); + socket.close(); + reject(err); + }); + }); +} + +module.exports = { + checkSupportReusePort, + options, +}; diff --git a/test/js/node/test/parallel/test-dgram-address.js b/test/js/node/test/parallel/test-dgram-address.js new file mode 100644 index 0000000000..4571aaeeeb --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-address.js @@ -0,0 +1,81 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +{ + // IPv4 Test + const socket = dgram.createSocket('udp4'); + + socket.on('listening', common.mustCall(() => { + const address = socket.address(); + + assert.strictEqual(address.address, common.localhostIPv4); + assert.strictEqual(typeof address.port, 'number'); + assert.ok(isFinite(address.port)); + assert.ok(address.port > 0); + assert.strictEqual(address.family, 'IPv4'); + socket.close(); + })); + + socket.on('error', (err) => { + socket.close(); + assert.fail(`Unexpected error on udp4 socket. ${err.toString()}`); + }); + + socket.bind(0, common.localhostIPv4); +} + +if (common.hasIPv6) { + // IPv6 Test + const socket = dgram.createSocket('udp6'); + const localhost = '::1'; + + socket.on('listening', common.mustCall(() => { + const address = socket.address(); + + assert.strictEqual(address.address, localhost); + assert.strictEqual(typeof address.port, 'number'); + assert.ok(isFinite(address.port)); + assert.ok(address.port > 0); + assert.strictEqual(address.family, 'IPv6'); + socket.close(); + })); + + socket.on('error', (err) => { + socket.close(); + assert.fail(`Unexpected error on udp6 socket. ${err.toString()}`); + }); + + socket.bind(0, localhost); +} + +{ + // Verify that address() throws if the socket is not bound. + const socket = dgram.createSocket('udp4'); + + assert.throws(() => { + socket.address(); + }, /^Error: getsockname EBADF$|Socket is not running/); +} diff --git a/test/js/node/test/parallel/test-dgram-bind-error-repeat.js b/test/js/node/test/parallel/test-dgram-bind-error-repeat.js new file mode 100644 index 0000000000..a520d30a51 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-bind-error-repeat.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); +const dgram = require('dgram'); + +// Regression test for https://github.com/nodejs/node/issues/30209 +// No warning should be emitted when re-trying `.bind()` on UDP sockets +// repeatedly. + +process.on('warning', common.mustNotCall()); + +const reservePortSocket = dgram.createSocket('udp4'); +reservePortSocket.bind(() => { + const { port } = reservePortSocket.address(); + + const newSocket = dgram.createSocket('udp4'); + + let errors = 0; + newSocket.on('error', common.mustCall(() => { + if (++errors < 20) { + newSocket.bind(port, common.mustNotCall()); + } else { + newSocket.close(); + reservePortSocket.close(); + } + }, 20)); + newSocket.bind(port, common.mustNotCall()); +}); diff --git a/test/js/node/test/parallel/test-dgram-bind-socket-close-before-lookup.js b/test/js/node/test/parallel/test-dgram-bind-socket-close-before-lookup.js new file mode 100644 index 0000000000..96ca71c3de --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-bind-socket-close-before-lookup.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const dgram = require('dgram'); + +// Do not emit error event in callback which is called by lookup when socket is closed +const socket = dgram.createSocket({ + type: 'udp4', + lookup: (...args) => { + // Call lookup callback after 1s + setTimeout(() => { + args.at(-1)(new Error('an error')); + }, 1000); + } +}); + +socket.on('error', common.mustNotCall()); +socket.bind(12345, 'localhost'); +// Close the socket before calling DNS lookup callback +socket.close(); diff --git a/test/js/node/test/parallel/test-dgram-close-during-bind.js b/test/js/node/test/parallel/test-dgram-close-during-bind.js new file mode 100644 index 0000000000..e0336ecf57 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-close-during-bind.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const dgram = require('dgram'); +const socket = dgram.createSocket('udp4'); +const kStateSymbol = Object.getOwnPropertySymbols(socket).filter(sym => sym.description === "state symbol")[0]; +const { handle } = socket[kStateSymbol]; +const lookup = handle.lookup; + + +// Test the scenario where the socket is closed during a bind operation. +handle.bind = common.mustNotCall('bind() should not be called.'); + +handle.lookup = common.mustCall(function(address, callback) { + socket.close(common.mustCall(() => { + lookup.call(this, address, callback); + })); +}); + +socket.bind(common.mustNotCall('Socket should not bind.')); diff --git a/test/js/node/test/parallel/test-dgram-close.js b/test/js/node/test/parallel/test-dgram-close.js new file mode 100644 index 0000000000..65b7290ae8 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-close.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Ensure that if a dgram socket is closed before the DNS lookup completes, it +// won't crash. + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const buf = Buffer.alloc(1024, 42); + +let socket = dgram.createSocket('udp4'); +const kStateSymbol = Object.getOwnPropertySymbols(socket).filter(sym => sym.description === "state symbol")[0]; +const { handle } = socket[kStateSymbol]; + +// Get a random port for send +const portGetter = dgram.createSocket('udp4') + .bind(0, 'localhost', common.mustCall(() => { + socket.send(buf, 0, buf.length, + portGetter.address().port, + portGetter.address().address); + + assert.strictEqual(socket.close(common.mustCall()), socket); + socket.on('close', common.mustCall()); + socket = null; + + // Verify that accessing handle after closure doesn't throw + setImmediate(function() { + setImmediate(function() { + console.log('Handle fd is: ', handle.fd); + }); + }); + + portGetter.close(); + })); diff --git a/test/js/node/test/parallel/test-dgram-cluster-close-during-bind.js b/test/js/node/test/parallel/test-dgram-cluster-close-during-bind.js index 065ff094f1..169a9f985b 100644 --- a/test/js/node/test/parallel/test-dgram-cluster-close-during-bind.js +++ b/test/js/node/test/parallel/test-dgram-cluster-close-during-bind.js @@ -35,4 +35,9 @@ if (cluster.isPrimary) { }); socket.bind(common.mustNotCall('Socket should not bind.')); + + setTimeout(() => { + console.error("Timed out"); + process.exit(1); + }, 5000).unref(); } diff --git a/test/js/node/test/parallel/test-dgram-connect.js b/test/js/node/test/parallel/test-dgram-connect.js new file mode 100644 index 0000000000..25a7afda62 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-connect.js @@ -0,0 +1,66 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const PORT = 12345; + +const client = dgram.createSocket('udp4'); +client.connect(PORT, common.mustCall(() => { + const remoteAddr = client.remoteAddress(); + assert.strictEqual(remoteAddr.port, PORT); + assert.throws(() => { + client.connect(PORT, common.mustNotCall()); + }, { + name: 'Error', + message: /Socket is connected|Already connected/, + code: 'ERR_SOCKET_DGRAM_IS_CONNECTED' + }); + + client.disconnect(); + assert.throws(() => { + client.disconnect(); + }, { + name: 'Error', + message: /(Socket is n|N)ot connected/, + code: 'ERR_SOCKET_DGRAM_NOT_CONNECTED' + }); + + assert.throws(() => { + client.remoteAddress(); + }, { + name: 'Error', + message: /(Socket is n|N)ot connected/, + code: 'ERR_SOCKET_DGRAM_NOT_CONNECTED' + }); + + client.once('connect', common.mustCall(() => client.close())); + client.connect(PORT); +})); + +assert.throws(() => { + client.connect(PORT); +}, { + name: 'Error', + message: /Socket is connected|Already connected/, + code: 'ERR_SOCKET_DGRAM_IS_CONNECTED' +}); + +assert.throws(() => { + client.disconnect(); +}, { + name: 'Error', + message: /(Socket is n|N)ot connected/, + code: 'ERR_SOCKET_DGRAM_NOT_CONNECTED' +}); + +[ 0, null, 78960, undefined ].forEach((port) => { + assert.throws(() => { + client.connect(port); + }, { + name: 'RangeError', + message: /(Port|"Port") should be > 0 and < 65536/, + code: 'ERR_SOCKET_BAD_PORT' + }); +}); diff --git a/test/js/node/test/parallel/test-dgram-custom-lookup.js b/test/js/node/test/parallel/test-dgram-custom-lookup.js new file mode 100644 index 0000000000..f17dc7e2ad --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-custom-lookup.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const dns = require('dns'); + +{ + // Verify that the provided lookup function is called. + const lookup = common.mustCall((host, family, callback) => { + dns.lookup(host, family, callback); + }); + + const socket = dgram.createSocket({ type: 'udp4', lookup }); + + socket.bind(common.mustCall(() => { + socket.close(); + })); +} + +{ + // Verify that lookup defaults to dns.lookup(). + const originalLookup = dns.lookup; + + dns.lookup = common.mustCall((host, family, callback) => { + dns.lookup = originalLookup; + originalLookup(host, family, callback); + }); + + const socket = dgram.createSocket({ type: 'udp4' }); + + socket.bind(common.mustCall(() => { + socket.close(); + })); +} + +{ + // Verify that non-functions throw. + [null, true, false, 0, 1, NaN, '', 'foo', {}, Symbol()].forEach((value) => { + assert.throws(() => { + dgram.createSocket({ type: 'udp4', lookup: value }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "lookup" argument must be of type function/ + }); + }); +} diff --git a/test/js/node/test/parallel/test-dgram-deprecation-error.js b/test/js/node/test/parallel/test-dgram-deprecation-error.js new file mode 100644 index 0000000000..c544a917b0 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-deprecation-error.js @@ -0,0 +1,84 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const fork = require('child_process').fork; + +const sock = dgram.createSocket('udp4'); + +const testNumber = parseInt(process.argv[2], 10); + +const propertiesToTest = [ + '_handle', + '_receiving', + '_bindState', + '_queue', + '_reuseAddr', +]; + +const methodsToTest = [ + '_healthCheck', + '_stopReceiving', +]; + +const propertyCases = propertiesToTest.map((propName) => { + return [ + () => { + // Test property getter + common.expectWarning( + 'DeprecationWarning', + `Socket.prototype.${propName} is deprecated`, + 'DEP0112' + ); + sock[propName]; // eslint-disable-line no-unused-expressions + }, + () => { + // Test property setter + common.expectWarning( + 'DeprecationWarning', + `Socket.prototype.${propName} is deprecated`, + 'DEP0112' + ); + sock[propName] = null; + }, + ]; +}); + +const methodCases = methodsToTest.map((propName) => { + return () => { + common.expectWarning( + 'DeprecationWarning', + `Socket.prototype.${propName}() is deprecated`, + 'DEP0112' + ); + sock[propName](); + }; +}); + +const cases = [].concat( + ...propertyCases, + ...methodCases +); + +// If we weren't passed a test ID then we need to spawn all of the cases. +// We run the cases in child processes since deprecations print once. +if (Number.isNaN(testNumber)) { + const children = cases.map((_case, i) => + fork(process.argv[1], [ String(i) ])); + + children.forEach((child) => { + child.on('close', (code) => { + // Pass on child exit code + if (code > 0) { + process.exit(code); + } + }); + }); + + return; +} + +// We were passed a test ID - run the test case +assert.ok(cases[testNumber]); +cases[testNumber](); diff --git a/test/js/node/test/parallel/test-dgram-error-message-address.js b/test/js/node/test/parallel/test-dgram-error-message-address.js new file mode 100644 index 0000000000..cf243ed2e8 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-error-message-address.js @@ -0,0 +1,57 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +// IPv4 Test +const socket_ipv4 = dgram.createSocket('udp4'); + +socket_ipv4.on('listening', common.mustNotCall()); + +socket_ipv4.on('error', common.mustCall(function(e) { + assert.strictEqual(e.port, undefined); + assert.strictEqual(e.message, 'bind EADDRNOTAVAIL 1.1.1.1'); + assert.strictEqual(e.address, '1.1.1.1'); + assert.strictEqual(e.code, 'EADDRNOTAVAIL'); + socket_ipv4.close(); +})); + +socket_ipv4.bind(0, '1.1.1.1'); + +// IPv6 Test +const socket_ipv6 = dgram.createSocket('udp6'); + +socket_ipv6.on('listening', common.mustNotCall()); + +socket_ipv6.on('error', common.mustCall(function(e) { + // EAFNOSUPPORT or EPROTONOSUPPORT means IPv6 is disabled on this system. + const allowed = ['EADDRNOTAVAIL', 'EAFNOSUPPORT', 'EPROTONOSUPPORT']; + assert(allowed.includes(e.code), `'${e.code}' was not one of ${allowed}.`); + assert.strictEqual(e.port, undefined); + assert.strictEqual(e.message, `bind ${e.code} 111::1`); + assert.strictEqual(e.address, '111::1'); + socket_ipv6.close(); +})); + +socket_ipv6.bind(0, '111::1'); diff --git a/test/js/node/test/parallel/test-dgram-ipv6only.js b/test/js/node/test/parallel/test-dgram-ipv6only.js new file mode 100644 index 0000000000..1187f3084a --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-ipv6only.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); + +const dgram = require('dgram'); + +// This test ensures that dual-stack support is disabled when +// we specify the `ipv6Only` option in `dgram.createSocket()`. +const socket = dgram.createSocket({ + type: 'udp6', + ipv6Only: true, +}); + +socket.bind({ + port: 0, + address: '::', +}, common.mustCall(() => { + const { port } = socket.address(); + const client = dgram.createSocket('udp4'); + + // We can still bind to '0.0.0.0'. + client.bind({ + port, + address: '0.0.0.0', + }, common.mustCall(() => { + client.close(); + socket.close(); + })); + + client.on('error', common.mustNotCall()); +})); diff --git a/test/js/node/test/parallel/test-dgram-membership.js b/test/js/node/test/parallel/test-dgram-membership.js new file mode 100644 index 0000000000..f45fc12ec6 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-membership.js @@ -0,0 +1,154 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const multicastAddress = '224.0.0.114'; + +const setup = dgram.createSocket.bind(dgram, { type: 'udp4', reuseAddr: true }); + +// addMembership() on closed socket should throw +{ + const socket = setup(); + socket.close(common.mustCall(() => { + assert.throws(() => { + socket.addMembership(multicastAddress); + }, { + code: 'ERR_SOCKET_DGRAM_NOT_RUNNING', + name: 'Error', + message: /not running/i + }); + })); +} + +// dropMembership() on closed socket should throw +{ + const socket = setup(); + socket.close(common.mustCall(() => { + assert.throws(() => { + socket.dropMembership(multicastAddress); + }, { + code: 'ERR_SOCKET_DGRAM_NOT_RUNNING', + name: 'Error', + message: /not running/i + }); + })); +} + +// addMembership() with no argument should throw +{ + const socket = setup(); + assert.throws(() => { + socket.addMembership(); + }, { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + message: /^The "multicastAddress" argument must be specified$/ + }); + socket.close(); +} + +// dropMembership() with no argument should throw +{ + const socket = setup(); + assert.throws(() => { + socket.dropMembership(); + }, { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + message: /^The "multicastAddress" argument must be specified$/ + }); + socket.close(); +} + +// addMembership() with invalid multicast address should throw +{ + const socket = setup(); + assert.throws(() => { socket.addMembership('256.256.256.256'); }, + /^Error: addMembership EINVAL$/); + socket.close(); +} + +// dropMembership() with invalid multicast address should throw +{ + const socket = setup(); + assert.throws(() => { socket.dropMembership('256.256.256.256'); }, + /^Error: dropMembership EINVAL$/); + socket.close(); +} + +// addSourceSpecificMembership with invalid sourceAddress should throw +{ + const socket = setup(); + assert.throws(() => { + socket.addSourceSpecificMembership(0, multicastAddress); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "sourceAddress" argument must be of type string. ' + + 'Received type number (0)' + }); + socket.close(); +} + +// addSourceSpecificMembership with invalid sourceAddress should throw +{ + const socket = setup(); + assert.throws(() => { + socket.addSourceSpecificMembership(multicastAddress, 0); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "groupAddress" argument must be of type string. ' + + 'Received type number (0)' + }); + socket.close(); +} + +// addSourceSpecificMembership with invalid groupAddress should throw +{ + const socket = setup(); + assert.throws(() => { + socket.addSourceSpecificMembership(multicastAddress, '0'); + }, { + code: 'EINVAL', + message: 'addSourceSpecificMembership EINVAL' + }); + socket.close(); +} + +// dropSourceSpecificMembership with invalid sourceAddress should throw +{ + const socket = setup(); + assert.throws(() => { + socket.dropSourceSpecificMembership(0, multicastAddress); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "sourceAddress" argument must be of type string. ' + + 'Received type number (0)' + }); + socket.close(); +} + +// dropSourceSpecificMembership with invalid groupAddress should throw +{ + const socket = setup(); + assert.throws(() => { + socket.dropSourceSpecificMembership(multicastAddress, 0); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "groupAddress" argument must be of type string. ' + + 'Received type number (0)' + }); + socket.close(); +} + +// dropSourceSpecificMembership with invalid UDP should throw +{ + const socket = setup(); + assert.throws(() => { + socket.dropSourceSpecificMembership(multicastAddress, '0'); + }, { + code: 'EINVAL', + message: 'dropSourceSpecificMembership EINVAL' + }); + socket.close(); +} diff --git a/test/js/node/test/parallel/test-dgram-msgsize.js b/test/js/node/test/parallel/test-dgram-msgsize.js new file mode 100644 index 0000000000..166c375469 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-msgsize.js @@ -0,0 +1,39 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +// Send a too big datagram. The destination doesn't matter because it's +// not supposed to get sent out anyway. +const buf = Buffer.allocUnsafe(256 * 1024); +const sock = dgram.createSocket('udp4'); +sock.send(buf, 0, buf.length, 12345, '127.0.0.1', common.mustCall(cb)); +function cb(err) { + assert(err instanceof Error); + assert.strictEqual(err.code, 'EMSGSIZE'); + assert.strictEqual(err.address, '127.0.0.1'); + assert.strictEqual(err.port, 12345); + assert.strictEqual(err.message, 'send EMSGSIZE 127.0.0.1:12345'); + sock.close(); +} diff --git a/test/js/node/test/parallel/test-dgram-multicast-loopback.js b/test/js/node/test/parallel/test-dgram-multicast-loopback.js new file mode 100644 index 0000000000..c1eedcd1c9 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-multicast-loopback.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +{ + const socket = dgram.createSocket('udp4'); + + assert.throws(() => { + socket.setMulticastLoopback(16); + }, /^Error: setMulticastLoopback EBADF$/); +} + +{ + const socket = dgram.createSocket('udp4'); + + socket.bind(0); + socket.on('listening', common.mustCall(() => { + assert.strictEqual(socket.setMulticastLoopback(16), 16); + assert.strictEqual(socket.setMulticastLoopback(0), 0); + socket.close(); + })); +} diff --git a/test/js/node/test/parallel/test-dgram-multicast-set-interface.js b/test/js/node/test/parallel/test-dgram-multicast-set-interface.js new file mode 100644 index 0000000000..78c4b2b1ae --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-multicast-set-interface.js @@ -0,0 +1,123 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +{ + const socket = dgram.createSocket('udp4'); + + socket.bind(0); + socket.on('listening', common.mustCall(() => { + // Explicitly request default system selection + socket.setMulticastInterface('0.0.0.0'); + + socket.close(); + })); +} + +{ + const socket = dgram.createSocket('udp4'); + + socket.bind(0); + socket.on('listening', common.mustCall(() => { + socket.close(common.mustCall(() => { + assert.throws(() => { socket.setMulticastInterface('0.0.0.0'); }, + /Not running/i); + })); + })); +} + +{ + const socket = dgram.createSocket('udp4'); + + socket.bind(0); + socket.on('listening', common.mustCall(() => { + // Try to set with an invalid interfaceAddress (wrong address class) + // + // This operation succeeds on some platforms, throws `EINVAL` on some + // platforms, and throws `ENOPROTOOPT` on others. This is unpleasant, but + // we should at least test for it. + try { + socket.setMulticastInterface('::'); + } catch (e) { + assert(e.code === 'EINVAL' || e.code === 'ENOPROTOOPT'); + } + + socket.close(); + })); +} + +{ + const socket = dgram.createSocket('udp4'); + + socket.bind(0); + socket.on('listening', common.mustCall(() => { + // Try to set with an invalid interfaceAddress (wrong Type) + assert.throws(() => { + socket.setMulticastInterface(1); + }, /TypeError/); + + socket.close(); + })); +} + +{ + const socket = dgram.createSocket('udp4'); + + socket.bind(0); + socket.on('listening', common.mustCall(() => { + // Try to set with an invalid interfaceAddress (non-unicast) + assert.throws(() => { + socket.setMulticastInterface('224.0.0.2'); + }, /Error/); + + socket.close(); + })); +} + +// If IPv6 is not supported, skip the rest of the test. However, don't call +// common.skip(), which calls process.exit() while there is outstanding +// common.mustCall() activity. +if (!common.hasIPv6) + return; + +{ + const socket = dgram.createSocket('udp6'); + + socket.bind(0); + socket.on('listening', common.mustCall(() => { + // Try to set with an invalid interfaceAddress ('undefined') + assert.throws(() => { + socket.setMulticastInterface(String(undefined)); + }, /EINVAL/); + + socket.close(); + })); +} + +{ + const socket = dgram.createSocket('udp6'); + + socket.bind(0); + socket.on('listening', common.mustCall(() => { + // Try to set with an invalid interfaceAddress ('') + assert.throws(() => { + socket.setMulticastInterface(''); + }, /EINVAL/); + + socket.close(); + })); +} + +{ + const socket = dgram.createSocket('udp6'); + + socket.bind(0); + socket.on('listening', common.mustCall(() => { + // Using lo0 for OsX, on all other OSes, an invalid Scope gets + // turned into #0 (default selection) which is also acceptable. + socket.setMulticastInterface('::%lo0'); + + socket.close(); + })); +} diff --git a/test/js/node/test/parallel/test-dgram-multicast-setTTL.js b/test/js/node/test/parallel/test-dgram-multicast-setTTL.js new file mode 100644 index 0000000000..9b3d40327c --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-multicast-setTTL.js @@ -0,0 +1,48 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const socket = dgram.createSocket('udp4'); + +socket.bind(0); +socket.on('listening', common.mustCall(() => { + const result = socket.setMulticastTTL(16); + assert.strictEqual(result, 16); + + // Try to set an invalid TTL (valid ttl is > 0 and < 256) + assert.throws(() => { + socket.setMulticastTTL(1000); + }, /^Error: .*EINVAL.*/); + + assert.throws(() => { + socket.setMulticastTTL('foo'); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "ttl" argument must be of type number. Received type string \(["']foo["']\)/ + }); + + // Close the socket + socket.close(); +})); diff --git a/test/js/node/test/parallel/test-dgram-recv-error.js b/test/js/node/test/parallel/test-dgram-recv-error.js new file mode 100644 index 0000000000..83b12cd684 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-recv-error.js @@ -0,0 +1,18 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const s = dgram.createSocket('udp4'); +const { handle } = s[Object.getOwnPropertySymbols(s).filter(sym => sym.description === "state symbol")[0]]; + +s.on('error', common.mustCall((err) => { + s.close(); + + // Don't check the full error message, as the errno is not important here. + assert.match(String(err), /^Error: recvmsg/); + assert.strictEqual(err.syscall, 'recvmsg'); +})); + +s.on('message', common.mustNotCall('no message should be received.')); +s.bind(common.mustCall(() => handle.onmessage(-1, handle, null, null))); diff --git a/test/js/node/test/parallel/test-dgram-reuseport.js b/test/js/node/test/parallel/test-dgram-reuseport.js new file mode 100644 index 0000000000..e5fd696581 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-reuseport.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const { checkSupportReusePort, options } = require('../common/udp'); +const dgram = require('dgram'); + +function test() { + const socket1 = dgram.createSocket(options); + const socket2 = dgram.createSocket(options); + socket1.bind(0, common.mustCall(() => { + socket2.bind(socket1.address().port, common.mustCall(() => { + socket1.close(); + socket2.close(); + })); + })); +} + +checkSupportReusePort().then(test, () => { + common.skip('The `reusePort` option is not supported'); +}); diff --git a/test/js/node/test/parallel/test-dgram-send-bad-arguments.js b/test/js/node/test/parallel/test-dgram-send-bad-arguments.js new file mode 100644 index 0000000000..83151538a4 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-bad-arguments.js @@ -0,0 +1,151 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const buf = Buffer.from('test'); +const host = '127.0.0.1'; +const sock = dgram.createSocket('udp4'); + +function checkArgs(connected) { + // First argument should be a buffer. + assert.throws( + () => { sock.send(); }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "buffer" argument must be of type (string or an instance of Buffer, TypedArray, or DataView|Buffer, TypedArray, DataView or string)\. Received undefined/ + } + ); + + // send(buf, offset, length, port, host) + if (connected) { + assert.throws( + () => { sock.send(buf, 1, 1, -1, host); }, + { + code: 'ERR_SOCKET_DGRAM_IS_CONNECTED', + name: 'Error', + message: 'Already connected' + } + ); + + assert.throws( + () => { sock.send(buf, 1, 1, 0, host); }, + { + code: 'ERR_SOCKET_DGRAM_IS_CONNECTED', + name: 'Error', + message: 'Already connected' + } + ); + + assert.throws( + () => { sock.send(buf, 1, 1, 65536, host); }, + { + code: 'ERR_SOCKET_DGRAM_IS_CONNECTED', + name: 'Error', + message: 'Already connected' + } + ); + + assert.throws( + () => { sock.send(buf, 1234, '127.0.0.1', common.mustNotCall()); }, + { + code: 'ERR_SOCKET_DGRAM_IS_CONNECTED', + name: 'Error', + message: 'Already connected' + } + ); + + const longArray = [1, 2, 3, 4, 5, 6, 7, 8]; + for (const input of ['hello', + Buffer.from('hello'), + Buffer.from('hello world').subarray(0, 5), + Buffer.from('hello world').subarray(4, 9), + Buffer.from('hello world').subarray(6), + new Uint8Array([1, 2, 3, 4, 5]), + new Uint8Array(longArray).subarray(0, 5), + new Uint8Array(longArray).subarray(2, 7), + new Uint8Array(longArray).subarray(3), + new DataView(new ArrayBuffer(5), 0), + new DataView(new ArrayBuffer(6), 1), + new DataView(new ArrayBuffer(7), 1, 5)]) { + assert.throws( + () => { sock.send(input, 6, 0); }, + { + code: 'ERR_BUFFER_OUT_OF_BOUNDS', + name: 'RangeError', + message: /"offset" is outside of buffer bounds|Attempt to access memory outside buffer bounds/, + } + ); + + assert.throws( + () => { sock.send(input, 0, 6); }, + { + code: 'ERR_BUFFER_OUT_OF_BOUNDS', + name: 'RangeError', + message: /"length" is outside of buffer bounds|Attempt to access memory outside buffer bounds/, + } + ); + + assert.throws( + () => { sock.send(input, 3, 4); }, + { + code: 'ERR_BUFFER_OUT_OF_BOUNDS', + name: 'RangeError', + message: /"length" is outside of buffer bounds|Attempt to access memory outside buffer bounds/, + } + ); + } + } else { + assert.throws(() => { sock.send(buf, 1, 1, -1, host); }, RangeError); + assert.throws(() => { sock.send(buf, 1, 1, 0, host); }, RangeError); + assert.throws(() => { sock.send(buf, 1, 1, 65536, host); }, RangeError); + } + + // send(buf, port, host) + assert.throws( + () => { sock.send(23, 12345, host); }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "buffer" argument must be of type (string or an instance of Buffer, TypedArray, or DataView|Buffer, TypedArray, DataView or string)\. Received type number \(23\)/, + } + ); + + // send([buf1, ..], port, host) + assert.throws( + () => { sock.send([buf, 23], 12345, host); }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "?buffer list arguments(" argument)? must be of type (string or an instance of Buffer, TypedArray, or DataView|Buffer, TypedArray, DataView or string)\. Received an instance of Array/ + } + ); +} + +checkArgs(); +sock.connect(12345, common.mustCall(() => { + checkArgs(true); + sock.close(); +})); diff --git a/test/js/node/test/parallel/test-dgram-send-default-host.js b/test/js/node/test/parallel/test-dgram-send-default-host.js new file mode 100644 index 0000000000..bf8911c64f --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-default-host.js @@ -0,0 +1,72 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp4'); + +const toSend = [Buffer.alloc(256, 'x'), + Buffer.alloc(256, 'y'), + Buffer.alloc(256, 'z'), + 'hello']; + +const received = []; +let totalBytesSent = 0; +let totalBytesReceived = 0; +const arrayBufferViewsCount = common.getArrayBufferViews( + Buffer.from('') +).length; + +client.on('listening', common.mustCall(() => { + const port = client.address().port; + + client.send(toSend[0], 0, toSend[0].length, port); + client.send(toSend[1], port); + client.send([toSend[2]], port); + client.send(toSend[3], 0, toSend[3].length, port); + + totalBytesSent += toSend.map((buf) => buf.length) + .reduce((a, b) => a + b, 0); + + for (const msgBuf of common.getArrayBufferViews(toSend[0])) { + client.send(msgBuf, 0, msgBuf.byteLength, port); + totalBytesSent += msgBuf.byteLength; + } + for (const msgBuf of common.getArrayBufferViews(toSend[1])) { + client.send(msgBuf, port); + totalBytesSent += msgBuf.byteLength; + } + for (const msgBuf of common.getArrayBufferViews(toSend[2])) { + client.send([msgBuf], port); + totalBytesSent += msgBuf.byteLength; + } +})); + +client.on('message', common.mustCall((buf, info) => { + received.push(buf.toString()); + totalBytesReceived += info.size; + + if (totalBytesReceived === totalBytesSent) { + client.close(); + } + // For every buffer in `toSend`, we send the raw Buffer, + // as well as every TypedArray in getArrayBufferViews() +}, toSend.length + (toSend.length - 1) * arrayBufferViewsCount)); + +client.on('close', common.mustCall((buf, info) => { + // The replies may arrive out of order -> sort them before checking. + received.sort(); + + const repeated = [...toSend]; + for (let i = 0; i < arrayBufferViewsCount; i++) { + repeated.push(...toSend.slice(0, 3)); + } + + assert.strictEqual(totalBytesSent, totalBytesReceived); + + const expected = repeated.map(String).sort(); + assert.deepStrictEqual(received, expected); +})); + +client.bind(0); diff --git a/test/js/node/test/parallel/test-dgram-send-error.js b/test/js/node/test/parallel/test-dgram-send-error.js new file mode 100644 index 0000000000..a406411013 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-error.js @@ -0,0 +1,73 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const { getSystemErrorName } = require('util'); +const mockError = new Error('mock DNS error'); + +let kStateSymbol; + +function getSocket(callback) { + const socket = dgram.createSocket('udp4'); + + if (!kStateSymbol) { + kStateSymbol = Object.getOwnPropertySymbols(socket).filter(sym => sym.description === "state symbol")[0]; + } + + socket.on('message', common.mustNotCall('Should not receive any messages.')); + socket.bind(common.mustCall(() => { + socket[kStateSymbol].handle.lookup = function(address, callback) { + process.nextTick(callback, mockError); + }; + + callback(socket); + })); + return socket; +} + +getSocket((socket) => { + socket.on('error', common.mustCall((err) => { + socket.close(); + assert.strictEqual(err, mockError); + })); + + socket.send('foo', socket.address().port, 'localhost'); +}); + +getSocket((socket) => { + const callback = common.mustCall((err) => { + socket.close(); + assert.strictEqual(err, mockError); + }); + + socket.send('foo', socket.address().port, 'localhost', callback); +}); + +{ + const socket = dgram.createSocket('udp4'); + + socket.on('message', common.mustNotCall('Should not receive any messages.')); + + socket.bind(common.mustCall(() => { + const port = socket.address().port; + const callback = common.mustCall((err, ...args) => { + socket.close(); + assert.strictEqual(err.code, 'UNKNOWN'); + assert.strictEqual(getSystemErrorName(err.errno), 'UNKNOWN'); + assert.strictEqual(err.syscall, 'send'); + assert.strictEqual(err.address, common.localhostIPv4); + assert.strictEqual(err.port, port); + assert.strictEqual( + err.message, + `${err.syscall} ${err.code} ${err.address}:${err.port}` + ); + }); + + socket[kStateSymbol].handle.socket.send = function() { + throw Object.assign(new Error("???"), {code: "UNKNOWN", errno: -4094, syscall: "send"}); + }; + + socket.send('foo', port, common.localhostIPv4, callback); + })); +} diff --git a/test/js/node/test/parallel/test-dgram-send-invalid-msg-type.js b/test/js/node/test/parallel/test-dgram-send-invalid-msg-type.js new file mode 100644 index 0000000000..7a85455ce9 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-send-invalid-msg-type.js @@ -0,0 +1,36 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); + +// This test ensures that a TypeError is raised when the argument to `send()` +// or `sendto()` is anything but a Buffer. +// https://github.com/nodejs/node-v0.x-archive/issues/4496 + +const assert = require('assert'); +const dgram = require('dgram'); + +// Should throw but not crash. +const socket = dgram.createSocket('udp4'); +assert.throws(function() { socket.send(true, 0, 1, 1, 'host'); }, TypeError); +assert.throws(function() { socket.sendto(5, 0, 1, 1, 'host'); }, TypeError); +socket.close(); diff --git a/test/js/node/test/parallel/test-dgram-sendto.js b/test/js/node/test/parallel/test-dgram-sendto.js new file mode 100644 index 0000000000..ab45a0fc34 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-sendto.js @@ -0,0 +1,33 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const socket = dgram.createSocket('udp4'); + +const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "offset" argument must be of type number. Received ' + + 'undefined' +}; +assert.throws(() => socket.sendto(), errObj); + +errObj.message = /The "length" argument must be of type number\. Received type string \(["']offset["']\)$/; +assert.throws( + () => socket.sendto('buffer', 1, 'offset', 'port', 'address', 'cb'), + errObj); + +errObj.message = /The "offset" argument must be of type number. Received type string \(["']offset["']\)$/; +assert.throws( + () => socket.sendto('buffer', 'offset', 1, 'port', 'address', 'cb'), + errObj); + +errObj.message = /The "address" argument must be of type string. Received (type boolean \(false\)|false)$/; +assert.throws( + () => socket.sendto('buffer', 1, 1, 10, false, 'cb'), + errObj); + +errObj.message = /The "port" argument must be of type number. Received (type boolean \(false\)|false)$/; +assert.throws( + () => socket.sendto('buffer', 1, 1, false, 'address', 'cb'), + errObj); diff --git a/test/js/node/test/parallel/test-dgram-setBroadcast.js b/test/js/node/test/parallel/test-dgram-setBroadcast.js new file mode 100644 index 0000000000..01c1e85786 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-setBroadcast.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +{ + // Should throw EBADF if the socket is never bound. + const socket = dgram.createSocket('udp4'); + + assert.throws(() => { + socket.setBroadcast(true); + }, /^Error: setBroadcast EBADF$/); +} + +{ + // Can call setBroadcast() after binding the socket. + const socket = dgram.createSocket('udp4'); + + socket.bind(0, common.mustCall(() => { + socket.setBroadcast(true); + socket.setBroadcast(false); + socket.close(); + + assert.throws(() => { + socket.setBroadcast(true); + }, /^Error: setBroadcast EBADF$/); + })); +} diff --git a/test/js/node/test/parallel/test-dgram-setTTL.js b/test/js/node/test/parallel/test-dgram-setTTL.js new file mode 100644 index 0000000000..6fc895083c --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-setTTL.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const socket = dgram.createSocket('udp4'); + +socket.bind(0); +socket.on('listening', common.mustCall(() => { + const result = socket.setTTL(16); + assert.strictEqual(result, 16); + + assert.throws(() => { + socket.setTTL('foo'); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "ttl" argument must be of type number. Received type string \(["']foo["']\)$/, + }); + + // TTL must be a number from > 0 to < 256 + assert.throws(() => { + socket.setTTL(1000); + }, /^Error: .*EINVAL.*/); + + socket.close(); +})); diff --git a/test/js/node/test/parallel/test-dgram-udp6-link-local-address.js b/test/js/node/test/parallel/test-dgram-udp6-link-local-address.js new file mode 100644 index 0000000000..882c35a33d --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-udp6-link-local-address.js @@ -0,0 +1,72 @@ +'use strict'; +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); + +const assert = require('assert'); +const dgram = require('dgram'); +const os = require('os'); + +const { isWindows } = common; + +function linklocal() { + const candidates = []; + + for (const [ifname, entries] of Object.entries(os.networkInterfaces())) { + for (const { address, family, scopeid } of entries) { + if (family === 'IPv6' && address.startsWith('fe80:') && !ifname.match(/tailscale/i)) { + candidates.push({ address, ifname, scopeid }); + } + } + } + + // Prefer en0 + for (const candidate of candidates) { + if (candidate.ifname === "en0") { + return candidate; + } + } + + // Prefer non-loopback interfaces + for (const candidate of candidates) { + if (!candidate.ifname.startsWith("lo")) { + return candidate; + } + } + + return candidates[0]; +} + +const iface = linklocal(); + +if (!iface) + common.skip('cannot find any IPv6 interfaces with a link local address'); + +const address = isWindows ? iface.address : `${iface.address}%${iface.ifname}`; +const message = 'Hello, local world!'; + +// Create a client socket for sending to the link-local address. +const client = dgram.createSocket('udp6'); + +// Create the server socket listening on the link-local address. +const server = dgram.createSocket('udp6'); + +server.on('listening', common.mustCall(() => { + const port = server.address().port; + client.send(message, 0, message.length, port, address); +})); + +server.on('message', common.mustCall((buf, info) => { + const received = buf.toString(); + assert.strictEqual(received, message); + // Check that the sender address is the one bound, + // including the link local scope identifier. + assert.strictEqual( + info.address, + isWindows ? `${iface.address}%${iface.scopeid}` : address + ); + server.close(); + client.close(); +}, 1)); + +server.bind({ address }); diff --git a/test/js/node/test/parallel/test-dgram-udp6-send-default-host.js b/test/js/node/test/parallel/test-dgram-udp6-send-default-host.js new file mode 100644 index 0000000000..b0780824b3 --- /dev/null +++ b/test/js/node/test/parallel/test-dgram-udp6-send-default-host.js @@ -0,0 +1,76 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); + +const assert = require('assert'); +const dgram = require('dgram'); + +const client = dgram.createSocket('udp6'); + +const toSend = [Buffer.alloc(256, 'x'), + Buffer.alloc(256, 'y'), + Buffer.alloc(256, 'z'), + 'hello']; + +const received = []; +let totalBytesSent = 0; +let totalBytesReceived = 0; +const arrayBufferViewLength = common.getArrayBufferViews( + Buffer.from('') +).length; + +client.on('listening', common.mustCall(() => { + const port = client.address().port; + + client.send(toSend[0], 0, toSend[0].length, port); + client.send(toSend[1], port); + client.send([toSend[2]], port); + client.send(toSend[3], 0, toSend[3].length, port); + + totalBytesSent += toSend.map((buf) => buf.length) + .reduce((a, b) => a + b, 0); + + for (const msgBuf of common.getArrayBufferViews(toSend[0])) { + client.send(msgBuf, 0, msgBuf.byteLength, port); + totalBytesSent += msgBuf.byteLength; + } + for (const msgBuf of common.getArrayBufferViews(toSend[1])) { + client.send(msgBuf, port); + totalBytesSent += msgBuf.byteLength; + } + for (const msgBuf of common.getArrayBufferViews(toSend[2])) { + client.send([msgBuf], port); + totalBytesSent += msgBuf.byteLength; + } +})); + +client.on('message', common.mustCall((buf, info) => { + received.push(buf.toString()); + totalBytesReceived += info.size; + + if (totalBytesReceived === totalBytesSent) { + client.close(); + } + // For every buffer in `toSend`, we send the raw Buffer, + // as well as every TypedArray in getArrayBufferViews() +}, toSend.length + (toSend.length - 1) * arrayBufferViewLength)); + +client.on('close', common.mustCall((buf, info) => { + // The replies may arrive out of order -> sort them before checking. + received.sort(); + + const repeated = [...toSend]; + for (let i = 0; i < arrayBufferViewLength; i++) { + // We get arrayBufferViews only for toSend[0..2]. + repeated.push(...toSend.slice(0, 3)); + } + + assert.strictEqual(totalBytesSent, totalBytesReceived); + + const expected = repeated.map(String).sort(); + assert.deepStrictEqual(received, expected); +})); + +client.bind(0); diff --git a/test/js/node/test/sequential/test-dgram-bind-shared-ports.js b/test/js/node/test/sequential/test-dgram-bind-shared-ports.js new file mode 100644 index 0000000000..c68cfac969 --- /dev/null +++ b/test/js/node/test/sequential/test-dgram-bind-shared-ports.js @@ -0,0 +1,113 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// This test asserts the semantics of dgram::socket.bind({ exclusive }) +// when called from a cluster.Worker + +const assert = require('assert'); +const cluster = require('cluster'); +const dgram = require('dgram'); +const BYE = 'bye'; +const WORKER2_NAME = 'wrker2'; + +if (cluster.isPrimary) { + const worker1 = cluster.fork(); + + if (common.isWindows) { + worker1.on('error', common.mustCall((err) => { + console.log(err); + assert.strictEqual(err.code, 'ENOTSUP'); + worker1.kill(); + })); + return; + } + + worker1.on('message', common.mustCall((msg) => { + console.log(msg); + assert.strictEqual(msg, 'success'); + + const worker2 = cluster.fork({ WORKER2_NAME }); + worker2.on('message', common.mustCall((msg) => { + console.log(msg); + assert.strictEqual(msg, 'socket3:EADDRINUSE'); + + // finish test + worker1.send(BYE); + worker2.send(BYE); + })); + worker2.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(signal, null); + assert.strictEqual(code, 0); + })); + })); + worker1.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(signal, null); + assert.strictEqual(code, 0); + })); + // end primary code +} else { + // worker code + process.on('message', common.mustCallAtLeast((msg) => { + if (msg === BYE) process.exit(0); + }), 1); + + const isSecondWorker = process.env.WORKER2_NAME === WORKER2_NAME; + const socket1 = dgram.createSocket('udp4', common.mustNotCall()); + const socket2 = dgram.createSocket('udp4', common.mustNotCall()); + const socket3 = dgram.createSocket('udp4', common.mustNotCall()); + + socket1.on('error', (err) => assert.fail(err)); + socket2.on('error', (err) => assert.fail(err)); + + // First worker should bind, second should err + const socket3OnBind = + isSecondWorker ? + common.mustNotCall() : + common.mustCall(() => { + const port3 = socket3.address().port; + assert.strictEqual(typeof port3, 'number'); + process.send('success'); + }); + // An error is expected only in the second worker + const socket3OnError = + !isSecondWorker ? + common.mustNotCall() : + common.mustCall((err) => { + process.send(`socket3:${err.code}`); + }); + const address = common.localhostIPv4; + const opt1 = { address, port: 0, exclusive: false }; + const opt2 = { address, port: common.PORT, exclusive: false }; + const opt3 = { address, port: common.PORT + 1, exclusive: true }; + socket1.bind(opt1, common.mustCall(() => { + const port1 = socket1.address().port; + assert.strictEqual(typeof port1, 'number'); + socket2.bind(opt2, common.mustCall(() => { + const port2 = socket2.address().port; + assert.strictEqual(typeof port2, 'number'); + socket3.on('error', socket3OnError); + socket3.bind(opt3, socket3OnBind); + })); + })); +} diff --git a/test/js/node/test/sequential/test-dgram-implicit-bind-failure.js b/test/js/node/test/sequential/test-dgram-implicit-bind-failure.js new file mode 100644 index 0000000000..554a0fc25b --- /dev/null +++ b/test/js/node/test/sequential/test-dgram-implicit-bind-failure.js @@ -0,0 +1,43 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const EventEmitter = require('events'); +const dgram = require('dgram'); +const dns = require('dns'); +const mockError = new Error('fake DNS'); + +// Monkey patch dns.lookup() so that it always fails. +dns.lookup = function(address, family, callback) { + process.nextTick(() => { callback(mockError); }); +}; + +const socket = dgram.createSocket('udp4'); + +socket.on(EventEmitter.errorMonitor, common.mustCall((err) => { + // The DNS lookup should fail since it is monkey patched. At that point in + // time, the send queue should be populated with the send() operation. + assert.strictEqual(err, mockError); + const kStateSymbol = Object.getOwnPropertySymbols(socket).filter(sym => sym.description == "state symbol")[0]; + assert(kStateSymbol); + assert(Array.isArray(socket[kStateSymbol].queue)); + assert.strictEqual(socket[kStateSymbol].queue.length, 1); +}, 3)); + +socket.on('error', common.mustCall((err) => { + assert.strictEqual(err, mockError); + const kStateSymbol = Object.getOwnPropertySymbols(socket).filter(sym => sym.description == "state symbol")[0]; + assert(kStateSymbol); + assert.strictEqual(socket[kStateSymbol].queue, undefined); +}, 3)); + +// Initiate a few send() operations, which will fail. +socket.send('foobar', common.PORT, 'localhost'); + +process.nextTick(() => { + socket.send('foobar', common.PORT, 'localhost'); +}); + +setImmediate(() => { + socket.send('foobar', common.PORT, 'localhost'); +}); diff --git a/test/js/node/test/sequential/test-dgram-pingpong.js b/test/js/node/test/sequential/test-dgram-pingpong.js new file mode 100644 index 0000000000..33b01fbe22 --- /dev/null +++ b/test/js/node/test/sequential/test-dgram-pingpong.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); + +function pingPongTest(port, host) { + + const server = dgram.createSocket('udp4', common.mustCall((msg, rinfo) => { + assert.strictEqual(msg.toString('ascii'), 'PING'); + server.send('PONG', 0, 4, rinfo.port, rinfo.address); + })); + + server.on('error', function(e) { + throw e; + }); + + server.on('listening', function() { + console.log(`server listening on ${port}`); + + const client = dgram.createSocket('udp4'); + + client.on('message', function(msg) { + assert.strictEqual(msg.toString('ascii'), 'PONG'); + + client.close(); + server.close(); + }); + + client.on('error', function(e) { + throw e; + }); + + console.log(`Client sending to ${port}`); + + function clientSend() { + client.send('PING', 0, 4, port, 'localhost'); + } + + clientSend(); + }); + server.bind(port, host); + return server; +} + +const server = pingPongTest(common.PORT, 'localhost'); +server.on('close', common.mustCall(pingPongTest.bind(undefined, common.PORT)));