Add diagnostic pinger; default settings

Also don't display DNSservers when empty.

Fixes firezone/backlog#135
Fixes firezone/backlog#123
Fixes firezone/backlog#130

Refs #333
This commit is contained in:
Jamil Bou Kheir
2021-11-30 10:20:05 -08:00
parent b67d11d260
commit 765976275e
80 changed files with 3065 additions and 411 deletions

View File

@@ -11,6 +11,50 @@ defaults:
shell: bash
jobs:
static-analysis:
runs-on: ubuntu-18.04
env:
MIX_ENV: dev
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
with:
otp-version: '24.1'
elixir-version: '1.12.3'
- uses: actions/cache@v2
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-
- name: Install Dependencies
run: mix deps.get --only dev
# Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones
# Cache key based on Elixir & Erlang version (also usefull when running in matrix)
- name: Restore PLT cache
uses: actions/cache@v2
id: plt_cache
with:
key: |
${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt
restore-keys: |
${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt
path: |
priv/plts
# Create PLTs if no cache was found
- name: Create PLTs
if: steps.plt_cache.outputs.cache-hit != 'true'
run: mix dialyzer --plt
- name: Run format check
run: mix format --check-formatted
- name: Run linter
run: mix credo --strict
- name: Run dialyzer
run: mix dialyzer --format dialyxir
unit-test:
runs-on: ubuntu-18.04
env:

4
.gitignore vendored
View File

@@ -33,6 +33,9 @@ npm-debug.log
# this depending on your deployment strategy.
/priv/static/
# Dialyxir output
/priv/plts/
# ElixirLS generates an .elixir_ls folder for user settings
.elixir_ls
@@ -51,7 +54,6 @@ npm-debug.log
/*.deb
/*.rpm
pkg/debian/opt
# Test screenshots
apps/fz_http/screenshots

View File

@@ -13,6 +13,12 @@ repos:
language: system
pass_filenames: false
files: \.exs*$
- id: mix-analysis
name: 'elixir: mix dialyzer'
entry: mix dialyzer --format dialyxir
language: system
pass_filenames: false
files: \.exs*$
- id: mix-compile
name: 'elixir: mix compile'
entry: mix compile --force --warnings-as-errors

View File

@@ -1,4 +1,5 @@
# These should match the versions used in the build
# These are used for the dev environment.
# This should match the versions used in the built product.
nodejs 14.18.1
elixir 1.12.3-otp-24
erlang 24.1.4

View File

@@ -0,0 +1,9 @@
defmodule FzCommon.FzInteger do
@moduledoc """
Utility functions for working with Integers.
"""
def clamp(num, min, _max) when is_integer(num) and num < min, do: min
def clamp(num, _min, max) when is_integer(num) and num > max, do: max
def clamp(num, _min, _max) when is_integer(num), do: num
end

View File

@@ -0,0 +1,21 @@
defmodule FzCommon.FzString do
@moduledoc """
Utility functions for working with Strings.
"""
def to_boolean(str) when is_binary(str) do
as_bool(String.downcase(str))
end
defp as_bool("true") do
true
end
defp as_bool("false") do
false
end
defp as_bool(unknown) do
raise "Unknown boolean: string #{unknown} not one of ['true', 'false']."
end
end

View File

@@ -3,7 +3,7 @@ defmodule FzCommon.FzCryptoTest do
alias FzCommon.FzCrypto
describe "rand_string" do
describe "rand_string/1" do
test "it returns a string of default length" do
assert 16 == String.length(FzCrypto.rand_string())
end
@@ -14,4 +14,27 @@ defmodule FzCommon.FzCryptoTest do
end
end
end
describe "rand_token/1" do
test "returns a token of default length" do
# 8 bytes is 12 chars in Base64
assert 12 == String.length(FzCrypto.rand_token())
end
test "returns a token of length 4 when bytes is 1" do
assert 4 == String.length(FzCrypto.rand_token(1))
end
test "returns a token of length 4 when bytes is 3" do
assert 4 == String.length(FzCrypto.rand_token(3))
end
test "returns a token of length 40_000 when bytes is 32_768" do
assert 44 == String.length(FzCrypto.rand_token(32))
end
test "returns a token of length 44 when bytes is 32" do
assert 43_692 == String.length(FzCrypto.rand_token(32_768))
end
end
end

View File

@@ -0,0 +1,31 @@
defmodule FzCommon.FzIntegerTest do
use ExUnit.Case, async: true
alias FzCommon.FzInteger
describe "clamp/3" do
test "clamps to min" do
min = 1
max = 5
num = 0
assert 1 == FzInteger.clamp(num, min, max)
end
test "clamps to max" do
min = 1
max = 5
num = 7
assert 5 == FzInteger.clamp(num, min, max)
end
test "returns num if in range" do
min = 1
max = 5
num = 3
assert 3 == FzInteger.clamp(num, min, max)
end
end
end

View File

@@ -18,4 +18,18 @@ defmodule FzCommon.FzMapTest do
assert FzMap.compact(@data, "") == %{foo: "bar"}
end
end
describe "stringify_keys/1" do
@data %{foo: "bar", bar: "", map: %{foo: "bar"}}
test "stringifies the keys" do
assert FzMap.stringify_keys(@data) == %{
"foo" => "bar",
"bar" => "",
"map" => %{
foo: "bar"
}
}
end
end
end

View File

@@ -0,0 +1,23 @@
defmodule FzCommon.FzStringTest do
use ExUnit.Case, async: true
alias FzCommon.FzString
describe "to_boolean/1" do
test "converts to true" do
assert true == FzString.to_boolean("True")
end
test "converts to false" do
assert false == FzString.to_boolean("False")
end
test "raises exception on unknowns" do
message = "Unknown boolean: string foobar not one of ['true', 'false']."
assert_raise RuntimeError, message, fn ->
FzString.to_boolean("foobar")
end
end
end
end

View File

@@ -30,6 +30,8 @@ npm-debug.log
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
/assets/lib/node_modules/
/assets/bin/
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment

View File

@@ -6,8 +6,16 @@
@import "./email.scss";
@import "./tables.scss";
/* Font Awesome */
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "~@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "~@fortawesome/fontawesome-free/scss/solid.scss";
@import "~@fortawesome/fontawesome-free/scss/regular.scss";
@import "~@fortawesome/fontawesome-free/scss/brands.scss";
/* Material Design Icons */
$mdi-font-path: "~@mdi/font/fonts";
@import "~@mdi/font/scss/materialdesignicons.scss";
/* Bulma Tooltip */
@import "~@creativebulma/bulma-tooltip/src/sass/index.sass";

View File

@@ -3,6 +3,8 @@
// its own CSS file.
import css from "../css/app.scss"
import "@fontsource/fira-sans"
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".

File diff suppressed because it is too large Load Diff

View File

@@ -21,12 +21,15 @@
"phoenix": "file:../../../deps/phoenix",
"phoenix_html": "file:../../../deps/phoenix_html",
"phoenix_live_view": "file:../../../deps/phoenix_live_view",
"qrcode": "^1.4.4"
"qrcode": "^1.3.3"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.0",
"@creativebulma/bulma-tooltip": "^1.2.0",
"@fontsource/fira-sans": "^4.5.0",
"@fortawesome/fontawesome-free": "^5.15.3",
"@mdi/font": "^6.5.95",
"admin-one-bulma-dashboard": "file:local_modules/admin-one-bulma-dashboard",
"autoprefixer": "^9.8.8",
"babel-loader": "^8.2.3",

View File

@@ -6,28 +6,10 @@ defmodule FzHttp.Application do
use Application
def start(_type, _args) do
children =
case Application.get_env(:fz_http, :minimal) do
true ->
[
FzHttp.Repo,
FzHttp.Vault
]
_ ->
[
FzHttp.Server,
FzHttp.Repo,
FzHttp.Vault,
{Phoenix.PubSub, name: FzHttp.PubSub},
FzHttpWeb.Endpoint
]
end
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: FzHttp.Supervisor]
Supervisor.start_link(children, opts)
Supervisor.start_link(children(), opts)
end
# Tell Phoenix to update the endpoint configuration
@@ -36,4 +18,27 @@ defmodule FzHttp.Application do
FzHttpWeb.Endpoint.config_change(changed, removed)
:ok
end
defp children, do: children(Application.fetch_env!(:fz_http, :supervision_tree_mode))
defp children(:full) do
[
FzHttp.Server,
FzHttp.Repo,
FzHttp.Vault,
FzHttpWeb.Endpoint,
{Phoenix.PubSub, name: FzHttp.PubSub},
FzHttp.ConnectivityCheckService
]
end
defp children(:test) do
[
FzHttp.Server,
FzHttp.Repo,
FzHttp.Vault,
FzHttpWeb.Endpoint,
{Phoenix.PubSub, name: FzHttp.PubSub}
]
end
end

View File

@@ -0,0 +1,78 @@
defmodule FzHttp.ConnectivityCheckService do
@moduledoc """
A simple GenServer to periodically check for WAN connectivity by issuing
POSTs to https://ping[-dev].firez.one/{version}.
"""
use GenServer
require Logger
alias FzHttp.ConnectivityChecks
def start_link(_) do
http_client().start()
GenServer.start_link(__MODULE__, %{})
end
@impl GenServer
def init(state) do
if enabled?() do
send(self(), :perform)
:timer.send_interval(interval(), :perform)
end
{:ok, state}
end
@impl GenServer
def handle_info(:perform, _state) do
# XXX: Consider passing state here to implement exponential backoff in the
# case of errors.
{:noreply, post_request()}
end
def post_request, do: post_request(url())
def post_request(request_url) do
body = ""
case http_client().post(request_url, body) do
{:ok, response} ->
ConnectivityChecks.create_connectivity_check(%{
response_body: response.body,
response_code: response.status_code,
response_headers: Map.new(response.headers),
url: request_url
})
response
{:error, error} ->
Logger.error("""
An unexpected error occurred while performing a Firezone connectivity check to #{request_url}. Reason: #{error.reason}
""")
error
end
end
defp url do
Application.fetch_env!(:fz_http, :connectivity_checks_url) <> version()
end
defp http_client do
Application.fetch_env!(:fz_http, :http_client)
end
defp version do
Application.spec(:fz_http, :vsn) |> to_string()
end
defp interval do
Application.fetch_env!(:fz_http, :connectivity_checks_interval) * 1_000
end
defp enabled? do
Application.fetch_env!(:fz_http, :connectivity_checks_enabled)
end
end

View File

@@ -0,0 +1,138 @@
defmodule FzHttp.ConnectivityChecks do
@moduledoc """
The ConnectivityChecks context.
"""
import Ecto.Query, warn: false
alias FzHttp.Repo
alias FzHttp.ConnectivityChecks.ConnectivityCheck
@doc """
Returns the list of connectivity_checks.
## Examples
iex> list_connectivity_checks()
[%ConnectivityCheck{}, ...]
"""
def list_connectivity_checks do
Repo.all(ConnectivityCheck)
end
def list_connectivity_checks(limit: limit) when is_integer(limit) do
Repo.all(
from(
c in ConnectivityCheck,
limit: ^limit,
order_by: [desc: :inserted_at]
)
)
end
@doc """
Gets a single connectivity_check.
Raises `Ecto.NoResultsError` if the ConnectivityCheck does not exist.
## Examples
iex> get_connectivity_check!(123)
%ConnectivityCheck{}
iex> get_connectivity_check!(456)
** (Ecto.NoResultsError)
"""
def get_connectivity_check!(id), do: Repo.get!(ConnectivityCheck, id)
@doc """
Creates a connectivity_check.
## Examples
iex> create_connectivity_check(%{field: value})
{:ok, %ConnectivityCheck{}}
iex> create_connectivity_check(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_connectivity_check(attrs \\ %{}) do
%ConnectivityCheck{}
|> ConnectivityCheck.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a connectivity_check.
## Examples
iex> update_connectivity_check(connectivity_check, %{field: new_value})
{:ok, %ConnectivityCheck{}}
iex> update_connectivity_check(connectivity_check, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_connectivity_check(%ConnectivityCheck{} = connectivity_check, attrs) do
connectivity_check
|> ConnectivityCheck.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a connectivity_check.
## Examples
iex> delete_connectivity_check(connectivity_check)
{:ok, %ConnectivityCheck{}}
iex> delete_connectivity_check(connectivity_check)
{:error, %Ecto.Changeset{}}
"""
def delete_connectivity_check(%ConnectivityCheck{} = connectivity_check) do
Repo.delete(connectivity_check)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking connectivity_check changes.
## Examples
iex> change_connectivity_check(connectivity_check)
%Ecto.Changeset{data: %ConnectivityCheck{}}
"""
def change_connectivity_check(%ConnectivityCheck{} = connectivity_check, attrs \\ %{}) do
ConnectivityCheck.changeset(connectivity_check, attrs)
end
@doc """
Returns the latest connectivity_check.
"""
def latest_connectivity_check do
Repo.one(
from(
c in ConnectivityCheck,
limit: 1,
order_by: [desc: :inserted_at]
)
)
end
@doc """
Returns the latest connectivity_check's response_body which should contain the resolved public
IP.
"""
def endpoint do
case latest_connectivity_check() do
nil -> nil
connectivity_check -> connectivity_check.response_body
end
end
end

View File

@@ -0,0 +1,27 @@
defmodule FzHttp.ConnectivityChecks.ConnectivityCheck do
@moduledoc """
Manages the connectivity_checks table
"""
use Ecto.Schema
import Ecto.Changeset
@url_regex ~r<\Ahttps://ping(?:-dev)?\.firez\.one/\d+\.\d+\.\d+(?:\+git\.\d+\.[0-9a-fA-F]{7,})?\z>
schema "connectivity_checks" do
field :response_body, :string
field :response_code, :integer
field :response_headers, :map
field :url, :string
timestamps(type: :utc_datetime_usec)
end
@doc false
def changeset(connectivity_check, attrs) do
connectivity_check
|> cast(attrs, [:url, :response_body, :response_code, :response_headers])
|> validate_required([:url, :response_code])
|> validate_format(:url, @url_regex)
|> validate_number(:response_code, greater_than_or_equal_to: 100, less_than: 600)
end
end

View File

@@ -5,7 +5,7 @@ defmodule FzHttp.Devices do
import Ecto.Query, warn: false
alias FzCommon.NameGenerator
alias FzHttp.{Devices.Device, Repo, Users.User}
alias FzHttp.{ConnectivityChecks, Devices.Device, Repo, Settings, Users.User}
@ipv4_prefix "10.3.2."
@ipv6_prefix "fd00:3:2::"
@@ -38,8 +38,8 @@ defmodule FzHttp.Devices do
Repo.delete(device)
end
def change_device(%Device{} = device) do
Device.update_changeset(device, %{})
def change_device(%Device{} = device, attrs \\ %{}) do
Device.update_changeset(device, attrs)
end
def rand_name do
@@ -62,4 +62,34 @@ defmodule FzHttp.Devices do
}
end
end
def allowed_ips(device) do
if device.use_default_allowed_ips do
Settings.default_device_allowed_ips()
else
device.allowed_ips
end
end
def dns_servers(device) do
if device.use_default_dns_servers do
Settings.default_device_dns_servers()
else
device.dns_servers
end
end
def endpoint(device) do
if device.use_default_endpoint do
Settings.default_device_endpoint() || ConnectivityChecks.endpoint()
else
device.endpoint
end
end
def defaults(changeset) do
~w(use_default_allowed_ips use_default_dns_servers use_default_endpoint)a
|> Enum.map(fn field -> {field, Device.field(changeset, field)} end)
|> Map.new()
end
end

View File

@@ -6,10 +6,13 @@ defmodule FzHttp.Devices.Device do
use Ecto.Schema
import Ecto.Changeset
import FzCommon.FzNet,
import FzHttp.SharedValidators,
only: [
valid_ip?: 1,
valid_cidr?: 1
validate_ip: 2,
validate_omitted: 2,
validate_list_of_ips: 2,
validate_no_duplicates: 2,
validate_list_of_ips_or_cidrs: 2
]
alias FzHttp.Users.User
@@ -17,8 +20,12 @@ defmodule FzHttp.Devices.Device do
schema "devices" do
field :name, :string
field :public_key, :string
field :allowed_ips, :string, read_after_writes: true
field :dns_servers, :string, read_after_writes: true
field :use_default_allowed_ips, :boolean, read_after_writes: true, default: true
field :use_default_dns_servers, :boolean, read_after_writes: true, default: true
field :use_default_endpoint, :boolean, read_after_writes: true, default: true
field :endpoint, :string
field :allowed_ips, :string
field :dns_servers, :string
field :private_key, FzHttp.Encrypted.Binary
field :server_public_key, :string
field :remote_ip, EctoNetwork.INET
@@ -32,25 +39,30 @@ defmodule FzHttp.Devices.Device do
def create_changeset(device, attrs) do
device
|> cast(attrs, [
:allowed_ips,
:dns_servers,
:remote_ip,
:address,
:server_public_key,
:private_key,
:user_id,
:name,
:public_key
])
|> shared_cast(attrs)
|> shared_changeset()
end
def update_changeset(device, attrs) do
device
|> shared_cast(attrs)
|> shared_changeset()
|> validate_required(:address)
end
def field(changeset, field) do
get_field(changeset, field)
end
defp shared_cast(device, attrs) do
device
|> cast(attrs, [
:use_default_allowed_ips,
:use_default_dns_servers,
:use_default_endpoint,
:allowed_ips,
:dns_servers,
:endpoint,
:remote_ip,
:address,
:server_public_key,
@@ -59,8 +71,6 @@ defmodule FzHttp.Devices.Device do
:name,
:public_key
])
|> shared_changeset()
|> validate_required(:address)
end
defp shared_changeset(changeset) do
@@ -72,9 +82,12 @@ defmodule FzHttp.Devices.Device do
:server_public_key,
:private_key
])
|> validate_required_unless_default([:allowed_ips, :dns_servers, :endpoint])
|> validate_omitted_if_default([:allowed_ips, :dns_servers, :endpoint])
|> validate_list_of_ips_or_cidrs(:allowed_ips)
|> validate_list_of_ips(:dns_servers)
|> validate_no_duplicates(:dns_servers)
|> validate_ip(:endpoint)
|> unique_constraint(:address)
|> validate_number(:address, greater_than_or_equal_to: 2, less_than_or_equal_to: 254)
|> unique_constraint(:public_key)
@@ -82,66 +95,25 @@ defmodule FzHttp.Devices.Device do
|> unique_constraint([:user_id, :name])
end
defp validate_no_duplicates(changeset, field) when is_atom(field) do
validate_change(changeset, field, fn _current_field, value ->
try do
trimmed = Enum.map(String.split(value, ","), fn el -> String.trim(el) end)
dupes = Enum.uniq(trimmed -- Enum.uniq(trimmed))
defp validate_omitted_if_default(changeset, fields) when is_list(fields) do
fields_to_validate =
defaulted_fields(changeset, fields)
|> Enum.map(fn field ->
String.trim(Atom.to_string(field), "use_default_") |> String.to_atom()
end)
if length(dupes) > 0 do
throw(dupes)
end
[]
catch
dupes ->
[
{field,
"is invalid: duplicate DNS servers are not allowed: #{Enum.join(dupes, ", ")}"}
]
end
end)
validate_omitted(changeset, fields_to_validate)
end
defp validate_list_of_ips(changeset, field) when is_atom(field) do
validate_change(changeset, field, fn _current_field, value ->
try do
for ip <- String.split(value, ",") do
unless valid_ip?(String.trim(ip)) do
throw(ip)
end
end
[]
catch
ip ->
[{field, "is invalid: #{String.trim(ip)} is not a valid IPv4 / IPv6 address"}]
end
end)
defp validate_required_unless_default(changeset, fields) when is_list(fields) do
fields_as_atoms = Enum.map(fields, fn field -> String.to_atom("use_default_#{field}") end)
fields_to_validate = fields_as_atoms -- defaulted_fields(changeset, fields)
validate_required(changeset, fields_to_validate)
end
defp validate_list_of_ips_or_cidrs(changeset, field) when is_atom(field) do
validate_change(changeset, field, fn _current_field, value ->
try do
for ip_or_cidr <- String.split(value, ",") do
trimmed_ip_or_cidr = String.trim(ip_or_cidr)
unless valid_ip?(trimmed_ip_or_cidr) or valid_cidr?(trimmed_ip_or_cidr) do
throw(ip_or_cidr)
end
end
[]
catch
ip_or_cidr ->
[
{field,
"""
is invalid: #{String.trim(ip_or_cidr)} is not a valid IPv4 / IPv6 address or \
CIDR range\
"""}
]
end
end)
defp defaulted_fields(changeset, fields) do
fields
|> Enum.map(fn field -> String.to_atom("use_default_#{field}") end)
|> Enum.filter(fn field -> get_field(changeset, field) end)
end
end

View File

@@ -0,0 +1,17 @@
defmodule FzHttp.Macros do
@moduledoc """
Metaprogramming macros
"""
defmacro def_settings(keys) do
quote bind_quoted: [keys: keys] do
Enum.each(keys, fn key ->
fun_name = key |> String.replace(".", "_") |> String.to_atom() |> Macro.var(__MODULE__)
def unquote(fun_name) do
get_setting!(key: unquote(key)).value
end
end)
end
end
end

View File

@@ -0,0 +1,32 @@
defmodule FzHttp.MockHttpClient do
@moduledoc """
Mocks http requests in place of HTTPoison
"""
@success_response {
:ok,
%{
headers: [
{"content-length", 9},
{"date", "Tue, 07 Dec 2021 19:57:02 GMT"}
],
status_code: 200,
body: "127.0.0.1"
}
}
@error_sentinel "invalid-url"
@error_response {:error, %{reason: :nxdomain}}
def start, do: nil
@doc """
Simulates a POST. Include @error_sentinel in the request URL to simulate an error.
"""
def post(url, _body) do
if String.contains?(url, @error_sentinel) do
@error_response
else
@success_response
end
end
end

View File

@@ -0,0 +1,94 @@
defmodule FzHttp.Settings do
@moduledoc """
The Settings context.
"""
import FzHttp.Macros
import Ecto.Query, warn: false
alias FzHttp.Repo
alias FzHttp.Settings.Setting
def_settings(~w(
default.device.allowed_ips
default.device.dns_servers
default.device.endpoint
))
@doc """
Returns the list of settings.
## Examples
iex> list_settings()
[%Setting{}, ...]
"""
def list_settings do
Repo.all(Setting)
end
@doc """
Gets a single setting by its ID.
Raises `Ecto.NoResultsError` if the Setting does not exist.
## Examples
iex> get_setting!(123)
%Setting{}
iex> get_setting!(456)
** (Ecto.NoResultsError)
"""
def get_setting!(key: key) do
Repo.one!(from s in Setting, where: s.key == ^key)
end
def get_setting!(id), do: Repo.get!(Setting, id)
@doc """
Updates a setting.
## Examples
iex> update_setting(setting, %{field: new_value})
{:ok, %Setting{}}
iex> update_setting(setting, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_setting(%Setting{} = setting, attrs) do
setting
|> Setting.changeset(attrs)
|> Repo.update()
end
def update_setting(key, value) when is_binary(key) do
get_setting!(key: key)
|> update_setting(%{value: value})
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking setting changes.
## Examples
iex> change_setting(setting)
%Ecto.Changeset{data: %Setting{}}
"""
def change_setting(%Setting{} = setting, attrs \\ %{}) do
Setting.changeset(setting, attrs)
end
@doc """
Returns a list of all the settings beginning with the specified key prefix.
"""
def to_list(prefix \\ "") do
starts_with = prefix <> "%"
Repo.all(from s in Setting, where: ilike(s.key, ^starts_with))
end
end

View File

@@ -0,0 +1,69 @@
defmodule FzHttp.Settings.Setting do
@moduledoc """
Represents Firezone runtime configuration settings.
Each record in the table has a unique key corresponding to a configuration setting.
Settings values can be changed at application runtime on the fly.
Settings cannot be created or destroyed by the running application.
Settings are created / destroyed in migrations.
"""
use Ecto.Schema
import Ecto.Changeset
import FzHttp.SharedValidators,
only: [
validate_list_of_ips: 2,
validate_list_of_ips_or_cidrs: 2,
validate_no_duplicates: 2
]
schema "settings" do
field :key, :string
field :value, :string
timestamps(type: :utc_datetime_usec)
end
@doc false
def changeset(setting, attrs) do
setting
|> cast(attrs, [:key, :value])
|> validate_required([:key])
|> validate_setting()
end
defp validate_setting(%{data: %{key: key}, changes: %{value: _value}} = changeset) do
changeset
|> validate_kv_pair(key)
end
defp validate_setting(changeset), do: changeset
defp validate_kv_pair(changeset, "default.device.dns_servers") do
changeset
|> validate_list_of_ips(:value)
|> validate_no_duplicates(:value)
end
defp validate_kv_pair(changeset, "default.device.allowed_ips") do
changeset
|> validate_required(:value)
|> validate_list_of_ips_or_cidrs(:value)
|> validate_no_duplicates(:value)
end
defp validate_kv_pair(changeset, "default.device.endpoint") do
changeset
|> validate_list_of_ips_or_cidrs(:value)
|> validate_no_duplicates(:value)
end
defp validate_kv_pair(changeset, unknown_key) do
validate_change(changeset, :key, fn _current_field, _value ->
[{:key, "is invalid: #{unknown_key} is not a valid setting"}]
end)
end
end

View File

@@ -0,0 +1,109 @@
defmodule FzHttp.SharedValidators do
@moduledoc """
Shared validators to use between schemas.
"""
import Ecto.Changeset
import FzCommon.FzNet,
only: [
valid_ip?: 1,
valid_cidr?: 1
]
def validate_no_duplicates(changeset, field) when is_atom(field) do
validate_change(changeset, field, fn _current_field, value ->
try do
trimmed = Enum.map(String.split(value, ","), fn el -> String.trim(el) end)
dupes = Enum.uniq(trimmed -- Enum.uniq(trimmed))
if length(dupes) > 0 do
throw(dupes)
end
[]
catch
dupes ->
[
{field,
"is invalid: duplicate DNS servers are not allowed: #{Enum.join(dupes, ", ")}"}
]
end
end)
end
def validate_ip(changeset, field) when is_atom(field) do
validate_change(changeset, field, fn _current_field, value ->
try do
for ip <- String.split(value, ",") do
unless valid_ip?(String.trim(ip)) do
throw(ip)
end
end
[]
catch
ip ->
[{field, "is invalid: #{String.trim(ip)} is not a valid IPv4 / IPv6 address"}]
end
end)
end
def validate_list_of_ips(changeset, field) when is_atom(field) do
validate_change(changeset, field, fn _current_field, value ->
try do
for ip <- String.split(value, ",") do
unless valid_ip?(String.trim(ip)) do
throw(ip)
end
end
[]
catch
ip ->
[{field, "is invalid: #{String.trim(ip)} is not a valid IPv4 / IPv6 address"}]
end
end)
end
def validate_list_of_ips_or_cidrs(changeset, field) when is_atom(field) do
validate_change(changeset, field, fn _current_field, value ->
try do
for ip_or_cidr <- String.split(value, ",") do
trimmed_ip_or_cidr = String.trim(ip_or_cidr)
unless valid_ip?(trimmed_ip_or_cidr) or valid_cidr?(trimmed_ip_or_cidr) do
throw(ip_or_cidr)
end
end
[]
catch
ip_or_cidr ->
[
{field,
"""
is invalid: #{String.trim(ip_or_cidr)} is not a valid IPv4 / IPv6 address or \
CIDR range\
"""}
]
end
end)
end
def validate_omitted(changeset, fields) when is_list(fields) do
Enum.reduce(fields, changeset, fn field, accumulated_changeset ->
validate_omitted(accumulated_changeset, field)
end)
end
def validate_omitted(changeset, field) when is_atom(field) do
validate_change(changeset, field, fn _current_field, value ->
if is_nil(value) do
[]
else
[{field, "must not be present"}]
end
end)
end
end

View File

@@ -10,7 +10,6 @@ defmodule FzHttp.Users.User do
import Ecto.Changeset
import FzHttp.Users.PasswordHelpers
alias FzCommon.FzMap
alias FzHttp.Devices.Device
schema "users" do
@@ -62,7 +61,7 @@ defmodule FzHttp.Users.User do
|> validate_required([:sign_in_token, :sign_in_token_created_at])
end
# Password updated with user logged in
# If password isn't being changed, remove it from list of attributes to validate
def update_changeset(
user,
%{
@@ -71,16 +70,24 @@ defmodule FzHttp.Users.User do
"current_password" => nil
} = attrs
) do
update_changeset(user, FzMap.compact(attrs))
update_changeset(
user,
Map.drop(attrs, ["password", "password_confirmation", "current_password"])
)
end
# If password isn't being changed, remove it from list of attributes to validate
def update_changeset(
user,
%{"password" => "", "password_confirmation" => "", "current_password" => ""} = attrs
) do
update_changeset(user, FzMap.compact(attrs, ""))
update_changeset(
user,
Map.drop(attrs, ["password", "password_confirmation", "current_password"])
)
end
# Password and other fields are being changed
def update_changeset(
user,
%{
@@ -109,7 +116,7 @@ defmodule FzHttp.Users.User do
"password_confirmation" => ""
} = attrs
) do
update_changeset(user, FzMap.compact(attrs, ""))
update_changeset(user, Map.drop(attrs, ["password", "password_confirmation"]))
end
# Password updated from token or admin

View File

@@ -6,7 +6,7 @@ defmodule FzHttpWeb.AccountLive.Show do
alias FzHttp.Users
@impl true
@impl Phoenix.LiveView
def mount(params, session, socket) do
{:ok,
socket
@@ -14,7 +14,7 @@ defmodule FzHttpWeb.AccountLive.Show do
|> assign(:page_title, "Account")}
end
@impl true
@impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
{:noreply, socket}
end

View File

@@ -0,0 +1,41 @@
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="content">
<p>
Firezone periodically checks for WAN connectivity to the Internet and logs
the result here. This is used to determine the public IP address of this
server for populating the default endpoint field in device configurations.
</p>
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<thead>
<tr>
<th>Checked At</th>
<th>Resolved IP</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<%= for connectivity_check <- @connectivity_checks do %>
<tr>
<td id={"connectivity_check-#{connectivity_check.id}"}
phx-hook="FormatTimestamp"
data-timestamp={connectivity_check.inserted_at}>
</td>
<td><%= connectivity_check.response_body %></td>
<td>
<span
data-tooltip={"HTTP Response Code: #{connectivity_check.response_code}"}
class={connectivity_check_span_class(connectivity_check.response_code)}>
<i class={connectivity_check_icon_class(connectivity_check.response_code)}></i>
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</section>

View File

@@ -0,0 +1,20 @@
defmodule FzHttpWeb.ConnectivityCheckLive.Index do
@moduledoc """
Manages the connectivity_checks view.
"""
use FzHttpWeb, :live_view
alias FzHttp.ConnectivityChecks
@impl Phoenix.LiveView
def mount(params, session, socket) do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)
|> assign(:page_title, "Connectivity Checks")}
end
defp load_data(_params, socket) do
assign(socket, :connectivity_checks, ConnectivityChecks.list_connectivity_checks(limit: 20))
end
end

View File

@@ -4,17 +4,32 @@ defmodule FzHttpWeb.DeviceLive.FormComponent do
"""
use FzHttpWeb, :live_component
alias FzHttp.Devices
alias FzHttp.{ConnectivityChecks, Devices, Settings}
def update(assigns, socket) do
changeset = Devices.change_device(assigns.device)
device = assigns.device
changeset = Devices.change_device(device)
default_device_endpoint = Settings.default_device_endpoint() || ConnectivityChecks.endpoint()
{:ok,
socket
|> assign(assigns)
|> assign(Devices.defaults(changeset))
|> assign(:default_device_allowed_ips, Settings.default_device_allowed_ips())
|> assign(:default_device_dns_servers, Settings.default_device_dns_servers())
|> assign(:default_device_endpoint, default_device_endpoint)
|> assign(:changeset, changeset)}
end
def handle_event("change", %{"device" => device_params}, socket) do
changeset = Devices.change_device(socket.assigns.device, device_params)
{:noreply,
socket
|> assign(:changeset, changeset)
|> assign(Devices.defaults(changeset))}
end
def handle_event("save", %{"device" => device_params}, socket) do
device = socket.assigns.device

View File

@@ -1,8 +1,7 @@
<div>
<.form let={f} for={@changeset} id="edit-device" phx-target={@myself} phx-submit="save">
<.form let={f} for={@changeset} id="edit-device" phx-change="change" phx-target={@myself} phx-submit="save">
<div class="field">
<%= label f, :name, class: "label" %>
<div class="control">
<%= text_input f, :name, class: "input" %>
</div>
@@ -12,10 +11,26 @@
</div>
<div class="field">
<%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
<%= label f, :use_default_allowed_ips, "Use Default Allowed IPs", class: "label" %>
<div class="control">
<%= text_input f, :allowed_ips, class: "input" %>
<label class="radio">
<%= radio_button f, :use_default_allowed_ips, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_default_allowed_ips, false %>
No
</label>
</div>
<p class="help">
Default: <%= @default_device_allowed_ips %>
</p>
</div>
<div class="field">
<%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
<div class="control">
<%= text_input f, :allowed_ips, class: "input", disabled: @use_default_allowed_ips %>
</div>
<p class="help is-danger">
<%= error_tag f, :allowed_ips %>
@@ -23,16 +38,60 @@
</div>
<div class="field">
<%= label f, :dns_servers, "DNS Servers", class: "label" %>
<%= label f, :use_default_dns_servers, "Use Default DNS Servers", class: "label" %>
<div class="control">
<%= text_input f, :dns_servers, class: "input" %>
<label class="radio">
<%= radio_button f, :use_default_dns_servers, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_default_dns_servers, false %>
No
</label>
</div>
<p class="help">
Default: <%= @default_device_dns_servers %>
</p>
</div>
<div class="field">
<%= label f, :dns_servers, "DNS Servers", class: "label" %>
<div class="control">
<%= text_input f, :dns_servers, class: "input", disabled: @use_default_dns_servers %>
</div>
<p class="help is-danger">
<%= error_tag f, :dns_servers %>
</p>
</div>
<div class="field">
<%= label f, :use_default_endpoint, "Use Default Endpoint", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_default_endpoint, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_default_endpoint, false %>
No
</label>
</div>
<p class="help">
Default: <%= @default_device_endpoint %>
</p>
</div>
<div class="field">
<%= label f, :endpoint, "Server Endpoint", class: "label" %>
<p>The IP of the server this device should connect to.</p>
<div class="control">
<%= text_input f, :endpoint, class: "input", disabled: @use_default_endpoint %>
</div>
<p class="help is-danger">
<%= error_tag f, :endpoint %>
</p>
</div>
<div class="field">
<%= label f, :address, "Interface Address (last octet only)", class: "label" %>

View File

@@ -34,10 +34,13 @@
</dd>
<dt><strong>Allowed IPs</strong></dt>
<dd><%= @device.allowed_ips %></dd>
<dd><%= @allowed_ips %></dd>
<dt><strong>DNS Servers</strong></dt>
<dd><%= @device.dns_servers %></dd>
<dd><%= @dns_servers || "None" %></dd>
<dt><strong>Endpoint</strong></dt>
<dd><%= @endpoint %></dd>
<dt><strong>Public key</strong></dt>
<dd class="code"><%= @device.public_key %></dd>
@@ -88,12 +91,12 @@
[Interface]
PrivateKey = <%= @device.private_key %>
Address = <%= FzHttp.Devices.ipv4_address(@device) %>/32, <%= FzHttp.Devices.ipv6_address(@device) %>/128
DNS = <%= @device.dns_servers %>
<%= @dns_servers %>
[Peer]
PublicKey = <%= @device.server_public_key %>
AllowedIPs = <%= @device.allowed_ips %>
Endpoint = <%= @wireguard_endpoint %>:<%= @wireguard_port %></code></pre>
AllowedIPs = <%= @allowed_ips %>
Endpoint = <%= @endpoint %>:<%= @wireguard_port %></code></pre>
<hr>
<h6 class="is-6 title">
Or scan the QR code with your mobile phone:

View File

@@ -51,11 +51,34 @@ defmodule FzHttpWeb.DeviceLive.Show do
device: device,
user: Users.get_user!(device.user_id),
page_title: device.name,
wireguard_endpoint: Application.fetch_env!(:fz_vpn, :wireguard_endpoint),
allowed_ips: Devices.allowed_ips(device),
dns_servers: dns_servers(device),
endpoint: Devices.endpoint(device),
wireguard_port: Application.fetch_env!(:fz_vpn, :wireguard_port)
)
else
not_authorized(socket)
end
end
defp dns_servers(device) when is_struct(device) do
dns_servers = Devices.dns_servers(device)
if dns_servers_empty?(dns_servers) do
""
else
"DNS = #{dns_servers}"
end
end
defp dns_servers_empty?(nil), do: true
defp dns_servers_empty?(dns_servers) when is_binary(dns_servers) do
len =
dns_servers
|> String.trim()
|> String.length()
len == 0
end
end

View File

@@ -0,0 +1,49 @@
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="tile is-ancestor is-flex-wrap-wrap">
<div class="tile is-parent">
<div class="card tile is-child">
<header class="card-header">
<p class="card-header-title">Device</p>
</header>
<div class="card-content">
<div class="content">
<%= live_component(
@socket,
FzHttpWeb.SettingLive.FormComponent,
label_text: "Allowed IPs",
placeholder: nil,
changeset: @changesets["default.device.allowed_ips"],
help_text: @help_texts.allowed_ips,
id: :allowed_ips_form_component) %>
<hr>
<%= live_component(
@socket,
FzHttpWeb.SettingLive.FormComponent,
label_text: "DNS Servers",
placeholder: nil,
changeset: @changesets["default.device.dns_servers"],
help_text: @help_texts.dns_servers,
id: :dns_servers_form_component) %>
<hr>
<%= live_component(
@socket,
FzHttpWeb.SettingLive.FormComponent,
label_text: "Endpoint",
placeholder: @endpoint_placeholder,
changeset: @changesets["default.device.endpoint"],
help_text: @help_texts.endpoint,
id: :endpoint_form_component) %>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,46 @@
defmodule FzHttpWeb.SettingLive.Default do
@moduledoc """
Manages the defaults view.
"""
use FzHttpWeb, :live_view
alias FzHttp.{ConnectivityChecks, Settings}
@help_texts %{
allowed_ips: """
Configures the default AllowedIPs setting for devices.
AllowedIPs determines which destination IPs get routed through
Firezone. Specify a comma-separated list of IPs or CIDRs here to achieve split tunneling, or use
<code>0.0.0.0/0, ::/0</code> to route all device traffic through this Firezone server.
""",
dns_servers: """
Comma-separated list of DNS servers to use for devices.
Leaving this blank will omit the <code>DNS</code> section in
generated device configs.
""",
endpoint: """
IPv4 or IPv6 address that devices will be configured to connect
to. Defaults to this server's public IP if not set.
"""
}
@impl Phoenix.LiveView
def mount(params, session, socket) do
{:ok,
socket
|> assign_defaults(params, session)
|> assign(:help_texts, @help_texts)
|> assign(:changesets, load_changesets())
|> assign(:endpoint_placeholder, endpoint_placeholder())
|> assign(:page_title, "Default Settings")}
end
defp endpoint_placeholder do
ConnectivityChecks.endpoint()
end
defp load_changesets do
Settings.to_list("default.")
|> Map.new(fn setting -> {setting.key, Settings.change_setting(setting)} end)
end
end

View File

@@ -0,0 +1,62 @@
defmodule FzHttpWeb.SettingLive.FormComponent do
@moduledoc """
Handles updating setting values, one at a time
"""
use FzHttpWeb, :live_component
alias FzHttp.Settings
@impl Phoenix.LiveComponent
def update(assigns, socket) do
{:ok,
socket
|> assign(:input_class, "input")
|> assign(:form_changed, false)
|> assign(:input_icon, "")
|> assign(assigns)}
end
@impl Phoenix.LiveComponent
def handle_event("save", %{"setting" => %{"value" => value}}, socket) do
key = socket.assigns.changeset.data.key
case Settings.update_setting(key, value) do
{:ok, setting} ->
{:noreply,
socket
|> assign(:input_class, input_class(false))
|> assign(:input_icon, input_icon(false))
|> assign(:changeset, Settings.change_setting(setting, %{}))}
{:error, changeset} ->
{:noreply,
socket
|> assign(:input_class, input_class(true))
|> assign(:input_icon, input_icon(true))
|> assign(:changeset, changeset)}
end
end
@impl Phoenix.LiveComponent
def handle_event("change", _params, socket) do
{:noreply,
socket
|> assign(:form_changed, true)}
end
defp input_icon(false) do
"mdi mdi-check-circle"
end
defp input_icon(true) do
"mdi mdi-alert-circle"
end
defp input_class(false) do
"input is-success"
end
defp input_class(true) do
"input is-danger"
end
end

View File

@@ -0,0 +1,26 @@
<div>
<.form let={f} for={@changeset} id={@id} phx-target={@myself} phx-change="change" phx-submit="save">
<div class="field">
<%= label f, :value, @label_text, class: "label" %>
<div class="field has-addons">
<div class="control has-icons-right is-expanded">
<%= text_input f, :value, placeholder: @placeholder, class: @input_class %>
<span class="icon is-small is-right">
<i class={@input_icon}></i>
</span>
<div class="help is-danger">
<%= error_tag f, :value %>
</div>
<div class="help">
<%= raw @help_text %>
</div>
</div>
<%= if @form_changed do %>
<div class="control">
<%= submit "Save", class: "button is-primary" %>
</div>
<% end %>
</div>
</div>
</.form>
</div>

View File

@@ -1,6 +1,8 @@
defmodule FzHttpWeb.LiveHelpers do
@moduledoc """
Helpers available to all LiveViews.
XXX: Consider splitting these up using one of the techniques at
https://bernheisel.com/blog/phoenix-liveview-and-views
"""
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
@@ -43,4 +45,33 @@ defmodule FzHttpWeb.LiveHelpers do
modal_opts = [id: :modal, return_to: path, component: component, opts: opts]
live_component(FzHttpWeb.ModalComponent, modal_opts)
end
def connectivity_check_span_class(response_code) do
if http_success?(status_digit(response_code)) do
"icon has-text-success"
else
"icon has-text-danger"
end
end
def connectivity_check_icon_class(response_code) do
if http_success?(status_digit(response_code)) do
"mdi mdi-check-circle"
else
"mdi mdi-alert-circle"
end
end
defp status_digit(response_code) when is_integer(response_code) do
[status_digit | _tail] = Integer.digits(response_code)
status_digit
end
defp http_success?(2) do
true
end
defp http_success?(_) do
false
end
end

View File

@@ -24,9 +24,6 @@ defmodule FzHttpWeb.Router do
get "/", DeviceController, :index
resources "/session", SessionController, only: [:new, :create, :delete], singleton: true
live "/account", AccountLive.Show, :show
live "/account/edit", AccountLive.Show, :edit
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id", UserLive.Show, :show
@@ -38,6 +35,13 @@ defmodule FzHttpWeb.Router do
live "/devices/:id", DeviceLive.Show, :show
live "/devices/:id/edit", DeviceLive.Show, :edit
live "/settings/default", SettingLive.Default, :default
live "/account", AccountLive.Show, :show
live "/account/edit", AccountLive.Show, :edit
live "/connectivity_checks", ConnectivityCheckLive.Index, :index
get "/sign_in/:token", SessionController, :create
delete "/user", UserController, :delete
end

View File

@@ -17,10 +17,6 @@
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="331700">
<meta name="theme-color" content="331700">
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Fira%20Sans" rel="stylesheet" type="text/css">
</head>
<body>
<div id="app">
@@ -44,8 +40,8 @@
</a>
<div class="navbar-dropdown">
<%= link(to: Routes.account_show_path(@conn, :show), class: "navbar-item") do %>
<span class="icon"><i class="mdi mdi-settings"></i></span>
<span>Settings</span>
<span class="icon"><i class="mdi mdi-account"></i></span>
<span>Account Settings</span>
<% end %>
<hr class="navbar-divider">
<%= link(to: Routes.session_path(@conn, :delete), method: :delete, class: "navbar-item") do %>
@@ -78,7 +74,7 @@
</li>
<li>
<%= link(to: Routes.device_index_path(@conn, :index), class: nav_class(@conn.request_path, "devices")) do %>
<span class="icon"><i class="mdi mdi-laptop-windows"></i></span>
<span class="icon"><i class="mdi mdi-laptop"></i></span>
<span class="menu-item-label">Devices</span>
<% end %>
</li>
@@ -91,6 +87,12 @@
</ul>
<p class="menu-label">Settings</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.setting_default_path(@conn, :default), class: nav_class(@conn.request_path, "defaults")) do %>
<span class="icon"><i class="mdi mdi-cog"></i></span>
<span class="menu-item-label">Defaults</span>
<% end %>
</li>
<li>
<%= link(to: Routes.account_show_path(@conn, :show), class: nav_class(@conn.request_path, "account")) do %>
<span class="icon"><i class="mdi mdi-account"></i></span>
@@ -98,6 +100,15 @@
<% end %>
</li>
</ul>
<p class="menu-label">Diagnostics</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.connectivity_check_index_path(@conn, :index), class: nav_class(@conn.request_path, "connectivity_checks")) do %>
<span class="icon"><i class="mdi mdi-access-point"></i></span>
<span class="menu-item-label">Connectivity Checks</span>
<% end %>
</li>
</ul>
</div>
</aside>
@@ -109,7 +120,7 @@
<div class="level-left">
<div class="level-item">
<%= link(to: "mailto:" <> feedback_recipient()) do %>
Leave us feedback!
Click here to leave feedback
<% end %>
</div>
</div>
@@ -129,7 +140,5 @@
</div>
</footer>
</div>
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/4.9.95/css/materialdesignicons.min.css">
</body>
</html>

