diff --git a/config/ci.exs b/config/ci.exs index 46160fb0..5dd541e3 100644 --- a/config/ci.exs +++ b/config/ci.exs @@ -6,7 +6,6 @@ config :jellyfish, server_api_token: "development" # you can enable the server option below. config :jellyfish, JellyfishWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 4002], - secret_key_base: "DtVd7qfpae0tk5zRgAM75hOaCc+phk38gDFVvLPyqVN/vvVg0EPmksTSm5JcyjoJ", server: false # Print only warnings and errors during test diff --git a/config/dev.exs b/config/dev.exs index 34109d39..ab0e4a21 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -15,7 +15,6 @@ config :jellyfish, JellyfishWeb.Endpoint, check_origin: false, code_reloader: true, debug_errors: true, - secret_key_base: "eUQ+pDd8FmAwDrE4taJYgcYtaMqFMLygkVRBPfH8G98U1aveWB3Oa9TkOoehK61t", watchers: [] # ## SSL Support diff --git a/config/prod.exs b/config/prod.exs index 12e68b19..d80d20ec 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -8,7 +8,9 @@ import Config config :logger, level: :info # run the server automatically when using prod release -config :jellyfish, JellyfishWeb.Endpoint, server: true +config :jellyfish, JellyfishWeb.Endpoint, + http: [ip: {0, 0, 0, 0, 0, 0, 0, 0}, port: 8080], + server: true # Runtime production configuration, including reading # of environment variables, is done on config/runtime.exs. diff --git a/config/runtime.exs b/config/runtime.exs index f04c3175..20f3bfa9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,75 +1,19 @@ import Config +alias Jellyfish.ConfigReader + # config/runtime.exs is executed for all environments, including # during releases. It is executed after compilation and before the # system starts, so it is typically used to load production configuration # and secrets from environment variables or elsewhere. Do not define # any compile-time configuration in here, as it won't be applied. # The block below contains prod specific runtime configuration. -defmodule ConfigParser do - def parse_integrated_turn_ip(addr) do - addr = addr |> to_charlist() - - case :inet.parse_address(addr) do - {:ok, parsed_ip} -> - parsed_ip - - _error -> - with {:ok, parsed_ip} <- :inet.getaddr(addr, :inet) do - parsed_ip - else - _error -> - raise(""" - Bad integrated TURN address. Expected IPv4 or a valid hostname, got: \ - #{inspect(addr)} - """) - end - end - end - - def parse_integrated_turn_port_range(range) do - with [str1, str2] <- String.split(range, "-"), - from when from in 0..65_535 <- String.to_integer(str1), - to when to in from..65_535 and from <= to <- String.to_integer(str2) do - {from, to} - else - _else -> - raise(""" - Bad INTEGRATED_TURN_PORT_RANGE environment variable value. Expected "from-to", where `from` and `to` \ - are numbers between 0 and 65535 and `from` is not bigger than `to`, got: \ - #{inspect(range)} - """) - end - end - - def parse_port_number(nil, _var_name), do: nil - - def parse_port_number(var_value, var_name) do - with {port, _sufix} when port in 1..65535 <- Integer.parse(var_value) do - port - else - _var -> - raise( - "Bad #{var_name} environment variable value. Expected valid port number, got: #{inspect(var_value)}" - ) - end - end - - def get_env!(env_key) do - case System.get_env(env_key) do - nil -> raise("Environmental variable #{env_key} was not set properly") - env_val -> env_val - end - end -end +config :ex_dtls, impl: :nif +config :opentelemetry, traces_exporter: :none -hosts = - System.get_env("NODES", "") - |> String.split(" ") - |> Enum.reject(&(&1 == "")) - |> Enum.map(&String.to_atom(&1)) +hosts = ConfigReader.read_nodes("NODES") -unless Enum.empty?(hosts) do +if hosts do config :libcluster, topologies: [ epmd_cluster: [ @@ -82,109 +26,53 @@ end prod? = config_env() == :prod host = - case System.get_env("VIRTUAL_HOST") do - nil when prod? -> raise "Unset VIRTUAL_HOST environment variable" + case System.get_env("HOST") do + nil when prod? -> raise "Unset HOST environment variable" nil -> "localhost" other -> other end port = - case System.get_env("PORT") do - nil when prod? -> raise "Unset PORT environment variable" - nil -> 5002 - other -> String.to_integer(other) - end - -jellyfish_address = System.get_env("JELLYFISH_ADDRESS") || "#{host}:#{port}" - -config :ex_dtls, impl: :nif + ConfigReader.read_port("PORT") || + Application.get_env(:jellyfish, JellyfishWeb.Endpoint)[:http][:port] config :jellyfish, - webrtc_used: String.downcase(System.get_env("WEBRTC_USED", "true")) not in ["false", "f", "0"], - integrated_turn_ip: - System.get_env("INTEGRATED_TURN_IP", "127.0.0.1") |> ConfigParser.parse_integrated_turn_ip(), - integrated_turn_listen_ip: - System.get_env("INTEGRATED_TURN_LISTEN_IP", "127.0.0.1") - |> ConfigParser.parse_integrated_turn_ip(), + webrtc_used: ConfigReader.read_boolean("WEBRTC_USED") || true, + integrated_turn_ip: ConfigReader.read_ip("INTEGRATED_TURN_IP") || {127, 0, 0, 1}, + integrated_turn_listen_ip: ConfigReader.read_ip("INTEGRATED_TURN_LISTEN_IP") || {127, 0, 0, 1}, integrated_turn_port_range: - System.get_env("INTEGRATED_TURN_PORT_RANGE", "50000-59999") - |> ConfigParser.parse_integrated_turn_port_range(), - integrated_turn_tcp_port: - System.get_env("INTEGRATED_TURN_TCP_PORT") - |> ConfigParser.parse_port_number("INTEGRATED_TURN_TCP_PORT"), + ConfigReader.read_port_range("INTEGRATED_TURN_PORT_RANGE") || {50_000, 59_999}, + integrated_turn_tcp_port: ConfigReader.read_port("INTEGRATED_TURN_TCP_PORT"), jwt_max_age: 24 * 3600, output_base_path: System.get_env("OUTPUT_BASE_PATH", "jellyfish_output") |> Path.expand(), - address: jellyfish_address + address: System.get_env("JELLYFISH_ADDRESS") || "#{host}:#{port}", + metrics_ip: ConfigReader.read_ip("METRICS_IP") || {127, 0, 0, 1}, + metrics_port: ConfigReader.read_port("METRICS_PORT") || 9568 -config :opentelemetry, traces_exporter: :none - -if prod? do - token = - System.fetch_env!("SERVER_API_TOKEN") || - raise """ - environment variable SERVER_API_TOKEN is missing. - SERVER_API_TOKEN is used for HTTP requests and - server WebSocket authorization. - """ +config :jellyfish, JellyfishWeb.Endpoint, + secret_key_base: + System.get_env("SECRET_KEY_BASE") || Base.encode64(:crypto.strong_rand_bytes(48)), + http: [port: port] - config :jellyfish, server_api_token: token +if check_origin = ConfigReader.read_boolean("CHECK_ORIGIN") do + config :jellyfish, JellyfishWeb.Endpoint, check_origin: check_origin +end - # The secret key base is used to sign/encrypt cookies and other secrets. - # A default value is used in config/dev.exs and config/test.exs but you - # want to use a different value for prod and you most likely don't want - # to check this value into version control, so we use an environment - # variable instead. - secret_key_base = - System.get_env("SECRET_KEY_BASE") || - raise """ - environment variable SECRET_KEY_BASE is missing. - You can generate one by calling: mix phx.gen.secret - """ +case System.get_env("SERVER_API_TOKEN") do + nil when prod? == true -> + raise """ + environment variable SERVER_API_TOKEN is missing. + SERVER_API_TOKEN is used for HTTP requests and + server WebSocket authorization. + """ - check_origin? = System.get_env("CHECK_ORIGIN", "true") == "true" + nil -> + :ok - config :jellyfish, JellyfishWeb.Endpoint, - url: [host: host, port: 443, scheme: "https"], - check_origin: check_origin?, - http: [ - # Enable IPv6 and bind on all interfaces. - # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. - # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html - # for details about using IPv6 vs IPv4 and loopback vs public addresses. - ip: {0, 0, 0, 0, 0, 0, 0, 0}, - port: port - ], - secret_key_base: secret_key_base + token -> + config :jellyfish, server_api_token: token +end - # ## SSL Support - # - # To get SSL working, you will need to add the `https` key - # to your endpoint configuration: - # - # config :jellyfish, JellyfishWeb.Endpoint, - # https: [ - # ..., - # port: 443, - # cipher_suite: :strong, - # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), - # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") - # ] - # - # The `cipher_suite` is set to `:strong` to support only the - # latest and more secure SSL ciphers. This means old browsers - # and clients may not be supported. You can set it to - # `:compatible` for wider support. - # - # `:keyfile` and `:certfile` expect an absolute path to the key - # and cert in disk or a relative path inside priv, for example - # "priv/ssl/server.key". For all supported SSL configuration - # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 - # - # We also recommend setting `force_ssl` in your endpoint, ensuring - # no data is ever sent via http, always redirecting to https: - # - # config :jellyfish, JellyfishWeb.Endpoint, - # force_ssl: [hsts: true] - # - # Check `Plug.SSL` for all available options in `force_ssl`. +if prod? do + config :jellyfish, JellyfishWeb.Endpoint, url: [host: host, port: 443, scheme: "https"] end diff --git a/config/test.exs b/config/test.exs index eb9df258..46bb0ab7 100644 --- a/config/test.exs +++ b/config/test.exs @@ -8,7 +8,6 @@ config :jellyfish, # you can enable the server option below. config :jellyfish, JellyfishWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 4002], - secret_key_base: "DtVd7qfpae0tk5zRgAM75hOaCc+phk38gDFVvLPyqVN/vvVg0EPmksTSm5JcyjoJ", server: false # Print only warnings and errors during test diff --git a/docker-compose.yaml b/docker-compose.yaml index 7e42a5e2..8f90236b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,8 +5,7 @@ x-jellyfish-template: &jellyfish-template environment: &jellyfish-environment ERLANG_COOKIE: "panuozzo-pollo-e-pancetta" SERVER_API_TOKEN: "development" - SECRET_KEY_BASE: "super-secret-key" - VIRTUAL_HOST: "localhost" + HOST: "localhost" NODES: "app@app1 app@app2" networks: - net1 diff --git a/lib/jellyfish/config_reader.ex b/lib/jellyfish/config_reader.ex new file mode 100644 index 00000000..f90edeaf --- /dev/null +++ b/lib/jellyfish/config_reader.ex @@ -0,0 +1,66 @@ +defmodule Jellyfish.ConfigReader do + @moduledoc false + + def read_port_range(env) do + if value = System.get_env(env) do + with [str1, str2] <- String.split(value, "-"), + from when from in 0..65_535 <- String.to_integer(str1), + to when to in from..65_535 and from <= to <- String.to_integer(str2) do + {from, to} + else + _else -> + raise(""" + Bad #{env} environment variable value. Expected "from-to", where `from` and `to` \ + are numbers between 0 and 65535 and `from` is not bigger than `to`, got: \ + #{value} + """) + end + end + end + + def read_ip(env) do + if value = System.get_env(env) do + value = value |> to_charlist() + + case :inet.parse_address(value) do + {:ok, parsed_ip} -> + parsed_ip + + _error -> + raise(""" + Bad #{env} environment variable value. Expected valid ip address, got: #{value}" + """) + end + end + end + + def read_port(env) do + if value = System.get_env(env) do + case Integer.parse(value) do + {port, _sufix} when port in 1..65_535 -> + port + + _other -> + raise(""" + Bad #{env} environment variable value. Expected valid port number, got: #{value} + """) + end + end + end + + def read_nodes(env) do + value = System.get_env(env) + + if value not in ["", nil] do + value + |> String.split(" ", trim: true) + |> Enum.map(&String.to_atom(&1)) + end + end + + def read_boolean(env) do + if value = System.get_env(env) do + String.downcase(value) not in ["false", "f", "0"] + end + end +end diff --git a/lib/jellyfish_web/telemetry.ex b/lib/jellyfish_web/telemetry.ex index 4587fbbb..e7b63446 100644 --- a/lib/jellyfish_web/telemetry.ex +++ b/lib/jellyfish_web/telemetry.ex @@ -11,11 +11,17 @@ defmodule JellyfishWeb.Telemetry do @impl true def init(_arg) do - children = [ - MetricsAggregator, - {TelemetryMetricsPrometheus, metrics: metrics(&last_value/2)} + metrics_ip = Application.fetch_env!(:jellyfish, :metrics_ip) + metrics_port = Application.fetch_env!(:jellyfish, :metrics_port) + + metrics_opts = [ + metrics: metrics(&last_value/2), + port: metrics_port, + plug_cowboy_opts: [ip: metrics_ip] ] + children = [MetricsAggregator, {TelemetryMetricsPrometheus, metrics_opts}] + Supervisor.init(children, strategy: :one_for_one) end diff --git a/test/jellyfish/config_reader_test.exs b/test/jellyfish/config_reader_test.exs new file mode 100644 index 00000000..f80db9ec --- /dev/null +++ b/test/jellyfish/config_reader_test.exs @@ -0,0 +1,94 @@ +defmodule Jellyfish.ConfigReaderTest do + use ExUnit.Case, async: true + + alias Jellyfish.ConfigReader + + defmacrop with_env(env, do: body) do + # get current env value, + # execute test code, + # put back original env value + # + # if env was not set, we have + # to call System.delete_env as + # System.put_env does not accept `nil` + quote do + old = System.get_env(unquote(env)) + unquote(body) + + if old do + System.put_env(unquote(env), old) + else + System.delete_env(unquote(env)) + end + end + end + + test "read_ip/1" do + env_name = "JF_CONF_READER_TEST_IP" + + with_env env_name do + System.put_env(env_name, "127.0.0.1") + assert ConfigReader.read_ip(env_name) == {127, 0, 0, 1} + System.delete_env(env_name) + assert ConfigReader.read_ip(env_name) == nil + System.put_env(env_name, "example.com") + assert_raise RuntimeError, fn -> ConfigReader.read_ip(env_name) end + end + end + + test "read_port/1" do + env_name = "JF_CONF_READER_TEST_PORT" + + with_env env_name do + System.put_env(env_name, "20000") + assert ConfigReader.read_port(env_name) == 20_000 + System.put_env(env_name, "65536") + assert_raise RuntimeError, fn -> ConfigReader.read_port(env_name) end + System.put_env(env_name, "-1") + assert_raise RuntimeError, fn -> ConfigReader.read_port(env_name) end + :os.unsetenv(to_charlist(env_name)) + assert ConfigReader.read_port(env_name) == nil + end + end + + test "read_boolean/1" do + env_name = "JF_CONF_READER_TEST_BOOL" + + with_env env_name do + for {env_value, expected_value} <- [ + {"f", false}, + {"0", false}, + {"false", false}, + {"1", true}, + {"true", true} + ] do + System.put_env(env_name, env_value) + assert ConfigReader.read_boolean(env_name) == expected_value + end + end + end + + test "read_port_range/1" do + env_name = "JF_CONF_READER_TEST_PORT_RANGE" + + with_env env_name do + System.put_env(env_name, "50000-60000") + assert ConfigReader.read_port_range(env_name) == {50_000, 60_000} + System.put_env(env_name, "50000-65536") + assert_raise RuntimeError, fn -> ConfigReader.read_port_range(env_name) end + System.put_env(env_name, "-1-65536") + assert_raise RuntimeError, fn -> ConfigReader.read_port_range(env_name) end + end + end + + test "read_nodes/1" do + env_name = "JF_CONF_READER_TEST_NODES" + + with_env env_name do + System.put_env(env_name, "app1@127.0.0.1 app2@127.0.0.2") + assert ConfigReader.read_nodes(env_name) == [:"app1@127.0.0.1", :"app2@127.0.0.2"] + System.put_env(env_name, "") + assert ConfigReader.read_nodes(env_name) == nil + end + end +end