From 5436b95ea326c088e35ab56f1cbfde213e96e0a2 Mon Sep 17 00:00:00 2001 From: Oleg Okunevych Date: Fri, 13 Jun 2025 18:59:26 +0300 Subject: [PATCH 1/7] Add RTMPS support --- .../rtmp_server/listener.ex | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex index a24312e..fa54038 100644 --- a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex +++ b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex @@ -39,13 +39,59 @@ defmodule Membrane.RTMPServer.Listener do end {:ok, socket} = options.socket_module.listen(options.port, listen_options) - send(options.server, {:port, :inet.port(socket)}) + + port = + case options.socket_module do + :gen_tcp -> + {:ok, {_ip, port}} = :inet.sockname(socket) + port + + :ssl -> + {:ok, {_ip, port}} = :ssl.sockname(socket) + port + end + + send(options.server, {:port, port}) + + # send(options.server, {:port, :inet.port(socket)}) |> dbg() accept_loop(socket, options) end defp accept_loop(socket, options) do - {:ok, client} = :gen_tcp.accept(socket) + client = + case options.socket_module do + :gen_tcp -> + {:ok, client} = :gen_tcp.accept(socket) + client + + :ssl -> + {:ok, client} = :ssl.transport_accept(socket) + Logger.debug("SSL transport accept successful, starting handshake...") + + # Start with very basic SSL options + ssl_opts = [ + verify: :verify_none, + fail_if_no_peer_cert: false + ] + + case :ssl.handshake(client, ssl_opts, 10000) do + {:ok, ssl_socket} -> + Logger.info("SSL handshake successful") + ssl_socket + + :ok -> + Logger.info("SSL handshake successful (ok)") + client + + {:error, reason} -> + Logger.error("SSL handshake failed: #{inspect(reason)}") + :ssl.close(client) + accept_loop(socket, options) + end + end + + client |> dbg() {:ok, client_reference} = GenServer.start_link(ClientHandler, @@ -56,7 +102,7 @@ defmodule Membrane.RTMPServer.Listener do client_timeout: options.client_timeout ) - case :gen_tcp.controlling_process(client, client_reference) do + case options.socket_module.controlling_process(client, client_reference) do :ok -> send(client_reference, :control_granted) From a687e33a281e84daa50d83ed2d9dcfe5d977c0bf Mon Sep 17 00:00:00 2001 From: Oleg Okunevych Date: Mon, 16 Jun 2025 18:47:03 +0300 Subject: [PATCH 2/7] Add Membrane.RTMPServer.Config for SSL configuration --- docs/ssl_configuration.md | 221 +++++++++++ docs/ssl_examples.md | 80 ++++ lib/membrane_rtmp_plugin/rtmp_server.ex | 40 +- .../rtmp_server/config.ex | 363 ++++++++++++++++++ .../rtmp_server/listener.ex | 81 ++-- .../rtmp_server_config_test.exs | 330 ++++++++++++++++ .../rtmp_ssl_listener_test.exs | 99 +++++ 7 files changed, 1184 insertions(+), 30 deletions(-) create mode 100644 docs/ssl_configuration.md create mode 100644 docs/ssl_examples.md create mode 100644 lib/membrane_rtmp_plugin/rtmp_server/config.ex create mode 100644 test/membrane_rtmp_plugin/rtmp_server_config_test.exs create mode 100644 test/membrane_rtmp_plugin/rtmp_ssl_listener_test.exs diff --git a/docs/ssl_configuration.md b/docs/ssl_configuration.md new file mode 100644 index 0000000..bb734c7 --- /dev/null +++ b/docs/ssl_configuration.md @@ -0,0 +1,221 @@ +# SSL Configuration for Membrane RTMP Plugin + +This document shows how to configure SSL options for the RTMP server, including certificate paths and advanced SSL settings. + +## Application Configuration (config/config.exs) + +### Basic SSL Configuration +```elixir +import Config + +config :membrane_rtmp_plugin, :ssl, + certfile: "/path/to/your/certificate.pem", + keyfile: "/path/to/your/private_key.pem", + verify: :verify_none, + fail_if_no_peer_cert: false +``` + +### Advanced SSL Configuration +```elixir +config :membrane_rtmp_plugin, :ssl, + # Certificate files + certfile: "/path/to/your/certificate.pem", + keyfile: "/path/to/your/private_key.pem", + cacertfile: "/path/to/ca-bundle.pem", + certchain: "/path/to/cert-chain.pem", + password: "certificate_password", + + # SSL verification settings + verify: :verify_peer, + fail_if_no_peer_cert: true, + depth: 3, + + # TLS protocol settings + versions: [:"tlsv1.2", :"tlsv1.3"], + honor_cipher_order: true, + secure_renegotiate: true, + reuse_sessions: true, + + # Advanced options + alpn_advertised_protocols: ["h2", "http/1.1"], + alpn_preferred_protocols: ["h2", "http/1.1"], + log_level: :notice +``` + +### SSL Listen vs Handshake Options + +The library distinguishes between SSL options used for socket listening and those used during SSL handshake: + +**SSL Listen Options** (used when creating the SSL listening socket): +- Certificate and key files (`certfile`, `keyfile`, `cacertfile`, `certchain`) +- Certificate password (`password`) +- TLS versions (`versions`) +- Logging level (`log_level`) + +**SSL Handshake Options** (used during client SSL handshake): +- Verification settings (`verify`, `fail_if_no_peer_cert`, `depth`) +- Connection settings (`honor_cipher_order`, `secure_renegotiate`, `reuse_sessions`) +- Cipher configuration (`ciphers`) +- CA certificate for verification (`cacertfile`) + +This separation ensures that certificate-related options are properly configured during socket creation, while connection and verification options are applied during the handshake phase. + +## Certificate Path Resolution + +The library supports multiple ways to specify certificate paths: + +1. **Absolute paths**: `/etc/ssl/certs/server.pem` +2. **Relative paths**: The library will try to resolve relative paths in the following order: + - Relative to current working directory + - Relative to the application's `priv` directory +3. **Environment variable expansion**: Paths can use environment variables + +## Runtime Configuration + +You can also pass SSL options when starting the server: + +```elixir +{:ok, server} = Membrane.RTMPServer.start_link( + port: 1935, + use_ssl?: true, + ssl_options: [ + certfile: "/path/to/your/certificate.pem", + keyfile: "/path/to/your/private_key.pem", + verify: :verify_none, + fail_if_no_peer_cert: false, + versions: [:"tlsv1.2", :"tlsv1.3"], + log_level: :info + ], + handle_new_client: fn client_ref, app, stream_key -> + # Your client handler logic here + MyApp.ClientHandler + end +) +``` + +## Configuration Debugging + +To debug your SSL configuration, you can get a summary of all configuration sources: + +```elixir +# Get configuration summary +summary = Membrane.RTMPServer.Config.get_ssl_config_summary() + +# This returns a map with: +# %{ +# defaults: [...], # Default SSL options +# app_config: [...], # From application config +# runtime: [...], # Runtime options passed to the function +# final: [...] # Final merged configuration +# } + +IO.inspect(summary, label: "SSL Configuration Summary") +``` + +## Priority Order + +SSL options are applied in the following priority order (highest to lowest): + +1. **Runtime options** passed to `start_link/1` +2. **Application configuration** (`:membrane_rtmp_plugin, :ssl`) +3. **Default SSL options** + +Higher priority sources will override settings from lower priority sources. + +## Generating Self-Signed Certificates for Testing + +For testing purposes, you can generate self-signed certificates: + +```bash +# Generate private key +openssl genrsa -out private_key.pem 2048 + +# Generate certificate +openssl req -new -x509 -key private_key.pem -out certificate.pem -days 365 \ + -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" +``` + +## SSL Options Reference + +### Certificate Configuration +- `certfile`: Path to the certificate file (PEM format) +- `keyfile`: Path to the private key file (PEM format) +- `cacertfile`: Path to CA certificate bundle (for client verification) +- `certchain`: Path to certificate chain file (PEM format) +- `password`: Password for encrypted certificate files + +### Verification Settings +- `verify`: `:verify_none` or `:verify_peer` +- `fail_if_no_peer_cert`: Boolean, whether to fail if client doesn't provide certificate +- `depth`: Maximum certificate chain depth for verification + +### Protocol Settings +- `versions`: List of supported TLS versions (e.g., `[:"tlsv1.2", :"tlsv1.3"]`) +- `honor_cipher_order`: Boolean, whether to honor server cipher order +- `secure_renegotiate`: Boolean, whether to use secure renegotiation +- `reuse_sessions`: Boolean, whether to reuse SSL sessions + +### Advanced Options +- `ciphers`: List of allowed cipher suites +- `alpn_advertised_protocols`: List of ALPN protocols to advertise +- `alpn_preferred_protocols`: List of preferred ALPN protocols +- `sni_hosts`: SNI host configuration (for multi-domain certificates) +- `log_level`: SSL logging level (`:none`, `:error`, `:warning`, `:notice`, `:info`, `:debug`, `:all`) + +## Common Configuration Examples + +### Production Configuration (High Security) +```elixir +config :membrane_rtmp_plugin, :ssl, + certfile: "/etc/ssl/certs/server.pem", + keyfile: "/etc/ssl/private/server.key", + cacertfile: "/etc/ssl/certs/ca-bundle.pem", + verify: :verify_peer, + fail_if_no_peer_cert: true, + depth: 5, + versions: [:"tlsv1.2", :"tlsv1.3"], + honor_cipher_order: true, + secure_renegotiate: true, + reuse_sessions: true, + log_level: :warning +``` + +### Development Configuration (Self-Signed) +```elixir +config :membrane_rtmp_plugin, :ssl, + certfile: "priv/ssl/dev_cert.pem", + keyfile: "priv/ssl/dev_key.pem", + verify: :verify_none, + fail_if_no_peer_cert: false, + versions: [:"tlsv1.2", :"tlsv1.3"], + log_level: :info +``` + +### Testing Configuration +```elixir +config :membrane_rtmp_plugin, :ssl, + certfile: "test/fixtures/ssl/test_cert.pem", + keyfile: "test/fixtures/ssl/test_key.pem", + verify: :verify_none, + fail_if_no_peer_cert: false, + log_level: :debug +``` + +## SSL Requirements + +When enabling SSL (`use_ssl?: true`), the following are **required**: + +1. **Certificate file** (`certfile`): Path to your SSL certificate in PEM format +2. **Private key file** (`keyfile`): Path to your SSL private key in PEM format + +Without these, the SSL listener will fail to start with a helpful error message. + +## Quick Start + +To quickly get started with SSL, follow these steps: + +1. Obtain or generate your SSL certificate and private key files. +2. Place them in a secure directory on your server. +3. Update your Membrane RTMP configuration to include the paths to these files. +4. Set `use_ssl?: true` when starting your RTMP server. +5. Optionally, configure advanced SSL options as needed. diff --git a/docs/ssl_examples.md b/docs/ssl_examples.md new file mode 100644 index 0000000..8b8243b --- /dev/null +++ b/docs/ssl_examples.md @@ -0,0 +1,80 @@ +# SSL Configuration Examples for Membrane RTMP Plugin + +This directory contains example SSL configurations for different environments. + +## Quick Start + +1. **Development (Self-Signed Certificates)** + ```elixir + # config/dev.exs + import Config + + config :membrane_rtmp_plugin, :ssl, + certfile: "priv/ssl/dev_cert.pem", + keyfile: "priv/ssl/dev_key.pem", + verify: :verify_none, + fail_if_no_peer_cert: false, + log_level: :info + ``` + +2. **Production (CA-Signed Certificates)** + ```elixir + # config/prod.exs + import Config + + config :membrane_rtmp_plugin, :ssl, + certfile: "/etc/ssl/certs/server.pem", + keyfile: "/etc/ssl/private/server.key", + cacertfile: "/etc/ssl/certs/ca-bundle.pem", + verify: :verify_peer, + fail_if_no_peer_cert: true, + depth: 5, + versions: [:"tlsv1.2", :"tlsv1.3"], + honor_cipher_order: true, + log_level: :warning + ``` + +3. **Using Environment Variables** + ```bash + export RTMP_SSL_CERTFILE="/path/to/cert.pem" + export RTMP_SSL_KEYFILE="/path/to/key.pem" + export RTMP_SSL_CACERTFILE="/path/to/ca-bundle.pem" + ``` + +4. **Runtime Configuration** + ```elixir + {:ok, server} = Membrane.RTMPServer.start_link( + port: 1935, + use_ssl?: true, + ssl_options: [ + certfile: "/runtime/cert.pem", + keyfile: "/runtime/key.pem", + verify: :verify_none + ], + handle_new_client: &MyApp.handle_client/3 + ) + ``` + +## Configuration Priority + +SSL options are applied in this priority order (highest to lowest): +1. Runtime options (passed to `start_link/1`) +2. Application configuration (`:membrane_rtmp_plugin, :ssl`) +3. Environment variables +4. Default SSL options + +## Debugging Configuration + +To debug your SSL configuration, use the configuration summary: + +```elixir +summary = Membrane.RTMPServer.Config.get_ssl_config_summary() +IO.inspect(summary, label: "SSL Config Summary") +``` + +## Certificate Path Resolution + +The library automatically: +- Expands relative paths to absolute paths +- Tries to resolve relative paths in the application's `priv` directory +- Validates certificate file existence (when enabled) diff --git a/lib/membrane_rtmp_plugin/rtmp_server.ex b/lib/membrane_rtmp_plugin/rtmp_server.ex index 1e3a6e1..eceaf46 100644 --- a/lib/membrane_rtmp_plugin/rtmp_server.ex +++ b/lib/membrane_rtmp_plugin/rtmp_server.ex @@ -10,9 +10,40 @@ defmodule Membrane.RTMPServer do like sending the reference to another process. The function should return a `t:#{inspect(__MODULE__)}.client_behaviour_spec/0` which defines how the client should behave. - port: Port on which RTMP server will listen. Defaults to 1935. - - use_ssl?: If true, SSL socket (for RTMPS) will be used. Othwerwise, TCP socket (for RTMP) will be used. Defaults to false. + - use_ssl?: If true, SSL socket (for RTMPS) will be used. Otherwise, TCP socket (for RTMP) will be used. Defaults to false. + - ssl_options: Additional SSL options to override the configured ones. See `Membrane.RTMPServer.Config` for details. - client_timeout: Time after which an unused client connection is automatically closed, expressed in `Membrane.Time.t()` units. Defaults to 5 seconds. - name: If not nil, value of this field will be used as a name under which the server's process will be registered. Defaults to nil. + + ## SSL Configuration + + SSL options can be configured at the application level or passed as runtime options. + + ### Application Configuration + + config :membrane_rtmp_plugin, :ssl, + certfile: "/path/to/cert.pem", + keyfile: "/path/to/key.pem", + verify: :verify_none, + fail_if_no_peer_cert: false, + versions: [:"tlsv1.2", :"tlsv1.3"] + + ### Environment Variables (fallback) + + - `RTMP_SSL_CERTFILE` or `CERT_PATH` - Path to certificate file + - `RTMP_SSL_KEYFILE` or `CERT_KEY_PATH` - Path to private key file + + ### Runtime Options + + Membrane.RTMPServer.start_link( + port: 1935, + use_ssl?: true, + ssl_options: [ + certfile: "/path/to/cert.pem", + keyfile: "/path/to/key.pem" + ], + handle_new_client: &my_handler/3 + ) """ use GenServer @@ -26,15 +57,18 @@ defmodule Membrane.RTMPServer do @type t :: [ port: :inet.port_number(), use_ssl?: boolean(), + ssl_options: keyword(), name: atom() | nil, - handle_new_client: (client_ref :: pid(), app :: String.t(), stream_key :: String.t() -> - client_behaviour_spec()), + handle_new_client: + (client_ref :: pid(), app :: String.t(), stream_key :: String.t() -> + client_behaviour_spec()), client_timeout: Membrane.Time.t() ] @default_options %{ port: 1935, use_ssl?: false, + ssl_options: [], name: nil, client_timeout: Membrane.Time.seconds(5) } diff --git a/lib/membrane_rtmp_plugin/rtmp_server/config.ex b/lib/membrane_rtmp_plugin/rtmp_server/config.ex new file mode 100644 index 0000000..e644111 --- /dev/null +++ b/lib/membrane_rtmp_plugin/rtmp_server/config.ex @@ -0,0 +1,363 @@ +defmodule Membrane.RTMPServer.Config do + @moduledoc """ + Configuration module for RTMP server SSL settings. + + This module provides functions to retrieve SSL configuration from two sources: + - Application configuration (:membrane_rtmp_plugin app config) + - Runtime options (highest priority) + + ## Configuration Options + + SSL options can be configured in your application config: + + config :membrane_rtmp_plugin, :ssl, + certfile: "/path/to/cert.pem", + keyfile: "/path/to/key.pem", + verify: :verify_none, + fail_if_no_peer_cert: false, + versions: [:"tlsv1.2", :"tlsv1.3"], + ciphers: :ssl.cipher_suites(:default, :"tlsv1.2"), + honor_cipher_order: true, + # Additional certificate configuration + cacertfile: "/path/to/ca-bundle.pem", + certchain: "/path/to/cert-chain.pem", + password: "cert_password", + # Advanced SSL options + alpn_advertised_protocols: ["h2", "http/1.1"], + alpn_preferred_protocols: ["h2", "http/1.1"], + sni_hosts: [], + log_level: :notice + """ + + @type ssl_option :: + {:certfile, Path.t()} + | {:keyfile, Path.t()} + | {:verify, :verify_none | :verify_peer} + | {:fail_if_no_peer_cert, boolean()} + | {:versions, [:ssl.tls_version()]} + | {:ciphers, [:ssl.cipher()]} + | {:honor_cipher_order, boolean()} + | {:secure_renegotiate, boolean()} + | {:reuse_sessions, boolean()} + | {:cacertfile, Path.t()} + | {:certchain, Path.t()} + | {:depth, non_neg_integer()} + | {:password, String.t()} + | {:alpn_advertised_protocols, [String.t()]} + | {:alpn_preferred_protocols, [String.t()]} + | {:sni_hosts, keyword()} + | {:log_level, :ssl.log_level()} + + @type ssl_options :: [ssl_option()] + + @doc """ + Gets SSL options for the listener socket. + + Priority order: + 1. Runtime options passed to the function + 2. Application configuration (:membrane_rtmp_plugin, :ssl) + 3. Default SSL options + """ + @spec get_ssl_options(runtime_opts :: ssl_options(), validate_files :: boolean()) :: + ssl_options() + def get_ssl_options(runtime_opts \\ [], validate_files \\ true) do + default_opts = get_default_ssl_options() + app_config_opts = get_app_config_ssl_options() + + default_opts + |> Keyword.merge(app_config_opts) + |> Keyword.merge(runtime_opts) + |> process_certificate_paths() + |> validate_ssl_options(validate_files) + end + + @doc """ + Gets SSL options specifically for the SSL listen socket. + This excludes certificate files and handshake-specific options that should only be used during handshake. + """ + @spec get_ssl_listen_options(runtime_opts :: ssl_options(), validate_files :: boolean()) :: + ssl_options() + def get_ssl_listen_options(runtime_opts \\ [], validate_files \\ true) do + all_opts = get_ssl_options(runtime_opts, validate_files) + + # Only include options that are known to work with :ssl.listen/2 + # Based on Erlang/OTP ssl documentation + ssl_listen_opts = + all_opts + |> Keyword.take([ + :certfile, + :keyfile, + :cacertfile, + :password, + :versions + ]) + + # Ensure we have the minimum required options + if ssl_listen_opts == [] do + [] + else + ssl_listen_opts + end + end + + @doc """ + Gets SSL options specifically for the SSL handshake. + This includes verification and connection-specific options. + """ + @spec get_ssl_handshake_options(runtime_opts :: ssl_options(), validate_files :: boolean()) :: + ssl_options() + def get_ssl_handshake_options(runtime_opts \\ [], validate_files \\ false) do + get_ssl_options(runtime_opts, validate_files) + |> Keyword.take([ + :verify, + :fail_if_no_peer_cert, + :versions, + :ciphers, + :honor_cipher_order, + :secure_renegotiate, + :reuse_sessions, + :cacertfile, + :depth, + :log_level + ]) + end + + @doc """ + Gets the basic socket options for listening (non-SSL specific). + """ + @spec get_listen_options() :: [:inet.socket_option()] + def get_listen_options() do + [ + :binary, + packet: :raw, + active: false, + reuseaddr: true + ] + end + + @doc """ + Validates that required SSL files exist and options are valid. + Set validate_files to false to skip file existence checks (useful for testing). + """ + @spec validate_ssl_options(ssl_options(), validate_files :: boolean()) :: ssl_options() + def validate_ssl_options(opts, validate_files \\ true) do + opts = validate_certificate_files(opts, validate_files) + opts = validate_ssl_configuration(opts) + opts + end + + @doc """ + Gets a summary of the current SSL configuration from all sources. + Useful for debugging configuration issues. + """ + @spec get_ssl_config_summary(runtime_opts :: ssl_options()) :: %{ + defaults: ssl_options(), + app_config: ssl_options(), + runtime: ssl_options(), + final: ssl_options() + } + def get_ssl_config_summary(runtime_opts \\ []) do + defaults = get_default_ssl_options() + app_config = get_app_config_ssl_options() + final = get_ssl_options(runtime_opts, false) + + %{ + defaults: defaults, + app_config: app_config, + runtime: runtime_opts, + final: final + } + end + + # Private functions + + @spec get_default_ssl_options() :: ssl_options() + defp get_default_ssl_options() do + [ + verify: :verify_none, + fail_if_no_peer_cert: false, + # Use only TLS 1.2 for better compatibility + versions: [:"tlsv1.2"], + secure_renegotiate: true, + reuse_sessions: true, + # More verbose logging to debug handshake issues + log_level: :info + ] + end + + @spec get_app_config_ssl_options() :: ssl_options() + defp get_app_config_ssl_options() do + Application.get_env(:membrane_rtmp_plugin, :ssl, []) + end + + @spec process_certificate_paths(ssl_options()) :: ssl_options() + defp process_certificate_paths(opts) do + opts + |> expand_certificate_paths() + |> resolve_relative_paths() + end + + @spec expand_certificate_paths(ssl_options()) :: ssl_options() + defp expand_certificate_paths(opts) do + opts + |> maybe_expand_path(:certfile) + |> maybe_expand_path(:keyfile) + |> maybe_expand_path(:cacertfile) + |> maybe_expand_path(:certchain) + end + + @spec maybe_expand_path(ssl_options(), atom()) :: ssl_options() + defp maybe_expand_path(opts, key) do + case opts[key] do + nil -> opts + path when is_binary(path) -> Keyword.put(opts, key, Path.expand(path)) + _other -> opts + end + end + + @spec resolve_relative_paths(ssl_options()) :: ssl_options() + defp resolve_relative_paths(opts) do + # If certificate files are specified with relative paths, + # try to resolve them relative to the app's priv directory + priv_dir = Application.app_dir(:membrane_rtmp_plugin, "priv") + + opts + |> maybe_resolve_relative_to_priv(:certfile, priv_dir) + |> maybe_resolve_relative_to_priv(:keyfile, priv_dir) + |> maybe_resolve_relative_to_priv(:cacertfile, priv_dir) + |> maybe_resolve_relative_to_priv(:certchain, priv_dir) + end + + @spec maybe_resolve_relative_to_priv(ssl_options(), atom(), String.t()) :: ssl_options() + defp maybe_resolve_relative_to_priv(opts, key, priv_dir) do + case opts[key] do + nil -> + opts + + path when is_binary(path) -> + resolve_path_relative_to_priv(opts, key, path, priv_dir) + + _other -> + opts + end + end + + @spec resolve_path_relative_to_priv(ssl_options(), atom(), String.t(), String.t()) :: + ssl_options() + defp resolve_path_relative_to_priv(opts, key, path, priv_dir) do + if Path.absname(path) == path do + # Already absolute + opts + else + priv_path = Path.join(priv_dir, path) + + if File.exists?(priv_path) do + Keyword.put(opts, key, priv_path) + else + # Keep original path + opts + end + end + end + + @spec validate_certificate_files(ssl_options(), boolean()) :: ssl_options() + defp validate_certificate_files(opts, validate_files) do + case {opts[:certfile], opts[:keyfile]} do + {nil, nil} -> + opts + + {certfile, keyfile} when is_binary(certfile) and is_binary(keyfile) -> + validate_cert_and_key_files(opts, validate_files, certfile, keyfile) + + {certfile, nil} when is_binary(certfile) -> + validate_single_cert_file( + validate_files, + "SSL certificate file provided but key file is missing" + ) + + opts + + {nil, keyfile} when is_binary(keyfile) -> + validate_single_cert_file( + validate_files, + "SSL key file provided but certificate file is missing" + ) + + opts + end + end + + @spec validate_cert_and_key_files(ssl_options(), boolean(), String.t(), String.t()) :: + ssl_options() + defp validate_cert_and_key_files(opts, validate_files, certfile, keyfile) do + if validate_files do + validate_file_exists(certfile, "SSL certificate") + validate_file_exists(keyfile, "SSL key") + validate_additional_cert_files(opts) + end + + opts + end + + @spec validate_additional_cert_files(ssl_options()) :: :ok + defp validate_additional_cert_files(opts) do + if opts[:cacertfile], do: validate_file_exists(opts[:cacertfile], "SSL CA certificate") + if opts[:certchain], do: validate_file_exists(opts[:certchain], "SSL certificate chain") + :ok + end + + @spec validate_single_cert_file(boolean(), String.t()) :: :ok + defp validate_single_cert_file(validate_files, error_message) do + if validate_files do + raise ArgumentError, error_message + end + + :ok + end + + @spec validate_file_exists(String.t(), String.t()) :: :ok + defp validate_file_exists(file_path, file_type) do + unless File.exists?(file_path) do + raise ArgumentError, "#{file_type} file does not exist: #{file_path}" + end + + :ok + end + + @spec validate_ssl_configuration(ssl_options()) :: ssl_options() + defp validate_ssl_configuration(opts) do + # Validate TLS versions + if versions = opts[:versions] do + valid_versions = [:tlsv1, :"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] + invalid_versions = versions -- valid_versions + + unless Enum.empty?(invalid_versions) do + raise ArgumentError, + "Invalid TLS versions: #{inspect(invalid_versions)}. " <> + "Valid versions are: #{inspect(valid_versions)}" + end + end + + # Validate verify option + if verify = opts[:verify] do + unless verify in [:verify_none, :verify_peer] do + raise ArgumentError, + "Invalid verify option: #{inspect(verify)}. " <> + "Must be :verify_none or :verify_peer" + end + end + + # Validate log level + if log_level = opts[:log_level] do + valid_levels = [:none, :error, :warning, :notice, :info, :debug, :all] + + unless log_level in valid_levels do + raise ArgumentError, + "Invalid SSL log level: #{inspect(log_level)}. " <> + "Valid levels are: #{inspect(valid_levels)}" + end + end + + opts + end +end diff --git a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex index fa54038..1ebc0cd 100644 --- a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex +++ b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex @@ -5,14 +5,15 @@ defmodule Membrane.RTMPServer.Listener do use Task require Logger - alias Membrane.RTMPServer.ClientHandler + alias Membrane.RTMPServer.{ClientHandler, Config} @spec run( options :: %{ use_ssl?: boolean(), socket_module: :gen_tcp | :ssl, server: pid(), - port: non_neg_integer() + port: non_neg_integer(), + ssl_options: keyword() } ) :: no_return() def run(options) do @@ -20,22 +21,48 @@ defmodule Membrane.RTMPServer.Listener do listen_options = if options.use_ssl? do - certfile = System.get_env("CERT_PATH") - keyfile = System.get_env("CERT_KEY_PATH") - - [ - :binary, - packet: :raw, - active: false, - certfile: certfile, - keyfile: keyfile - ] + ssl_opts = Config.get_ssl_listen_options(Map.get(options, :ssl_options, []), true) + + Logger.debug("SSL options for listen: #{inspect(ssl_opts)}") + + # SSL listen requires at least certfile and keyfile + unless ssl_opts[:certfile] && ssl_opts[:keyfile] do + raise ArgumentError, """ + SSL is enabled but certificate files are not configured. + Please configure SSL certificate files via: + + 1. Application config: + config :membrane_rtmp_plugin, :ssl, + certfile: "/path/to/cert.pem", + keyfile: "/path/to/key.pem" + + 2. Environment variables: + RTMP_SSL_CERTFILE="/path/to/cert.pem" + RTMP_SSL_KEYFILE="/path/to/key.pem" + + 3. Runtime options: + ssl_options: [ + certfile: "/path/to/cert.pem", + keyfile: "/path/to/key.pem" + ] + """ + end + + # Additional validation for certificate files + if ssl_opts[:certfile] && !File.exists?(ssl_opts[:certfile]) do + raise ArgumentError, "SSL certificate file does not exist: #{ssl_opts[:certfile]}" + end + + if ssl_opts[:keyfile] && !File.exists?(ssl_opts[:keyfile]) do + raise ArgumentError, "SSL key file does not exist: #{ssl_opts[:keyfile]}" + end + + basic_opts = Config.get_listen_options() + combined = basic_opts ++ ssl_opts + Logger.debug("Combined listen options: #{inspect(combined)}") + combined else - [ - :binary, - packet: :raw, - active: false - ] + Config.get_listen_options() end {:ok, socket} = options.socket_module.listen(options.port, listen_options) @@ -53,8 +80,6 @@ defmodule Membrane.RTMPServer.Listener do send(options.server, {:port, port}) - # send(options.server, {:port, :inet.port(socket)}) |> dbg() - accept_loop(socket, options) end @@ -69,13 +94,17 @@ defmodule Membrane.RTMPServer.Listener do {:ok, client} = :ssl.transport_accept(socket) Logger.debug("SSL transport accept successful, starting handshake...") - # Start with very basic SSL options - ssl_opts = [ - verify: :verify_none, - fail_if_no_peer_cert: false - ] + ssl_handshake_opts = + Config.get_ssl_handshake_options(Map.get(options, :ssl_options, []), false) + + ssl_handshake_opts = + ssl_handshake_opts + |> Keyword.put(:verify, :verify_none) + |> Keyword.put(:fail_if_no_peer_cert, false) - case :ssl.handshake(client, ssl_opts, 10000) do + Logger.debug("SSL handshake options: #{inspect(ssl_handshake_opts)}") + + case :ssl.handshake(client, ssl_handshake_opts, 10_000) do {:ok, ssl_socket} -> Logger.info("SSL handshake successful") ssl_socket @@ -91,8 +120,6 @@ defmodule Membrane.RTMPServer.Listener do end end - client |> dbg() - {:ok, client_reference} = GenServer.start_link(ClientHandler, socket: client, diff --git a/test/membrane_rtmp_plugin/rtmp_server_config_test.exs b/test/membrane_rtmp_plugin/rtmp_server_config_test.exs new file mode 100644 index 0000000..832fb94 --- /dev/null +++ b/test/membrane_rtmp_plugin/rtmp_server_config_test.exs @@ -0,0 +1,330 @@ +defmodule Membrane.RTMPServer.ConfigTest do + use ExUnit.Case, async: false + + alias Membrane.RTMPServer.Config + + describe "get_ssl_options/1" do + setup do + # Clean up any existing config before each test + Application.delete_env(:membrane_rtmp_plugin, :ssl) + :ok + end + + test "returns default options when no configuration is provided" do + options = Config.get_ssl_options([], false) + + assert options[:verify] == :verify_none + assert options[:fail_if_no_peer_cert] == false + assert options[:versions] == [:"tlsv1.2"] + assert options[:secure_renegotiate] == true + assert options[:reuse_sessions] == true + end + + test "merges application configuration with defaults" do + Application.put_env(:membrane_rtmp_plugin, :ssl, + certfile: "/app/cert.pem", + keyfile: "/app/key.pem", + verify: :verify_peer + ) + + options = Config.get_ssl_options([], false) + + assert options[:certfile] == "/app/cert.pem" + assert options[:keyfile] == "/app/key.pem" + assert options[:verify] == :verify_peer + # Default options should still be present + assert options[:fail_if_no_peer_cert] == false + assert options[:versions] == [:"tlsv1.2"] + end + + test "runtime options override application configuration" do + Application.put_env(:membrane_rtmp_plugin, :ssl, + certfile: "/app/cert.pem", + verify: :verify_peer + ) + + runtime_opts = [ + certfile: "/runtime/cert.pem", + keyfile: "/runtime/key.pem" + ] + + options = Config.get_ssl_options(runtime_opts, false) + + assert options[:certfile] == "/runtime/cert.pem" + assert options[:keyfile] == "/runtime/key.pem" + # App config should still be applied for non-overridden options + assert options[:verify] == :verify_peer + end + end + + describe "get_listen_options/0" do + test "returns basic socket options" do + options = Config.get_listen_options() + + assert :binary in options + assert options[:packet] == :raw + assert options[:active] == false + assert options[:reuseaddr] == true + end + end + + describe "validate_ssl_options/1" do + @tag :tmp_dir + test "validates existing certificate files", %{tmp_dir: tmp_dir} do + cert_path = Path.join(tmp_dir, "cert.pem") + key_path = Path.join(tmp_dir, "key.pem") + + File.write!(cert_path, "dummy cert") + File.write!(key_path, "dummy key") + + options = [certfile: cert_path, keyfile: key_path] + validated = Config.validate_ssl_options(options) + + assert validated == options + end + + test "raises error for non-existent certificate file" do + options = [certfile: "/non/existent/cert.pem", keyfile: "/non/existent/key.pem"] + + assert_raise ArgumentError, ~r/SSL certificate file does not exist/, fn -> + Config.validate_ssl_options(options) + end + end + + @tag :tmp_dir + test "raises error for missing key file when cert is provided", %{tmp_dir: tmp_dir} do + cert_path = Path.join(tmp_dir, "cert.pem") + File.write!(cert_path, "dummy cert") + + options = [certfile: cert_path] + + assert_raise ArgumentError, ~r/SSL certificate file provided but key file is missing/, fn -> + Config.validate_ssl_options(options) + end + end + + test "allows empty options" do + options = [] + validated = Config.validate_ssl_options(options) + + assert validated == [] + end + end + + describe "get_ssl_config_summary/1" do + setup do + # Clean up any existing config before each test + Application.delete_env(:membrane_rtmp_plugin, :ssl) + :ok + end + + test "provides comprehensive configuration overview" do + # Set up different configuration sources + Application.put_env(:membrane_rtmp_plugin, :ssl, + certfile: "/app/cert.pem", + verify: :verify_peer + ) + + runtime_opts = [keyfile: "/runtime/key.pem", versions: [:"tlsv1.3"]] + + summary = Config.get_ssl_config_summary(runtime_opts) + + # Check that all configuration sources are represented + assert is_list(summary.defaults) + assert is_list(summary.app_config) + assert is_list(summary.runtime) + assert is_list(summary.final) + + # Verify app config + assert summary.app_config[:certfile] == "/app/cert.pem" + assert summary.app_config[:verify] == :verify_peer + + # Verify runtime config + assert summary.runtime[:keyfile] == "/runtime/key.pem" + assert summary.runtime[:versions] == [:"tlsv1.3"] + + # Verify final config has proper priority + # Runtime overrides defaults + assert summary.final[:keyfile] == "/runtime/key.pem" + # From app config + assert summary.final[:verify] == :verify_peer + # From runtime + assert summary.final[:versions] == [:"tlsv1.3"] + end + end + + describe "SSL configuration validation" do + test "validates TLS versions" do + options = [versions: [:invalid_version]] + + assert_raise ArgumentError, ~r/Invalid TLS versions/, fn -> + Config.validate_ssl_options(options, false) + end + end + + test "validates verify option" do + options = [verify: :invalid_verify] + + assert_raise ArgumentError, ~r/Invalid verify option/, fn -> + Config.validate_ssl_options(options, false) + end + end + + test "validates log level" do + options = [log_level: :invalid_level] + + assert_raise ArgumentError, ~r/Invalid SSL log level/, fn -> + Config.validate_ssl_options(options, false) + end + end + + test "accepts valid configuration" do + options = [ + versions: [:"tlsv1.2", :"tlsv1.3"], + verify: :verify_peer, + log_level: :info + ] + + validated = Config.validate_ssl_options(options, false) + assert validated == options + end + end + + describe "certificate path processing" do + @tag :tmp_dir + test "expands relative paths", %{tmp_dir: tmp_dir} do + cert_name = "test_cert.pem" + File.write!(Path.join(tmp_dir, cert_name), "dummy cert") + + # Change to tmp_dir to test relative path resolution + original_cwd = File.cwd!() + File.cd!(tmp_dir) + + try do + options = [certfile: cert_name] + processed = Config.get_ssl_options(options, false) + + # Should be expanded to absolute path + assert Path.absname(processed[:certfile]) == processed[:certfile] + assert String.ends_with?(processed[:certfile], cert_name) + after + File.cd!(original_cwd) + end + end + + @tag :tmp_dir + test "validates additional certificate files", %{tmp_dir: tmp_dir} do + cert_path = Path.join(tmp_dir, "cert.pem") + key_path = Path.join(tmp_dir, "key.pem") + ca_path = Path.join(tmp_dir, "ca.pem") + + File.write!(cert_path, "dummy cert") + File.write!(key_path, "dummy key") + # Don't create CA file to test validation + + options = [ + certfile: cert_path, + keyfile: key_path, + cacertfile: ca_path + ] + + assert_raise ArgumentError, ~r/SSL CA certificate file does not exist/, fn -> + Config.validate_ssl_options(options, true) + end + end + end + + describe "SSL listen vs handshake options" do + setup do + # Clean up any existing config before each test + Application.delete_env(:membrane_rtmp_plugin, :ssl) + :ok + end + + test "get_ssl_listen_options includes certificate and basic SSL options" do + options = + Config.get_ssl_listen_options( + [ + certfile: "/path/cert.pem", + keyfile: "/path/key.pem", + verify: :verify_peer, + versions: [:"tlsv1.3"], + honor_cipher_order: true + ], + false + ) + + # Should include certificate options for SSL context + assert options[:certfile] == "/path/cert.pem" + assert options[:keyfile] == "/path/key.pem" + assert options[:versions] == [:"tlsv1.3"] + + # Should NOT include handshake-specific options + refute Keyword.has_key?(options, :verify) + refute Keyword.has_key?(options, :honor_cipher_order) + end + + test "get_ssl_handshake_options includes verification and connection options" do + options = + Config.get_ssl_handshake_options( + [ + certfile: "/path/cert.pem", + keyfile: "/path/key.pem", + verify: :verify_peer, + versions: [:"tlsv1.3"], + honor_cipher_order: true, + fail_if_no_peer_cert: true + ], + false + ) + + # Should include handshake options + assert options[:verify] == :verify_peer + assert options[:versions] == [:"tlsv1.3"] + assert options[:honor_cipher_order] == true + assert options[:fail_if_no_peer_cert] == true + + # Should NOT include certificate file paths (these should be in listen options) + refute Keyword.has_key?(options, :certfile) + refute Keyword.has_key?(options, :keyfile) + end + + test "listen and handshake options are complementary" do + full_config = [ + certfile: "/path/cert.pem", + keyfile: "/path/key.pem", + cacertfile: "/path/ca.pem", + verify: :verify_peer, + versions: [:"tlsv1.3"], + honor_cipher_order: true, + fail_if_no_peer_cert: true, + secure_renegotiate: true + ] + + listen_opts = Config.get_ssl_listen_options(full_config, false) + handshake_opts = Config.get_ssl_handshake_options(full_config, false) + + # Ensure no overlap in critical options + listen_keys = Keyword.keys(listen_opts) + handshake_keys = Keyword.keys(handshake_opts) + + # These should only appear in listen options + assert :certfile in listen_keys + assert :keyfile in listen_keys + refute :certfile in handshake_keys + refute :keyfile in handshake_keys + + # These should only appear in handshake options + assert :verify in handshake_keys + assert :honor_cipher_order in handshake_keys + refute :verify in listen_keys + refute :honor_cipher_order in listen_keys + + # These can appear in both + assert :versions in listen_keys + assert :versions in handshake_keys + # Needed for verification + assert :cacertfile in handshake_keys + end + end +end diff --git a/test/membrane_rtmp_plugin/rtmp_ssl_listener_test.exs b/test/membrane_rtmp_plugin/rtmp_ssl_listener_test.exs new file mode 100644 index 0000000..1315ad8 --- /dev/null +++ b/test/membrane_rtmp_plugin/rtmp_ssl_listener_test.exs @@ -0,0 +1,99 @@ +defmodule Membrane.RTMPServer.SSLListenerTest do + @moduledoc """ + Test to verify that SSL listener works correctly with proper option separation. + """ + + use ExUnit.Case, async: false + + alias Membrane.RTMPServer.{Config, Listener} + + @tag :tmp_dir + test "SSL listen options don't cause argument errors", %{tmp_dir: tmp_dir} do + # Create dummy certificate files + cert_path = Path.join(tmp_dir, "cert.pem") + key_path = Path.join(tmp_dir, "key.pem") + + # Create minimal valid certificate content for testing + File.write!(cert_path, """ + -----BEGIN CERTIFICATE----- + MIICdTCCAd4CCQDKn4iM3Jm8ZzANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMC + VVMxCzAJBgNVBAgMAlRYMQ8wDQYDVQQHDAZBdXN0aW4xEjAQBgNVBAoMCVRlc3Qg + Q29ycDELMAkGA1UECwwCSVQxDDAKBgNVBAMMA3d3dzElMCMGCSqGSIb3DQEJARYW + dGVzdEBleGFtcGxlLmNvbQ== + -----END CERTIFICATE----- + """) + + File.write!(key_path, """ + -----BEGIN PRIVATE KEY----- + MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC5w9Y+7Y+7Y+7Y + +7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+ + 7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+ + -----END PRIVATE KEY----- + """) + + # Test SSL listen options separately + ssl_config = [ + certfile: cert_path, + keyfile: key_path, + verify: :verify_none, + fail_if_no_peer_cert: false, + versions: [:"tlsv1.2", :"tlsv1.3"] + ] + + # Get SSL listen options - these should be safe for :ssl.listen/2 + listen_opts = Config.get_ssl_listen_options(ssl_config, true) + basic_opts = Config.get_listen_options() + combined_opts = basic_opts ++ listen_opts + + # Verify the options contain what we expect for listening + assert listen_opts[:certfile] == cert_path + assert listen_opts[:keyfile] == key_path + assert listen_opts[:versions] == [:"tlsv1.2", :"tlsv1.3"] + + # Verify handshake options are separate + handshake_opts = Config.get_ssl_handshake_options(ssl_config, false) + assert handshake_opts[:verify] == :verify_none + assert handshake_opts[:fail_if_no_peer_cert] == false + + # Verify that listen options don't contain handshake-only options + refute Keyword.has_key?(listen_opts, :verify) + refute Keyword.has_key?(listen_opts, :fail_if_no_peer_cert) + + # The real test would be to try :ssl.listen/2, but that requires a proper certificate + # For now, we verify the option separation is working correctly + assert is_list(combined_opts) + assert length(combined_opts) > 0 + end + + test "SSL listener provides helpful error when no certificates configured" do + # Clear any existing SSL configuration + Application.delete_env(:membrane_rtmp_plugin, :ssl) + + # Clean up SSL environment variables + ssl_env_vars = [ + "RTMP_SSL_CERTFILE", + "CERT_PATH", + "RTMP_SSL_KEYFILE", + "CERT_KEY_PATH", + "RTMP_SSL_CACERTFILE", + "CA_CERT_PATH" + ] + + Enum.each(ssl_env_vars, &System.delete_env/1) + + # Create options without SSL certificates + options = %{ + use_ssl?: true, + ssl_options: [], + server: self(), + port: 0, + handle_new_client: fn _client_ref, _app, _stream_key -> :ok end, + client_timeout: 1000 + } + + # Should raise a helpful ArgumentError + assert_raise ArgumentError, ~r/SSL is enabled but certificate files are not configured/, fn -> + Listener.run(options) + end + end +end From de3722c2cf4640661c9467a709987ce9247c97b0 Mon Sep 17 00:00:00 2001 From: Oleg Okunevych Date: Thu, 19 Jun 2025 12:29:07 +0300 Subject: [PATCH 3/7] Addressed Code Review comments --- .../rtmp_server/config.ex | 105 +++++------------- .../rtmp_server/listener.ex | 32 ------ .../rtmp_server_config_test.exs | 15 +-- .../rtmp_source_bin_test.exs | 2 +- 4 files changed, 35 insertions(+), 119 deletions(-) diff --git a/lib/membrane_rtmp_plugin/rtmp_server/config.ex b/lib/membrane_rtmp_plugin/rtmp_server/config.ex index e644111..e372bfc 100644 --- a/lib/membrane_rtmp_plugin/rtmp_server/config.ex +++ b/lib/membrane_rtmp_plugin/rtmp_server/config.ex @@ -78,26 +78,15 @@ defmodule Membrane.RTMPServer.Config do @spec get_ssl_listen_options(runtime_opts :: ssl_options(), validate_files :: boolean()) :: ssl_options() def get_ssl_listen_options(runtime_opts \\ [], validate_files \\ true) do - all_opts = get_ssl_options(runtime_opts, validate_files) - - # Only include options that are known to work with :ssl.listen/2 - # Based on Erlang/OTP ssl documentation - ssl_listen_opts = - all_opts - |> Keyword.take([ - :certfile, - :keyfile, - :cacertfile, - :password, - :versions - ]) - - # Ensure we have the minimum required options - if ssl_listen_opts == [] do - [] - else - ssl_listen_opts - end + runtime_opts + |> get_ssl_options(validate_files) + |> Keyword.take([ + :certfile, + :keyfile, + :cacertfile, + :password, + :versions + ]) end @doc """ @@ -141,9 +130,9 @@ defmodule Membrane.RTMPServer.Config do """ @spec validate_ssl_options(ssl_options(), validate_files :: boolean()) :: ssl_options() def validate_ssl_options(opts, validate_files \\ true) do - opts = validate_certificate_files(opts, validate_files) - opts = validate_ssl_configuration(opts) opts + |> validate_certificate_files(validate_files) + |> validate_ssl_configuration() end @doc """ @@ -171,7 +160,6 @@ defmodule Membrane.RTMPServer.Config do # Private functions - @spec get_default_ssl_options() :: ssl_options() defp get_default_ssl_options() do [ verify: :verify_none, @@ -185,19 +173,16 @@ defmodule Membrane.RTMPServer.Config do ] end - @spec get_app_config_ssl_options() :: ssl_options() defp get_app_config_ssl_options() do Application.get_env(:membrane_rtmp_plugin, :ssl, []) end - @spec process_certificate_paths(ssl_options()) :: ssl_options() defp process_certificate_paths(opts) do opts |> expand_certificate_paths() |> resolve_relative_paths() end - @spec expand_certificate_paths(ssl_options()) :: ssl_options() defp expand_certificate_paths(opts) do opts |> maybe_expand_path(:certfile) @@ -206,7 +191,6 @@ defmodule Membrane.RTMPServer.Config do |> maybe_expand_path(:certchain) end - @spec maybe_expand_path(ssl_options(), atom()) :: ssl_options() defp maybe_expand_path(opts, key) do case opts[key] do nil -> opts @@ -215,7 +199,6 @@ defmodule Membrane.RTMPServer.Config do end end - @spec resolve_relative_paths(ssl_options()) :: ssl_options() defp resolve_relative_paths(opts) do # If certificate files are specified with relative paths, # try to resolve them relative to the app's priv directory @@ -228,7 +211,6 @@ defmodule Membrane.RTMPServer.Config do |> maybe_resolve_relative_to_priv(:certchain, priv_dir) end - @spec maybe_resolve_relative_to_priv(ssl_options(), atom(), String.t()) :: ssl_options() defp maybe_resolve_relative_to_priv(opts, key, priv_dir) do case opts[key] do nil -> @@ -242,8 +224,6 @@ defmodule Membrane.RTMPServer.Config do end end - @spec resolve_path_relative_to_priv(ssl_options(), atom(), String.t(), String.t()) :: - ssl_options() defp resolve_path_relative_to_priv(opts, key, path, priv_dir) do if Path.absname(path) == path do # Already absolute @@ -260,62 +240,49 @@ defmodule Membrane.RTMPServer.Config do end end - @spec validate_certificate_files(ssl_options(), boolean()) :: ssl_options() - defp validate_certificate_files(opts, validate_files) do + defp validate_certificate_files(opts, true) do case {opts[:certfile], opts[:keyfile]} do - {nil, nil} -> - opts - - {certfile, keyfile} when is_binary(certfile) and is_binary(keyfile) -> - validate_cert_and_key_files(opts, validate_files, certfile, keyfile) - {certfile, nil} when is_binary(certfile) -> - validate_single_cert_file( - validate_files, - "SSL certificate file provided but key file is missing" - ) + validate_single_cert_file("SSL certificate file provided but key file is missing") opts {nil, keyfile} when is_binary(keyfile) -> - validate_single_cert_file( - validate_files, - "SSL key file provided but certificate file is missing" - ) + validate_single_cert_file("SSL key file provided but certificate file is missing") opts + + {certfile, keyfile} -> + validate_cert_and_key_files(opts, certfile, keyfile) end end - @spec validate_cert_and_key_files(ssl_options(), boolean(), String.t(), String.t()) :: - ssl_options() - defp validate_cert_and_key_files(opts, validate_files, certfile, keyfile) do - if validate_files do - validate_file_exists(certfile, "SSL certificate") - validate_file_exists(keyfile, "SSL key") - validate_additional_cert_files(opts) - end + defp validate_certificate_files(opts, false) do + opts + end + + defp validate_cert_and_key_files(opts, certfile, keyfile) do + validate_file_exists(certfile, "SSL certificate") + validate_file_exists(keyfile, "SSL key") + validate_additional_cert_files(opts) opts end - @spec validate_additional_cert_files(ssl_options()) :: :ok defp validate_additional_cert_files(opts) do if opts[:cacertfile], do: validate_file_exists(opts[:cacertfile], "SSL CA certificate") if opts[:certchain], do: validate_file_exists(opts[:certchain], "SSL certificate chain") :ok end - @spec validate_single_cert_file(boolean(), String.t()) :: :ok - defp validate_single_cert_file(validate_files, error_message) do - if validate_files do - raise ArgumentError, error_message - end + defp validate_single_cert_file(error_message) do + raise ArgumentError, error_message + end - :ok + defp validate_file_exists(nil, file_type) do + raise ArgumentError, "#{file_type} file is not configured" end - @spec validate_file_exists(String.t(), String.t()) :: :ok defp validate_file_exists(file_path, file_type) do unless File.exists?(file_path) do raise ArgumentError, "#{file_type} file does not exist: #{file_path}" @@ -324,7 +291,6 @@ defmodule Membrane.RTMPServer.Config do :ok end - @spec validate_ssl_configuration(ssl_options()) :: ssl_options() defp validate_ssl_configuration(opts) do # Validate TLS versions if versions = opts[:versions] do @@ -347,17 +313,6 @@ defmodule Membrane.RTMPServer.Config do end end - # Validate log level - if log_level = opts[:log_level] do - valid_levels = [:none, :error, :warning, :notice, :info, :debug, :all] - - unless log_level in valid_levels do - raise ArgumentError, - "Invalid SSL log level: #{inspect(log_level)}. " <> - "Valid levels are: #{inspect(valid_levels)}" - end - end - opts end end diff --git a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex index 1ebc0cd..189b83f 100644 --- a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex +++ b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex @@ -25,38 +25,6 @@ defmodule Membrane.RTMPServer.Listener do Logger.debug("SSL options for listen: #{inspect(ssl_opts)}") - # SSL listen requires at least certfile and keyfile - unless ssl_opts[:certfile] && ssl_opts[:keyfile] do - raise ArgumentError, """ - SSL is enabled but certificate files are not configured. - Please configure SSL certificate files via: - - 1. Application config: - config :membrane_rtmp_plugin, :ssl, - certfile: "/path/to/cert.pem", - keyfile: "/path/to/key.pem" - - 2. Environment variables: - RTMP_SSL_CERTFILE="/path/to/cert.pem" - RTMP_SSL_KEYFILE="/path/to/key.pem" - - 3. Runtime options: - ssl_options: [ - certfile: "/path/to/cert.pem", - keyfile: "/path/to/key.pem" - ] - """ - end - - # Additional validation for certificate files - if ssl_opts[:certfile] && !File.exists?(ssl_opts[:certfile]) do - raise ArgumentError, "SSL certificate file does not exist: #{ssl_opts[:certfile]}" - end - - if ssl_opts[:keyfile] && !File.exists?(ssl_opts[:keyfile]) do - raise ArgumentError, "SSL key file does not exist: #{ssl_opts[:keyfile]}" - end - basic_opts = Config.get_listen_options() combined = basic_opts ++ ssl_opts Logger.debug("Combined listen options: #{inspect(combined)}") diff --git a/test/membrane_rtmp_plugin/rtmp_server_config_test.exs b/test/membrane_rtmp_plugin/rtmp_server_config_test.exs index 832fb94..e6811ee 100644 --- a/test/membrane_rtmp_plugin/rtmp_server_config_test.exs +++ b/test/membrane_rtmp_plugin/rtmp_server_config_test.exs @@ -103,11 +103,12 @@ defmodule Membrane.RTMPServer.ConfigTest do end end - test "allows empty options" do + test "doesn't allow options without certificate file" do options = [] - validated = Config.validate_ssl_options(options) - assert validated == [] + assert_raise ArgumentError, ~r/SSL certificate file is not configured/, fn -> + Config.validate_ssl_options(options) + end end end @@ -170,14 +171,6 @@ defmodule Membrane.RTMPServer.ConfigTest do end end - test "validates log level" do - options = [log_level: :invalid_level] - - assert_raise ArgumentError, ~r/Invalid SSL log level/, fn -> - Config.validate_ssl_options(options, false) - end - end - test "accepts valid configuration" do options = [ versions: [:"tlsv1.2", :"tlsv1.3"], diff --git a/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs b/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs index 5729b23..cd2aa25 100644 --- a/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs +++ b/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs @@ -233,7 +233,7 @@ defmodule Membrane.RTMP.SourceBin.IntegrationTest do client_timeout: Membrane.Time.seconds(3) ) - {:ok, assigned_port} = Membrane.RTMPServer.get_port(server_pid) + assigned_port = Membrane.RTMPServer.get_port(server_pid) send(parent, {:port, assigned_port}) From f9c538b7153e8995e578195bf7bb8190338a0130 Mon Sep 17 00:00:00 2001 From: Oleg Okunevych Date: Mon, 7 Jul 2025 13:51:44 +0300 Subject: [PATCH 4/7] Remove Membrane.RTMPServer.Config module. Enable RTMPS Server test --- docs/ssl_configuration.md | 221 ------------ docs/ssl_examples.md | 80 ----- lib/membrane_rtmp_plugin/rtmp_server.ex | 20 +- .../rtmp_server/config.ex | 318 ----------------- .../rtmp_server/listener.ex | 44 ++- .../rtmp_server_config_test.exs | 323 ------------------ .../rtmp_source_bin_test.exs | 40 ++- .../rtmp_ssl_listener_test.exs | 99 ------ test/test_helper.exs | 2 +- 9 files changed, 88 insertions(+), 1059 deletions(-) delete mode 100644 docs/ssl_configuration.md delete mode 100644 docs/ssl_examples.md delete mode 100644 lib/membrane_rtmp_plugin/rtmp_server/config.ex delete mode 100644 test/membrane_rtmp_plugin/rtmp_server_config_test.exs delete mode 100644 test/membrane_rtmp_plugin/rtmp_ssl_listener_test.exs diff --git a/docs/ssl_configuration.md b/docs/ssl_configuration.md deleted file mode 100644 index bb734c7..0000000 --- a/docs/ssl_configuration.md +++ /dev/null @@ -1,221 +0,0 @@ -# SSL Configuration for Membrane RTMP Plugin - -This document shows how to configure SSL options for the RTMP server, including certificate paths and advanced SSL settings. - -## Application Configuration (config/config.exs) - -### Basic SSL Configuration -```elixir -import Config - -config :membrane_rtmp_plugin, :ssl, - certfile: "/path/to/your/certificate.pem", - keyfile: "/path/to/your/private_key.pem", - verify: :verify_none, - fail_if_no_peer_cert: false -``` - -### Advanced SSL Configuration -```elixir -config :membrane_rtmp_plugin, :ssl, - # Certificate files - certfile: "/path/to/your/certificate.pem", - keyfile: "/path/to/your/private_key.pem", - cacertfile: "/path/to/ca-bundle.pem", - certchain: "/path/to/cert-chain.pem", - password: "certificate_password", - - # SSL verification settings - verify: :verify_peer, - fail_if_no_peer_cert: true, - depth: 3, - - # TLS protocol settings - versions: [:"tlsv1.2", :"tlsv1.3"], - honor_cipher_order: true, - secure_renegotiate: true, - reuse_sessions: true, - - # Advanced options - alpn_advertised_protocols: ["h2", "http/1.1"], - alpn_preferred_protocols: ["h2", "http/1.1"], - log_level: :notice -``` - -### SSL Listen vs Handshake Options - -The library distinguishes between SSL options used for socket listening and those used during SSL handshake: - -**SSL Listen Options** (used when creating the SSL listening socket): -- Certificate and key files (`certfile`, `keyfile`, `cacertfile`, `certchain`) -- Certificate password (`password`) -- TLS versions (`versions`) -- Logging level (`log_level`) - -**SSL Handshake Options** (used during client SSL handshake): -- Verification settings (`verify`, `fail_if_no_peer_cert`, `depth`) -- Connection settings (`honor_cipher_order`, `secure_renegotiate`, `reuse_sessions`) -- Cipher configuration (`ciphers`) -- CA certificate for verification (`cacertfile`) - -This separation ensures that certificate-related options are properly configured during socket creation, while connection and verification options are applied during the handshake phase. - -## Certificate Path Resolution - -The library supports multiple ways to specify certificate paths: - -1. **Absolute paths**: `/etc/ssl/certs/server.pem` -2. **Relative paths**: The library will try to resolve relative paths in the following order: - - Relative to current working directory - - Relative to the application's `priv` directory -3. **Environment variable expansion**: Paths can use environment variables - -## Runtime Configuration - -You can also pass SSL options when starting the server: - -```elixir -{:ok, server} = Membrane.RTMPServer.start_link( - port: 1935, - use_ssl?: true, - ssl_options: [ - certfile: "/path/to/your/certificate.pem", - keyfile: "/path/to/your/private_key.pem", - verify: :verify_none, - fail_if_no_peer_cert: false, - versions: [:"tlsv1.2", :"tlsv1.3"], - log_level: :info - ], - handle_new_client: fn client_ref, app, stream_key -> - # Your client handler logic here - MyApp.ClientHandler - end -) -``` - -## Configuration Debugging - -To debug your SSL configuration, you can get a summary of all configuration sources: - -```elixir -# Get configuration summary -summary = Membrane.RTMPServer.Config.get_ssl_config_summary() - -# This returns a map with: -# %{ -# defaults: [...], # Default SSL options -# app_config: [...], # From application config -# runtime: [...], # Runtime options passed to the function -# final: [...] # Final merged configuration -# } - -IO.inspect(summary, label: "SSL Configuration Summary") -``` - -## Priority Order - -SSL options are applied in the following priority order (highest to lowest): - -1. **Runtime options** passed to `start_link/1` -2. **Application configuration** (`:membrane_rtmp_plugin, :ssl`) -3. **Default SSL options** - -Higher priority sources will override settings from lower priority sources. - -## Generating Self-Signed Certificates for Testing - -For testing purposes, you can generate self-signed certificates: - -```bash -# Generate private key -openssl genrsa -out private_key.pem 2048 - -# Generate certificate -openssl req -new -x509 -key private_key.pem -out certificate.pem -days 365 \ - -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" -``` - -## SSL Options Reference - -### Certificate Configuration -- `certfile`: Path to the certificate file (PEM format) -- `keyfile`: Path to the private key file (PEM format) -- `cacertfile`: Path to CA certificate bundle (for client verification) -- `certchain`: Path to certificate chain file (PEM format) -- `password`: Password for encrypted certificate files - -### Verification Settings -- `verify`: `:verify_none` or `:verify_peer` -- `fail_if_no_peer_cert`: Boolean, whether to fail if client doesn't provide certificate -- `depth`: Maximum certificate chain depth for verification - -### Protocol Settings -- `versions`: List of supported TLS versions (e.g., `[:"tlsv1.2", :"tlsv1.3"]`) -- `honor_cipher_order`: Boolean, whether to honor server cipher order -- `secure_renegotiate`: Boolean, whether to use secure renegotiation -- `reuse_sessions`: Boolean, whether to reuse SSL sessions - -### Advanced Options -- `ciphers`: List of allowed cipher suites -- `alpn_advertised_protocols`: List of ALPN protocols to advertise -- `alpn_preferred_protocols`: List of preferred ALPN protocols -- `sni_hosts`: SNI host configuration (for multi-domain certificates) -- `log_level`: SSL logging level (`:none`, `:error`, `:warning`, `:notice`, `:info`, `:debug`, `:all`) - -## Common Configuration Examples - -### Production Configuration (High Security) -```elixir -config :membrane_rtmp_plugin, :ssl, - certfile: "/etc/ssl/certs/server.pem", - keyfile: "/etc/ssl/private/server.key", - cacertfile: "/etc/ssl/certs/ca-bundle.pem", - verify: :verify_peer, - fail_if_no_peer_cert: true, - depth: 5, - versions: [:"tlsv1.2", :"tlsv1.3"], - honor_cipher_order: true, - secure_renegotiate: true, - reuse_sessions: true, - log_level: :warning -``` - -### Development Configuration (Self-Signed) -```elixir -config :membrane_rtmp_plugin, :ssl, - certfile: "priv/ssl/dev_cert.pem", - keyfile: "priv/ssl/dev_key.pem", - verify: :verify_none, - fail_if_no_peer_cert: false, - versions: [:"tlsv1.2", :"tlsv1.3"], - log_level: :info -``` - -### Testing Configuration -```elixir -config :membrane_rtmp_plugin, :ssl, - certfile: "test/fixtures/ssl/test_cert.pem", - keyfile: "test/fixtures/ssl/test_key.pem", - verify: :verify_none, - fail_if_no_peer_cert: false, - log_level: :debug -``` - -## SSL Requirements - -When enabling SSL (`use_ssl?: true`), the following are **required**: - -1. **Certificate file** (`certfile`): Path to your SSL certificate in PEM format -2. **Private key file** (`keyfile`): Path to your SSL private key in PEM format - -Without these, the SSL listener will fail to start with a helpful error message. - -## Quick Start - -To quickly get started with SSL, follow these steps: - -1. Obtain or generate your SSL certificate and private key files. -2. Place them in a secure directory on your server. -3. Update your Membrane RTMP configuration to include the paths to these files. -4. Set `use_ssl?: true` when starting your RTMP server. -5. Optionally, configure advanced SSL options as needed. diff --git a/docs/ssl_examples.md b/docs/ssl_examples.md deleted file mode 100644 index 8b8243b..0000000 --- a/docs/ssl_examples.md +++ /dev/null @@ -1,80 +0,0 @@ -# SSL Configuration Examples for Membrane RTMP Plugin - -This directory contains example SSL configurations for different environments. - -## Quick Start - -1. **Development (Self-Signed Certificates)** - ```elixir - # config/dev.exs - import Config - - config :membrane_rtmp_plugin, :ssl, - certfile: "priv/ssl/dev_cert.pem", - keyfile: "priv/ssl/dev_key.pem", - verify: :verify_none, - fail_if_no_peer_cert: false, - log_level: :info - ``` - -2. **Production (CA-Signed Certificates)** - ```elixir - # config/prod.exs - import Config - - config :membrane_rtmp_plugin, :ssl, - certfile: "/etc/ssl/certs/server.pem", - keyfile: "/etc/ssl/private/server.key", - cacertfile: "/etc/ssl/certs/ca-bundle.pem", - verify: :verify_peer, - fail_if_no_peer_cert: true, - depth: 5, - versions: [:"tlsv1.2", :"tlsv1.3"], - honor_cipher_order: true, - log_level: :warning - ``` - -3. **Using Environment Variables** - ```bash - export RTMP_SSL_CERTFILE="/path/to/cert.pem" - export RTMP_SSL_KEYFILE="/path/to/key.pem" - export RTMP_SSL_CACERTFILE="/path/to/ca-bundle.pem" - ``` - -4. **Runtime Configuration** - ```elixir - {:ok, server} = Membrane.RTMPServer.start_link( - port: 1935, - use_ssl?: true, - ssl_options: [ - certfile: "/runtime/cert.pem", - keyfile: "/runtime/key.pem", - verify: :verify_none - ], - handle_new_client: &MyApp.handle_client/3 - ) - ``` - -## Configuration Priority - -SSL options are applied in this priority order (highest to lowest): -1. Runtime options (passed to `start_link/1`) -2. Application configuration (`:membrane_rtmp_plugin, :ssl`) -3. Environment variables -4. Default SSL options - -## Debugging Configuration - -To debug your SSL configuration, use the configuration summary: - -```elixir -summary = Membrane.RTMPServer.Config.get_ssl_config_summary() -IO.inspect(summary, label: "SSL Config Summary") -``` - -## Certificate Path Resolution - -The library automatically: -- Expands relative paths to absolute paths -- Tries to resolve relative paths in the application's `priv` directory -- Validates certificate file existence (when enabled) diff --git a/lib/membrane_rtmp_plugin/rtmp_server.ex b/lib/membrane_rtmp_plugin/rtmp_server.ex index eceaf46..8b5e24a 100644 --- a/lib/membrane_rtmp_plugin/rtmp_server.ex +++ b/lib/membrane_rtmp_plugin/rtmp_server.ex @@ -11,7 +11,7 @@ defmodule Membrane.RTMPServer do which defines how the client should behave. - port: Port on which RTMP server will listen. Defaults to 1935. - use_ssl?: If true, SSL socket (for RTMPS) will be used. Otherwise, TCP socket (for RTMP) will be used. Defaults to false. - - ssl_options: Additional SSL options to override the configured ones. See `Membrane.RTMPServer.Config` for details. + - ssl_options: SSL options to configure the SSL socket. - client_timeout: Time after which an unused client connection is automatically closed, expressed in `Membrane.Time.t()` units. Defaults to 5 seconds. - name: If not nil, value of this field will be used as a name under which the server's process will be registered. Defaults to nil. @@ -28,11 +28,6 @@ defmodule Membrane.RTMPServer do fail_if_no_peer_cert: false, versions: [:"tlsv1.2", :"tlsv1.3"] - ### Environment Variables (fallback) - - - `RTMP_SSL_CERTFILE` or `CERT_PATH` - Path to certificate file - - `RTMP_SSL_KEYFILE` or `CERT_KEY_PATH` - Path to private key file - ### Runtime Options Membrane.RTMPServer.start_link( @@ -57,7 +52,7 @@ defmodule Membrane.RTMPServer do @type t :: [ port: :inet.port_number(), use_ssl?: boolean(), - ssl_options: keyword(), + ssl_options: keyword() | nil, name: atom() | nil, handle_new_client: (client_ref :: pid(), app :: String.t(), stream_key :: String.t() -> @@ -68,7 +63,7 @@ defmodule Membrane.RTMPServer do @default_options %{ port: 1935, use_ssl?: false, - ssl_options: [], + ssl_options: nil, name: nil, client_timeout: Membrane.Time.seconds(5) } @@ -92,6 +87,15 @@ defmodule Membrane.RTMPServer do server_options_map = Enum.into(server_options, %{}) server_options_map = Map.merge(@default_options, server_options_map) + ssl_options_map = + if is_nil(server_options[:ssl_options]) do + %{ssl_options: Application.get_env(:membrane_rtmp_plugin, :ssl, [])} + else + %{ssl_options: server_options[:ssl_options]} + end + + server_options_map = Map.merge(server_options_map, ssl_options_map) + GenServer.start_link(__MODULE__, server_options_map, gen_server_opts) end diff --git a/lib/membrane_rtmp_plugin/rtmp_server/config.ex b/lib/membrane_rtmp_plugin/rtmp_server/config.ex deleted file mode 100644 index e372bfc..0000000 --- a/lib/membrane_rtmp_plugin/rtmp_server/config.ex +++ /dev/null @@ -1,318 +0,0 @@ -defmodule Membrane.RTMPServer.Config do - @moduledoc """ - Configuration module for RTMP server SSL settings. - - This module provides functions to retrieve SSL configuration from two sources: - - Application configuration (:membrane_rtmp_plugin app config) - - Runtime options (highest priority) - - ## Configuration Options - - SSL options can be configured in your application config: - - config :membrane_rtmp_plugin, :ssl, - certfile: "/path/to/cert.pem", - keyfile: "/path/to/key.pem", - verify: :verify_none, - fail_if_no_peer_cert: false, - versions: [:"tlsv1.2", :"tlsv1.3"], - ciphers: :ssl.cipher_suites(:default, :"tlsv1.2"), - honor_cipher_order: true, - # Additional certificate configuration - cacertfile: "/path/to/ca-bundle.pem", - certchain: "/path/to/cert-chain.pem", - password: "cert_password", - # Advanced SSL options - alpn_advertised_protocols: ["h2", "http/1.1"], - alpn_preferred_protocols: ["h2", "http/1.1"], - sni_hosts: [], - log_level: :notice - """ - - @type ssl_option :: - {:certfile, Path.t()} - | {:keyfile, Path.t()} - | {:verify, :verify_none | :verify_peer} - | {:fail_if_no_peer_cert, boolean()} - | {:versions, [:ssl.tls_version()]} - | {:ciphers, [:ssl.cipher()]} - | {:honor_cipher_order, boolean()} - | {:secure_renegotiate, boolean()} - | {:reuse_sessions, boolean()} - | {:cacertfile, Path.t()} - | {:certchain, Path.t()} - | {:depth, non_neg_integer()} - | {:password, String.t()} - | {:alpn_advertised_protocols, [String.t()]} - | {:alpn_preferred_protocols, [String.t()]} - | {:sni_hosts, keyword()} - | {:log_level, :ssl.log_level()} - - @type ssl_options :: [ssl_option()] - - @doc """ - Gets SSL options for the listener socket. - - Priority order: - 1. Runtime options passed to the function - 2. Application configuration (:membrane_rtmp_plugin, :ssl) - 3. Default SSL options - """ - @spec get_ssl_options(runtime_opts :: ssl_options(), validate_files :: boolean()) :: - ssl_options() - def get_ssl_options(runtime_opts \\ [], validate_files \\ true) do - default_opts = get_default_ssl_options() - app_config_opts = get_app_config_ssl_options() - - default_opts - |> Keyword.merge(app_config_opts) - |> Keyword.merge(runtime_opts) - |> process_certificate_paths() - |> validate_ssl_options(validate_files) - end - - @doc """ - Gets SSL options specifically for the SSL listen socket. - This excludes certificate files and handshake-specific options that should only be used during handshake. - """ - @spec get_ssl_listen_options(runtime_opts :: ssl_options(), validate_files :: boolean()) :: - ssl_options() - def get_ssl_listen_options(runtime_opts \\ [], validate_files \\ true) do - runtime_opts - |> get_ssl_options(validate_files) - |> Keyword.take([ - :certfile, - :keyfile, - :cacertfile, - :password, - :versions - ]) - end - - @doc """ - Gets SSL options specifically for the SSL handshake. - This includes verification and connection-specific options. - """ - @spec get_ssl_handshake_options(runtime_opts :: ssl_options(), validate_files :: boolean()) :: - ssl_options() - def get_ssl_handshake_options(runtime_opts \\ [], validate_files \\ false) do - get_ssl_options(runtime_opts, validate_files) - |> Keyword.take([ - :verify, - :fail_if_no_peer_cert, - :versions, - :ciphers, - :honor_cipher_order, - :secure_renegotiate, - :reuse_sessions, - :cacertfile, - :depth, - :log_level - ]) - end - - @doc """ - Gets the basic socket options for listening (non-SSL specific). - """ - @spec get_listen_options() :: [:inet.socket_option()] - def get_listen_options() do - [ - :binary, - packet: :raw, - active: false, - reuseaddr: true - ] - end - - @doc """ - Validates that required SSL files exist and options are valid. - Set validate_files to false to skip file existence checks (useful for testing). - """ - @spec validate_ssl_options(ssl_options(), validate_files :: boolean()) :: ssl_options() - def validate_ssl_options(opts, validate_files \\ true) do - opts - |> validate_certificate_files(validate_files) - |> validate_ssl_configuration() - end - - @doc """ - Gets a summary of the current SSL configuration from all sources. - Useful for debugging configuration issues. - """ - @spec get_ssl_config_summary(runtime_opts :: ssl_options()) :: %{ - defaults: ssl_options(), - app_config: ssl_options(), - runtime: ssl_options(), - final: ssl_options() - } - def get_ssl_config_summary(runtime_opts \\ []) do - defaults = get_default_ssl_options() - app_config = get_app_config_ssl_options() - final = get_ssl_options(runtime_opts, false) - - %{ - defaults: defaults, - app_config: app_config, - runtime: runtime_opts, - final: final - } - end - - # Private functions - - defp get_default_ssl_options() do - [ - verify: :verify_none, - fail_if_no_peer_cert: false, - # Use only TLS 1.2 for better compatibility - versions: [:"tlsv1.2"], - secure_renegotiate: true, - reuse_sessions: true, - # More verbose logging to debug handshake issues - log_level: :info - ] - end - - defp get_app_config_ssl_options() do - Application.get_env(:membrane_rtmp_plugin, :ssl, []) - end - - defp process_certificate_paths(opts) do - opts - |> expand_certificate_paths() - |> resolve_relative_paths() - end - - defp expand_certificate_paths(opts) do - opts - |> maybe_expand_path(:certfile) - |> maybe_expand_path(:keyfile) - |> maybe_expand_path(:cacertfile) - |> maybe_expand_path(:certchain) - end - - defp maybe_expand_path(opts, key) do - case opts[key] do - nil -> opts - path when is_binary(path) -> Keyword.put(opts, key, Path.expand(path)) - _other -> opts - end - end - - defp resolve_relative_paths(opts) do - # If certificate files are specified with relative paths, - # try to resolve them relative to the app's priv directory - priv_dir = Application.app_dir(:membrane_rtmp_plugin, "priv") - - opts - |> maybe_resolve_relative_to_priv(:certfile, priv_dir) - |> maybe_resolve_relative_to_priv(:keyfile, priv_dir) - |> maybe_resolve_relative_to_priv(:cacertfile, priv_dir) - |> maybe_resolve_relative_to_priv(:certchain, priv_dir) - end - - defp maybe_resolve_relative_to_priv(opts, key, priv_dir) do - case opts[key] do - nil -> - opts - - path when is_binary(path) -> - resolve_path_relative_to_priv(opts, key, path, priv_dir) - - _other -> - opts - end - end - - defp resolve_path_relative_to_priv(opts, key, path, priv_dir) do - if Path.absname(path) == path do - # Already absolute - opts - else - priv_path = Path.join(priv_dir, path) - - if File.exists?(priv_path) do - Keyword.put(opts, key, priv_path) - else - # Keep original path - opts - end - end - end - - defp validate_certificate_files(opts, true) do - case {opts[:certfile], opts[:keyfile]} do - {certfile, nil} when is_binary(certfile) -> - validate_single_cert_file("SSL certificate file provided but key file is missing") - - opts - - {nil, keyfile} when is_binary(keyfile) -> - validate_single_cert_file("SSL key file provided but certificate file is missing") - - opts - - {certfile, keyfile} -> - validate_cert_and_key_files(opts, certfile, keyfile) - end - end - - defp validate_certificate_files(opts, false) do - opts - end - - defp validate_cert_and_key_files(opts, certfile, keyfile) do - validate_file_exists(certfile, "SSL certificate") - validate_file_exists(keyfile, "SSL key") - validate_additional_cert_files(opts) - - opts - end - - defp validate_additional_cert_files(opts) do - if opts[:cacertfile], do: validate_file_exists(opts[:cacertfile], "SSL CA certificate") - if opts[:certchain], do: validate_file_exists(opts[:certchain], "SSL certificate chain") - :ok - end - - defp validate_single_cert_file(error_message) do - raise ArgumentError, error_message - end - - defp validate_file_exists(nil, file_type) do - raise ArgumentError, "#{file_type} file is not configured" - end - - defp validate_file_exists(file_path, file_type) do - unless File.exists?(file_path) do - raise ArgumentError, "#{file_type} file does not exist: #{file_path}" - end - - :ok - end - - defp validate_ssl_configuration(opts) do - # Validate TLS versions - if versions = opts[:versions] do - valid_versions = [:tlsv1, :"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] - invalid_versions = versions -- valid_versions - - unless Enum.empty?(invalid_versions) do - raise ArgumentError, - "Invalid TLS versions: #{inspect(invalid_versions)}. " <> - "Valid versions are: #{inspect(valid_versions)}" - end - end - - # Validate verify option - if verify = opts[:verify] do - unless verify in [:verify_none, :verify_peer] do - raise ArgumentError, - "Invalid verify option: #{inspect(verify)}. " <> - "Must be :verify_none or :verify_peer" - end - end - - opts - end -end diff --git a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex index 189b83f..b1042a9 100644 --- a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex +++ b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex @@ -5,7 +5,35 @@ defmodule Membrane.RTMPServer.Listener do use Task require Logger - alias Membrane.RTMPServer.{ClientHandler, Config} + alias Membrane.RTMPServer.ClientHandler + + @listen_opts [ + :binary, + packet: :raw, + active: false, + reuseaddr: true + ] + + @ssl_handshake_opts [ + :certfile, + :keyfile, + :cacertfile, + :password, + :versions + ] + + @ssl_listen_opts [ + :verify, + :fail_if_no_peer_cert, + :versions, + :ciphers, + :honor_cipher_order, + :secure_renegotiate, + :reuse_sessions, + :cacertfile, + :depth, + :log_level + ] @spec run( options :: %{ @@ -21,16 +49,18 @@ defmodule Membrane.RTMPServer.Listener do listen_options = if options.use_ssl? do - ssl_opts = Config.get_ssl_listen_options(Map.get(options, :ssl_options, []), true) + ssl_opts = + options + |> Map.get(:ssl_options, []) + |> Keyword.take(@ssl_listen_opts) Logger.debug("SSL options for listen: #{inspect(ssl_opts)}") - basic_opts = Config.get_listen_options() - combined = basic_opts ++ ssl_opts + combined = @listen_opts ++ ssl_opts Logger.debug("Combined listen options: #{inspect(combined)}") combined else - Config.get_listen_options() + @listen_opts end {:ok, socket} = options.socket_module.listen(options.port, listen_options) @@ -63,7 +93,9 @@ defmodule Membrane.RTMPServer.Listener do Logger.debug("SSL transport accept successful, starting handshake...") ssl_handshake_opts = - Config.get_ssl_handshake_options(Map.get(options, :ssl_options, []), false) + options + |> Map.get(:ssl_options, []) + |> Keyword.take(@ssl_handshake_opts) ssl_handshake_opts = ssl_handshake_opts diff --git a/test/membrane_rtmp_plugin/rtmp_server_config_test.exs b/test/membrane_rtmp_plugin/rtmp_server_config_test.exs deleted file mode 100644 index e6811ee..0000000 --- a/test/membrane_rtmp_plugin/rtmp_server_config_test.exs +++ /dev/null @@ -1,323 +0,0 @@ -defmodule Membrane.RTMPServer.ConfigTest do - use ExUnit.Case, async: false - - alias Membrane.RTMPServer.Config - - describe "get_ssl_options/1" do - setup do - # Clean up any existing config before each test - Application.delete_env(:membrane_rtmp_plugin, :ssl) - :ok - end - - test "returns default options when no configuration is provided" do - options = Config.get_ssl_options([], false) - - assert options[:verify] == :verify_none - assert options[:fail_if_no_peer_cert] == false - assert options[:versions] == [:"tlsv1.2"] - assert options[:secure_renegotiate] == true - assert options[:reuse_sessions] == true - end - - test "merges application configuration with defaults" do - Application.put_env(:membrane_rtmp_plugin, :ssl, - certfile: "/app/cert.pem", - keyfile: "/app/key.pem", - verify: :verify_peer - ) - - options = Config.get_ssl_options([], false) - - assert options[:certfile] == "/app/cert.pem" - assert options[:keyfile] == "/app/key.pem" - assert options[:verify] == :verify_peer - # Default options should still be present - assert options[:fail_if_no_peer_cert] == false - assert options[:versions] == [:"tlsv1.2"] - end - - test "runtime options override application configuration" do - Application.put_env(:membrane_rtmp_plugin, :ssl, - certfile: "/app/cert.pem", - verify: :verify_peer - ) - - runtime_opts = [ - certfile: "/runtime/cert.pem", - keyfile: "/runtime/key.pem" - ] - - options = Config.get_ssl_options(runtime_opts, false) - - assert options[:certfile] == "/runtime/cert.pem" - assert options[:keyfile] == "/runtime/key.pem" - # App config should still be applied for non-overridden options - assert options[:verify] == :verify_peer - end - end - - describe "get_listen_options/0" do - test "returns basic socket options" do - options = Config.get_listen_options() - - assert :binary in options - assert options[:packet] == :raw - assert options[:active] == false - assert options[:reuseaddr] == true - end - end - - describe "validate_ssl_options/1" do - @tag :tmp_dir - test "validates existing certificate files", %{tmp_dir: tmp_dir} do - cert_path = Path.join(tmp_dir, "cert.pem") - key_path = Path.join(tmp_dir, "key.pem") - - File.write!(cert_path, "dummy cert") - File.write!(key_path, "dummy key") - - options = [certfile: cert_path, keyfile: key_path] - validated = Config.validate_ssl_options(options) - - assert validated == options - end - - test "raises error for non-existent certificate file" do - options = [certfile: "/non/existent/cert.pem", keyfile: "/non/existent/key.pem"] - - assert_raise ArgumentError, ~r/SSL certificate file does not exist/, fn -> - Config.validate_ssl_options(options) - end - end - - @tag :tmp_dir - test "raises error for missing key file when cert is provided", %{tmp_dir: tmp_dir} do - cert_path = Path.join(tmp_dir, "cert.pem") - File.write!(cert_path, "dummy cert") - - options = [certfile: cert_path] - - assert_raise ArgumentError, ~r/SSL certificate file provided but key file is missing/, fn -> - Config.validate_ssl_options(options) - end - end - - test "doesn't allow options without certificate file" do - options = [] - - assert_raise ArgumentError, ~r/SSL certificate file is not configured/, fn -> - Config.validate_ssl_options(options) - end - end - end - - describe "get_ssl_config_summary/1" do - setup do - # Clean up any existing config before each test - Application.delete_env(:membrane_rtmp_plugin, :ssl) - :ok - end - - test "provides comprehensive configuration overview" do - # Set up different configuration sources - Application.put_env(:membrane_rtmp_plugin, :ssl, - certfile: "/app/cert.pem", - verify: :verify_peer - ) - - runtime_opts = [keyfile: "/runtime/key.pem", versions: [:"tlsv1.3"]] - - summary = Config.get_ssl_config_summary(runtime_opts) - - # Check that all configuration sources are represented - assert is_list(summary.defaults) - assert is_list(summary.app_config) - assert is_list(summary.runtime) - assert is_list(summary.final) - - # Verify app config - assert summary.app_config[:certfile] == "/app/cert.pem" - assert summary.app_config[:verify] == :verify_peer - - # Verify runtime config - assert summary.runtime[:keyfile] == "/runtime/key.pem" - assert summary.runtime[:versions] == [:"tlsv1.3"] - - # Verify final config has proper priority - # Runtime overrides defaults - assert summary.final[:keyfile] == "/runtime/key.pem" - # From app config - assert summary.final[:verify] == :verify_peer - # From runtime - assert summary.final[:versions] == [:"tlsv1.3"] - end - end - - describe "SSL configuration validation" do - test "validates TLS versions" do - options = [versions: [:invalid_version]] - - assert_raise ArgumentError, ~r/Invalid TLS versions/, fn -> - Config.validate_ssl_options(options, false) - end - end - - test "validates verify option" do - options = [verify: :invalid_verify] - - assert_raise ArgumentError, ~r/Invalid verify option/, fn -> - Config.validate_ssl_options(options, false) - end - end - - test "accepts valid configuration" do - options = [ - versions: [:"tlsv1.2", :"tlsv1.3"], - verify: :verify_peer, - log_level: :info - ] - - validated = Config.validate_ssl_options(options, false) - assert validated == options - end - end - - describe "certificate path processing" do - @tag :tmp_dir - test "expands relative paths", %{tmp_dir: tmp_dir} do - cert_name = "test_cert.pem" - File.write!(Path.join(tmp_dir, cert_name), "dummy cert") - - # Change to tmp_dir to test relative path resolution - original_cwd = File.cwd!() - File.cd!(tmp_dir) - - try do - options = [certfile: cert_name] - processed = Config.get_ssl_options(options, false) - - # Should be expanded to absolute path - assert Path.absname(processed[:certfile]) == processed[:certfile] - assert String.ends_with?(processed[:certfile], cert_name) - after - File.cd!(original_cwd) - end - end - - @tag :tmp_dir - test "validates additional certificate files", %{tmp_dir: tmp_dir} do - cert_path = Path.join(tmp_dir, "cert.pem") - key_path = Path.join(tmp_dir, "key.pem") - ca_path = Path.join(tmp_dir, "ca.pem") - - File.write!(cert_path, "dummy cert") - File.write!(key_path, "dummy key") - # Don't create CA file to test validation - - options = [ - certfile: cert_path, - keyfile: key_path, - cacertfile: ca_path - ] - - assert_raise ArgumentError, ~r/SSL CA certificate file does not exist/, fn -> - Config.validate_ssl_options(options, true) - end - end - end - - describe "SSL listen vs handshake options" do - setup do - # Clean up any existing config before each test - Application.delete_env(:membrane_rtmp_plugin, :ssl) - :ok - end - - test "get_ssl_listen_options includes certificate and basic SSL options" do - options = - Config.get_ssl_listen_options( - [ - certfile: "/path/cert.pem", - keyfile: "/path/key.pem", - verify: :verify_peer, - versions: [:"tlsv1.3"], - honor_cipher_order: true - ], - false - ) - - # Should include certificate options for SSL context - assert options[:certfile] == "/path/cert.pem" - assert options[:keyfile] == "/path/key.pem" - assert options[:versions] == [:"tlsv1.3"] - - # Should NOT include handshake-specific options - refute Keyword.has_key?(options, :verify) - refute Keyword.has_key?(options, :honor_cipher_order) - end - - test "get_ssl_handshake_options includes verification and connection options" do - options = - Config.get_ssl_handshake_options( - [ - certfile: "/path/cert.pem", - keyfile: "/path/key.pem", - verify: :verify_peer, - versions: [:"tlsv1.3"], - honor_cipher_order: true, - fail_if_no_peer_cert: true - ], - false - ) - - # Should include handshake options - assert options[:verify] == :verify_peer - assert options[:versions] == [:"tlsv1.3"] - assert options[:honor_cipher_order] == true - assert options[:fail_if_no_peer_cert] == true - - # Should NOT include certificate file paths (these should be in listen options) - refute Keyword.has_key?(options, :certfile) - refute Keyword.has_key?(options, :keyfile) - end - - test "listen and handshake options are complementary" do - full_config = [ - certfile: "/path/cert.pem", - keyfile: "/path/key.pem", - cacertfile: "/path/ca.pem", - verify: :verify_peer, - versions: [:"tlsv1.3"], - honor_cipher_order: true, - fail_if_no_peer_cert: true, - secure_renegotiate: true - ] - - listen_opts = Config.get_ssl_listen_options(full_config, false) - handshake_opts = Config.get_ssl_handshake_options(full_config, false) - - # Ensure no overlap in critical options - listen_keys = Keyword.keys(listen_opts) - handshake_keys = Keyword.keys(handshake_opts) - - # These should only appear in listen options - assert :certfile in listen_keys - assert :keyfile in listen_keys - refute :certfile in handshake_keys - refute :keyfile in handshake_keys - - # These should only appear in handshake options - assert :verify in handshake_keys - assert :honor_cipher_order in handshake_keys - refute :verify in listen_keys - refute :honor_cipher_order in listen_keys - - # These can appear in both - assert :versions in listen_keys - assert :versions in handshake_keys - # Needed for verification - assert :cacertfile in handshake_keys - end - end -end diff --git a/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs b/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs index cd2aa25..759fe97 100644 --- a/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs +++ b/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs @@ -79,13 +79,45 @@ defmodule Membrane.RTMP.SourceBin.IntegrationTest do assert ffmpeg_result == :error end + @tag :tmp_dir @tag :rtmps - test "SourceBin allows for RTMPS connection" do + test "SourceBin allows for RTMPS connection", %{tmp_dir: tmp_dir} do self = self() + # Create dummy certificate files + cert_path = Path.join(tmp_dir, "cert.pem") + key_path = Path.join(tmp_dir, "key.pem") + + # Create minimal valid certificate content for testing + File.write!(cert_path, """ + -----BEGIN CERTIFICATE----- + MIICdTCCAd4CCQDKn4iM3Jm8ZzANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMC + VVMxCzAJBgNVBAgMAlRYMQ8wDQYDVQQHDAZBdXN0aW4xEjAQBgNVBAoMCVRlc3Qg + Q29ycDELMAkGA1UECwwCSVQxDDAKBgNVBAMMA3d3dzElMCMGCSqGSIb3DQEJARYW + dGVzdEBleGFtcGxlLmNvbQ== + -----END CERTIFICATE----- + """) + + File.write!(key_path, """ + -----BEGIN PRIVATE KEY----- + MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC5w9Y+7Y+7Y+7Y + +7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+ + 7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+ + -----END PRIVATE KEY----- + """) + + # Test SSL listen options separately + ssl_config = [ + certfile: cert_path, + keyfile: key_path, + verify: :verify_none, + fail_if_no_peer_cert: false, + versions: [:"tlsv1.2", :"tlsv1.3"] + ] + pipeline_startup_task = Task.async(fn -> - start_pipeline_with_external_rtmp_server(@app, @stream_key, self, 0, true) + start_pipeline_with_external_rtmp_server(@app, @stream_key, self, 0, true, ssl_config) end) port = @@ -216,7 +248,8 @@ defmodule Membrane.RTMP.SourceBin.IntegrationTest do stream_key, parent, port \\ 0, - use_ssl? \\ false + use_ssl? \\ false, + ssl_options \\ [] ) do parent_process_pid = self() @@ -229,6 +262,7 @@ defmodule Membrane.RTMP.SourceBin.IntegrationTest do Membrane.RTMPServer.start_link( port: port, use_ssl?: use_ssl?, + ssl_options: ssl_options, handle_new_client: handle_new_client, client_timeout: Membrane.Time.seconds(3) ) diff --git a/test/membrane_rtmp_plugin/rtmp_ssl_listener_test.exs b/test/membrane_rtmp_plugin/rtmp_ssl_listener_test.exs deleted file mode 100644 index 1315ad8..0000000 --- a/test/membrane_rtmp_plugin/rtmp_ssl_listener_test.exs +++ /dev/null @@ -1,99 +0,0 @@ -defmodule Membrane.RTMPServer.SSLListenerTest do - @moduledoc """ - Test to verify that SSL listener works correctly with proper option separation. - """ - - use ExUnit.Case, async: false - - alias Membrane.RTMPServer.{Config, Listener} - - @tag :tmp_dir - test "SSL listen options don't cause argument errors", %{tmp_dir: tmp_dir} do - # Create dummy certificate files - cert_path = Path.join(tmp_dir, "cert.pem") - key_path = Path.join(tmp_dir, "key.pem") - - # Create minimal valid certificate content for testing - File.write!(cert_path, """ - -----BEGIN CERTIFICATE----- - MIICdTCCAd4CCQDKn4iM3Jm8ZzANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMC - VVMxCzAJBgNVBAgMAlRYMQ8wDQYDVQQHDAZBdXN0aW4xEjAQBgNVBAoMCVRlc3Qg - Q29ycDELMAkGA1UECwwCSVQxDDAKBgNVBAMMA3d3dzElMCMGCSqGSIb3DQEJARYW - dGVzdEBleGFtcGxlLmNvbQ== - -----END CERTIFICATE----- - """) - - File.write!(key_path, """ - -----BEGIN PRIVATE KEY----- - MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC5w9Y+7Y+7Y+7Y - +7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+ - 7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+ - -----END PRIVATE KEY----- - """) - - # Test SSL listen options separately - ssl_config = [ - certfile: cert_path, - keyfile: key_path, - verify: :verify_none, - fail_if_no_peer_cert: false, - versions: [:"tlsv1.2", :"tlsv1.3"] - ] - - # Get SSL listen options - these should be safe for :ssl.listen/2 - listen_opts = Config.get_ssl_listen_options(ssl_config, true) - basic_opts = Config.get_listen_options() - combined_opts = basic_opts ++ listen_opts - - # Verify the options contain what we expect for listening - assert listen_opts[:certfile] == cert_path - assert listen_opts[:keyfile] == key_path - assert listen_opts[:versions] == [:"tlsv1.2", :"tlsv1.3"] - - # Verify handshake options are separate - handshake_opts = Config.get_ssl_handshake_options(ssl_config, false) - assert handshake_opts[:verify] == :verify_none - assert handshake_opts[:fail_if_no_peer_cert] == false - - # Verify that listen options don't contain handshake-only options - refute Keyword.has_key?(listen_opts, :verify) - refute Keyword.has_key?(listen_opts, :fail_if_no_peer_cert) - - # The real test would be to try :ssl.listen/2, but that requires a proper certificate - # For now, we verify the option separation is working correctly - assert is_list(combined_opts) - assert length(combined_opts) > 0 - end - - test "SSL listener provides helpful error when no certificates configured" do - # Clear any existing SSL configuration - Application.delete_env(:membrane_rtmp_plugin, :ssl) - - # Clean up SSL environment variables - ssl_env_vars = [ - "RTMP_SSL_CERTFILE", - "CERT_PATH", - "RTMP_SSL_KEYFILE", - "CERT_KEY_PATH", - "RTMP_SSL_CACERTFILE", - "CA_CERT_PATH" - ] - - Enum.each(ssl_env_vars, &System.delete_env/1) - - # Create options without SSL certificates - options = %{ - use_ssl?: true, - ssl_options: [], - server: self(), - port: 0, - handle_new_client: fn _client_ref, _app, _stream_key -> :ok end, - client_timeout: 1000 - } - - # Should raise a helpful ArgumentError - assert_raise ArgumentError, ~r/SSL is enabled but certificate files are not configured/, fn -> - Listener.run(options) - end - end -end diff --git a/test/test_helper.exs b/test/test_helper.exs index 788bbf8..0ee42f2 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start(capture_log: true, exclude: [:rtmps]) +ExUnit.start(capture_log: true, exclude: []) From fe811c6cb769bcad761a53fdfce49e48596d35f4 Mon Sep 17 00:00:00 2001 From: Oleg Okunevych Date: Mon, 7 Jul 2025 15:15:42 +0300 Subject: [PATCH 5/7] use openssl to generate ssl cert fixtures --- .../rtmp_source_bin_test.exs | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs b/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs index 759fe97..0dbf5f5 100644 --- a/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs +++ b/test/membrane_rtmp_plugin/rtmp_source_bin_test.exs @@ -84,27 +84,9 @@ defmodule Membrane.RTMP.SourceBin.IntegrationTest do test "SourceBin allows for RTMPS connection", %{tmp_dir: tmp_dir} do self = self() - # Create dummy certificate files - cert_path = Path.join(tmp_dir, "cert.pem") - key_path = Path.join(tmp_dir, "key.pem") - - # Create minimal valid certificate content for testing - File.write!(cert_path, """ - -----BEGIN CERTIFICATE----- - MIICdTCCAd4CCQDKn4iM3Jm8ZzANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMC - VVMxCzAJBgNVBAgMAlRYMQ8wDQYDVQQHDAZBdXN0aW4xEjAQBgNVBAoMCVRlc3Qg - Q29ycDELMAkGA1UECwwCSVQxDDAKBgNVBAMMA3d3dzElMCMGCSqGSIb3DQEJARYW - dGVzdEBleGFtcGxlLmNvbQ== - -----END CERTIFICATE----- - """) - - File.write!(key_path, """ - -----BEGIN PRIVATE KEY----- - MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC5w9Y+7Y+7Y+7Y - +7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+ - 7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+7Y+ - -----END PRIVATE KEY----- - """) + cert_path = Path.join(tmp_dir, "test_cert.pem") + key_path = Path.join(tmp_dir, "test_key.pem") + generate_test_certificates(cert_path, key_path) # Test SSL listen options separately ssl_config = [ @@ -361,4 +343,29 @@ defmodule Membrane.RTMP.SourceBin.IntegrationTest do |> Map.merge(%{stream_length: stream_length, last_dts: -1, buffers: 0}) |> assert_buffers() end + + defp generate_test_certificates(cert_path, key_path) do + {_, 0} = + System.cmd("openssl", [ + "genrsa", + "-out", + key_path, + "2048" + ]) + + {_, 0} = + System.cmd("openssl", [ + "req", + "-new", + "-x509", + "-key", + key_path, + "-out", + cert_path, + "-days", + "1", + "-subj", + "/C=US/ST=Test/L=Test/O=Test/CN=localhost" + ]) + end end From a221c6a045c2c02919cd9644deedbb0aba0a2613 Mon Sep 17 00:00:00 2001 From: Oleg Okunevych Date: Mon, 7 Jul 2025 15:43:31 +0300 Subject: [PATCH 6/7] fix linter issue (broken formatter config) --- lib/membrane_rtmp_plugin/rtmp_server.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/membrane_rtmp_plugin/rtmp_server.ex b/lib/membrane_rtmp_plugin/rtmp_server.ex index 8b5e24a..302622d 100644 --- a/lib/membrane_rtmp_plugin/rtmp_server.ex +++ b/lib/membrane_rtmp_plugin/rtmp_server.ex @@ -54,9 +54,8 @@ defmodule Membrane.RTMPServer do use_ssl?: boolean(), ssl_options: keyword() | nil, name: atom() | nil, - handle_new_client: - (client_ref :: pid(), app :: String.t(), stream_key :: String.t() -> - client_behaviour_spec()), + handle_new_client: (client_ref :: pid(), app :: String.t(), stream_key :: String.t() -> + client_behaviour_spec()), client_timeout: Membrane.Time.t() ] From 7ca84f04f6ad59577a58ea9fd7380bb7c11a1994 Mon Sep 17 00:00:00 2001 From: Oleg Okunevych Date: Mon, 7 Jul 2025 16:03:49 +0300 Subject: [PATCH 7/7] Fix Dialyzer --- lib/membrane_rtmp_plugin/rtmp_server/listener.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex index b1042a9..ebedbdb 100644 --- a/lib/membrane_rtmp_plugin/rtmp_server/listener.ex +++ b/lib/membrane_rtmp_plugin/rtmp_server/listener.ex @@ -109,10 +109,6 @@ defmodule Membrane.RTMPServer.Listener do Logger.info("SSL handshake successful") ssl_socket - :ok -> - Logger.info("SSL handshake successful (ok)") - client - {:error, reason} -> Logger.error("SSL handshake failed: #{inspect(reason)}") :ssl.close(client)