View File

@@ -1,3 +0,0 @@
defmodule FzHttpWeb.AdminView do
use FzHttpWeb, :view
end

View File

@@ -58,6 +58,7 @@ defmodule FzHttp.MixProject do
{:cloak_ecto, "~> 1.2"},
{:excoveralls, "~> 0.14", only: :test},
{:floki, ">= 0.0.0", only: :test},
{:httpoison, "~> 1.8"},
{:argon2_elixir, "~> 2.0"},
{:phoenix_pubsub, "~> 2.0"},
{:phoenix_ecto, "~> 4.4"},

View File

@@ -0,0 +1,16 @@
defmodule FzHttp.Repo.Migrations.CreateConnectivityChecks do
use Ecto.Migration
def change do
create table(:connectivity_checks) do
add :url, :string
add :response_body, :string
add :response_code, :integer
add :response_headers, :map
timestamps(type: :utc_datetime_usec)
end
create index(:connectivity_checks, :inserted_at)
end
end

View File

@@ -0,0 +1,25 @@
defmodule FzHttp.Repo.Migrations.CreateSettings do
use Ecto.Migration
def change do
create table(:settings) do
add :key, :string
add :value, :string
timestamps(type: :utc_datetime_usec)
end
create unique_index(:settings, :key)
flush()
now = DateTime.utc_now()
execute """
INSERT INTO settings (key, value, inserted_at, updated_at) VALUES \
('default.device.dns_servers', '1.1.1.1, 1.0.0.1', '#{now}', '#{now}'),
('default.device.allowed_ips', '0.0.0.0/0, ::/0', '#{now}', '#{now}'),
('default.device.endpoint', null, '#{now}', '#{now}')
"""
end
end

