diff --git a/.github/workflows/build-and-test-macos.yaml b/.github/workflows/build-and-test-macos.yaml index 63ce352dc..bc0b85325 100644 --- a/.github/workflows/build-and-test-macos.yaml +++ b/.github/workflows/build-and-test-macos.yaml @@ -44,7 +44,7 @@ jobs: submodules: 'recursive' - name: "Install deps" - run: brew install gperf doxygen erlang@${{ matrix.otp }} ninja + run: brew install gperf doxygen erlang@${{ matrix.otp }} ninja mbedtls # Builder info - name: "System info" diff --git a/.github/workflows/build-and-test-on-freebsd.yaml b/.github/workflows/build-and-test-on-freebsd.yaml index f7e2ff595..6ef6828d8 100644 --- a/.github/workflows/build-and-test-on-freebsd.yaml +++ b/.github/workflows/build-and-test-on-freebsd.yaml @@ -57,7 +57,7 @@ jobs: echo "%%" echo "**freebsd-version:**" freebsd-version - sudo pkg install -y cmake gperf erlang elixir + sudo pkg install -y cmake gperf erlang elixir mbedtls echo "**uname:**" uname -a echo "**C Compiler version:**" @@ -73,7 +73,7 @@ jobs: echo "%%" mkdir build cd build - cmake .. + cmake .. -DMBEDTLS_ROOT_DIR=/usr/local echo "%%" echo "%% Building AtomVM ..." diff --git a/.github/workflows/build-and-test-other.yaml b/.github/workflows/build-and-test-other.yaml index 5564ae47d..c927e9f07 100644 --- a/.github/workflows/build-and-test-other.yaml +++ b/.github/workflows/build-and-test-other.yaml @@ -98,7 +98,7 @@ jobs: apt update && apt install -y -t stretch-backports-sloppy libarchive13 && apt install -y -t stretch-backports cmake && - apt install -y file gcc g++ binutils make doxygen gperf zlib1g-dev libssl-dev + apt install -y file gcc g++ binutils make doxygen gperf zlib1g-dev libssl-dev libmbedtls-dev - arch: "arm32v7" platform: "arm/v7" diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 6e1dd0171..934ef4beb 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -174,7 +174,7 @@ jobs: cmake_opts_other: "-DOPENSSL_CRYPTO_LIBRARY=/usr/lib/i386-linux-gnu/libcrypto.so -DAVM_CREATE_STACKTRACES=off" arch: "i386" compiler_pkgs: "gcc-10 g++-10 gcc-10-multilib g++-10-multilib libc6-dev-i386 - libc6-dbg:i386 zlib1g-dev:i386 libssl-dev:i386" + libc6-dbg:i386 zlib1g-dev:i386 libssl-dev:i386 libmbedtls-dev:i386" env: CC: ${{ matrix.cc }} @@ -202,7 +202,7 @@ jobs: run: sudo apt update -y - name: "Install deps" - run: sudo apt install -y ${{ matrix.compiler_pkgs}} cmake gperf zlib1g-dev doxygen valgrind + run: sudo apt install -y ${{ matrix.compiler_pkgs}} cmake gperf zlib1g-dev doxygen valgrind libmbedtls-dev # Builder info - name: "System info" diff --git a/CHANGELOG.md b/CHANGELOG.md index 235462385..e830350c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for interrupts to STM32 GPIO port driver. - Added suppoprt for PicoW extra gpio pins (led) to the gpio driver. - Added support for `net:getaddrinfo/1,2` +- Added minimal support for the OTP `ssl` interface. ## [0.6.0-alpha.1] - 2023-10-09 diff --git a/CMakeModules/MbedTLS.cmake b/CMakeModules/MbedTLS.cmake new file mode 100644 index 000000000..52f0409d1 --- /dev/null +++ b/CMakeModules/MbedTLS.cmake @@ -0,0 +1,97 @@ +# +# This file is part of AtomVM. +# +# Copyright 2023 Paul Guyot +# +# 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 +# + +# Find MbedTLS +# Search for MbedTLS 2.x or 3.x and define libraries like MbedTLS 3.x does. + +# This script is not called FindMbedTLS.cmake because it would conflict with +# installed MbedTLS 3.x + +# If MBEDTLS_ROOT_DIR is set, no heuristic is applied. +# It must be set to the parent directory of include/mbedtls/version.h +# Libraries are at ${MBEDTLS_LIBRARIES_DIR} or, if unset, ${MBEDTLS_ROOT_DIR}/lib/ + +# If MBEDTLS_ROOT_DIR is not set, apply the following heuristic: +# Try to find mbedtls 3.x CMake package with find_package +# If it doesn't work, search for MBEDTLS_VERSION_NUMBER symbol as well as +# the three libraries we need with check_symbol_exists and find_library + +if (MBEDTLS_ROOT_DIR) + set(MbedTLS_FOUND TRUE) + if (NOT MBEDTLS_LIBRARIES_DIR) + set(MBEDTLS_LIBRARIES_DIR ${MBEDTLS_ROOT_DIR}/lib) + endif() + message(STATUS "Will use MbedTLS from ${MBEDTLS_ROOT_DIR} and ${MBEDTLS_LIBRARIES_DIR}") + + add_library(MbedTLS::mbedcrypto SHARED IMPORTED) + set_target_properties(MbedTLS::mbedcrypto PROPERTIES + IMPORTED_LOCATION "${MBEDTLS_LIBRARIES_DIR}/libmbedcrypto${CMAKE_SHARED_LIBRARY_SUFFIX}" + INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_ROOT_DIR}/include/" + ) + + add_library(MbedTLS::mbedx509 SHARED IMPORTED) + set_target_properties(MbedTLS::mbedx509 PROPERTIES + IMPORTED_LOCATION "${MBEDTLS_LIBRARIES_DIR}/libmbedx509${CMAKE_SHARED_LIBRARY_SUFFIX}" + INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_ROOT_DIR}/include/" + INTERFACE_LINK_LIBRARIES "MbedTLS::mbedcrypto" + ) + + add_library(MbedTLS::mbedtls SHARED IMPORTED) + set_target_properties(MbedTLS::mbedtls PROPERTIES + IMPORTED_LOCATION "${MBEDTLS_LIBRARIES_DIR}/libmbedtls${CMAKE_SHARED_LIBRARY_SUFFIX}" + INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_ROOT_DIR}/include/" + INTERFACE_LINK_LIBRARIES "MbedTLS::mbedx509" + ) +else() + # MbedTLS 3.x is installed as a CMake package + find_package(MbedTLS QUIET) + if (MbedTLS_FOUND) + message(STATUS "Found MbedTLS package ${MbedTLS_FOUND}") + else() + include(CheckSymbolExists) + check_symbol_exists(MBEDTLS_VERSION_NUMBER "mbedtls/version.h" HAVE_MBEDTLS_VERSION_NUMBER) + find_library(MBEDCRYPTO mbedcrypto) + find_library(MBEDX509 mbedx509) + find_library(MBEDTLS mbedtls) + if (HAVE_MBEDTLS_VERSION_NUMBER + AND NOT ${MBEDCRYPTO} STREQUAL "MBEDCRYPTO-NOTFOUND" + AND NOT ${MBEDX509} STREQUAL "MBEDX509-NOTFOUND" + AND NOT ${MBEDTLS} STREQUAL "MBEDTLS-NOTFOUND") + message(STATUS "Found MbedTLS with mbedcrypto ${MBEDCRYPTO}, mbedx509 ${MBEDX509} and mbedtls ${MBEDTLS}") + set(MbedTLS_FOUND TRUE) + add_library(MbedTLS::mbedcrypto SHARED IMPORTED) + set_target_properties(MbedTLS::mbedcrypto PROPERTIES + IMPORTED_LOCATION "${MBEDCRYPTO}" + ) + + add_library(MbedTLS::mbedx509 SHARED IMPORTED) + set_target_properties(MbedTLS::mbedx509 PROPERTIES + IMPORTED_LOCATION "${MBEDX509}" + INTERFACE_LINK_LIBRARIES "MbedTLS::mbedcrypto" + ) + + add_library(MbedTLS::mbedtls SHARED IMPORTED) + set_target_properties(MbedTLS::mbedtls PROPERTIES + IMPORTED_LOCATION "${MBEDTLS}" + INTERFACE_LINK_LIBRARIES "MbedTLS::mbedx509" + ) + endif() + endif() +endif() diff --git a/README.Md b/README.Md index 7a298e6fa..5c71a3e5f 100644 --- a/README.Md +++ b/README.Md @@ -38,6 +38,7 @@ Required for building: * gperf ([GNU Perfect Hash Function Generator](https://www.gnu.org/software/gperf/manual/gperf.html)) * erlc ([erlang compiler](https://www.erlang.org/)) * elixirc ([elixir compiler](https://elixir-lang.org)) +* Mbed TLS ([portable TLS library, optionally required to support SSL](https://www.trustedfirmware.org/projects/mbed-tls/)) * zlib ([zlib compression and decompression library](https://zlib.net/)) Documentation and Coverage: diff --git a/doc/src/build-instructions.md b/doc/src/build-instructions.md index bd3d415be..c6e9671db 100644 --- a/doc/src/build-instructions.md +++ b/doc/src/build-instructions.md @@ -58,6 +58,7 @@ The following software is required in order to build AtomVM in generic UNIX syst * `make` * `gperf` * `zlib` +* `Mbed TLS` * Erlang/OTP compiler (`erlc`) * Elixir compiler diff --git a/libs/estdlib/src/CMakeLists.txt b/libs/estdlib/src/CMakeLists.txt index 35c5db10b..68a4141ae 100644 --- a/libs/estdlib/src/CMakeLists.txt +++ b/libs/estdlib/src/CMakeLists.txt @@ -46,6 +46,7 @@ set(ERLANG_MODULES logger_std_h proplists socket + ssl string timer unicode diff --git a/libs/estdlib/src/gen_tcp.erl b/libs/estdlib/src/gen_tcp.erl index f245411b0..ca53230ab 100644 --- a/libs/estdlib/src/gen_tcp.erl +++ b/libs/estdlib/src/gen_tcp.erl @@ -86,7 +86,7 @@ %% @end %%----------------------------------------------------------------------------- -spec connect( - Address :: inet:address() | inet:hostname(), + Address :: inet:ip_address() | inet:hostname(), Port :: inet:port_number(), Options :: [connect_option()] ) -> diff --git a/libs/estdlib/src/gen_udp.erl b/libs/estdlib/src/gen_udp.erl index cf5d97460..23bd518a2 100644 --- a/libs/estdlib/src/gen_udp.erl +++ b/libs/estdlib/src/gen_udp.erl @@ -103,7 +103,7 @@ open(PortNum, Options) -> %%----------------------------------------------------------------------------- -spec send( Socket :: inet:socket(), - Address :: inet:address(), + Address :: inet:ip_address(), PortNum :: inet:port_number(), Packet :: packet() ) -> ok | {error, reason()}. @@ -121,7 +121,7 @@ send(Socket, Address, PortNum, Packet) -> %% @end %%----------------------------------------------------------------------------- -spec recv(Socket :: inet:socket(), Length :: non_neg_integer()) -> - {ok, {inet:address(), inet:port_number(), packet()}} | {error, reason()}. + {ok, {inet:ip_address(), inet:port_number(), packet()}} | {error, reason()}. recv(Socket, Length) -> recv(Socket, Length, infinity). @@ -143,7 +143,7 @@ recv(Socket, Length) -> %% @end %%----------------------------------------------------------------------------- -spec recv(Socket :: inet:socket(), Length :: non_neg_integer(), Timeout :: timeout()) -> - {ok, {inet:address(), inet:port_number(), packet()}} | {error, reason()}. + {ok, {inet:ip_address(), inet:port_number(), packet()}} | {error, reason()}. recv(Socket, Length, Timeout) -> call(Socket, {recvfrom, Length, Timeout}). diff --git a/libs/estdlib/src/inet.erl b/libs/estdlib/src/inet.erl index cea2a9f67..369a5d8d7 100644 --- a/libs/estdlib/src/inet.erl +++ b/libs/estdlib/src/inet.erl @@ -24,12 +24,11 @@ -type port_number() :: 0..65535. -type socket() :: pid(). --type address() :: ipv4_address(). --type ipv4_address() :: {octet(), octet(), octet(), octet()}. --type octet() :: 0..255. +-type ip_address() :: ip4_address(). +-type ip4_address() :: {0..255, 0..255, 0..255, 0..255}. -type hostname() :: iodata(). --export_type([socket/0, port_number/0, address/0, ipv4_address/0, octet/0, hostname/0]). +-export_type([socket/0, port_number/0, ip_address/0, ip4_address/0, hostname/0]). %%----------------------------------------------------------------------------- %% @param Socket the socket from which to obtain the port number @@ -61,7 +60,8 @@ close(Socket) -> %% This function should be called on a running socket instance. %% @end %%----------------------------------------------------------------------------- --spec sockname(Socket :: socket()) -> {ok, {address(), port_number()}} | {error, Reason :: term()}. +-spec sockname(Socket :: socket()) -> + {ok, {ip_address(), port_number()}} | {error, Reason :: term()}. sockname(Socket) -> call(Socket, {sockname}). @@ -72,7 +72,8 @@ sockname(Socket) -> %% This function should be called on a running socket instance. %% @end %%----------------------------------------------------------------------------- --spec peername(Socket :: socket()) -> {ok, {address(), port_number()}} | {error, Reason :: term()}. +-spec peername(Socket :: socket()) -> + {ok, {ip_address(), port_number()}} | {error, Reason :: term()}. peername(Socket) -> call(Socket, {peername}). diff --git a/libs/estdlib/src/ssl.erl b/libs/estdlib/src/ssl.erl new file mode 100644 index 000000000..ce74f6fe1 --- /dev/null +++ b/libs/estdlib/src/ssl.erl @@ -0,0 +1,397 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Paul Guyot +% +% 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(ssl). + +-export([ + start/0, + stop/0, + connect/3, + close/1, + send/2, + recv/2 +]). + +-behaviour(gen_server). +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2 +]). + +-export([ + nif_close_notify/1, + nif_conf_authmode/2, + nif_conf_rng/2, + nif_config_defaults/3, + nif_config_init/0, + nif_ctr_drbg_init/0, + nif_ctr_drbg_seed/3, + nif_entropy_init/0, + nif_handshake_step/1, + nif_init/0, + nif_read/2, + nif_set_bio/2, + nif_set_hostname/2, + nif_setup/2, + nif_write/2 +]). + +% Resources +-type entropy() :: binary(). +-type ctrdrbg() :: binary(). +-type sslcontext() :: binary(). +-type sslconfig() :: binary(). + +-opaque sslsocket() :: {sslcontext(), socket:socket()}. + +-export_type([ + sslsocket/0, + host/0, + hostname/0 +]). + +-type host() :: hostname() | ip_address(). +-type hostname() :: string(). +-type ip_address() :: inet:ip_address(). +-type tls_client_option() :: client_option(). +-type client_option() :: + {server_name_indication, sni()}. +-type sni() :: hostname() | disabled. +-type reason() :: any(). + +-spec start() -> ok. +start() -> + try + {ok, _Pid} = gen_server:start({local, ?MODULE}, ?MODULE, [], []) + catch + error:{badmatch, {error, {already_started, _}}} -> + ok + end, + ok. + +-spec stop() -> ok. +stop() -> + ok = gen_server:call(?MODULE, stop). + +-record(state, { + ctr_drbg :: ctrdrbg(), + entropy :: entropy() +}). + +init([]) -> + Entropy = ?MODULE:nif_entropy_init(), + CtrDrbg = ?MODULE:nif_ctr_drbg_init(), + ok = ?MODULE:nif_ctr_drbg_seed(CtrDrbg, Entropy, <<"AtomVM">>), + {ok, #state{entropy = Entropy, ctr_drbg = CtrDrbg}}. + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(get_ctr_drbg, _From, #state{ctr_drbg = CtrDrbg} = State) -> + {reply, CtrDrbg, State}; +handle_call(get_entropy, _From, #state{entropy = Entropy} = State) -> + {reply, Entropy, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Msg, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +-spec connect(Host :: host(), Port :: inet:port_number(), TLSOptions :: [tls_client_option()]) -> + {ok, sslsocket()} | {error, reason()}. +connect(Hostname, Port, TLSOptions) when + is_list(Hostname) andalso is_integer(Port) andalso is_list(TLSOptions) +-> + % Erlang OTP actually first checks some options + case net:getaddrinfo(Hostname) of + {ok, Results} -> + case + [ + Addr + || #{addr := #{addr := Addr}, type := stream, protocol := tcp, family := inet} <- + Results + ] + of + [TCPAddr | _] -> + NewTLSOptions = + case lists:keyfind(server_name_indication, 1, TLSOptions) of + false -> [{server_name_indication, Hostname} | TLSOptions]; + _ -> TLSOptions + end, + connect(TCPAddr, Port, NewTLSOptions); + [] -> + {error, nxdomain} + end; + {error, _} -> + {error, nxdomain} + end; +connect(Addr, Port, TLSOptions) when + is_tuple(Addr) andalso is_integer(Port) andalso is_list(TLSOptions) +-> + {ok, Socket} = socket:open(inet, stream, tcp), + case socket:connect(Socket, #{family => inet, addr => Addr, port => Port}) of + ok -> + connect(Socket, TLSOptions); + {error, _Reason} -> + {error, _Reason} + end. + +-spec connect(Socket :: socket:socket(), TLSOptions :: [tls_client_option()]) -> + {ok, sslsocket()} | {error, reason()}. +connect(Socket, TLSOptions) -> + SSLContext = ?MODULE:nif_init(), + ok = ?MODULE:nif_set_bio(SSLContext, Socket), + SSLConfig = ?MODULE:nif_config_init(), + ok = ?MODULE:nif_config_defaults(SSLConfig, client, stream), + process_options(SSLContext, SSLConfig, TLSOptions), + CtrDrbg = gen_server:call(?MODULE, get_ctr_drbg), + ok = ?MODULE:nif_conf_rng(SSLConfig, CtrDrbg), + ok = ?MODULE:nif_setup(SSLContext, SSLConfig), + handshake_loop(SSLContext, Socket). + +handshake_loop(SSLContext, Socket) -> + case ?MODULE:nif_handshake_step(SSLContext) of + ok -> + handshake_loop(SSLContext, Socket); + done -> + {ok, {SSLContext, Socket}}; + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + handshake_loop(SSLContext, Socket); + {closed, Ref} -> + ok = socket:close(Socket), + {error, closed} + end; + {error, _Reason} = Error -> + socket:close(Socket), + Error + end; + want_write -> + % We're currrently missing non-blocking writes + handshake_loop(SSLContext, Socket); + {error, _Reason} = Error -> + socket:close(Socket), + Error + end. + +-spec process_options( + SSLContext :: sslcontext(), SSLConfig :: sslconfig(), TLSOptions :: [tls_client_option()] +) -> ok. +process_options(_SSLContext, _SSLConfig, []) -> + ok; +process_options(SSLContext, SSLConfig, [{server_name_indication, disabled} | Tail]) -> + process_options(SSLContext, SSLConfig, Tail); +process_options(SSLContext, SSLConfig, [{server_name_indication, Hostname} | Tail]) -> + ok = ?MODULE:nif_set_hostname(SSLContext, Hostname), + process_options(SSLContext, SSLConfig, Tail); +process_options(SSLContext, SSLConfig, [{verify, verify_none} | Tail]) -> + ok = ?MODULE:nif_conf_authmode(SSLConfig, none), + process_options(SSLContext, SSLConfig, Tail); +process_options(SSLContext, SSLConfig, [{binary, true} | Tail]) -> + process_options(SSLContext, SSLConfig, Tail); +process_options(SSLContext, SSLConfig, [{active, false} | Tail]) -> + process_options(SSLContext, SSLConfig, Tail). + +-spec close(sslsocket()) -> ok. +close({SSLContext, Socket}) -> + _ = close_notify_loop(SSLContext, Socket), + ok = socket:close(Socket), + ok. + +close_notify_loop(SSLContext, Socket) -> + case ?MODULE:nif_close_notify(SSLContext) of + ok -> + ok; + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + close_notify_loop(SSLContext, Socket); + {closed, Ref} -> + ok = socket:close(Socket), + {error, closed} + end; + {error, _Reason} = Error -> + socket:close(Socket), + Error + end; + want_write -> + % We're currrently missing non-blocking writes + close_notify_loop(SSLContext, Socket); + {error, _Reason} = Error -> + socket:close(Socket), + Error + end. + +-spec send(Socket :: sslsocket(), Data :: iodata()) -> ok | {error, reason()}. +send(SSLSocket, IOList) when is_list(IOList) -> + send(SSLSocket, iolist_to_binary(IOList)); +send({SSLContext, Socket} = SSLSocket, Binary) -> + case ?MODULE:nif_write(SSLContext, Binary) of + ok -> + ok; + {ok, Rest} -> + send(SSLSocket, Rest); + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + send(SSLSocket, Binary); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + % We're currrently missing non-blocking writes + send(SSLSocket, Binary); + {error, _Reason} = Error -> + Error + end. + +-spec recv(Socket :: sslsocket(), Length :: non_neg_integer()) -> ok | {error, reason()}. +recv(SSLSocket, Length) -> + recv0(SSLSocket, Length, []). + +recv0(_SSLSocket, 0, Acc) -> + {ok, list_to_binary(lists:reverse(Acc))}; +recv0({SSLContext, Socket} = SSLSocket, Remaining, Acc) -> + case ?MODULE:nif_read(SSLContext, Remaining) of + {ok, Data} -> + Len = byte_size(Data), + recv0(SSLSocket, Remaining - Len, [Data | Acc]); + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + recv0(SSLSocket, Remaining, Acc); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + % We're currrently missing non-blocking writes + recv0(SSLSocket, Remaining, Acc); + {error, _Reason} = Error -> + Error + end. + +%%----------------------------------------------------------------------------- +%% NIF Functions +%%----------------------------------------------------------------------------- + +%% @private +-spec nif_entropy_init() -> entropy(). +nif_entropy_init() -> + erlang:nif_error(undefined). + +%% @private +-spec nif_ctr_drbg_init() -> ctrdrbg(). +nif_ctr_drbg_init() -> + erlang:nif_error(undefined). + +%% @private +-spec nif_ctr_drbg_seed(CtrDrbg :: ctrdrbg(), Entropy :: entropy(), Custom :: binary()) -> ok. +nif_ctr_drbg_seed(_CtrDrbg, _Entropy, _Custom) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_init() -> sslcontext(). +nif_init() -> + erlang:nif_error(undefined). + +%% @private +-spec nif_config_init() -> sslconfig(). +nif_config_init() -> + erlang:nif_error(undefined). + +%% @private +-spec nif_config_defaults( + Config :: sslconfig(), Endpoint :: client | server, Transport :: stream | dgram +) -> ok. +nif_config_defaults(_Config, _Endpoint, _Transport) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_conf_authmode(Config :: sslconfig(), none | optional | required) -> ok. +nif_conf_authmode(_Config, _AuthMode) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_conf_rng(Config :: sslconfig(), CtrDrbg :: ctrdrbg()) -> ok. +nif_conf_rng(_Config, _CtrDrbg) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_setup(Context :: sslcontext(), Config :: sslconfig()) -> ok. +nif_setup(_Context, _Config) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_set_bio(Context :: sslcontext(), Socket :: socket:socket()) -> ok. +nif_set_bio(_Context, _Socket) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_set_hostname(Context :: sslcontext(), Hostname :: hostname()) -> ok. +nif_set_hostname(_Context, _Hostname) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_handshake_step(Context :: sslcontext()) -> + ok | done | want_read | want_write | {error, reason()}. +nif_handshake_step(_Context) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_close_notify(Context :: sslcontext()) -> ok | want_read | want_write | {error, reason()}. +nif_close_notify(_Context) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_read(Context :: sslcontext(), Length :: non_neg_integer()) -> + {ok, binary()} | want_read | want_write | {error, reason()}. +nif_read(_Context, _Length) -> + erlang:nif_error(undefined). + +%% @private +-spec nif_write(Context :: sslcontext(), Len :: non_neg_integer()) -> + {ok, binary()} | want_read | want_write | {error, reason()}. +nif_write(_Context, _Binary) -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index 25dded5ec..62ff40533 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -216,9 +216,17 @@ COLD_FUNC void globalcontext_destroy(GlobalContext *glb) synclist_destroy(&glb->select_events); // Destroy refc binaries including resources + // (this list should be empty if resources were properly refcounted) struct ListHead *refc_binaries = synclist_nolock(&glb->refc_binaries); MUTABLE_LIST_FOR_EACH (item, tmp, refc_binaries) { struct RefcBinary *refc = GET_LIST_ENTRY(item, struct RefcBinary, head); +#ifndef NDEBUG + if (refc->resource_type) { + fprintf(stderr, "Warning, dangling resource of type %s, ref_count = %d\n", refc->resource_type->name, (int) refc->ref_count); + } else { + fprintf(stderr, "Warning, dangling refc binary, ref_count = %d\n", (int) refc->ref_count); + } +#endif refc_binary_destroy(refc, glb); } synclist_destroy(&glb->refc_binaries); diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index a950552fe..8fca3e3ee 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -557,12 +557,18 @@ static term nif_socket_open(Context *ctx, int argc, term argv[]) } } -static term get_socket(term socket_term) +bool term_to_otp_socket(term socket_term, struct SocketResource **rsrc_obj, Context *ctx) { - return term_get_tuple_element(socket_term, 0); + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), term_get_tuple_element(socket_term, 0), socket_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; + + return true; } -static bool term_is_socket(term socket_term) +bool term_is_otp_socket(term socket_term) { bool ret = term_is_tuple(socket_term) && term_get_tuple_arity(socket_term) == 2 @@ -609,13 +615,12 @@ static term nif_socket_close(Context *ctx, int argc, term argv[]) TRACE("nif_socket_close\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd) { if (rsrc_obj->selecting_process_id != INVALID_PROCESS_ID) { @@ -867,17 +872,16 @@ static term nif_socket_select_read(Context *ctx, int argc, term argv[]) UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); term select_ref_term = argv[1]; if (select_ref_term != UNDEFINED_ATOM) { VALIDATE_VALUE(select_ref_term, term_is_reference); } - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; ErlNifEnv *env = erl_nif_env_from_context(ctx); if (rsrc_obj->selecting_process_id != ctx->process_id && rsrc_obj->selecting_process_id != INVALID_PROCESS_ID) { @@ -961,13 +965,12 @@ static term nif_socket_select_stop(Context *ctx, int argc, term argv[]) TRACE("nif_socket_stop\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (UNLIKELY(enif_select(erl_nif_env_from_context(ctx), rsrc_obj->fd, ERL_NIF_SELECT_STOP, rsrc_obj, NULL, term_nil()) < 0)) { RAISE_ERROR(BADARG_ATOM); @@ -994,15 +997,14 @@ static term nif_socket_setopt(Context *ctx, int argc, term argv[]) TRACE("nif_socket_setopt\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { @@ -1082,15 +1084,14 @@ static term nif_socket_sockname(Context *ctx, int argc, term argv[]) TRACE("nif_socket_sockname\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { #elif OTP_SOCKET_LWIP @@ -1150,15 +1151,14 @@ static term nif_socket_peername(Context *ctx, int argc, term argv[]) TRACE("nif_socket_peername\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { @@ -1220,15 +1220,14 @@ static term nif_socket_bind(Context *ctx, int argc, term argv[]) TRACE("nif_socket_bind\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD TRACE("rsrc_obj->fd=%i\n", (int) rsrc_obj->fd); if (rsrc_obj->fd == 0) { @@ -1326,14 +1325,13 @@ static term nif_socket_listen(Context *ctx, int argc, term argv[]) GlobalContext *global = ctx->global; - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); VALIDATE_VALUE(argv[1], term_is_integer); - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); @@ -1405,15 +1403,14 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) TRACE("nif_socket_accept\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); @@ -1506,6 +1503,146 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) // // recv/recvfrom // +#if OTP_SOCKET_LWIP +static size_t copy_pbuf_data(struct pbuf *src, size_t offset, size_t count, uint8_t *dst) +{ + size_t copied = 0; + while (count > 0 && src != NULL) { + if (offset > src->len) { + offset -= src->len; + src = src->next; + continue; + } + size_t chunk_count = MIN(count, src->len - offset); + memcpy(dst, ((const uint8_t *) src->payload) + offset, chunk_count); + count -= chunk_count; + copied += chunk_count; + dst += chunk_count; + src = src->next; + } + return copied; +} +#endif + +ssize_t socket_recv(struct SocketResource *rsrc_obj, uint8_t *buf, size_t len, int flags, term *from, Heap *heap) +{ +#if OTP_SOCKET_BSD + // + // receive data on the socket + // + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + ssize_t res; + if (from) { + struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); + GlobalContext *global = rsrc_refc->resource_type->global; + + res = recvfrom(rsrc_obj->fd, buf, len, flags, (struct sockaddr *) &addr, &addrlen); + + term address = inet_make_addr4(ntohl(addr.sin_addr.s_addr), heap); + term port_number = term_from_int(ntohs(addr.sin_port)); + + term map = term_alloc_map(2, heap); + term_set_map_assoc(map, 0, ADDR_ATOM, address); + term_set_map_assoc(map, 1, PORT_ATOM, port_number); + *from = map; + } else { + res = recv(rsrc_obj->fd, buf, len, flags); + } + if (res == 0) { + return SocketClosed; + } + if (res < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return SocketWouldBlock; + } + return SocketOtherError; + } + return res; +#elif OTP_SOCKET_LWIP + UNUSED(flags); + + uint32_t ip4_u32; + uint16_t port_u16; + + size_t remaining = len; + uint8_t *ptr = buf; + bool closed = false; + err_t err = ERR_OK; + // Use lwIP lock + LWIP_BEGIN(); + if (rsrc_obj->socket_state & SocketStateTCP) { + size_t pos = rsrc_obj->pos; + struct ListHead *item; + struct ListHead *tmp; + MUTABLE_LIST_FOR_EACH (item, tmp, &rsrc_obj->received_list) { + struct TCPReceivedItem *received_item = GET_LIST_ENTRY(item, struct TCPReceivedItem, list_head); + if (received_item->buf == NULL || received_item->err != ERR_OK) { + closed = received_item->buf == NULL; + err = received_item->err; + break; + } + if (pos < received_item->buf->tot_len) { + size_t copied = copy_pbuf_data(received_item->buf, pos, remaining, ptr); + ptr += copied; + remaining -= copied; + tcp_recved(rsrc_obj->tcp_pcb, copied); + if (copied + pos == received_item->buf->tot_len) { + // all data was copied. + list_remove(item); + pbuf_free(received_item->buf); + pos = 0; + } else { + pos = pos + copied; + } + if (remaining == 0) { + break; + } + } else { + pos -= received_item->buf->tot_len; + } + } + rsrc_obj->pos = pos; + if (from) { + ip4_u32 = ntohl(ip_addr_get_ip4_u32(&rsrc_obj->tcp_pcb->remote_ip)); + port_u16 = rsrc_obj->tcp_pcb->remote_port; + } + } else { + struct UDPReceivedItem *first_item = CONTAINER_OF(list_first(&rsrc_obj->received_list), struct UDPReceivedItem, list_head); + size_t copied = copy_pbuf_data(first_item->buf, 0, remaining, ptr); + remaining -= copied; + if (from) { + ip4_u32 = first_item->addr; + port_u16 = first_item->port; + } + list_remove(&first_item->list_head); + pbuf_free(first_item->buf); + free(first_item); + } + LWIP_END(); + if (remaining < len) { + if (from) { + struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); + GlobalContext *global = rsrc_refc->resource_type->global; + + term address = inet_make_addr4(ip4_u32, heap); + term port_number = term_from_int(port_u16); + + term map = term_alloc_map(2, heap); + term_set_map_assoc(map, 0, globalcontext_make_atom(global, addr_atom), address); + term_set_map_assoc(map, 1, PORT_ATOM, port_number); + + *from = map; + } + + return len - remaining; + } + if (closed) { + return SocketClosed; + } + return err == ERR_OK ? SocketWouldBlock : SocketOtherError; +#endif +} #if OTP_SOCKET_BSD static term nif_socket_recv_with_peek(Context *ctx, struct SocketResource *rsrc_obj, size_t len, bool is_recvfrom) @@ -1530,7 +1667,7 @@ static term nif_socket_recv_with_peek(Context *ctx, struct SocketResource *rsrc_ // {ok, Data :: binary()} // {ok, {Source :: #{addr => Address :: {0..255, 0..255, 0..255, 0..255}, port => Port :: non_neg_integer()}, Data :: binary()}} size_t ensure_packet_avail = term_binary_data_size_in_terms(buffer_size) + BINARY_HEADER_SIZE; - size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + TUPLE_SIZE(4) + term_map_size_in_terms(2)) : 0); + size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) : 0); if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); @@ -1538,25 +1675,18 @@ static term nif_socket_recv_with_peek(Context *ctx, struct SocketResource *rsrc_ } term data = term_create_uninitialized_binary(buffer_size, &ctx->heap, global); - const char *buffer = term_binary_data(data); + uint8_t *buffer = (uint8_t *) term_binary_data(data); // // receive data on the socket // - struct sockaddr_in addr; - socklen_t addrlen = sizeof(addr); - res = recvfrom(rsrc_obj->fd, (char *) buffer, buffer_size, flags, (struct sockaddr *) &addr, &addrlen); + term map = term_invalid_term(); + res = socket_recv(rsrc_obj, buffer, buffer_size, flags, is_recvfrom ? &map : NULL, &ctx->heap); TRACE("otp_socket.recv_handler: received data on fd: %i available=%lu, read=%lu\n", rsrc_obj->fd, (unsigned long) res, (unsigned long) buffer_size); - term payload = term_invalid_term(); + term payload; if (is_recvfrom) { - term address = inet_make_addr4(ntohl(addr.sin_addr.s_addr), &ctx->heap); - term port_number = term_from_int(ntohs(addr.sin_port)); - - term map = term_alloc_map(2, &ctx->heap); - term_set_map_assoc(map, 0, ADDR_ATOM, address); - term_set_map_assoc(map, 1, PORT_ATOM, port_number); term tuple = port_heap_create_tuple2(&ctx->heap, map, data); payload = port_heap_create_ok_tuple(&ctx->heap, tuple); } else { @@ -1575,7 +1705,7 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs // TODO plumb through buffer size size_t buffer_size = len == 0 ? DEFAULT_BUFFER_SIZE : len; - char *buffer = malloc(buffer_size); + uint8_t *buffer = malloc(buffer_size); term payload = term_invalid_term(); if (IS_NULL_PTR(buffer)) { @@ -1584,10 +1714,15 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs } else { - int flags = 0; - struct sockaddr_in addr; - socklen_t addrlen = sizeof(addr); - ssize_t res = recvfrom(rsrc_obj->fd, (char *) buffer, buffer_size, flags, (struct sockaddr *) &addr, &addrlen); + term map = term_invalid_term(); + if (is_recvfrom) { + if (UNLIKELY(memory_ensure_free(ctx, INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + } + + ssize_t res = socket_recv(rsrc_obj, buffer, buffer_size, 0, is_recvfrom ? &map : NULL, &ctx->heap); if (res < 0) { @@ -1613,9 +1748,9 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs // {ok, Data :: binary()} // {ok, {Source :: #{addr => Address :: {0..255, 0..255, 0..255, 0..255}, port => Port :: non_neg_integer()}, Data :: binary()}} size_t ensure_packet_avail = term_binary_data_size_in_terms(len) + BINARY_HEADER_SIZE; - size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + TUPLE_SIZE(4) + term_map_size_in_terms(2)) : 0); + size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? TUPLE_SIZE(2) : 0); - if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, is_recvfrom ? 1 : 0, &map, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -1623,12 +1758,6 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs term data = term_from_literal_binary(buffer, len, &ctx->heap, global); if (is_recvfrom) { - term address = inet_make_addr4(ntohl(addr.sin_addr.s_addr), &ctx->heap); - term port_number = term_from_int(ntohs(addr.sin_port)); - - term map = term_alloc_map(2, &ctx->heap); - term_set_map_assoc(map, 0, ADDR_ATOM, address); - term_set_map_assoc(map, 1, PORT_ATOM, port_number); term tuple = port_heap_create_tuple2(&ctx->heap, map, data); payload = port_heap_create_ok_tuple(&ctx->heap, tuple); } else { @@ -1642,24 +1771,6 @@ static term nif_socket_recv_without_peek(Context *ctx, struct SocketResource *rs } #elif OTP_SOCKET_LWIP -static size_t copy_pbuf_data(struct pbuf *src, size_t offset, size_t count, uint8_t *dst) -{ - size_t copied = 0; - while (count > 0 && src != NULL) { - if (offset > src->len) { - offset -= src->len; - src = src->next; - continue; - } - size_t chunk_count = MIN(count, src->len - offset); - memcpy(dst, src->payload, chunk_count); - count -= chunk_count; - copied += chunk_count; - dst += chunk_count; - src = src->next; - } - return copied; -} static term nif_socket_recv_lwip(Context *ctx, struct SocketResource *rsrc_obj, size_t len, bool is_recvfrom) { @@ -1713,7 +1824,7 @@ static term nif_socket_recv_lwip(Context *ctx, struct SocketResource *rsrc_obj, } size_t ensure_packet_avail = term_binary_data_size_in_terms(len) + BINARY_HEADER_SIZE; - size_t requested_size = REF_SIZE + 2 * TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + term_map_size_in_terms(2)) : 0); + size_t requested_size = REF_SIZE + 2 * TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) : 0); if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -1723,66 +1834,14 @@ static term nif_socket_recv_lwip(Context *ctx, struct SocketResource *rsrc_obj, uint8_t *ptr = (uint8_t *) term_binary_data(data); size_t remaining = buffer_size; - uint32_t ip4_u32; - uint16_t port_u16; + term map = term_invalid_term(); - // Use lwIP lock - LWIP_BEGIN(); - if (rsrc_obj->socket_state & SocketStateTCP) { - size_t pos = rsrc_obj->pos; - struct ListHead *item; - struct ListHead *tmp; - MUTABLE_LIST_FOR_EACH (item, tmp, &rsrc_obj->received_list) { - struct TCPReceivedItem *received_item = GET_LIST_ENTRY(item, struct TCPReceivedItem, list_head); - if (received_item->buf == NULL || received_item->err != ERR_OK) { - break; - } - if (pos < received_item->buf->tot_len) { - size_t copied = copy_pbuf_data(received_item->buf, pos, remaining, ptr); - ptr += copied; - tcp_recved(rsrc_obj->tcp_pcb, copied); - if (copied + pos == received_item->buf->tot_len) { - // all data was copied. - list_remove(item); - pbuf_free(received_item->buf); - pos = 0; - } else { - pos = pos + copied; - } - if (remaining == 0) { - break; - } - } else { - pos -= received_item->buf->tot_len; - } - } - rsrc_obj->pos = pos; - if (is_recvfrom) { - ip4_u32 = ntohl(ip_addr_get_ip4_u32(&rsrc_obj->tcp_pcb->remote_ip)); - port_u16 = rsrc_obj->tcp_pcb->remote_port; - } - } else { - struct UDPReceivedItem *first_item = CONTAINER_OF(list_first(&rsrc_obj->received_list), struct UDPReceivedItem, list_head); - copy_pbuf_data(first_item->buf, 0, remaining, ptr); - if (is_recvfrom) { - ip4_u32 = first_item->addr; - port_u16 = first_item->port; - } - list_remove(&first_item->list_head); - pbuf_free(first_item->buf); - free(first_item); - } - LWIP_END(); + ssize_t result = socket_recv(rsrc_obj, ptr, remaining, 0, is_recvfrom ? &map : NULL, &ctx->heap); + UNUSED(result); term payload; if (is_recvfrom) { - term address = inet_make_addr4(ip4_u32, &ctx->heap); - term port_number = term_from_int(port_u16); - - term map = term_alloc_map(2, &ctx->heap); - term_set_map_assoc(map, 0, ADDR_ATOM, address); - term_set_map_assoc(map, 1, PORT_ATOM, port_number); term tuple = port_heap_create_tuple2(&ctx->heap, map, data); payload = port_heap_create_ok_tuple(&ctx->heap, tuple); } else { @@ -1795,7 +1854,7 @@ static term nif_socket_recv_lwip(Context *ctx, struct SocketResource *rsrc_obj, static term nif_socket_recv_internal(Context *ctx, term argv[], bool is_recvfrom) { - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); VALIDATE_VALUE(argv[1], term_is_integer); avm_int_t len = term_to_int(argv[1]); // We raise badarg but return error tuples for POSIX errors @@ -1803,11 +1862,10 @@ static term nif_socket_recv_internal(Context *ctx, term argv[], bool is_recvfrom RAISE_ERROR(BADARG_ATOM); } - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { - return make_error_tuple(posix_errno_to_term(EINVAL, ctx->global), ctx); + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { + RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; #if OTP_SOCKET_BSD if (rsrc_obj->fd == 0) { return make_error_tuple(posix_errno_to_term(EBADF, ctx->global), ctx); @@ -1851,50 +1909,13 @@ static term nif_socket_recvfrom(Context *ctx, int argc, term argv[]) // // send/sendto // -static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool is_sendto) +ssize_t socket_send(struct SocketResource *rsrc_obj, const uint8_t *buf, size_t len, term dest) { - TRACE("nif_socket_send_internal\n"); - UNUSED(argc); - - VALIDATE_VALUE(argv[0], term_is_socket); - VALIDATE_VALUE(argv[1], term_is_binary); - - GlobalContext *global = ctx->global; - - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { - RAISE_ERROR(BADARG_ATOM); - } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; - -#if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); - } -#elif OTP_SOCKET_LWIP - if (rsrc_obj->socket_state == SocketStateClosed) { - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); - } - if (rsrc_obj->socket_state & SocketStateListening) { - return make_error_tuple(posix_errno_to_term(EOPNOTSUPP, global), ctx); - } -#endif - - term data = argv[1]; - term dest = term_invalid_term(); - if (is_sendto) { - dest = argv[2]; - } - - const char *buf = term_binary_data(data); - size_t len = term_binary_size(data); - ssize_t sent_data = -1; - #if OTP_SOCKET_BSD - // TODO make non-blocking - - if (is_sendto) { + if (!term_is_invalid_term(dest)) { + struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); + GlobalContext *global = rsrc_refc->resource_type->global; struct sockaddr_in destaddr; memset(&destaddr, 0, sizeof(destaddr)); @@ -1902,7 +1923,7 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i term port = interop_kv_get_value_default(dest, port_atom, term_from_int(0), global); destaddr.sin_port = htons(term_to_int(port)); - term addr = interop_kv_get_value(dest, addr_atom, ctx->global); + term addr = interop_kv_get_value(dest, addr_atom, global); if (globalcontext_is_term_equal_to_atom_string(global, addr, loopback_atom)) { destaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); } else { @@ -1914,20 +1935,31 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i } else { sent_data = send(rsrc_obj->fd, buf, len, 0); } - + if (sent_data == 0) { + return SocketClosed; + } + if (sent_data < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return SocketWouldBlock; + } + return SocketOtherError; + } + return sent_data; #elif OTP_SOCKET_LWIP err_t err; ip_addr_t ip_addr; uint16_t port_u16; - if (is_sendto) { + if (!term_is_invalid_term(dest)) { + struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); + GlobalContext *global = rsrc_refc->resource_type->global; term port_term = interop_kv_get_value_default(dest, port_atom, term_from_int(0), global); avm_int_t port_number = term_to_int(port_term); if (port_number < 0 || port_number > 65535) { - RAISE_ERROR(BADARG_ATOM); + return SocketOtherError; } port_u16 = (uint16_t) port_number; - term addr = interop_kv_get_value(dest, addr_atom, ctx->global); + term addr = interop_kv_get_value(dest, addr_atom, global); if (globalcontext_is_term_equal_to_atom_string(global, addr, loopback_atom)) { ip_addr_set_loopback(false, &ip_addr); } else { @@ -1938,9 +1970,9 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i LWIP_BEGIN(); if (rsrc_obj->socket_state & SocketStateUDP) { struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); - char *bytes = (char *) p->payload; + uint8_t *bytes = (uint8_t *) p->payload; memcpy(bytes, buf, len); - if (is_sendto) { + if (!term_is_invalid_term(dest)) { err = udp_sendto(rsrc_obj->udp_pcb, p, &ip_addr, port_u16); } else { err = udp_send(rsrc_obj->udp_pcb, p); @@ -1970,8 +2002,58 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i } } LWIP_END(); + if (err == ERR_CLSD) { + return SocketClosed; + } + if (sent_data == 0) { + return SocketWouldBlock; + } + if (err != ERR_OK) { + return SocketOtherError; + } + return sent_data; +#endif +} + +static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool is_sendto) +{ + TRACE("nif_socket_send_internal\n"); + UNUSED(argc); + VALIDATE_VALUE(argv[0], term_is_otp_socket); + VALIDATE_VALUE(argv[1], term_is_binary); + + GlobalContext *global = ctx->global; + + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { + RAISE_ERROR(BADARG_ATOM); + } + +#if OTP_SOCKET_BSD + if (rsrc_obj->fd == 0) { + return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + } +#elif OTP_SOCKET_LWIP + if (rsrc_obj->socket_state == SocketStateClosed) { + return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + } + if (rsrc_obj->socket_state & SocketStateListening) { + return make_error_tuple(posix_errno_to_term(EOPNOTSUPP, global), ctx); + } #endif + + term data = argv[1]; + term dest = term_invalid_term(); + if (is_sendto) { + dest = argv[2]; + } + + const uint8_t *buf = (const uint8_t *) term_binary_data(data); + size_t len = term_binary_size(data); + + ssize_t sent_data = socket_send(rsrc_obj, buf, len, dest); + // {ok, RestData} | {error, Reason} size_t rest_len = len - sent_data; @@ -2078,16 +2160,15 @@ static term nif_socket_connect(Context *ctx, int argc, term argv[]) TRACE("nif_socket_connect\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); VALIDATE_VALUE(argv[1], term_is_map); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; term sockaddr = argv[1]; term port = interop_kv_get_value_default(sockaddr, port_atom, term_from_int(0), ctx->global); @@ -2181,16 +2262,15 @@ static term nif_socket_shutdown(Context *ctx, int argc, term argv[]) TRACE("nif_socket_shutdown\n"); UNUSED(argc); - VALIDATE_VALUE(argv[0], term_is_socket); + VALIDATE_VALUE(argv[0], term_is_otp_socket); VALIDATE_VALUE(argv[1], term_is_atom); GlobalContext *global = ctx->global; - void *rsrc_obj_ptr; - if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), get_socket(argv[0]), socket_resource_type, &rsrc_obj_ptr))) { + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { RAISE_ERROR(BADARG_ATOM); } - struct SocketResource *rsrc_obj = (struct SocketResource *) rsrc_obj_ptr; int how; int val = interop_atom_term_select_int(otp_socket_shutdown_direction_table, argv[1], global); diff --git a/src/libAtomVM/otp_socket.h b/src/libAtomVM/otp_socket.h index 04d12e0c3..e42e3abd7 100644 --- a/src/libAtomVM/otp_socket.h +++ b/src/libAtomVM/otp_socket.h @@ -39,9 +39,59 @@ extern "C" { #endif #endif +enum SocketErrors +{ + SocketClosed = 0, + SocketWouldBlock = -1, + SocketOtherError = -2 +}; + +struct SocketResource; + const struct Nif *otp_socket_nif_get_nif(const char *nifname); void otp_socket_init(GlobalContext *global); +/** + * @brief Get the resource object associated with a socket term. + * + * @param socket_term the term with the socket + * @param otp_socket on output, the socket resource + * @return true in case of success + */ +bool term_to_otp_socket(term socket_term, struct SocketResource **otp_socket, Context *ctx); + +/** + * @brief Determine if a term is a socket term. + * + * @param socket_term the term to test + * @return true if it is a term + */ +bool term_is_otp_socket(term socket_term); + +/** + * @brief Send data to a socket (without blocking) + * + * @param otp_socket the socket resource + * @param buf buffer to send + * @param len number of bytes + * @param dest destination address or invalid term for sendto/send + * @return the number of written bytes or a value from SocketErrors + */ +ssize_t socket_send(struct SocketResource *socket, const uint8_t *buf, size_t len, term dest); + +/** + * @brief Read data from a socket. + * + * @param otp_socket the socket resource + * @param buf buffer to store data + * @param len number of bytes + * @param flags flags passed to recvfrom + * @param from filled with origin address using recvfrom (can be NULL) + * @param heap heap to build the origin address term (can be NULL if from is NULL) + * @return the number of read bytes or a value from SocketErrors + */ +ssize_t socket_recv(struct SocketResource *socket, uint8_t *buf, size_t len, int flags, term *from, Heap *heap); + #if OTP_SOCKET_LWIP struct LWIPEvent { diff --git a/src/libAtomVM/otp_ssl.c b/src/libAtomVM/otp_ssl.c new file mode 100644 index 000000000..b2a485386 --- /dev/null +++ b/src/libAtomVM/otp_ssl.c @@ -0,0 +1,795 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Paul Guyot + * + * 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 +#include +#include + +#include + +#include +#include +#include + +// #define ENABLE_TRACE +#include + +#ifndef MBEDTLS_PRIVATE +#define MBEDTLS_PRIVATE(member) member +#endif + +// Default read buffer if mbedtls_ssl_get_max_in_record_payload fails +#define DEFAULT_READ_BUFFER_FALLBACK 512 + +#if defined(MBEDTLS_DEBUG_C) && defined(ENABLE_TRACE) + +#include + +static void mbedtls_debug_cb(void *ctx, int level, const char *filename, int line, const char *msg) +{ + UNUSED(ctx); + UNUSED(level); + + TRACE("%s:%d: %s", filename, line, msg); +} + +#endif + +// +// Resources +// + +struct EntropyContextResource +{ + mbedtls_entropy_context context; +}; + +struct CtrDrbgResource +{ + mbedtls_ctr_drbg_context context; +}; + +struct SSLContextResource +{ + mbedtls_ssl_context context; +}; + +struct SSLConfigResource +{ + mbedtls_ssl_config config; +}; + +static void entropycontext_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + + struct EntropyContextResource *rsrc_obj = (struct EntropyContextResource *) obj; + mbedtls_entropy_free(&rsrc_obj->context); +} + +static void ctrdrbg_dtor(ErlNifEnv *caller_env, void *obj) +{ + TRACE("%s\n", __func__); + UNUSED(caller_env); + + struct CtrDrbgResource *rsrc_obj = (struct CtrDrbgResource *) obj; + // Release the entropy + mbedtls_entropy_context *entropy_context = rsrc_obj->context.MBEDTLS_PRIVATE(p_entropy); + if (entropy_context) { + struct EntropyContextResource *entropy_obj = CONTAINER_OF(entropy_context, struct EntropyContextResource, context); + struct RefcBinary *entropy_refc = refc_binary_from_data(entropy_obj); + refc_binary_decrement_refcount(entropy_refc, caller_env->global); + } + mbedtls_ctr_drbg_free(&rsrc_obj->context); +} + +static void sslcontext_dtor(ErlNifEnv *caller_env, void *obj) +{ + TRACE("%s\n", __func__); + UNUSED(caller_env); + + struct SSLContextResource *rsrc_obj = (struct SSLContextResource *) obj; + // Release the config + const mbedtls_ssl_config *config = rsrc_obj->context.MBEDTLS_PRIVATE(conf); + if (config) { + struct SSLConfigResource *config_obj = CONTAINER_OF(config, struct SSLConfigResource, config); + struct RefcBinary *config_refc = refc_binary_from_data(config_obj); + refc_binary_decrement_refcount(config_refc, caller_env->global); + } + mbedtls_ssl_free(&rsrc_obj->context); +} + +static void sslconfig_dtor(ErlNifEnv *caller_env, void *obj) +{ + TRACE("%s\n", __func__); + UNUSED(caller_env); + + struct SSLConfigResource *rsrc_obj = (struct SSLConfigResource *) obj; + mbedtls_ssl_config_free(&rsrc_obj->config); +} + +static const ErlNifResourceTypeInit EntropyContextResourceTypeInit = { + .members = 1, + .dtor = entropycontext_dtor, +}; +static const ErlNifResourceTypeInit CtrDrbgResourceTypeInit = { + .members = 1, + .dtor = ctrdrbg_dtor, +}; +static const ErlNifResourceTypeInit SSLContextResourceTypeInit = { + .members = 1, + .dtor = sslcontext_dtor, +}; +static const ErlNifResourceTypeInit SSLConfigResourceTypeInit = { + .members = 1, + .dtor = sslconfig_dtor, +}; + +static ErlNifResourceType *entropycontext_resource_type; +static ErlNifResourceType *ctrdrbg_resource_type; +static ErlNifResourceType *sslcontext_resource_type; +static ErlNifResourceType *sslconfig_resource_type; + +// +// Interface with sockets +// +int mbedtls_ssl_send_cb(void *ctx, const unsigned char *buf, size_t len) +{ + TRACE("%s\n", __func__); + ssize_t res = socket_send((struct SocketResource *) ctx, buf, len, term_invalid_term()); + if (res == SocketWouldBlock) { + return MBEDTLS_ERR_SSL_WANT_WRITE; + } + return res; +} + +int mbedtls_ssl_recv_cb(void *ctx, unsigned char *buf, size_t len) +{ + TRACE("%s\n", __func__); + ssize_t res = socket_recv((struct SocketResource *) ctx, buf, len, 0, NULL, NULL); + if (res == SocketWouldBlock) { + return MBEDTLS_ERR_SSL_WANT_READ; + } + return res; +} + +// +// Interop +// + +#define UNKNOWN_TABLE_VALUE -1 + +static const AtomStringIntPair endpoint_table[] = { + { ATOM_STR("\x6", "client"), MBEDTLS_SSL_IS_CLIENT }, + { ATOM_STR("\x6", "server"), MBEDTLS_SSL_IS_SERVER }, + SELECT_INT_DEFAULT(UNKNOWN_TABLE_VALUE) +}; + +static const AtomStringIntPair authmode_table[] = { + { ATOM_STR("\x4", "none"), MBEDTLS_SSL_VERIFY_NONE }, + { ATOM_STR("\x8", "optional"), MBEDTLS_SSL_VERIFY_OPTIONAL }, + { ATOM_STR("\x8", "required"), MBEDTLS_SSL_VERIFY_REQUIRED }, + SELECT_INT_DEFAULT(UNKNOWN_TABLE_VALUE) +}; + +// +// Nifs +// + +static term nif_ssl_entropy_init(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + struct EntropyContextResource *rsrc_obj = enif_alloc_resource(entropycontext_resource_type, sizeof(struct EntropyContextResource)); + if (IS_NULL_PTR(rsrc_obj)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + + mbedtls_entropy_init(&rsrc_obj->context); + + return obj; +} + +static term nif_ssl_ctr_drbg_init(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + struct CtrDrbgResource *rsrc_obj = enif_alloc_resource(ctrdrbg_resource_type, sizeof(struct CtrDrbgResource)); + if (IS_NULL_PTR(rsrc_obj)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + + mbedtls_ctr_drbg_init(&rsrc_obj->context); + + return obj; +} + +static term nif_ssl_ctr_drbg_seed(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + VALIDATE_VALUE(argv[2], term_is_binary); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], ctrdrbg_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct CtrDrbgResource *ctrdrbg_obj = (struct CtrDrbgResource *) rsrc_obj_ptr; + + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[1], entropycontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct EntropyContextResource *entropy_obj = (struct EntropyContextResource *) rsrc_obj_ptr; + + int err = mbedtls_ctr_drbg_seed(&ctrdrbg_obj->context, mbedtls_entropy_func, &entropy_obj->context, (const unsigned char *) term_binary_data(argv[2]), term_binary_size(argv[2])); + if (UNLIKELY(err)) { + RAISE_ERROR(BADARG_ATOM); + } + + struct RefcBinary *entropy_refc = refc_binary_from_data(entropy_obj); + refc_binary_increment_refcount(entropy_refc); + + return OK_ATOM; +} + +static term nif_ssl_init(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + struct SSLContextResource *rsrc_obj = enif_alloc_resource(sslcontext_resource_type, sizeof(struct SSLContextResource)); + if (IS_NULL_PTR(rsrc_obj)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + + mbedtls_ssl_init(&rsrc_obj->context); + + return obj; +} + +static term nif_ssl_set_bio(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + VALIDATE_VALUE(argv[1], term_is_otp_socket); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *rsrc_obj = (struct SSLContextResource *) rsrc_obj_ptr; + + struct SocketResource *socket_resource; + if (UNLIKELY(!term_to_otp_socket(argv[1], &socket_resource, ctx))) { + RAISE_ERROR(BADARG_ATOM); + } + + mbedtls_ssl_set_bio(&rsrc_obj->context, socket_resource, mbedtls_ssl_send_cb, mbedtls_ssl_recv_cb, NULL); + + return OK_ATOM; +} + +static term nif_ssl_config_init(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + struct SSLConfigResource *rsrc_obj = enif_alloc_resource(sslconfig_resource_type, sizeof(struct SSLConfigResource)); + if (IS_NULL_PTR(rsrc_obj)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + + mbedtls_ssl_config_init(&rsrc_obj->config); + +#if defined(MBEDTLS_DEBUG_C) && defined(ENABLE_TRACE) + mbedtls_ssl_conf_dbg(&rsrc_obj->config, mbedtls_debug_cb, NULL); +#endif + + return obj; +} + +static term nif_ssl_config_defaults(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + VALIDATE_VALUE(argv[1], term_is_atom); + VALIDATE_VALUE(argv[2], term_is_atom); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslconfig_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLConfigResource *rsrc_obj = (struct SSLConfigResource *) rsrc_obj_ptr; + + int endpoint = interop_atom_term_select_int(endpoint_table, argv[1], ctx->global); + if (UNLIKELY(endpoint == UNKNOWN_TABLE_VALUE)) { + RAISE_ERROR(BADARG_ATOM); + } + + enum inet_type transport_type = inet_atom_to_type(argv[2], ctx->global); + if (UNLIKELY(transport_type != InetStreamType && transport_type != InetDgramType)) { + RAISE_ERROR(BADARG_ATOM); + } + int transport = transport_type == InetStreamType ? MBEDTLS_SSL_TRANSPORT_STREAM : MBEDTLS_SSL_TRANSPORT_DATAGRAM; + + int err = mbedtls_ssl_config_defaults(&rsrc_obj->config, endpoint, transport, MBEDTLS_SSL_PRESET_DEFAULT); + if (UNLIKELY(err != 0)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + return OK_ATOM; +} + +static term nif_ssl_set_hostname(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *rsrc_obj = (struct SSLContextResource *) rsrc_obj_ptr; + + int ok; + char *host_str = interop_term_to_string(argv[1], &ok); + if (!ok) { + RAISE_ERROR(BADARG_ATOM); + } + + int err = mbedtls_ssl_set_hostname(&rsrc_obj->context, host_str); + free(host_str); + + if (UNLIKELY(err == MBEDTLS_ERR_SSL_ALLOC_FAILED)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + if (UNLIKELY(err)) { // MBEDTLS_ERR_SSL_BAD_INPUT_DATA or any undocumented error + RAISE_ERROR(BADARG_ATOM); + } + + return OK_ATOM; +} + +static term nif_ssl_conf_authmode(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslconfig_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLConfigResource *rsrc_obj = (struct SSLConfigResource *) rsrc_obj_ptr; + + int authmode = interop_atom_term_select_int(authmode_table, argv[1], ctx->global); + if (UNLIKELY(authmode == UNKNOWN_TABLE_VALUE)) { + RAISE_ERROR(BADARG_ATOM); + } + + mbedtls_ssl_conf_authmode(&rsrc_obj->config, authmode); + + return OK_ATOM; +} + +static term nif_ssl_conf_rng(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslconfig_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLConfigResource *conf_obj = (struct SSLConfigResource *) rsrc_obj_ptr; + + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[1], ctrdrbg_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct CtrDrbgResource *ctr_drbg_obj = (struct CtrDrbgResource *) rsrc_obj_ptr; + + mbedtls_ssl_conf_rng(&conf_obj->config, mbedtls_ctr_drbg_random, &ctr_drbg_obj->context); + + return OK_ATOM; +} + +static term nif_ssl_setup(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[1], sslconfig_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLConfigResource *config_rsrc = (struct SSLConfigResource *) rsrc_obj_ptr; + + int err = mbedtls_ssl_setup(&context_rsrc->context, &config_rsrc->config); + if (UNLIKELY(err == MBEDTLS_ERR_SSL_ALLOC_FAILED)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + if (UNLIKELY(err)) { // Any undocumented error + RAISE_ERROR(BADARG_ATOM); + } + + struct RefcBinary *config_refc = refc_binary_from_data(config_rsrc); + refc_binary_increment_refcount(config_refc); + + return OK_ATOM; +} + +static term make_err_result(int err, Context *ctx) +{ + switch (err) { + case 0: + return OK_ATOM; + case MBEDTLS_ERR_SSL_WANT_READ: + return globalcontext_make_atom(ctx->global, ATOM_STR("\x9", "want_read")); + case MBEDTLS_ERR_SSL_WANT_WRITE: + return globalcontext_make_atom(ctx->global, ATOM_STR("\xA", "want_write")); +#if MBEDTLS_VERSION_NUMBER >= 0x020B0000 + case MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS: + return globalcontext_make_atom(ctx->global, ATOM_STR("\xA", "async_in_progress")); +#if MBEDTLS_VERSION_NUMBER >= 0x020E0000 + case MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS: + return globalcontext_make_atom(ctx->global, ATOM_STR("\xA", "crypto_in_progress")); +#endif +#endif + default: { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + 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, term_from_int(err)); + return error_tuple; + } + } +} + +static term nif_ssl_handshake_step(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + int err = mbedtls_ssl_handshake_step(&context_rsrc->context); + +#if MBEDTLS_VERSION_NUMBER >= 0x03020000 + if (err == 0 && mbedtls_ssl_is_handshake_over(&context_rsrc->context)) { + return globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "done")); + } +#else + if (err == 0 && context_rsrc->context.state >= MBEDTLS_SSL_HANDSHAKE_OVER) { + return globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "done")); + } +#endif + return make_err_result(err, ctx); +} + +static term nif_ssl_close_notify(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + int err = mbedtls_ssl_close_notify(&context_rsrc->context); + return make_err_result(err, ctx); +} + +static term nif_ssl_write(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + VALIDATE_VALUE(argv[1], term_is_binary); + + term data = argv[1]; + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + const uint8_t *buffer = (const uint8_t *) term_binary_data(data); + size_t len = term_binary_size(data); + + int res = mbedtls_ssl_write(&context_rsrc->context, buffer, len); + + if (res == (int) len) { + return OK_ATOM; + } + if (LIKELY(res >= 0)) { // ensure we don't return OK if res is 0 + size_t rest_len = len - res; + size_t requested_size = term_sub_binary_heap_size(data, rest_len); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) + requested_size, 1, &data, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term rest = term_maybe_create_sub_binary(data, res, rest_len, &ctx->heap, ctx->global); + return port_create_tuple2(ctx, OK_ATOM, rest); + } + + return make_err_result(res, ctx); +} + +static term nif_ssl_read(Context *ctx, int argc, term argv[]) +{ + TRACE("%s\n", __func__); + UNUSED(argc); + VALIDATE_VALUE(argv[1], term_is_integer); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], sslcontext_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SSLContextResource *context_rsrc = (struct SSLContextResource *) rsrc_obj_ptr; + + avm_int_t len = term_to_int(argv[1]); + if (len < 0) { + RAISE_ERROR(BADARG_ATOM); + } +#if MBEDTLS_VERSION_NUMBER >= 0x03000000 + if (len == 0) { + len = mbedtls_ssl_get_max_in_record_payload(&context_rsrc->context); + } +#endif + if (len <= 0) { + len = DEFAULT_READ_BUFFER_FALLBACK; + } + size_t ensure_packet_avail = term_binary_data_size_in_terms(len) + BINARY_HEADER_SIZE; + size_t requested_size = TUPLE_SIZE(2) + ensure_packet_avail; + + if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term data = term_create_uninitialized_binary(len, &ctx->heap, ctx->global); + uint8_t *buffer = (uint8_t *) term_binary_data(data); + + int res = mbedtls_ssl_read(&context_rsrc->context, buffer, len); + + if (res == len) { + return port_create_tuple2(ctx, OK_ATOM, data); + } + + if (res >= 0 && res < len) { + size_t requested_size = term_sub_binary_heap_size(data, res); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) + requested_size, 1, &data, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term rest = term_maybe_create_sub_binary(data, 0, res, &ctx->heap, ctx->global); + return port_create_tuple2(ctx, OK_ATOM, rest); + } + + return make_err_result(res, ctx); +} + +static const struct Nif ssl_entropy_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_entropy_init +}; +static const struct Nif ssl_ctr_drbg_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_ctr_drbg_init +}; +static const struct Nif ssl_ctr_drbg_seed_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_ctr_drbg_seed +}; +static const struct Nif ssl_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_init +}; +static const struct Nif ssl_set_bio_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_set_bio +}; +static const struct Nif ssl_config_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_config_init +}; +static const struct Nif ssl_config_defaults_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_config_defaults +}; +static const struct Nif ssl_conf_authmode_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_conf_authmode +}; +static const struct Nif ssl_conf_rng_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_conf_rng +}; +static const struct Nif ssl_set_hostname_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_set_hostname +}; +static const struct Nif ssl_setup_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_setup +}; +static const struct Nif ssl_handshake_step_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_handshake_step +}; +static const struct Nif ssl_close_notify_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_close_notify +}; +static const struct Nif ssl_write_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_write +}; +static const struct Nif ssl_read_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ssl_read +}; + +// +// Entrypoints +// + +const struct Nif *otp_ssl_nif_get_nif(const char *nifname) +{ + if (strncmp("ssl:", nifname, 4) == 0) { + const char *rest = nifname + 4; + if (strcmp("nif_entropy_init/0", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_entropy_init_nif; + } + if (strcmp("nif_ctr_drbg_init/0", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_ctr_drbg_init_nif; + } + if (strcmp("nif_ctr_drbg_seed/3", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_ctr_drbg_seed_nif; + } + if (strcmp("nif_init/0", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_init_nif; + } + if (strcmp("nif_set_bio/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_set_bio_nif; + } + if (strcmp("nif_config_init/0", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_config_init_nif; + } + if (strcmp("nif_config_defaults/3", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_config_defaults_nif; + } + if (strcmp("nif_conf_authmode/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_conf_authmode_nif; + } + if (strcmp("nif_conf_rng/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_conf_rng_nif; + } + if (strcmp("nif_set_hostname/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_set_hostname_nif; + } + if (strcmp("nif_setup/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_setup_nif; + } + if (strcmp("nif_handshake_step/1", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_handshake_step_nif; + } + if (strcmp("nif_close_notify/1", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_close_notify_nif; + } + if (strcmp("nif_write/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_write_nif; + } + if (strcmp("nif_read/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &ssl_read_nif; + } + } + return NULL; +} + +void otp_ssl_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + entropycontext_resource_type = enif_init_resource_type(&env, "entropycontext", &EntropyContextResourceTypeInit, ERL_NIF_RT_CREATE, NULL); + ctrdrbg_resource_type = enif_init_resource_type(&env, "ctr_drbg", &CtrDrbgResourceTypeInit, ERL_NIF_RT_CREATE, NULL); + sslcontext_resource_type = enif_init_resource_type(&env, "sslcontext", &SSLContextResourceTypeInit, ERL_NIF_RT_CREATE, NULL); + sslconfig_resource_type = enif_init_resource_type(&env, "sslconfig", &SSLConfigResourceTypeInit, ERL_NIF_RT_CREATE, NULL); + +#if defined(MBEDTLS_DEBUG_C) && defined(ENABLE_TRACE) + mbedtls_debug_set_threshold(5); +#endif +} diff --git a/src/libAtomVM/otp_ssl.h b/src/libAtomVM/otp_ssl.h new file mode 100644 index 000000000..25ff7617a --- /dev/null +++ b/src/libAtomVM/otp_ssl.h @@ -0,0 +1,38 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Paul Guyot + * + * 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_SSL_H_ +#define _OTP_SSL_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +const struct Nif *otp_ssl_nif_get_nif(const char *nifname); +void otp_ssl_init(GlobalContext *global); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt index d5063dc3a..72b5dd7fa 100644 --- a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt @@ -30,6 +30,7 @@ set(AVM_BUILTIN_COMPONENT_SRCS "uart_driver.c" "otp_net_platform.c" "otp_socket_platform.c" + "otp_ssl_platform.c" ) if (IDF_VERSION_MAJOR GREATER_EQUAL 5) @@ -38,6 +39,10 @@ else() set(ADDITIONAL_PRIV_REQUIRES "") endif() +if(CONFIG_AVM_ENABLE_OTP_SSL_NIFS) + set(ADDITIONAL_PRIV_REQUIRES ${ADDITIONAL_PRIV_REQUIRES} "mbedtls") +endif() + # WHOLE_ARCHIVE option is supported only with esp-idf 5.x # A link option will be used with esp-idf 4.x if (IDF_VERSION_MAJOR EQUAL 5) diff --git a/src/platforms/esp32/components/avm_builtins/Kconfig b/src/platforms/esp32/components/avm_builtins/Kconfig index 94c4c7f00..61c164090 100644 --- a/src/platforms/esp32/components/avm_builtins/Kconfig +++ b/src/platforms/esp32/components/avm_builtins/Kconfig @@ -74,4 +74,8 @@ config AVM_ENABLE_OTP_NET_NIFS bool "Enable OTP Net NIFs" default y +config AVM_ENABLE_OTP_SSL_NIFS + bool "Enable OTP SSL NIFs" + default y + endmenu diff --git a/src/platforms/esp32/components/avm_builtins/otp_ssl_platform.c b/src/platforms/esp32/components/avm_builtins/otp_ssl_platform.c new file mode 100644 index 000000000..a9bc7f8ab --- /dev/null +++ b/src/platforms/esp32/components/avm_builtins/otp_ssl_platform.c @@ -0,0 +1,31 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Paul Guyot + * + * 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_SSL_NIFS + +REGISTER_NIF_COLLECTION(otp_ssl, otp_ssl_init, NULL, otp_ssl_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 240fa466c..b9030be34 100644 --- a/src/platforms/esp32/components/avm_sys/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_sys/CMakeLists.txt @@ -28,6 +28,7 @@ set(AVM_SYS_COMPONENT_SRCS "../../../../libAtomVM/inet.c" "../../../../libAtomVM/otp_net.c" "../../../../libAtomVM/otp_socket.c" + "../../../../libAtomVM/otp_ssl.c" ) if (IDF_VERSION_MAJOR GREATER_EQUAL 5) diff --git a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt index d82c488ce..fb907ded1 100644 --- a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt +++ b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt @@ -47,6 +47,7 @@ compile_erlang(test_net) compile_erlang(test_rtc_slow) compile_erlang(test_select) compile_erlang(test_socket) +compile_erlang(test_ssl) compile_erlang(test_time_and_processes) compile_erlang(test_tz) @@ -64,6 +65,7 @@ add_custom_command( test_rtc_slow.beam test_select.beam test_socket.beam + test_ssl.beam test_time_and_processes.beam test_tz.beam DEPENDS @@ -78,6 +80,7 @@ add_custom_command( "${CMAKE_CURRENT_BINARY_DIR}/test_rtc_slow.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_select.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_socket.beam" + "${CMAKE_CURRENT_BINARY_DIR}/test_ssl.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_time_and_processes.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_tz.beam" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} diff --git a/src/platforms/esp32/test/main/test_erl_sources/test_ssl.erl b/src/platforms/esp32/test/main/test_erl_sources/test_ssl.erl new file mode 100644 index 000000000..15f7a835e --- /dev/null +++ b/src/platforms/esp32/test/main/test_erl_sources/test_ssl.erl @@ -0,0 +1,162 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Paul Guyot +% +% 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_ssl). +-export([start/0]). + +start() -> + % start SSL + Entropy = ssl:nif_entropy_init(), + CtrDrbg = ssl:nif_ctr_drbg_init(), + ok = ssl:nif_ctr_drbg_seed(CtrDrbg, Entropy, <<"AtomVM">>), + % Get address of github.com + {ok, Results} = net:getaddrinfo_nif("github.com", undefined), + [TCPAddr | _] = [ + Addr + || #{addr := #{addr := Addr}, type := stream, protocol := tcp, family := inet} <- Results + ], + % Connect to github.com:443 + {ok, Socket} = socket:open(inet, stream, tcp), + ok = socket:connect(Socket, #{family => inet, addr => TCPAddr, port => 443}), + % Initialize SSL Socket and config + SSLContext = ssl:nif_init(), + ok = ssl:nif_set_bio(SSLContext, Socket), + SSLConfig = ssl:nif_config_init(), + ok = ssl:nif_config_defaults(SSLConfig, client, stream), + ok = ssl:nif_set_hostname(SSLContext, "github.com"), + ok = ssl:nif_conf_authmode(SSLConfig, none), + ok = ssl:nif_conf_rng(SSLConfig, CtrDrbg), + ok = ssl:nif_setup(SSLContext, SSLConfig), + % Handshake + ok = handshake_loop(SSLContext, Socket), + % Write + ok = send_loop( + SSLContext, + Socket, + <<"GET / HTTP/1.1\r\nHost: atomvm.net\r\nUser-Agent: AtomVM within qemu\r\n\r\n">> + ), + % Read + {ok, <<"HTTP/1.1">>} = recv_loop(SSLContext, Socket, 8, []), + % Close + ok = close_notify_loop(SSLContext, Socket), + ok = socket:close(Socket), + ok. + +handshake_loop(SSLContext, Socket) -> + case ssl:nif_handshake_step(SSLContext) of + ok -> + handshake_loop(SSLContext, Socket); + done -> + ok; + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + handshake_loop(SSLContext, Socket); + {closed, Ref} -> + ok = socket:close(Socket), + {error, closed} + end; + {error, _Reason} = Error -> + socket:close(Socket), + Error + end; + want_write -> + handshake_loop(SSLContext, Socket); + {error, _Reason} = Error -> + socket:close(Socket), + Error + end. + +send_loop(SSLContext, Socket, Binary) -> + case ssl:nif_write(SSLContext, Binary) of + ok -> + ok; + {ok, Rest} -> + send_loop(SSLContext, Socket, Rest); + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + send_loop(SSLContext, Socket, Binary); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + send_loop(SSLContext, Socket, Binary); + {error, _Reason} = Error -> + Error + end. + +recv_loop(_SSLContext, _Socket, 0, Acc) -> + {ok, list_to_binary(lists:reverse(Acc))}; +recv_loop(SSLContext, Socket, Remaining, Acc) -> + case ssl:nif_read(SSLContext, Remaining) of + {ok, Data} -> + Len = byte_size(Data), + recv_loop(SSLContext, Socket, Remaining - Len, [Data | Acc]); + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + recv_loop(SSLContext, Socket, Remaining, Acc); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + recv_loop(SSLContext, Socket, Remaining, Acc); + {error, _Reason} = Error -> + Error + end. + +close_notify_loop(SSLContext, Socket) -> + case ssl:nif_close_notify(SSLContext) of + ok -> + ok; + want_read -> + Ref = erlang:make_ref(), + case socket:nif_select_read(Socket, Ref) of + ok -> + receive + {select, _SocketResource, Ref, ready_input} -> + close_notify_loop(SSLContext, Socket); + {closed, Ref} -> + {error, closed} + end; + {error, _Reason} = Error -> + Error + end; + want_write -> + close_notify_loop(SSLContext, Socket); + {error, _Reason} = Error -> + Error + end. diff --git a/src/platforms/esp32/test/main/test_main.c b/src/platforms/esp32/test/main/test_main.c index 4455ee729..7bedbcded 100644 --- a/src/platforms/esp32/test/main/test_main.c +++ b/src/platforms/esp32/test/main/test_main.c @@ -161,6 +161,8 @@ term avm_test_case(const char *test_module) term_display(stdout, ret_value, ctx); fprintf(stdout, "\n"); + context_destroy(ctx); + nif_collection_destroy_all(glb); port_driver_destroy_all(glb); @@ -462,6 +464,26 @@ TEST_CASE("test_socket", "[test_run]") eth_stop(eth_netif); } +TEST_CASE("test_ssl", "[test_run]") +{ + // esp_netif_init() was called by network_driver_init + ESP_LOGI(TAG, "Registering handler\n"); + network_got_ip = false; + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_event_handler, NULL)); + ESP_LOGI(TAG, "Starting network\n"); + esp_netif_t *eth_netif = eth_start(); + + while (!network_got_ip) { + vTaskDelay(1); + } + + term ret_value = avm_test_case("test_ssl.beam"); + TEST_ASSERT(ret_value == OK_ATOM); + + ESP_LOGI(TAG, "Stopping network\n"); + eth_stop(eth_netif); +} + TEST_CASE("test_rtc_slow", "[test_run]") { term ret_value = avm_test_case("test_rtc_slow.beam"); diff --git a/src/platforms/generic_unix/lib/CMakeLists.txt b/src/platforms/generic_unix/lib/CMakeLists.txt index 8fba09b36..72525665d 100644 --- a/src/platforms/generic_unix/lib/CMakeLists.txt +++ b/src/platforms/generic_unix/lib/CMakeLists.txt @@ -61,6 +61,20 @@ define_if_function_exists(libAtomVM${PLATFORM_LIB_SUFFIX} getservbyname "netdb.h target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC libAtomVM) include_directories(${CMAKE_SOURCE_DIR}/src/platforms/generic_unix/lib) +include(MbedTLS) +if (MbedTLS_FOUND) + target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC MbedTLS::mbedtls) + target_compile_definitions(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ATOMVM_HAS_MBEDTLS) + target_sources(libAtomVM${PLATFORM_LIB_SUFFIX} + PRIVATE + ../../../libAtomVM/otp_ssl.c + ../../../libAtomVM/otp_ssl.h + ) +else() + message("WARNING: Could NOT find MbedTLS, SSL will not be supported. Install MbedTLS 3.x or try to set MBEDTLS_ROOT_DIR to installation prefix of MbedTLS 2.x") +endif() + +# For now we still use OpenSSL for random and crypto find_package(OpenSSL) if (${OPENSSL_FOUND} STREQUAL TRUE) target_include_directories(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${OPENSSL_INCLUDE_DIR}) diff --git a/src/platforms/generic_unix/lib/platform_nifs.c b/src/platforms/generic_unix/lib/platform_nifs.c index 3f2307254..ab9279435 100644 --- a/src/platforms/generic_unix/lib/platform_nifs.c +++ b/src/platforms/generic_unix/lib/platform_nifs.c @@ -27,6 +27,7 @@ #include "nifs.h" #include "otp_net.h" #include "otp_socket.h" +#include "otp_ssl.h" #include "platform_defaultatoms.h" #include "term.h" #include @@ -263,9 +264,15 @@ const struct Nif *platform_nifs_get_nif(const char *nifname) return &atomvm_platform_nif; } const struct Nif *nif = otp_net_nif_get_nif(nifname); - if (nif == NULL) { - return otp_socket_nif_get_nif(nifname); - } else { + if (nif) { return nif; } + nif = otp_socket_nif_get_nif(nifname); +#if defined ATOMVM_HAS_MBEDTLS + if (nif) { + return nif; + } + nif = otp_ssl_nif_get_nif(nifname); +#endif + return nif; } diff --git a/src/platforms/generic_unix/lib/sys.c b/src/platforms/generic_unix/lib/sys.c index 85f230d55..3a817c249 100644 --- a/src/platforms/generic_unix/lib/sys.c +++ b/src/platforms/generic_unix/lib/sys.c @@ -31,6 +31,10 @@ #include "smp.h" #include "utils.h" +#if ATOMVM_HAS_MBEDTLS +#include "otp_ssl.h" +#endif + #include #include #include @@ -562,6 +566,9 @@ void sys_init_platform(GlobalContext *global) otp_net_init(global); otp_socket_init(global); +#if ATOMVM_HAS_MBEDTLS + otp_ssl_init(global); +#endif global->platform_data = platform; } diff --git a/src/platforms/rp2040/src/lib/CMakeLists.txt b/src/platforms/rp2040/src/lib/CMakeLists.txt index 2fa764dc9..2ddc275d1 100644 --- a/src/platforms/rp2040/src/lib/CMakeLists.txt +++ b/src/platforms/rp2040/src/lib/CMakeLists.txt @@ -83,13 +83,16 @@ if (PICO_CYW43_SUPPORTED) ../../../../libAtomVM/inet.c otp_socket_platform.c ../../../../libAtomVM/otp_socket.c + otp_ssl_platform.c + ../../../../libAtomVM/otp_ssl.c ../../../../libAtomVM/inet.h otp_socket_platform.h ../../../../libAtomVM/otp_socket.h + ../../../../libAtomVM/otp_ssl.h otp_net_lwip_raw.c otp_net_lwip_raw.h) - target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC pico_cyw43_arch_lwip_threadsafe_background pico_lwip_sntp INTERFACE pan_lwip_dhserver) - target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,networkregister_port_driver -Wl,-u -Wl,otp_socket_nif -Wl,-u -Wl,otp_net_nif") + target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC pico_cyw43_arch_lwip_threadsafe_background pico_lwip_sntp pico_mbedtls INTERFACE pan_lwip_dhserver) + target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,networkregister_port_driver -Wl,-u -Wl,otp_socket_nif -Wl,-u -Wl,otp_net_nif -Wl,-u -Wl,otp_ssl_nif") endif() target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif") diff --git a/src/platforms/rp2040/src/lib/mbedtls_config.h b/src/platforms/rp2040/src/lib/mbedtls_config.h new file mode 100644 index 000000000..413dbbd2a --- /dev/null +++ b/src/platforms/rp2040/src/lib/mbedtls_config.h @@ -0,0 +1,93 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 Paul Guyot + * + * 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 + */ + +/* Workaround for some mbedtls source files using INT_MAX without including limits.h */ +#include + +// Reasonable config copied from pico samples + +// Protocols +#define MBEDTLS_SSL_PROTO_TLS1_2 + +// Options that enable ciphersuites +#define MBEDTLS_CIPHER_MODE_CBC +#define MBEDTLS_ECP_DP_SECP192R1_ENABLED +#define MBEDTLS_ECP_DP_SECP224R1_ENABLED +#define MBEDTLS_ECP_DP_SECP256R1_ENABLED +#define MBEDTLS_ECP_DP_SECP384R1_ENABLED +#define MBEDTLS_ECP_DP_SECP521R1_ENABLED +#define MBEDTLS_ECP_DP_SECP192K1_ENABLED +#define MBEDTLS_ECP_DP_SECP224K1_ENABLED +#define MBEDTLS_ECP_DP_SECP256K1_ENABLED +#define MBEDTLS_ECP_DP_BP256R1_ENABLED +#define MBEDTLS_ECP_DP_BP384R1_ENABLED +#define MBEDTLS_ECP_DP_BP512R1_ENABLED +#define MBEDTLS_ECP_DP_CURVE25519_ENABLED +// Following is unused until pico's mbedtls is upgraded +#define MBEDTLS_ECP_DP_CURVE448_ENABLED + +#define MBEDTLS_KEY_EXCHANGE_RSA_ENABLED +#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED +#define MBEDTLS_AES_C + +// Requirements +#define MBEDTLS_PKCS1_V15 +#define MBEDTLS_ASN1_PARSE_C +#define MBEDTLS_BIGNUM_C +#define MBEDTLS_CIPHER_C +#define MBEDTLS_CTR_DRBG_C +#define MBEDTLS_ENTROPY_C +#define MBEDTLS_MD_C +#define MBEDTLS_MD5_C +#define MBEDTLS_OID_C +#define MBEDTLS_PKCS5_C +#define MBEDTLS_PK_C +#define MBEDTLS_PK_PARSE_C +#define MBEDTLS_RSA_C +#define MBEDTLS_SHA1_C +#define MBEDTLS_SHA224_C +#define MBEDTLS_SHA256_C +#define MBEDTLS_SHA512_C +#define MBEDTLS_SSL_CLI_C +#define MBEDTLS_SSL_SRV_C +#define MBEDTLS_SSL_TLS_C +#define MBEDTLS_X509_CRT_PARSE_C +#define MBEDTLS_X509_USE_C +#define MBEDTLS_SSL_SERVER_NAME_INDICATION +#define MBEDTLS_GCM_C +#define MBEDTLS_ECDH_C +#define MBEDTLS_ECP_C +#define MBEDTLS_ECDSA_C +#define MBEDTLS_ASN1_WRITE_C + +// Pico port +#define MBEDTLS_PLATFORM_C +#define MBEDTLS_NO_PLATFORM_ENTROPY +#define MBEDTLS_HAVE_TIME +#define MBEDTLS_ENTROPY_HARDWARE_ALT + +// Options that reduce ROM or RAM usage at the expense of performance +// These default values match esp-idf's default configuration +#define MBEDTLS_SSL_OUT_CONTENT_LEN 4096 +// #define MBEDTLS_AES_FEWER_TABLES +// #define MBEDTLS_SHA256_SMALLER + +// Uncomment for debugging SSL otp_ssl.c +// #define MBEDTLS_DEBUG_C diff --git a/src/platforms/rp2040/src/lib/otp_ssl_platform.c b/src/platforms/rp2040/src/lib/otp_ssl_platform.c new file mode 100644 index 000000000..f17a0a5c5 --- /dev/null +++ b/src/platforms/rp2040/src/lib/otp_ssl_platform.c @@ -0,0 +1,25 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Paul Guyot + * + * 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 + +REGISTER_NIF_COLLECTION(otp_ssl, otp_ssl_init, NULL, otp_ssl_nif_get_nif) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0aa15bb18..20e8e8a8b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -54,6 +54,14 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") endif() endif() +include(MbedTLS) +if (MbedTLS_FOUND) + target_link_libraries(test-erlang PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-enif PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-mailbox PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-structs PRIVATE MbedTLS::mbedtls) +endif() + set( PLATFORM_LIB_SUFFIX ${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR} diff --git a/tests/libs/estdlib/CMakeLists.txt b/tests/libs/estdlib/CMakeLists.txt index b543f4ba5..5501a842b 100644 --- a/tests/libs/estdlib/CMakeLists.txt +++ b/tests/libs/estdlib/CMakeLists.txt @@ -35,6 +35,7 @@ set(ERLANG_MODULES test_maps test_net test_spawn + test_ssl test_string test_proplists test_timer diff --git a/tests/libs/estdlib/test_ssl.erl b/tests/libs/estdlib/test_ssl.erl new file mode 100644 index 000000000..fde3ec6ba --- /dev/null +++ b/tests/libs/estdlib/test_ssl.erl @@ -0,0 +1,80 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Paul Guyot +% +% 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_ssl). + +-export([test/0]). + +-include("etest.hrl"). + +test() -> + case is_ssl_available() of + true -> + test_ssl(); + false -> + io:format("Warning: skipping test_ssl as ssl is not available\n"), + ok + end. + +is_ssl_available() -> + case erlang:system_info(machine) of + "BEAM" -> + true; + _ -> + try + ssl:nif_init(), + true + catch + error:undef -> + false + end + end. + +test_ssl() -> + ok = ssl:start(), + ok = test_start_twice(), + ok = test_connect_close(), + ok = test_connect_error(), + ok = test_send_recv(), + ok = ssl:stop(), + ok. + +test_start_twice() -> + ok = ssl:start(). + +test_connect_close() -> + {ok, SSLSocket} = ssl:connect("atomvm.net", 443, [{verify, verify_none}, {active, false}]), + ok = ssl:close(SSLSocket). + +test_connect_error() -> + {error, _Error} = ssl:connect("atomvm.net", 80, [{verify, verify_none}, {active, false}]), + ok. + +test_send_recv() -> + {ok, SSLSocket} = ssl:connect("atomvm.net", 443, [ + {verify, verify_none}, {active, false}, {binary, true} + ]), + UserAgent = erlang:system_info(machine), + ok = ssl:send(SSLSocket, [ + <<"GET / HTTP/1.1\r\nHost: atomvm.net\r\nUser-Agent: ">>, UserAgent, <<"\r\n\r\n">> + ]), + {ok, <<"HTTP/1.1">>} = ssl:recv(SSLSocket, 8), + ok = ssl:close(SSLSocket), + ok. diff --git a/tests/libs/estdlib/tests.erl b/tests/libs/estdlib/tests.erl index 801fed6b6..5c4daab28 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, test_net | get_tests(undefined)]; + [test_tcp_socket, test_udp_socket, test_net, test_ssl | get_tests(undefined)]; get_tests(_OTPVersion) -> [ test_lists,