diff --git a/doc/src/programmers-guide.md b/doc/src/programmers-guide.md index 4f075bd05d..81fb66b557 100644 --- a/doc/src/programmers-guide.md +++ b/doc/src/programmers-guide.md @@ -1860,3 +1860,39 @@ For example: end Close a UDP socket just as you would a TCP socket, as described above. + +### Miscellaneous Networking APIs + +You can retrieve information about hostnames and services using the `net:getaddrinfo/1` and `net:getaddrinfo/2` functions. The return value is a list of maps each of which contains address information about the host, including its family (`inet`), protocol (`tcp` or `udp`), type (`stream` or `dgram`), and the address, currently an IPv4 tuple. + +For example: + + %% erlang + {ok, AddrInfos} = net:getaddrinfo("www.atomvm.net"), + + lists:foreach( + fun(AddrInfo) -> + #{ + family := Family, + protocol := Protocol, + type := Type, + address := Address + } = AddrInfo, + + io:format("family: ~p prototcol: ~p type: ~p address: ~p", [Family, Protocol, Type, Address]) + + end, + AddrInfos + ), + +The `host` parameter can be a domain name (typically) or a dotted pair IPv4 address. + +If you want to narrow the information you get back to a specific service type, you can specify a service name or port number (as a string value) as the second parameter: + + %% erlang + {ok, AddrInfos} = net:getaddrinfo("www.atomvm.net", "https"), + ... + +Service names are well-known identifiers on the internet, but they may vary from operating system to operating system. See the `services(3)` man pages for more information. + +> Note. Currently, the `net:getaddrinfo/1` function only supports reporting of IPv4 addresses. diff --git a/libs/estdlib/src/CMakeLists.txt b/libs/estdlib/src/CMakeLists.txt index db0a1f2ba3..35c5db10b9 100644 --- a/libs/estdlib/src/CMakeLists.txt +++ b/libs/estdlib/src/CMakeLists.txt @@ -41,6 +41,7 @@ set(ERLANG_MODULES lists maps math + net logger logger_std_h proplists diff --git a/libs/estdlib/src/net.erl b/libs/estdlib/src/net.erl new file mode 100644 index 0000000000..73385a333e --- /dev/null +++ b/libs/estdlib/src/net.erl @@ -0,0 +1,80 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Fred Dushin +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(net). + +-export([getaddrinfo/1, getaddrinfo/2]). + +%% nif call (so we can use guards at the API) +-export([getaddrinfo_nif/2]). + +-type addrinfo() :: #{ + family := socket:domain(), + socktype := socket:type(), + protocol := socket:protocol(), + %% NB. The erlang docs appear to be wrong about this + address := socket:sockaddr(), + addr := socket:sockaddr() +}. + +-type service() :: string(). + +-export_type([addrinfo/0, service/0]). + +%%----------------------------------------------------------------------------- +%% @param Host the host string for which to find address information +%% @returns Address info for the specified host +%% @equiv getaddrinfo(Host, undefined) +%% @doc Retrieve address information for a given hostname. +%% @end +%%----------------------------------------------------------------------------- +-spec getaddrinfo(Host :: string()) -> {ok, AddrInfo :: addrinfo()} | {error, Reason :: term()}. +getaddrinfo(Host) when is_list(Host) -> + ?MODULE:getaddrinfo(Host, undefined). + +%%----------------------------------------------------------------------------- +%% @param Host the host string for which to find address information +%% @param Service the service string for which to find address information +%% @returns Address info for the specified host and service +%% @doc Retrieve address information for a given hostname and service. +%% +%% The `Host' parameter may be a fully qualified host name or a string +%% containing a valid dotted pair IP address. (Currently, only IPv4 is +%% supported). +%% +%% The `Service' parameter may be the name of a service (as defined via +%% `services(3)` or a string containing a decimal value of the same. +%% +%% Note that the `Host' or `String' parameter may be `undefined', but +%% not both. +%% @end +%%----------------------------------------------------------------------------- +-spec getaddrinfo(Host :: string() | undefined, Service :: service() | undefined) -> + {ok, AddrInfo :: addrinfo()} | {error, Reason :: term()}. +getaddrinfo(Host, Service) when + (is_list(Host) orelse Host =:= undefined) andalso + (is_list(Service) orelse Service =:= undefined) andalso + not (Host =:= undefined andalso Service =:= undefined) +-> + ?MODULE:getaddrinfo_nif(Host, Service). + +%% @hidden +getaddrinfo_nif(_Host, _Service) -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/CMakeLists.txt b/src/libAtomVM/CMakeLists.txt index c26f939804..32824387a4 100644 --- a/src/libAtomVM/CMakeLists.txt +++ b/src/libAtomVM/CMakeLists.txt @@ -158,6 +158,7 @@ define_if_function_exists(libAtomVM open "fcntl.h" PUBLIC HAVE_OPEN) define_if_function_exists(libAtomVM close "unistd.h" PUBLIC HAVE_CLOSE) define_if_function_exists(libAtomVM mkfifo "sys/stat.h" PRIVATE HAVE_MKFIFO) define_if_function_exists(libAtomVM unlink "unistd.h" PRIVATE HAVE_UNLINK) +define_if_function_exists(libAtomVM getservbyname "netdb.h" PRIVATE HAVE_SERVBYNAME) define_if_symbol_exists(libAtomVM O_CLOEXEC "fcntl.h" PRIVATE HAVE_O_CLOEXEC) define_if_symbol_exists(libAtomVM O_DIRECTORY "fcntl.h" PRIVATE HAVE_O_DIRECTORY) define_if_symbol_exists(libAtomVM O_DSYNC "fcntl.h" PRIVATE HAVE_O_DSYNC) diff --git a/src/libAtomVM/interop.c b/src/libAtomVM/interop.c index b6b6479399..f1a65a8dc1 100644 --- a/src/libAtomVM/interop.c +++ b/src/libAtomVM/interop.c @@ -611,3 +611,25 @@ term interop_kv_get_value_default(term kv, AtomString key, term default_value, G return default_value; } } + +term interop_atom_term_select_atom(GlobalContext *global, const AtomStringIntPair *table, int value) +{ + int i; + for (i = 0; table[i].as_val != NULL; i++) { + if (value == table[i].i_val) { + int global_atom_index = globalcontext_insert_atom(global, table[i].as_val); + return term_from_atom_index(global_atom_index); + } + } + return term_invalid_term(); +} + +term interop_chars_to_list(const char *chars, size_t len, Heap *heap) +{ + term ret = term_nil(); + for (int i = (int) len - 1; i >= 0; --i) { + term c_term = term_from_int(chars[i]); + ret = term_list_prepend(c_term, ret, heap); + } + return ret; +} diff --git a/src/libAtomVM/interop.h b/src/libAtomVM/interop.h index d90d849574..c995dca9ac 100644 --- a/src/libAtomVM/interop.h +++ b/src/libAtomVM/interop.h @@ -74,6 +74,7 @@ term interop_proplist_get_value(term list, term key); term interop_proplist_get_value_default(term list, term key, term default_value); term interop_map_get_value(GlobalContext *glb, term map, term key); term interop_map_get_value_default(GlobalContext *glb, term map, term key, term default_value); +term interop_chars_to_list(const char *chars, size_t len, Heap *heap); NO_DISCARD InteropFunctionResult interop_iolist_size(term t, size_t *size); NO_DISCARD InteropFunctionResult interop_write_iolist(term t, char *p); @@ -147,4 +148,6 @@ static inline term interop_bytes_to_list(const void *bytes, int len, Heap *heap) } #endif +term interop_atom_term_select_atom(GlobalContext *global, const AtomStringIntPair *table, int value); + #endif diff --git a/src/libAtomVM/otp_net.c b/src/libAtomVM/otp_net.c new file mode 100644 index 0000000000..4049dfa5b5 --- /dev/null +++ b/src/libAtomVM/otp_net.c @@ -0,0 +1,328 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// #define ENABLE_TRACE +#include + +#define FAMILY_ATOM globalcontext_make_atom(global, ATOM_STR("\x6", "family")) +#define PROTOCOL_ATOM globalcontext_make_atom(global, ATOM_STR("\x8", "protocol")) +#define TYPE_ATOM globalcontext_make_atom(global, ATOM_STR("\x4", "type")) +#define INET_ATOM globalcontext_make_atom(global, ATOM_STR("\x4", "inet")) +#define ADDR_ATOM globalcontext_make_atom(global, ATOM_STR("\x4", "addr")) +// NB. The erlang documentation states that an address info structure is a map +// with an `address` key, but OTP-24 returns maps with an `addr` key. +#define ADDRESS_ATOM globalcontext_make_atom(global, ATOM_STR("\x7", "address")) + +#define UNKNOWN_TABLE_VALUE -1 + +// TODO support inet6 +// static const AtomStringIntPair family_table[] = { +// { ATOM_STR("\x4", "inet"), PF_INET }, +// SELECT_INT_DEFAULT(UNKNOWN_TABLE_VALUE) +// }; + +static const AtomStringIntPair protocol_table[] = { + { ATOM_STR("\x3", "tcp"), IPPROTO_TCP }, + { ATOM_STR("\x3", "udp"), IPPROTO_UDP }, + SELECT_INT_DEFAULT(UNKNOWN_TABLE_VALUE) +}; + +static const AtomStringIntPair type_table[] = { + { ATOM_STR("\x5", "dgram"), SOCK_DGRAM }, + { ATOM_STR("\x6", "stream"), SOCK_STREAM }, + SELECT_INT_DEFAULT(UNKNOWN_TABLE_VALUE) +}; + +// +// utilities +// + +static term socket_tuple_from_addr(Context *ctx, uint32_t addr) +{ + term terms[4]; + terms[0] = term_from_int32((addr >> 24) & 0xFF); + terms[1] = term_from_int32((addr >> 16) & 0xFF); + terms[2] = term_from_int32((addr >> 8) & 0xFF); + terms[3] = term_from_int32(addr & 0xFF); + + return port_create_tuple_n(ctx, 4, terms); +} + +static inline term make_error_tuple(term reason, Context *ctx) +{ + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, reason); + return error_tuple; +} + +static term eai_errno_to_term(int err, GlobalContext *glb) +{ + switch (err) { + case EAI_AGAIN: + return globalcontext_make_atom(glb, ATOM_STR("\x8", "eaiagain")); + case EAI_BADFLAGS: + return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaibadflags")); + case EAI_FAIL: + return globalcontext_make_atom(glb, ATOM_STR("\x7", "eaifail")); + case EAI_FAMILY: + return globalcontext_make_atom(glb, ATOM_STR("\x9", "eaifamily")); + case EAI_MEMORY: + return globalcontext_make_atom(glb, ATOM_STR("\x9", "eaimemory")); + case EAI_NONAME: + return globalcontext_make_atom(glb, ATOM_STR("\x9", "eainoname")); + case EAI_SERVICE: + return globalcontext_make_atom(glb, ATOM_STR("\xA", "eaiservice")); + case EAI_SOCKTYPE: + return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaisocktype")); + + // These constants are not supported on ESP32 + // case EAI_BADHINTS: + // return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaibadhints")); + // case EAI_OVERFLOW: + // return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaioverflow")); + // case EAI_PROTOCOL: + // return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaiprotocol")); + // case EAI_SYSTEM: + // return globalcontext_make_atom(glb, ATOM_STR("\x9", "eaisystem")); + } + return term_from_int(err); +} + +static inline term make_address(Context *ctx, struct sockaddr_in *addr) +{ + return socket_tuple_from_addr(ctx, ntohl(addr->sin_addr.s_addr)); +} + +// +// net:getaddrinfo/1 +// + +static term nif_net_getaddrinfo(Context *ctx, int argc, term argv[]) +{ + TRACE("nif_net_getaddrinfo\n"); + UNUSED(argc); + + GlobalContext *global = ctx->global; + + term host = argv[0]; + term service = argv[1]; + + if (host == UNDEFINED_ATOM && service == UNDEFINED_ATOM) { + TRACE("Host and Service params may not both be undefined"); + RAISE_ERROR(BADARG_ATOM); + } + + if (!(term_is_string(host) || host == UNDEFINED_ATOM)) { + RAISE_ERROR(BADARG_ATOM); + } + char *host_str = NULL; + if (term_is_string(host)) { + int ok; + host_str = interop_term_to_string(host, &ok); + if (!ok) { + RAISE_ERROR(BADARG_ATOM); + } + TRACE("Host: %s\n", host_str); + } + + if (!(term_is_string(service) || service == UNDEFINED_ATOM)) { + free(host_str); + RAISE_ERROR(BADARG_ATOM); + } + char *service_str = NULL; + if (term_is_string(service)) { + int ok; + service_str = interop_term_to_string(service, &ok); + if (!ok) { + free(host_str); + RAISE_ERROR(BADARG_ATOM); + } + TRACE("Service: %s\n", service_str); + } + + avm_uint_t port = 0; +#ifdef HAVE_SERVBYNAME + if (!IS_NULL_PTR(service_str)) { + struct servent *svt = getservbyname(service_str, NULL); + if (!IS_NULL_PTR(svt)) { + port = ntohs(svt->s_port); + } + } +#endif + TRACE("port: %zu\n", port); + + // for now, we are only supporting IPv4 addresses + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_INET; + + struct addrinfo *host_info; + int err = getaddrinfo(host_str, service_str, &hints, &host_info); + + // some implementations do not support service filters + if (err == EAI_SERVICE) { + fprintf(stderr, "WARNING: EAI_SERVICE unsupported on this platform.\n"); + err = getaddrinfo(host_str, NULL, &hints, &host_info); + } + + free(host_str); + free(service_str); + + if (err != 0 && err != EAI_SERVICE) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return make_error_tuple(eai_errno_to_term(err, global), ctx); + } + TRACE("getaddrinfo succeeded\n"); + + size_t num_addrinfos = 0; + for (struct addrinfo *p = host_info; p != NULL; p = p->ai_next) { + num_addrinfos++; + } + TRACE("num_addrinfos: %zu\n", num_addrinfos); + + if (num_addrinfos == 0) { + return term_nil(); + } + + // {ok, [#{ + // family => Family :: atom() + // protocol => Protocol :: atom() + // type -> Type :: atom() + // address, addr => + // #{ + // addr => Address :: {0..255, 0..255, 0..255, 0..255}, + // port => 0..65535, + // family => inet + // } + // }]} + // Note. We might over-allocate for some more esoteric calls + size_t requested_size = TUPLE_SIZE(2) + LIST_SIZE(num_addrinfos, (term_map_size_in_terms(4) + term_map_size_in_terms(3) + TUPLE_SIZE(4))); + if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term infos = term_nil(); + for (struct addrinfo *p = host_info; p != NULL; p = p->ai_next) { + + // the number of elements in the outer map. Add 1 because address and addr will point to the same (inner) map + // for compatibility with OTP behavior + // Cf. https://erlangforums.com/t/discrepancy-in-type-spec-for-net-getaddrinfo/2984 + int num_elts = (p->ai_family != 0) + (p->ai_protocol != 0) + (p->ai_socktype != 0) + (p->ai_addrlen != 0) + 1; + term elt = term_alloc_map(num_elts, &ctx->heap); + + // in the current implementation, this will always be `inet` + term family = INET_ATOM; + + int i = 0; + term_set_map_assoc(elt, i++, FAMILY_ATOM, family); + + if (p->ai_protocol) { + term protocol = interop_atom_term_select_atom(global, protocol_table, p->ai_protocol); + term_set_map_assoc(elt, i++, PROTOCOL_ATOM, term_is_invalid_term(protocol) ? UNDEFINED_ATOM : protocol); + } else { + TRACE("No protocol defined\n"); + } + + if (p->ai_socktype) { + term type = interop_atom_term_select_atom(global, type_table, p->ai_socktype); + term_set_map_assoc(elt, i++, TYPE_ATOM, term_is_invalid_term(type) ? UNDEFINED_ATOM : type); + } else { + TRACE("No socket type defined\n"); + } + + if (p->ai_addrlen == sizeof(struct sockaddr_in)) { + // The inner addr contains a family, port, and addr + term inner_addr = term_alloc_map(3, &ctx->heap); + term_set_map_assoc(inner_addr, 0, FAMILY_ATOM, family); + term_set_map_assoc(inner_addr, 1, PORT_ATOM, term_from_int(port)); + term address = make_address(ctx, (struct sockaddr_in *) p->ai_addr); + term_set_map_assoc(inner_addr, 2, ADDR_ATOM, address); + + // embed the inner_addr, but reference it from both address and addr + // for compatibility with OTP + term_set_map_assoc(elt, i++, ADDRESS_ATOM, inner_addr); + term_set_map_assoc(elt, i++, ADDR_ATOM, inner_addr); + } else { + TRACE("No address defined\n"); + } + infos = term_list_prepend(elt, infos, &ctx->heap); + } + freeaddrinfo(host_info); + + term ret = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(ret, 0, OK_ATOM); + term_put_tuple_element(ret, 1, infos); + +#ifdef ENABLE_TRACE + fprintf(stdout, "host info: "); + term_display(stdout, ret, ctx); + fprintf(stdout, "\n"); +#endif + + return ret; +} + +// +// Nifs +// + +static const struct Nif net_getaddrinfo_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_net_getaddrinfo +}; + +// +// Entrypoints +// + +const struct Nif *otp_net_nif_get_nif(const char *nifname) +{ + if (strncmp("net:", nifname, 4) == 0) { + const char *rest = nifname + 4; + if (strcmp("getaddrinfo_nif/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &net_getaddrinfo_nif; + } + } + return NULL; +} + +void otp_net_init(GlobalContext *global) +{ + UNUSED(global); + + // noop +} diff --git a/src/libAtomVM/otp_net.h b/src/libAtomVM/otp_net.h new file mode 100644 index 0000000000..916ad89169 --- /dev/null +++ b/src/libAtomVM/otp_net.h @@ -0,0 +1,38 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef _OTP_NET_H_ +#define _OTP_NET_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +const struct Nif *otp_net_nif_get_nif(const char *nifname); +void otp_net_init(GlobalContext *global); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index f4286ed168..1e34578144 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -87,6 +87,7 @@ extern "C" { #define TUPLE_SIZE(elems) ((int) (elems + 1)) #define CONS_SIZE 2 #define REFC_BINARY_CONS_OFFSET 4 +#define LIST_SIZE(num_elements, element_size) ((num_elements * element_size) * CONS_SIZE) #define TERM_BINARY_SIZE_IS_HEAP(size) ((size) < REFC_BINARY_MIN) diff --git a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt index 3762a92a51..cb71d43179 100644 --- a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt @@ -7,7 +7,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -28,6 +28,7 @@ set(AVM_BUILTIN_COMPONENT_SRCS "socket_driver.c" "spi_driver.c" "uart_driver.c" + "otp_net_platform.c" "otp_socket_platform.c" ) diff --git a/src/platforms/esp32/components/avm_builtins/Kconfig b/src/platforms/esp32/components/avm_builtins/Kconfig index 2f8fec6c55..94c4c7f009 100644 --- a/src/platforms/esp32/components/avm_builtins/Kconfig +++ b/src/platforms/esp32/components/avm_builtins/Kconfig @@ -70,4 +70,8 @@ config AVM_ENABLE_OTP_SOCKET_NIFS bool "Enable OTP Socket NIFs" default y +config AVM_ENABLE_OTP_NET_NIFS + bool "Enable OTP Net NIFs" + default y + endmenu diff --git a/src/platforms/esp32/components/avm_builtins/otp_net_platform.c b/src/platforms/esp32/components/avm_builtins/otp_net_platform.c new file mode 100644 index 0000000000..c791640611 --- /dev/null +++ b/src/platforms/esp32/components/avm_builtins/otp_net_platform.c @@ -0,0 +1,31 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include + + +#ifdef CONFIG_AVM_ENABLE_OTP_NET_NIFS + +REGISTER_NIF_COLLECTION(otp_net, otp_net_init, NULL, otp_net_nif_get_nif) + +#endif diff --git a/src/platforms/esp32/components/avm_sys/CMakeLists.txt b/src/platforms/esp32/components/avm_sys/CMakeLists.txt index bf4596aa0a..0167a364f3 100644 --- a/src/platforms/esp32/components/avm_sys/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_sys/CMakeLists.txt @@ -25,6 +25,7 @@ set(AVM_SYS_COMPONENT_SRCS "sys.c" "platform_nifs.c" "platform_defaultatoms.c" + "../../../../libAtomVM/otp_net.c" "../../../../libAtomVM/otp_socket.c" ) diff --git a/src/platforms/generic_unix/lib/CMakeLists.txt b/src/platforms/generic_unix/lib/CMakeLists.txt index 807aee3265..cf8b447559 100644 --- a/src/platforms/generic_unix/lib/CMakeLists.txt +++ b/src/platforms/generic_unix/lib/CMakeLists.txt @@ -26,6 +26,7 @@ set(HEADER_FILES generic_unix_sys.h mapped_file.h platform_defaultatoms.h + ../../../libAtomVM/otp_net.h ../../../libAtomVM/otp_socket.h ) @@ -37,6 +38,7 @@ set(SOURCE_FILES smp.c socket_driver.c sys.c + ../../../libAtomVM/otp_net.c ../../../libAtomVM/otp_socket.c ) diff --git a/src/platforms/generic_unix/lib/platform_nifs.c b/src/platforms/generic_unix/lib/platform_nifs.c index 3c201997ed..fa8c463bcb 100644 --- a/src/platforms/generic_unix/lib/platform_nifs.c +++ b/src/platforms/generic_unix/lib/platform_nifs.c @@ -25,6 +25,7 @@ #include "interop.h" #include "memory.h" #include "nifs.h" +#include "otp_net.h" #include "otp_socket.h" #include "platform_defaultatoms.h" #include "term.h" @@ -261,5 +262,10 @@ const struct Nif *platform_nifs_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &atomvm_platform_nif; } - return otp_socket_nif_get_nif(nifname); + const struct Nif *nif = otp_net_nif_get_nif(nifname); + if (nif == NULL) { + return otp_socket_nif_get_nif(nifname); + } else { + return nif; + } } diff --git a/src/platforms/generic_unix/lib/sys.c b/src/platforms/generic_unix/lib/sys.c index 48f8618ff7..85f230d553 100644 --- a/src/platforms/generic_unix/lib/sys.c +++ b/src/platforms/generic_unix/lib/sys.c @@ -25,6 +25,7 @@ #include "defaultatoms.h" #include "iff.h" #include "mapped_file.h" +#include "otp_net.h" #include "otp_socket.h" #include "scheduler.h" #include "smp.h" @@ -559,6 +560,7 @@ void sys_init_platform(GlobalContext *global) #endif #endif + otp_net_init(global); otp_socket_init(global); global->platform_data = platform; diff --git a/tests/libs/estdlib/CMakeLists.txt b/tests/libs/estdlib/CMakeLists.txt index eb390661d4..b543f4ba59 100644 --- a/tests/libs/estdlib/CMakeLists.txt +++ b/tests/libs/estdlib/CMakeLists.txt @@ -33,6 +33,7 @@ set(ERLANG_MODULES test_lists test_logger test_maps + test_net test_spawn test_string test_proplists diff --git a/tests/libs/estdlib/test_net.erl b/tests/libs/estdlib/test_net.erl new file mode 100644 index 0000000000..98bb681997 --- /dev/null +++ b/tests/libs/estdlib/test_net.erl @@ -0,0 +1,147 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Fred Dushin +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_net). + +-export([test/0]). + +test() -> + ok = test_getaddrinfo(), + ok = test_getaddrinfo2(), + ok. + +test_getaddrinfo() -> + ok = test_getaddrinfo("www.atomvm.net"), + ok. + +test_getaddrinfo(Host) -> + {ok, AddrInfos} = net:getaddrinfo(Host), + + lists:foreach( + fun(AddrInfo) -> + erlang:display({test_getaddrinfo, addr_info, AddrInfo}), + + inet = maps:get(family, AddrInfo), + Protocol = maps:get(protocol, AddrInfo, undefined), + Type = maps:get(type, AddrInfo, undefined), + Addr = get_addr(AddrInfo), + + case erlang:system_info(machine) of + "BEAM" -> + ok; + _ -> + true = (Protocol =:= tcp orelse Protocol =:= udp orelse Protocol =:= undefined), + true = (Type =:= dgram orelse Type =:= stream orelse Type =:= undefined) + end, + + {A, B, C, D} = Addr, + true = is_integer(A) andalso 0 =< A andalso A =< 255, + true = is_integer(B) andalso 0 =< B andalso B =< 255, + true = is_integer(C) andalso 0 =< C andalso C =< 255, + true = is_integer(D) andalso 0 =< D andalso D =< 255, + + ok + end, + maybe_filter_addrinfos(AddrInfos) + ), + + expect_failure(fun() -> net:getaddrinfo(not_a_list) end, function_clause), + expect_failure(fun() -> net:getaddrinfo([isnot, a, string]) end, badarg), + expect_failure(fun() -> net:getaddrinfo([$i, $m, $p, $r, $o, $p, $e | $r]) end, badarg), + + {error, _Reason} = net:getaddrinfo("ewkrkerkwe.oksjds.wee.org"), + + ok. + +test_getaddrinfo2() -> + ok = test_getaddrinfo2("www.atomvm.net", "https"), + ok = test_getaddrinfo2("www.atomvm.net", "443"), + ok = test_getaddrinfo2(undefined, "443"), + ok. + +test_getaddrinfo2(Host, Service) -> + {ok, AddrInfos} = net:getaddrinfo(Host, Service), + + lists:foreach( + fun(AddrInfo) -> + erlang:display({test_getaddrinfo2, addr_info, AddrInfo}), + + inet = maps:get(family, AddrInfo), + Protocol = maps:get(protocol, AddrInfo, undefined), + Type = maps:get(type, AddrInfo, undefined), + Addr = get_addr(AddrInfo), + + case erlang:system_info(machine) of + "BEAM" -> + ok; + _ -> + true = (Protocol =:= tcp orelse Protocol =:= udp orelse Protocol =:= undefined), + true = (Type =:= dgram orelse Type =:= stream orelse Type =:= undefined) + end, + + {A, B, C, D} = Addr, + true = is_integer(A) andalso 0 =< A andalso A =< 255, + true = is_integer(B) andalso 0 =< B andalso B =< 255, + true = is_integer(C) andalso 0 =< C andalso C =< 255, + true = is_integer(D) andalso 0 =< D andalso D =< 255, + + ok + end, + maybe_filter_addrinfos(AddrInfos) + ), + + expect_failure(fun() -> net:getaddrinfo(Host, not_a_list) end, function_clause), + expect_failure(fun() -> net:getaddrinfo(Host, [isnot, a, string]) end, badarg), + expect_failure(fun() -> net:getaddrinfo(Host, [$i, $m, $p, $r, $o, $p, $e | $r]) end, badarg), + expect_failure(fun() -> net:getaddrinfo(undefined, undefined) end, function_clause), + + {error, _Reason} = net:getaddrinfo("ewkrkerkwe.oksjds.wee.org", Service), + + ok. + +expect_failure(F, Expected) -> + try + F(), + fail + catch + _:E when E =:= Expected -> + ok + end. + +maybe_filter_addrinfos(AddrInfos) -> + case erlang:system_info(machine) of + "BEAM" -> + lists:filter( + fun(AddrInfo) -> + inet =:= maps:get(family, AddrInfo) + end, + AddrInfos + ); + _ -> + AddrInfos + end. + +get_addr(AddrInfo) -> + case erlang:system_info(machine) of + "BEAM" -> + maps:get(addr, maps:get(addr, AddrInfo)); + _ -> + maps:get(addr, maps:get(address, AddrInfo)) + end. diff --git a/tests/libs/estdlib/tests.erl b/tests/libs/estdlib/tests.erl index b42f75515e..801fed6b68 100644 --- a/tests/libs/estdlib/tests.erl +++ b/tests/libs/estdlib/tests.erl @@ -36,7 +36,7 @@ get_otp_version() -> get_tests(OTPVersion) when (is_integer(OTPVersion) andalso OTPVersion >= 24) orelse OTPVersion == atomvm -> - [test_tcp_socket, test_udp_socket | get_tests(undefined)]; + [test_tcp_socket, test_udp_socket, test_net | get_tests(undefined)]; get_tests(_OTPVersion) -> [ test_lists,