View File

@@ -0,0 +1,18 @@
defmodule FzHttp.Repo.Migrations.ChangeDeviceColumnDefaults do
@moduledoc """
Removes the device defaults in favor of using values from the
settings table.
"""
use Ecto.Migration
def change do
alter table("devices") do
add :use_default_endpoint, :boolean, default: true, null: false
add :use_default_allowed_ips, :boolean, default: true, null: false
add :use_default_dns_servers, :boolean, default: true, null: false
add :endpoint, :string, default: nil
modify :allowed_ips, :string, default: nil
modify :dns_servers, :string, default: nil
end
end
end

View File

@@ -10,7 +10,7 @@
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.
alias FzHttp.{Devices, Rules, Users}
alias FzHttp.{Devices, ConnectivityChecks, Rules, Users}
{:ok, user} =
Users.create_user(%{
@@ -34,3 +34,19 @@ alias FzHttp.{Devices, Rules, Users}
device_id: device.id,
destination: %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}
})
{:ok, _connectivity_check} =
ConnectivityChecks.create_connectivity_check(%{
response_headers: %{"Content-Type" => "text/plain"},
response_body: "127.0.0.1",
response_code: 200,
url: "https://ping-dev.firez.one/0.1.19"
})
{:ok, _connectivity_check} =
ConnectivityChecks.create_connectivity_check(%{
response_headers: %{"Content-Type" => "text/plain"},
response_body: "127.0.0.1",
response_code: 400,
url: "https://ping-dev.firez.one/0.20.0"
})

