From 19d91a052b2eaa414c619b8b916c64126eaf790d Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 4 Nov 2023 07:26:32 +0100 Subject: [PATCH] Add minimal support for ssl module using Mbed TLS and sockets Add support for ssl client in binary and passive modes, with no certificate verification Add APIs to otp_socket so it can be called from ssl bio callbacks Fix a bug in lwIP otp_socket's recv revealed by ssl tests Fix a bug in BSD otp_socket's recvfrom revealed by refactoring Fix a bug in esp32 tests where main context and its resoures were not properly destroyed Update documentation and workflows to reflect the requirement on Mbed TLS Fix exported types of inet module Signed-off-by: Paul Guyot --- .github/workflows/build-and-test-macos.yaml | 2 +- .../workflows/build-and-test-on-freebsd.yaml | 2 +- .github/workflows/build-and-test-other.yaml | 2 +- .github/workflows/build-and-test.yaml | 2 +- CHANGELOG.md | 1 + CMakeModules/MbedTLS.cmake | 92 ++ README.Md | 1 + doc/src/build-instructions.md | 1 + libs/estdlib/src/CMakeLists.txt | 1 + libs/estdlib/src/gen_tcp.erl | 2 +- libs/estdlib/src/gen_udp.erl | 6 +- libs/estdlib/src/inet.erl | 13 +- libs/estdlib/src/ssl.erl | 397 +++++++++ src/libAtomVM/globalcontext.c | 8 + src/libAtomVM/otp_socket.c | 478 ++++++----- src/libAtomVM/otp_socket.h | 49 ++ src/libAtomVM/otp_ssl.c | 791 ++++++++++++++++++ src/libAtomVM/otp_ssl.h | 38 + .../components/avm_builtins/CMakeLists.txt | 1 + .../esp32/components/avm_builtins/Kconfig | 4 + .../avm_builtins/otp_ssl_platform.c | 31 + .../esp32/components/avm_sys/CMakeLists.txt | 1 + .../test/main/test_erl_sources/CMakeLists.txt | 3 + .../test/main/test_erl_sources/test_ssl.erl | 162 ++++ src/platforms/esp32/test/main/test_main.c | 22 + src/platforms/generic_unix/lib/CMakeLists.txt | 14 + .../generic_unix/lib/platform_nifs.c | 13 +- src/platforms/generic_unix/lib/sys.c | 7 + src/platforms/rp2040/src/lib/CMakeLists.txt | 9 +- src/platforms/rp2040/src/lib/lwipopts.h | 2 +- src/platforms/rp2040/src/lib/mbedtls_config.h | 92 ++ .../rp2040/src/lib/otp_ssl_platform.c | 25 + tests/CMakeLists.txt | 8 + tests/libs/estdlib/CMakeLists.txt | 1 + tests/libs/estdlib/test_ssl.erl | 78 ++ tests/libs/estdlib/tests.erl | 2 +- 36 files changed, 2140 insertions(+), 221 deletions(-) create mode 100644 CMakeModules/MbedTLS.cmake create mode 100644 libs/estdlib/src/ssl.erl create mode 100644 src/libAtomVM/otp_ssl.c create mode 100644 src/libAtomVM/otp_ssl.h create mode 100644 src/platforms/esp32/components/avm_builtins/otp_ssl_platform.c create mode 100644 src/platforms/esp32/test/main/test_erl_sources/test_ssl.erl create mode 100644 src/platforms/rp2040/src/lib/mbedtls_config.h create mode 100644 src/platforms/rp2040/src/lib/otp_ssl_platform.c create mode 100644 tests/libs/estdlib/test_ssl.erl diff --git a/.github/workflows/build-and-test-macos.yaml b/.github/workflows/build-and-test-macos.yaml index 63ce352dc7..bc0b853257 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 005feb8d18..d7cb9a9189 100644 --- a/.github/workflows/build-and-test-on-freebsd.yaml +++ b/.github/workflows/build-and-test-on-freebsd.yaml @@ -55,7 +55,7 @@ jobs: echo "%%" echo "**freebsd-version:**" freebsd-version - sudo pkg install -y curl cmake gperf erlang elixir + sudo pkg install -y curl cmake gperf erlang elixir mbedtls echo "**uname:**" uname -a echo "**C Compiler version:**" diff --git a/.github/workflows/build-and-test-other.yaml b/.github/workflows/build-and-test-other.yaml index 5564ae47d6..c927e9f073 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 6e1dd01714..1188b97c25 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -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 235462385d..e830350c02 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 0000000000..149e6e57c6 --- /dev/null +++ b/CMakeModules/MbedTLS.cmake @@ -0,0 +1,92 @@ +# +# This file is part of AtomVM. +# +# Copyright 2019 Riccardo Binetti +# +# 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 thre 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() + + add_library(MbedTLS::mbedcrypto SHARED IMPORTED) + set_target_properties(MbedTLS::mbedcrypto PROPERTIES + IMPORTED_LOCATION "${MBEDTLS_LIBRARIES_DIR}/libmbedcrypto${IMPORT_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${IMPORT_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${IMPORT_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) + if (${MbedTLS_FOUND}) + set(MbedTLS_FOUND TRUE) + 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_NOTFOUND OR NOT MBEDX509_NOTFOUND AND NOT MBEDTLS_NOTFOUND) + 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 7a298e6fa3..5c71a3e5fa 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 bd3d415be9..c6e9671db1 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 35c5db10b9..68a4141aeb 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 f245411b09..ca53230ab8 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 cf5d974607..23bd518a28 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 cea2a9f673..369a5d8d7f 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 0000000000..86817d2703 --- /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 test 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 25dded5ecc..62ff405335 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 a950552fe1..8fca3e3eef 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 04d12e0c3f..3d938464e8 100644 --- a/src/libAtomVM/otp_socket.h +++ b/src/libAtomVM/otp_socket.h @@ -39,9 +39,58 @@ 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 of 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 0000000000..9a52ac2667 --- /dev/null +++ b/src/libAtomVM/otp_ssl.c @@ -0,0 +1,791 @@ +/* + * 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 +{ + struct mbedtls_entropy_context context; +}; + +struct CtrDrbgResource +{ + struct mbedtls_ctr_drbg_context context; +}; + +struct SSLContextResource +{ + struct mbedtls_ssl_context context; +}; + +struct SSLConfigResource +{ + struct 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 + struct 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 struct 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")); + case MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS: + return globalcontext_make_atom(ctx->global, ATOM_STR("\xA", "async_in_progress")); + case MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS: + return globalcontext_make_atom(ctx->global, ATOM_STR("\xA", "crypto_in_progress")); + 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 0000000000..25ff7617a3 --- /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 d5063dc3ae..5f154c1bc2 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) diff --git a/src/platforms/esp32/components/avm_builtins/Kconfig b/src/platforms/esp32/components/avm_builtins/Kconfig index 94c4c7f009..61c1640905 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 0000000000..a9bc7f8ab9 --- /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 240fa466cb..b9030be346 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 d82c488cea..fb907ded1c 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 0000000000..15f7a835e4 --- /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 4455ee7294..7bedbcded5 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 8fba09b365..72525665d5 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 3f23072542..ab92794356 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 85f230d553..3a817c2492 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 2fa764dc93..af2f98142a 100644 --- a/src/platforms/rp2040/src/lib/CMakeLists.txt +++ b/src/platforms/rp2040/src/lib/CMakeLists.txt @@ -76,20 +76,23 @@ if (PICO_CYW43_SUPPORTED) target_include_directories(pan_lwip_dhserver INTERFACE ${BTSTACK_3RD_PARTY_PATH}/lwip/dhcp-server ) - target_compile_options(libAtomVM${PLATFORM_LIB_SUFFIX} PRIVATE -DHAVE_LWIP_RAW=1) + target_compile_options(libAtomVM${PLATFORM_LIB_SUFFIX} PRIVATE -DHAVE_LWIP_RAW=1 -DHAVE_MBEDTLS_IS_HANDSHAKE_OVER=0) target_sources( libAtomVM${PLATFORM_LIB_SUFFIX} PRIVATE ../../../../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/lwipopts.h b/src/platforms/rp2040/src/lib/lwipopts.h index 116cd93d66..6a166db289 100644 --- a/src/platforms/rp2040/src/lib/lwipopts.h +++ b/src/platforms/rp2040/src/lib/lwipopts.h @@ -40,8 +40,8 @@ #define LWIP_ETHERNET 1 #define LWIP_ICMP 1 #define LWIP_RAW 1 -#define TCP_WND (8 * TCP_MSS) #define TCP_MSS 1460 +#define TCP_WND (8 * TCP_MSS) #define TCP_SND_BUF (8 * TCP_MSS) #define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) #define LWIP_NETIF_STATUS_CALLBACK 1 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 0000000000..4427d37687 --- /dev/null +++ b/src/platforms/rp2040/src/lib/mbedtls_config.h @@ -0,0 +1,92 @@ +/* + * 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 +#define MBEDTLS_SSL_OUT_CONTENT_LEN 2048 +#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 0000000000..f17a0a5c5f --- /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 0aa15bb18f..20e8e8a8bb 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 b543f4ba59..5501a842b8 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 0000000000..658b47fe38 --- /dev/null +++ b/tests/libs/estdlib/test_ssl.erl @@ -0,0 +1,78 @@ +% +% 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 801fed6b68..5c4daab28f 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,