View File

@@ -0,0 +1,51 @@
defmodule FzHttp.ConnectivityCheckServiceTest do
@moduledoc """
Tests the ConnectivityCheckService module.
"""
alias Ecto.Adapters.SQL.Sandbox
alias FzHttp.{ConnectivityChecks, ConnectivityCheckService, Repo}
use FzHttp.DataCase, async: true
describe "post_request/0 valid url" do
@expected_check %{
response_code: 200,
response_headers: %{"content-length" => 9, "date" => "Tue, 07 Dec 2021 19:57:02 GMT"},
response_body: "127.0.0.1"
}
test "inserts connectivity check" do
ConnectivityCheckService.post_request()
assert [@expected_check] = ConnectivityChecks.list_connectivity_checks()
end
end
describe "post_request/0 error" do
@expected_response %{reason: :nxdomain}
@url "invalid-url"
test "returns error reason" do
assert @expected_response = ConnectivityCheckService.post_request(@url)
assert ConnectivityChecks.list_connectivity_checks() == []
end
end
describe "handle_info/2" do
@expected_response %{
headers: [
{"content-length", 9},
{"date", "Tue, 07 Dec 2021 19:57:02 GMT"}
],
status_code: 200,
body: "127.0.0.1"
}
setup _tags do
%{test_pid: start_supervised!(ConnectivityCheckService)}
end
test ":perform", %{test_pid: test_pid} do
Sandbox.allow(Repo, self(), test_pid)
assert @expected_response == :sys.get_state(test_pid)
end
end
end

View File

@@ -0,0 +1,103 @@
defmodule FzHttp.ConnectivityChecksTest do
use FzHttp.DataCase, async: true
alias FzHttp.ConnectivityChecks
describe "connectivity_checks" do
alias FzHttp.ConnectivityChecks.ConnectivityCheck
import FzHttp.ConnectivityChecksFixtures
@invalid_attrs %{response_body: nil, response_code: nil, response_headers: nil, url: nil}
test "list_connectivity_checks/0 returns all connectivity_checks" do
connectivity_check = connectivity_check_fixture()
assert ConnectivityChecks.list_connectivity_checks() == [connectivity_check]
end
test "list_connectivity_checks/1 applies limit" do
connectivity_check_fixture()
connectivity_check = connectivity_check_fixture()
assert ConnectivityChecks.list_connectivity_checks(limit: 1) == [connectivity_check]
end
test "endpoint/0 returns latest check's response body" do
connectivity_check = connectivity_check_fixture()
assert ConnectivityChecks.endpoint() == connectivity_check.response_body
end
test "get_connectivity_check!/1 returns the connectivity_check with given id" do
connectivity_check = connectivity_check_fixture()
assert ConnectivityChecks.get_connectivity_check!(connectivity_check.id) ==
connectivity_check
end
test "create_connectivity_check/1 with valid data creates a connectivity_check" do
valid_attrs = %{
response_body: "some response_body",
response_code: 500,
response_headers: %{"updated_response" => "headers"},
url: "https://ping-dev.firez.one/1.1.1"
}
assert {:ok, %ConnectivityCheck{} = connectivity_check} =
ConnectivityChecks.create_connectivity_check(valid_attrs)
assert connectivity_check.response_body == "some response_body"
assert connectivity_check.response_code == 500
assert connectivity_check.response_headers == %{"updated_response" => "headers"}
assert connectivity_check.url == "https://ping-dev.firez.one/1.1.1"
end
test "create_connectivity_check/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} =
ConnectivityChecks.create_connectivity_check(@invalid_attrs)
end
test "update_connectivity_check/2 with valid data updates the connectivity_check" do
connectivity_check = connectivity_check_fixture()
update_attrs = %{
response_body: "some updated response_body",
response_code: 500,
response_headers: %{"updated" => "response headers"},
url: "https://ping.firez.one/6.6.6"
}
assert {:ok, %ConnectivityCheck{} = connectivity_check} =
ConnectivityChecks.update_connectivity_check(connectivity_check, update_attrs)
assert connectivity_check.response_body == "some updated response_body"
assert connectivity_check.response_code == 500
assert connectivity_check.response_headers == %{"updated" => "response headers"}
assert connectivity_check.url == "https://ping.firez.one/6.6.6"
end
test "update_connectivity_check/2 with invalid data returns error changeset" do
connectivity_check = connectivity_check_fixture()
assert {:error, %Ecto.Changeset{}} =
ConnectivityChecks.update_connectivity_check(connectivity_check, @invalid_attrs)
assert connectivity_check ==
ConnectivityChecks.get_connectivity_check!(connectivity_check.id)
end
test "delete_connectivity_check/1 deletes the connectivity_check" do
connectivity_check = connectivity_check_fixture()
assert {:ok, %ConnectivityCheck{}} =
ConnectivityChecks.delete_connectivity_check(connectivity_check)
assert_raise Ecto.NoResultsError, fn ->
ConnectivityChecks.get_connectivity_check!(connectivity_check.id)
end
end
test "change_connectivity_check/1 returns a connectivity_check changeset" do
connectivity_check = connectivity_check_fixture()
assert %Ecto.Changeset{} = ConnectivityChecks.change_connectivity_check(connectivity_check)
end
end
end

View File

@@ -52,10 +52,12 @@ defmodule FzHttp.DevicesTest do
@attrs %{
name: "Go hard or go home.",
allowed_ips: "0.0.0.0"
allowed_ips: "0.0.0.0",
use_default_allowed_ips: false
}
@valid_dns_servers_attrs %{
use_default_dns_servers: false,
dns_servers: "1.1.1.1, 1.0.0.1, 2606:4700:4700::1111, 2606:4700:4700::1001"
}
@@ -68,6 +70,7 @@ defmodule FzHttp.DevicesTest do
}
@valid_allowed_ips_attrs %{
use_default_allowed_ips: false,
allowed_ips: "0.0.0.0/0, ::/0, ::0/0, 192.168.1.0/24"
}

View File

@@ -0,0 +1,73 @@
defmodule FzHttp.SettingsTest do
use FzHttp.DataCase
alias FzHttp.Settings
@setting_keys ~w(
default.device.dns_servers
default.device.allowed_ips
default.device.endpoint
)
describe "settings" do
alias FzHttp.Settings.Setting
import FzHttp.SettingsFixtures
@valid_settings %{
"default.device.dns_servers" => "8.8.8.8",
"default.device.allowed_ips" => "::/0",
"default.device.endpoint" => "172.10.10.10"
}
@invalid_settings %{
"default.device.dns_servers" => "foobar",
"default.device.allowed_ips" => nil,
"default.device.endpoint" => "foobar"
}
test "get_setting!/1 returns the setting with given id" do
setting = setting_fixture()
assert Settings.get_setting!(setting.id) == setting
end
test "get_setting!/1 returns the setting with the given key" do
for key <- @setting_keys do
setting = Settings.get_setting!(key: key)
assert setting.key == key
end
end
test "update_setting/2 with valid data updates the setting via provided setting" do
for key <- @setting_keys do
value = @valid_settings[key]
setting = Settings.get_setting!(key: key)
assert {:ok, %Setting{} = setting} = Settings.update_setting(setting, %{value: value})
assert setting.key == key
assert setting.value == value
end
end
test "update_setting/2 with valid data updates the setting via key, value" do
for key <- @setting_keys do
value = @valid_settings[key]
assert {:ok, %Setting{} = setting} = Settings.update_setting(key, value)
assert setting.key == key
assert setting.value == value
end
end
test "update_setting/2 with invalid data returns error changeset" do
for key <- @setting_keys do
value = @invalid_settings[key]
assert {:error, %Ecto.Changeset{}} = Settings.update_setting(key, value)
setting = Settings.get_setting!(key: key)
refute setting.value == value
end
end
test "change_setting/1 returns a setting changeset" do
setting = setting_fixture()
assert %Ecto.Changeset{} = Settings.change_setting(setting)
end
end
end

View File

@@ -45,6 +45,26 @@ defmodule FzHttpWeb.AccountLive.ShowTest do
assert flash["info"] == "Account updated successfully."
end
test "doesn't allow empty email", %{authed_conn: conn} do
path = Routes.account_show_path(conn, :edit)
{:ok, view, _html} = live(conn, path)
test_view =
view
|> element("#account-edit")
|> render_submit(%{
"user" => %{
"email" => "",
"current_password" => "",
"password" => "",
"password_confirmation" => ""
}
})
refute_redirected(view, Routes.account_show_path(conn, :show))
assert test_view =~ "can&#39;t be blank"
end
test "renders validation errors", %{authed_conn: conn} do
path = Routes.account_show_path(conn, :edit)
{:ok, view, _html} = live(conn, path)

View File

@@ -0,0 +1,27 @@
defmodule FzHttpWeb.ConnectivityCheckLive.IndexTest do
use FzHttpWeb.ConnCase, async: true
describe "authenticated/connectivity_checks list" do
setup :create_connectivity_checks
test "show connectivity checks", %{
authed_conn: conn,
connectivity_checks: connectivity_checks
} do
path = Routes.connectivity_check_index_path(conn, :index)
{:ok, _view, html} = live(conn, path)
for connectivity_check <- connectivity_checks do
assert html =~ DateTime.to_iso8601(connectivity_check.inserted_at)
end
end
end
describe "unauthenticated/connectivity_checks list" do
test "mount redirects to session path", %{unauthed_conn: conn} do
path = Routes.connectivity_check_index_path(conn, :index)
expected_path = Routes.session_path(conn, :new)
assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
end
end
end

View File

@@ -6,8 +6,36 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do
@valid_params %{"device" => %{"name" => "new_name"}}
@invalid_params %{"device" => %{"name" => ""}}
@allowed_ips "2.2.2.2"
@allowed_ips_change %{
"device" => %{"use_default_allowed_ips" => "false", "allowed_ips" => @allowed_ips}
}
@allowed_ips_unchanged %{
"device" => %{"use_default_allowed_ips" => "true", "allowed_ips" => @allowed_ips}
}
@dns_servers "8.8.8.8, 8.8.4.4"
@dns_servers_change %{"device" => %{"dns_servers" => @dns_servers}}
@dns_servers_change %{
"device" => %{"use_default_dns_servers" => "false", "dns_servers" => @dns_servers}
}
@dns_servers_unchanged %{
"device" => %{"use_default_dns_servers" => "true", "dns_servers" => @dns_servers}
}
@wireguard_endpoint "6.6.6.6"
@endpoint_change %{
"device" => %{"use_default_endpoint" => "false", "endpoint" => @wireguard_endpoint}
}
@endpoint_unchanged %{
"device" => %{"use_default_endpoint" => "true", "dns_servers" => @wireguard_endpoint}
}
@default_allowed_ips_change %{
"device" => %{"use_default_allowed_ips" => "false"}
}
@default_dns_servers_change %{
"device" => %{"use_default_dns_servers" => "false"}
}
@default_endpoint_change %{
"device" => %{"use_default_endpoint" => "false"}
}
test "shows device details", %{authed_conn: conn, device: device} do
path = Routes.device_show_path(conn, :show, device)
@@ -39,6 +67,66 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do
assert flash["info"] == "Device updated successfully."
end
test "prevents allowed_ips changes when use_default_allowed_ips is true ", %{
authed_conn: conn,
device: device
} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
test_view =
view
|> form("#edit-device")
|> render_submit(@allowed_ips_unchanged)
assert test_view =~ "must not be present"
end
test "prevents dns_servers changes when use_default_dns_servers is true", %{
authed_conn: conn,
device: device
} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
test_view =
view
|> form("#edit-device")
|> render_submit(@dns_servers_unchanged)
assert test_view =~ "must not be present"
end
test "prevents endpoint changes when use_default_endpoint is true", %{
authed_conn: conn,
device: device
} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
test_view =
view
|> form("#edit-device")
|> render_submit(@endpoint_unchanged)
assert test_view =~ "must not be present"
end
test "allows allowed_ips changes", %{authed_conn: conn, device: device} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
view
|> form("#edit-device")
|> render_submit(@allowed_ips_change)
flash = assert_redirected(view, Routes.device_show_path(conn, :show, device))
assert flash["info"] == "Device updated successfully."
{:ok, _view, html} = live(conn, path)
assert html =~ "AllowedIPs = #{@allowed_ips}"
end
test "allows dns_servers changes", %{authed_conn: conn, device: device} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
@@ -54,6 +142,21 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do
assert html =~ "DNS = #{@dns_servers}"
end
test "allows endpoint changes", %{authed_conn: conn, device: device} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
view
|> form("#edit-device")
|> render_submit(@endpoint_change)
flash = assert_redirected(view, Routes.device_show_path(conn, :show, device))
assert flash["info"] == "Device updated successfully."
{:ok, _view, html} = live(conn, path)
assert html =~ "Endpoint = #{@wireguard_endpoint}:51820"
end
test "prevents empty names", %{authed_conn: conn, device: device} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
@@ -65,6 +168,48 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do
assert test_view =~ "can&#39;t be blank"
end
test "on use_default_allowed_ips change", %{authed_conn: conn, device: device} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
test_view =
view
|> form("#edit-device")
|> render_change(@default_allowed_ips_change)
assert test_view =~ """
<input class="input" id="edit-device_allowed_ips" name="device[allowed_ips]" type="text"/>\
"""
end
test "on use_default_dns_servers change", %{authed_conn: conn, device: device} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
test_view =
view
|> form("#edit-device")
|> render_change(@default_dns_servers_change)
assert test_view =~ """
<input class="input" id="edit-device_dns_servers" name="device[dns_servers]" type="text"/>\
"""
end
test "on use_default_endpoint change", %{authed_conn: conn, device: device} do
path = Routes.device_show_path(conn, :edit, device)
{:ok, view, _html} = live(conn, path)
test_view =
view
|> form("#edit-device")
|> render_change(@default_endpoint_change)
assert test_view =~ """
<input class="input" id="edit-device_endpoint" name="device[endpoint]" type="text"/>\
"""
end
end
describe "delete own device" do

View File

@@ -0,0 +1,169 @@
defmodule FzHttpWeb.SettingLive.DefaultTest do
use FzHttpWeb.ConnCase, async: true
alias FzHttp.Settings
describe "authenticated/settings default" do
@valid_allowed_ips %{
"setting" => %{"value" => "1.1.1.1"}
}
@valid_dns_servers %{
"setting" => %{"value" => "1.1.1.1"}
}
@valid_endpoint %{
"setting" => %{"value" => "1.1.1.1"}
}
@invalid_allowed_ips %{
"setting" => %{"value" => "foobar"}
}
@invalid_dns_servers %{
"setting" => %{"value" => "foobar"}
}
@invalid_endpoint %{
"setting" => %{"value" => "foobar"}
}
setup %{authed_conn: conn} do
path = Routes.setting_default_path(conn, :default)
{:ok, view, html} = live(conn, path)
%{html: html, view: view}
end
test "renders current settings", %{html: html} do
assert html =~ Settings.default_device_allowed_ips()
assert html =~ Settings.default_device_dns_servers()
assert html =~ """
id="endpoint_form_component"\
"""
end
test "hides Save button by default", %{html: html} do
refute html =~ """
<button class="button is-primary" type="submit">Save</button>\
"""
end
test "shows Save button after allowed_ips form is changed", %{view: view} do
test_view =
view
|> element("#allowed_ips_form_component")
|> render_change(@valid_allowed_ips)
assert test_view =~ """
<button class="button is-primary" type="submit">Save</button>\
"""
end
test "shows Save button after dns_servers form is changed", %{view: view} do
test_view =
view
|> element("#dns_servers_form_component")
|> render_change(@valid_dns_servers)
assert test_view =~ """
<button class="button is-primary" type="submit">Save</button>\
"""
end
test "shows Save button after endpoint form is changed", %{view: view} do
test_view =
view
|> element("#endpoint_form_component")
|> render_change(@valid_endpoint)
assert test_view =~ """
<button class="button is-primary" type="submit">Save</button>\
"""
end
test "updates default allowed_ips", %{view: view} do
test_view =
view
|> element("#allowed_ips_form_component")
|> render_submit(@valid_allowed_ips)
refute test_view =~ "is invalid"
assert test_view =~ """
<input class="input is-success" id="allowed_ips_form_component_value" name="setting[value]" type="text" value="1.1.1.1"/>\
"""
end
test "updates default dns_servers", %{view: view} do
test_view =
view
|> element("#dns_servers_form_component")
|> render_submit(@valid_dns_servers)
refute test_view =~ "is invalid"
assert test_view =~ """
<input class="input is-success" id="dns_servers_form_component_value" name="setting[value]" type="text" value="1.1.1.1"/>\
"""
end
test "updates default endpoint", %{view: view} do
test_view =
view
|> element("#endpoint_form_component")
|> render_submit(@valid_endpoint)
refute test_view =~ "is invalid"
assert test_view =~ """
<input class="input is-success" id="endpoint_form_component_value" name="setting[value]" type="text" value="1.1.1.1"/>\
"""
end
test "prevents invalid allowed_ips", %{view: view} do
test_view =
view
|> element("#allowed_ips_form_component")
|> render_submit(@invalid_allowed_ips)
assert test_view =~ "is invalid"
refute test_view =~ """
<input id="allowed_ips_form_component" class="input is-success"\
"""
end
test "prevents invalid dns_servers", %{view: view} do
test_view =
view
|> element("#dns_servers_form_component")
|> render_submit(@invalid_dns_servers)
assert test_view =~ "is invalid"
refute test_view =~ """
<input id="dns_servers_form_component" class="input is-success"\
"""
end
test "prevents invalid endpoint", %{view: view} do
test_view =
view
|> element("#endpoint_form_component")
|> render_submit(@invalid_endpoint)
assert test_view =~ "is invalid"
refute test_view =~ """
<input id="endpoint_form_component" class="input is-success"\
"""
end
end
describe "unauthenticated/settings default" do
@tag :unauthed
test "mount redirects to session path", %{unauthed_conn: conn} do
path = Routes.setting_default_path(conn, :default)
expected_path = Routes.session_path(conn, :new)
assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
end
end
end

View File

@@ -19,7 +19,7 @@ defmodule FzHttpWeb.ConnCase do
alias Ecto.Adapters.SQL.Sandbox
alias FzHttp.Fixtures
alias FzHttp.SessionsFixtures
using do
quote do
@@ -40,7 +40,7 @@ defmodule FzHttpWeb.ConnCase do
end
def authed_conn do
session = Fixtures.session()
session = SessionsFixtures.session()
{session.id,
new_conn()

View File

@@ -1,69 +0,0 @@
defmodule FzHttp.Fixtures do
@moduledoc """
Convenience helpers for inserting records
"""
alias FzHttp.{Devices, Repo, Rules, Sessions, Users, Users.User}
# return user specified by email, or generate a new otherwise
def user(attrs \\ %{}) do
email = Map.get(attrs, :email, "test-#{counter()}@test")
case Repo.get_by(User, email: email) do
nil ->
{:ok, user} =
%{email: email, password: "testtest", password_confirmation: "testtest"}
|> Map.merge(attrs)
|> Users.create_user()
user
%User{} = user ->
user
end
end
def device(attrs \\ %{}) do
# don't create a user if user_id is passed
user_id = Map.get_lazy(attrs, :user_id, fn -> user().id end)
default_attrs = %{
user_id: user_id,
public_key: "test-pubkey",
name: "factory",
private_key: "test-privkey",
server_public_key: "test-server-pubkey"
}
{:ok, device} = Devices.create_device(Map.merge(default_attrs, attrs))
device
end
def rule4(attrs \\ %{}) do
rule(attrs)
end
def rule6(attrs \\ %{}) do
rule(Map.merge(attrs, %{destination: "::/0"}))
end
def rule(attrs \\ %{}) do
default_attrs = %{
destination: "10.10.10.0/24"
}
{:ok, rule} = Rules.create_rule(Map.merge(default_attrs, attrs))
rule
end
def session(_attrs \\ %{}) do
email = user().email
record = Sessions.get_session!(email: email)
create_params = %{email: email, password: "testtest"}
{:ok, session} = Sessions.create_session(record, create_params)
session
end
defp counter do
System.unique_integer([:positive])
end
end

View File

@@ -0,0 +1,25 @@
defmodule FzHttp.ConnectivityChecksFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `FzHttp.ConnectivityChecks` context.
"""
alias FzHttp.ConnectivityChecks
@doc """
Generate a connectivity_check.
"""
def connectivity_check_fixture(attrs \\ %{}) do
{:ok, connectivity_check} =
attrs
|> Enum.into(%{
response_body: "some response_body",
response_code: 142,
response_headers: %{"Content-Type" => "text/plain"},
url: "https://ping.firez.one/0.0.0+git.0.deadbeef0"
})
|> ConnectivityChecks.create_connectivity_check()
connectivity_check
end
end

View File

@@ -0,0 +1,27 @@
defmodule FzHttp.DevicesFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `FzHttp.Devices` context.
"""
alias FzHttp.{Devices, UsersFixtures}
@doc """
Generate a device.
"""
def device(attrs \\ %{}) do
# Don't create a user if user_id is passed
user_id = Map.get_lazy(attrs, :user_id, fn -> UsersFixtures.user().id end)
default_attrs = %{
user_id: user_id,
public_key: "test-pubkey",
name: "factory",
private_key: "test-privkey",
server_public_key: "test-server-pubkey"
}
{:ok, device} = Devices.create_device(Map.merge(default_attrs, attrs))
device
end
end

View File

@@ -0,0 +1,25 @@
defmodule FzHttp.RulesFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `FzHttp.Rules` context.
"""
alias FzHttp.Rules
def rule4(attrs \\ %{}) do
rule(attrs)
end
def rule6(attrs \\ %{}) do
rule(Map.merge(attrs, %{destination: "::/0"}))
end
def rule(attrs \\ %{}) do
default_attrs = %{
destination: "10.10.10.0/24"
}
{:ok, rule} = Rules.create_rule(Map.merge(default_attrs, attrs))
rule
end
end

View File

@@ -0,0 +1,15 @@
defmodule FzHttp.SessionsFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `FzHttp.Sessions` context.
"""
alias FzHttp.{Sessions, UsersFixtures}
def session(_attrs \\ %{}) do
email = UsersFixtures.user().email
record = Sessions.get_session!(email: email)
create_params = %{email: email, password: "testtest"}
{:ok, session} = Sessions.create_session(record, create_params)
session
end
end

View File

@@ -0,0 +1,15 @@
defmodule FzHttp.SettingsFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `FzHttp.Settings` context.
"""
alias FzHttp.Settings
@doc """
Generate a setting.
"""
def setting_fixture(key \\ "default.device.dns_servers") do
Settings.get_setting!(key: key)
end
end

View File

@@ -0,0 +1,32 @@
defmodule FzHttp.UsersFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `FzHttp.Users` context.
"""
alias FzHttp.{Repo, Users, Users.User}
@doc """
Generate a user specified by email, or generate a new otherwise.
"""
def user(attrs \\ %{}) do
email = Map.get(attrs, :email, "test-#{counter()}@test")
case Repo.get_by(User, email: email) do
nil ->
{:ok, user} =
%{email: email, password: "testtest", password_confirmation: "testtest"}
|> Map.merge(attrs)
|> Users.create_user()
user
%User{} = user ->
user
end
end
defp counter do
System.unique_integer([:positive])
end
end

View File

@@ -1,21 +0,0 @@
defmodule FzHttp.MockHelpers do
@moduledoc """
Helpers for test life cycle
"""
import ExUnit.Callbacks
def mock_enable_signup do
mock_disable_signup(false)
end
def mock_disable_signup do
mock_disable_signup(true)
end
def mock_disable_signup(val) when val in [true, false] do
old_val = Application.get_env(:fz_http, :disable_signup)
Application.put_env(:fz_http, :disable_signup, val)
on_exit(fn -> Application.put_env(:fz_http, :disable_signup, old_val) end)
end
end

View File

@@ -2,7 +2,17 @@ defmodule FzHttp.TestHelpers do
@moduledoc """
Test setup helpers
"""
alias FzHttp.{Fixtures, Repo, Users, Users.User}
alias FzHttp.{
ConnectivityChecksFixtures,
DevicesFixtures,
Repo,
RulesFixtures,
SessionsFixtures,
Users,
Users.User,
UsersFixtures
}
def clear_users do
Repo.delete_all(User)
@@ -11,19 +21,19 @@ defmodule FzHttp.TestHelpers do
def create_device(tags) do
device =
if tags[:unauthed] || is_nil(tags[:user_id]) do
Fixtures.device()
DevicesFixtures.device()
else
Fixtures.device(%{user_id: tags[:user_id]})
DevicesFixtures.device(%{user_id: tags[:user_id]})
end
{:ok, device: device}
end
def create_other_user_device(_) do
user_id = Fixtures.user(%{email: "other_user@test"}).id
user_id = UsersFixtures.user(%{email: "other_user@test"}).id
device =
Fixtures.device(%{
DevicesFixtures.device(%{
user_id: user_id,
name: "other device",
public_key: "other-pubkey",
@@ -33,17 +43,26 @@ defmodule FzHttp.TestHelpers do
{:ok, other_device: device}
end
def create_connectivity_checks(_tags) do
connectivity_checks =
Enum.map(1..5, fn _i ->
ConnectivityChecksFixtures.connectivity_check_fixture()
end)
{:ok, connectivity_checks: connectivity_checks}
end
def create_devices(tags) do
user_id =
if tags[:unathed] || is_nil(tags[:user_id]) do
Fixtures.user().id
UsersFixtures.user().id
else
tags[:user_id]
end
devices =
Enum.map(1..5, fn num ->
Fixtures.device(%{
DevicesFixtures.device(%{
name: "device #{num}",
public_key: "#{num}",
private_key: "#{num}",
@@ -55,37 +74,37 @@ defmodule FzHttp.TestHelpers do
end
def create_user(_) do
user = Fixtures.user()
user = UsersFixtures.user()
{:ok, user: user}
end
def create_session(_) do
session = Fixtures.session()
session = SessionsFixtures.session()
{:ok, session: session}
end
def create_accept_rule(_) do
rule = Fixtures.rule(%{action: :accept})
rule = RulesFixtures.rule(%{action: :accept})
{:ok, rule: rule}
end
def create_drop_rule(_) do
rule = Fixtures.rule(%{action: :drop})
rule = RulesFixtures.rule(%{action: :drop})
{:ok, rule: rule}
end
def create_rule(_) do
rule = Fixtures.rule(%{})
rule = RulesFixtures.rule(%{})
{:ok, rule: rule}
end
def create_rule6(_) do
rule = Fixtures.rule6(%{})
rule = RulesFixtures.rule6(%{})
{:ok, rule6: rule}
end
def create_rule4(_) do
rule = Fixtures.rule4(%{})
rule = RulesFixtures.rule4(%{})
{:ok, rule4: rule}
end
@@ -97,26 +116,26 @@ defmodule FzHttp.TestHelpers do
1..5
|> Enum.map(fn num ->
destination = "#{num}.#{num}.#{num}.0/24"
Fixtures.rule(%{destination: destination})
RulesFixtures.rule(%{destination: destination})
end)
{:ok, rules: rules}
end
def create_user_with_valid_sign_in_token(_) do
{:ok, user: %User{} = Fixtures.user(Users.sign_in_keys())}
{:ok, user: %User{} = UsersFixtures.user(Users.sign_in_keys())}
end
def create_user_with_expired_sign_in_token(_) do
expired = DateTime.add(DateTime.utc_now(), -1 * 86_401)
params = %{Users.sign_in_keys() | sign_in_token_created_at: expired}
{:ok, user: %User{} = Fixtures.user(params)}
{:ok, user: %User{} = UsersFixtures.user(params)}
end
def create_users(%{count: count}) do
users =
Enum.map(1..count, fn i ->
Fixtures.user(%{email: "userlist#{i}@test"})
UsersFixtures.user(%{email: "userlist#{i}@test"})
end)
{:ok, users: users}

View File

@@ -36,13 +36,17 @@ github_sha =
end
config :fz_http,
supervision_tree_mode: :full,
http_client: HTTPoison,
connectivity_checks_enabled: true,
connectivity_checks_interval: 3_600,
connectivity_checks_url: "https://ping-dev.firez.one/",
github_sha: github_sha,
cookie_signing_salt: "Z9eq8iof",
ecto_repos: [FzHttp.Repo],
admin_email: "firezone@localhost",
default_admin_password: "firezone",
events_module: FzHttpWeb.Events,
disable_signup: true,
server_process_opts: [name: {:global, :fz_http_server}]
config :fz_wall,

View File

@@ -115,5 +115,3 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
config(:fz_http, :disable_signup, true)

View File

@@ -27,3 +27,6 @@ config :fz_http, FzHttpWeb.Endpoint,
# Do not print debug messages in production
config :logger, level: :info
config :fz_http,
connectivity_check_url: "https://ping.firez.one/"

View File

@@ -3,7 +3,7 @@
# although such is generally not recommended and you have to
# remember to add this file to your .gitignore.
import Config
alias FzCommon.CLI
alias FzCommon.{CLI, FzInteger, FzString}
# For releases, require that all these are set
database_name = System.fetch_env!("DATABASE_NAME")
@@ -16,13 +16,20 @@ url_host = System.fetch_env!("URL_HOST")
admin_email = System.fetch_env!("ADMIN_EMAIL")
default_admin_password = System.fetch_env!("DEFAULT_ADMIN_PASSWORD")
wireguard_interface_name = System.fetch_env!("WIREGUARD_INTERFACE_NAME")
wireguard_endpoint = System.fetch_env!("WIREGUARD_ENDPOINT")
wireguard_port = String.to_integer(System.fetch_env!("WIREGUARD_PORT"))
nft_path = System.fetch_env!("NFT_PATH")
wg_path = System.fetch_env!("WG_PATH")
egress_interface = System.fetch_env!("EGRESS_INTERFACE")
wireguard_public_key = System.fetch_env!("WIREGUARD_PUBLIC_KEY")
connectivity_checks_enabled =
FzString.to_boolean(System.fetch_env!("CONNECTIVITY_CHECKS_ENABLED"))
connectivity_checks_interval =
System.fetch_env!("CONNECTIVITY_CHECKS_INTERVAL")
|> String.to_integer()
|> FzInteger.clamp(60, 86_400)
# secrets
encryption_key = System.fetch_env!("DATABASE_ENCRYPTION_KEY")
secret_key_base = System.fetch_env!("SECRET_KEY_BASE")
@@ -32,9 +39,6 @@ cookie_signing_salt = System.fetch_env!("COOKIE_SIGNING_SALT")
# Password is not needed if using bundled PostgreSQL, so use nil if it's not set.
database_password = System.get_env("DATABASE_PASSWORD")
config :fz_http,
disable_signup: true
# Database configuration
connect_opts = [
database: database_name,
@@ -83,9 +87,10 @@ config :fz_wall,
config :fz_vpn,
wireguard_public_key: wireguard_public_key,
wireguard_interface_name: wireguard_interface_name,
wireguard_port: wireguard_port,
wireguard_endpoint: wireguard_endpoint
wireguard_port: wireguard_port
config :fz_http,
connectivity_checks_enabled: connectivity_checks_enabled,
connectivity_checks_interval: connectivity_checks_interval,
admin_email: admin_email,
default_admin_password: default_admin_password

View File

@@ -37,8 +37,12 @@ config :fz_http, FzHttpWeb.Endpoint,
],
server: true
config :fz_http, :sql_sandbox, true
config :fz_http, :events_module, FzHttpWeb.MockEvents
config :fz_http,
supervision_tree_mode: :test,
connectivity_checks_interval: 86_400,
sql_sandbox: true,
events_module: FzHttpWeb.MockEvents,
http_client: FzHttp.MockHttpClient
# Print only warnings and errors during test
config :logger, level: :warn

View File

@@ -30,6 +30,9 @@ defmodule FirezoneUmbrella.MixProject do
extras: ["README.md", "SECURITY.md", "CONTRIBUTING.md"]
],
deps: deps(),
dialyzer: [
plt_file: {:no_warn, "priv/plts/dialyzer.plt"}
],
aliases: aliases(),
default_release: :firezone,
releases: [

View File

@@ -10,37 +10,38 @@
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
"credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"},
"db_connection": {:hex, :db_connection, "2.4.0", "d04b1b73795dae60cead94189f1b8a51cc9e1f911c234cc23074017c43c031e5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad416c21ad9f61b3103d254a71b63696ecadb6a917b36f563921e0de00d7d7c8"},
"credo": {:hex, :credo, "1.6.1", "7dc76dcdb764a4316c1596804c48eada9fff44bd4b733a91ccbf0c0f368be61e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "698607fb5993720c7e93d2d8e76f2175bba024de964e160e2f7151ef3ab82ac5"},
"db_connection": {:hex, :db_connection, "2.4.1", "6411f6e23f1a8b68a82fa3a36366d4881f21f47fc79a9efb8c615e62050219da", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea36d226ec5999781a9a8ad64e5d8c4454ecedc7a4d643e4832bf08efca01f00"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"},
"earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"},
"ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"},
"ecto_network": {:hex, :ecto_network, "1.3.0", "1e77fa37c20e0f6a426d3862732f3317b0fa4c18f123d325f81752a491d7304e", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "053a5e46ef2837e8ea5ea97c82fa0f5494699209eddd764e663c85f11b2865bd"},
"ecto_sql": {:hex, :ecto_sql, "3.7.0", "2fcaad4ab0c8d76a5afbef078162806adbe709c04160aca58400d5cbbe8eeac6", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a26135dfa1d99bf87a928c464cfa25bba6535a4fe761eefa56077a4febc60f70"},
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
"ecto_sql": {:hex, :ecto_sql, "3.7.1", "8de624ef50b2a8540252d8c60506379fbbc2707be1606853df371cf53df5d053", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b42a32e2ce92f64aba5c88617891ab3b0ba34f3f3a503fa20009eae1a401c81"},
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.25.3", "3edf6a0d70a39d2eafde030b8895501b1c93692effcbd21347296c18e47618ce", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "9ebebc2169ec732a38e9e779fd0418c9189b3ca93f4a676c961be6c1527913f5"},
"excoveralls": {:hex, :excoveralls, "0.14.2", "f9f5fd0004d7bbeaa28ea9606251bb643c313c3d60710bad1f5809c845b748f0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ca6fd358621cb4d29311b29d4732c4d47dac70e622850979bc54ed9a3e50f3e1"},
"ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"},
"excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"floki": {:hex, :floki, "0.31.0", "f05ee8a8e6a3ced4e62beeb2c79a63bc8e12ab98fbaaf6e6a3d9b76b1278e23f", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "b05afa372f5c345a5bf240ac25ea1f0f3d5fcfd7490ac0beeb4a203f9444891e"},
"floki": {:hex, :floki, "0.32.0", "f915dc15258bc997d49be1f5ef7d3992f8834d6f5695270acad17b41f5bcc8e2", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "1c5a91cae1fd8931c26a4826b5e2372c284813904c8bacb468b5de39c7ececbd"},
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
"hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.1", "0de4c81303fe07806ebc2494d5321ce8fb4df106e34dd5f9d787b637ebadc256", [:mix], [], "hexpm", "7a86b920d2aedce5fb6280ac8261ac1a739ae6c1a1ad38f5eadf910063008942"},
"mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.6.0", "7b85023f7ddef9a5c70909a51cc37c8b868b474d853f90f4280efd26b0e7cce5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "52ffdd31f2daeb399b2e1eb57d468f99a1ad6eee5d8ea19d2353492f06c9fc96"},
"phoenix": {:hex, :phoenix, "1.6.2", "6cbd5c8ed7a797f25a919a37fafbc2fb1634c9cdb12a4448d7a5d0b26926f005", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7bbee475acae0c3abc229b7f189e210ea788e63bd168e585f60c299a4b2f9133"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "3.0.4", "232d41884fe6a9c42d09f48397c175cd6f0d443aaa34c7424da47604201df2e1", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ce17fd3cf815b2ed874114073e743507704b1f5288bb03c304a77458485efc8b"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
@@ -50,7 +51,7 @@
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"postgrex": {:hex, :postgrex, "0.15.11", "50abbb50f33d22d79af402e549b9a566ba4f0451b4f5fd39b72d9bbd49743d24", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6f0e5c3ea10f97468f5ff852277cb207f068399eb68b0c06c142ef68a4e82952"},
"postgrex": {:hex, :postgrex, "0.15.13", "7794e697481799aee8982688c261901de493eb64451feee6ea58207d7266d54a", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3ffb76e1a97cfefe5c6a95632a27ffb67f28871c9741fb585f9d1c3cd2af70f1"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},

View File

@@ -16,7 +16,7 @@ require 'etc'
#
# { "postgresql": { "enable": false } }
#
# for example, it will set the node['firezone']['postgresql']['enable'] attribute to false.
# for example, it will set the node['firezone']['postgresql']['enabled'] attribute to false.
# ## Top-level attributes
#
@@ -66,7 +66,7 @@ default['firezone']['sysvinit_id'] = 'SUP'
# These attributes control Firezone-specific portions of the Nginx
# configuration and the virtual host for the Firezone Phoenix app.
default['firezone']['nginx']['enable'] = true
default['firezone']['nginx']['enabled'] = true
default['firezone']['nginx']['force_ssl'] = true
default['firezone']['nginx']['non_ssl_port'] = 80
default['firezone']['nginx']['ssl_port'] = 443
@@ -80,7 +80,7 @@ default['firezone']['nginx']['log_x_forwarded_for'] = false
default['firezone']['nginx']['redirect_to_canonical'] = false
# Controls nginx caching, used to cache some endpoints
default['firezone']['nginx']['cache']['enable'] = false
default['firezone']['nginx']['cache']['enabled'] = false
default['firezone']['nginx']['cache']['directory'] = "#{node['firezone']['var_directory']}/nginx/cache"
# These attributes control the main nginx.conf, including the events and http
@@ -142,7 +142,7 @@ default['firezone']['nginx']['default']['modules'] = []
# ### Use the bundled Postgres instance (default, recommended):
#
default['firezone']['postgresql']['enable'] = true
default['firezone']['postgresql']['enabled'] = true
default['firezone']['postgresql']['username'] = node['firezone']['user']
default['firezone']['postgresql']['data_directory'] = "#{node['firezone']['var_directory']}/postgresql/13.3/data"
@@ -150,7 +150,7 @@ default['firezone']['postgresql']['data_directory'] = "#{node['firezone']['var_d
#
# Disable the provided Postgres instance and connect to your own:
#
# default['firezone']['postgresql']['enable'] = false
# default['firezone']['postgresql']['enabled'] = false
# default['firezone']['database']['user'] = 'my_db_user_name'
# default['firezone']['database']['name'] = 'my_db_name''
# default['firezone']['database']['host'] = 'my.db.server.address'
@@ -189,20 +189,20 @@ default['firezone']['database']['extensions'] = { 'plpgsql' => true, 'pg_trgm' =
# default['firezone']['database']['password'] = 'change_me'
# ## Phoenix
#
# ### The Phoenix web app for Firezone
default['firezone']['phoenix']['enable'] = true
default['firezone']['phoenix']['enabled'] = true
default['firezone']['phoenix']['port'] = 13000
default['firezone']['phoenix']['log_directory'] = "#{node['firezone']['log_directory']}/phoenix"
default['firezone']['phoenix']['log_rotation']['file_maxbytes'] = 104857600
default['firezone']['phoenix']['log_rotation']['num_to_keep'] = 10
# ## WireGuard
#
# ### Interface Management
# Enable management of the WireGuard interface itself. Set this to false if you
# want to manually create your WireGuard interface and manage its interface properties.
default['firezone']['wireguard']['enable'] = true
default['firezone']['wireguard']['enabled'] = true
default['firezone']['wireguard']['log_directory'] = "#{node['firezone']['log_directory']}/wireguard"
default['firezone']['wireguard']['log_rotation']['file_maxbytes'] = 104857600
default['firezone']['wireguard']['log_rotation']['num_to_keep'] = 10
@@ -214,13 +214,6 @@ default['firezone']['wireguard']['port'] = 51820
# WireGuard interface MTU
default['firezone']['wireguard']['mtu'] = 1420
# ### WireGuard endpoint
# IPv4, IPv6, or hostname that device configs will use to connect to this server.
# If left blank, this will be set to the IPv4 address of the default egress interface.
# Override this to your publicly routable IP if you're behind a NAT and need to
# set up port forwarding to your Firezone server.
default['firezone']['wireguard']['endpoint'] = nil
# ## Runit
# This is missing from the enterprise cookbook
@@ -246,9 +239,9 @@ default['firezone']['ssl']['ssl_dhparam'] = nil
# These are used in creating a self-signed cert if you haven't brought your own.
default['firezone']['ssl']['country_name'] = 'US'
default['firezone']['ssl']['state_name'] = 'WA'
default['firezone']['ssl']['locality_name'] = 'Seattle'
default['firezone']['ssl']['company_name'] = 'My Firezone'
default['firezone']['ssl']['state_name'] = 'CA'
default['firezone']['ssl']['locality_name'] = 'San Francisco'
default['firezone']['ssl']['company_name'] = 'My Company'
default['firezone']['ssl']['organizational_unit_name'] = 'Operations'
default['firezone']['ssl']['email_address'] = 'you@example.com'
@@ -289,3 +282,18 @@ default['firezone']['smtp_address'] = nil
default['firezone']['smtp_password'] = nil
default['firezone']['smtp_port'] = nil
default['firezone']['smtp_user_name'] = nil
# ## Diagnostics Settings
# ### Connectivity Checks
#
# By default, Firezone periodically checks for WAN connectivity to the Internet
# by issuing a POST request with an empty body to https://ping.firez.one. This
# is used to determine the server's publicly routable IP address for populating
# device configurations and setting up firewall rules. Set this to false to
# disable.
default['firezone']['connectivity_checks']['enabled'] = true
# Amount of time to sleep between connectivity checks, in seconds.
# Default: 3600 (1 hour). Minimum: 60 (1 minute). Maximum: 86400 (1 day).
default['firezone']['connectivity_checks']['interval'] = 3_600

View File

@@ -221,6 +221,7 @@ class Firezone
def self.app_env(attributes, reject = [])
attributes = attributes.reject { |k| reject.include?(k) }
# NOTE: All these variables must be Strings
env = {
'EGRESS_INTERFACE' => attributes['egress_interface'],
'WG_PATH' => "#{attributes['install_directory']}/embedded/bin/wg",
@@ -235,9 +236,10 @@ class Firezone
'URL_HOST' => attributes['fqdn'],
'ADMIN_EMAIL' => attributes['admin_email'],
'WIREGUARD_INTERFACE_NAME' => attributes['wireguard']['interface_name'],
'WIREGUARD_ENDPOINT' => attributes['wireguard']['endpoint'],
'WIREGUARD_PORT' => attributes['wireguard']['port'].to_s,
'WIREGUARD_PUBLIC_KEY' => attributes['wireguard_public_key'],
'CONNECTIVITY_CHECKS_ENABLED' => attributes['connectivity_checks']['enabled'].to_s,
'CONNECTIVITY_CHECKS_INTERVAL' => attributes['connectivity_checks']['interval'].to_s,
# secrets
'SECRET_KEY_BASE' => attributes['secret_key_base'],

View File

@@ -1,9 +1,10 @@
#
# frozen_string_literal: true
# Cookbook:: firezone
# Recipe:: config
#
# Copyright:: 2014 Chef Software, Inc.
# Copyright:: 2021 Firezone
# Copyright:: 2021 Firezone, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.

View File

@@ -46,7 +46,7 @@ template 'nginx.conf' do
variables(nginx: node['firezone']['nginx'])
end
if node['firezone']['nginx']['enable']
if node['firezone']['nginx']['enabled']
component_runit_service 'nginx' do
package 'firezone'
action :enable

View File

@@ -49,7 +49,7 @@ template 'phoenix.nginx.conf' do
app_directory: node['firezone']['app_directory'])
end
if node['firezone']['phoenix']['enable']
if node['firezone']['phoenix']['enabled']
component_runit_service 'phoenix' do
package 'firezone'
control ['t']

View File

@@ -35,7 +35,7 @@ directory node['firezone']['postgresql']['log_directory'] do
recursive true
end
if node['firezone']['postgresql']['enable']
if node['firezone']['postgresql']['enabled']
enterprise_pg_cluster 'firezone' do
data_dir node['firezone']['postgresql']['data_directory']
encoding 'UTF8'

View File

@@ -16,7 +16,7 @@ directory node['firezone']['wireguard']['log_directory'] do
recursive true
end
if node['firezone']['wireguard']['enable']
if node['firezone']['wireguard']['enabled']
component_runit_service 'wireguard' do
package 'firezone'
action :enable

View File

@@ -6,7 +6,7 @@ upstream phoenix {
server 127.0.0.1:<%= @phoenix['port'] %>;
}
<% if @nginx['cache']['enable'] -%>
<% if @nginx['cache']['enabled'] -%>
proxy_cache_path <%= @nginx['cache']['directory'] %>/firezone levels=1:2 keys_zone=firezone-cache:512m max_size=1000m inactive=600m;
proxy_temp_path <%= @nginx['cache']['directory'] %>/tmp;
@@ -55,7 +55,7 @@ server {
}
<% end -%>
<% if @nginx['cache']['enable'] -%>
<% if @nginx['cache']['enabled'] -%>
access_log <%= @nginx['log_directory'] %>/cache.log cache;
<% end -%>
@@ -76,7 +76,7 @@ server {
return 410;
}
<% if @nginx['cache']['enable'] -%>
<% if @nginx['cache']['enabled'] -%>
# TODO: Is this useful to Firezone?
location ~ ^/api/v1/cookbooks/.*/versions/.*(/download)?$ {
proxy_set_header HOST $host;