Show a dynamically updated list of NTS servers #1
|
@ -14,7 +14,7 @@ module.exports = {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
brand: "#FD4F00",
|
brand: "#DC7556",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,6 +30,9 @@ config :chronoscope, ChronoscopeWeb.Endpoint,
|
||||||
# at the `config/runtime.exs`.
|
# at the `config/runtime.exs`.
|
||||||
config :chronoscope, Chronoscope.Mailer, adapter: Swoosh.Adapters.Local
|
config :chronoscope, Chronoscope.Mailer, adapter: Swoosh.Adapters.Local
|
||||||
|
|
||||||
|
config :chronoscope, :nts_topic, "nts-servers"
|
||||||
|
config :chronoscope, :nts_file, "priv/nts.txt"
|
||||||
|
|
||||||
# Configure esbuild (the version is required)
|
# Configure esbuild (the version is required)
|
||||||
config :esbuild,
|
config :esbuild,
|
||||||
version: "0.17.11",
|
version: "0.17.11",
|
||||||
|
|
|
@ -33,13 +33,15 @@ if config_env() == :prod do
|
||||||
You can generate one by calling: mix phx.gen.secret
|
You can generate one by calling: mix phx.gen.secret
|
||||||
"""
|
"""
|
||||||
|
|
||||||
host = System.get_env("PHX_HOST") || "example.com"
|
host = System.get_env("PHX_HOST") || "localhost"
|
||||||
|
aux_host = System.get_env("PHX_AUX_HOST") || host
|
||||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||||
|
|
||||||
config :chronoscope, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
config :chronoscope, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
||||||
|
|
||||||
config :chronoscope, ChronoscopeWeb.Endpoint,
|
config :chronoscope, ChronoscopeWeb.Endpoint,
|
||||||
url: [host: host, port: 443, scheme: "https"],
|
url: [host: host, port: 443, scheme: "https"],
|
||||||
|
check_origin: ["https://" <> host, "https://" <> aux_host],
|
||||||
http: [
|
http: [
|
||||||
# Enable IPv6 and bind on all interfaces.
|
# Enable IPv6 and bind on all interfaces.
|
||||||
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
|
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
|
||||||
|
|
|
@ -10,6 +10,8 @@ config :chronoscope, ChronoscopeWeb.Endpoint,
|
||||||
# In test we don't send emails.
|
# In test we don't send emails.
|
||||||
config :chronoscope, Chronoscope.Mailer, adapter: Swoosh.Adapters.Test
|
config :chronoscope, Chronoscope.Mailer, adapter: Swoosh.Adapters.Test
|
||||||
|
|
||||||
|
config :chronoscope, :nts_file, "priv/nts-test.txt"
|
||||||
|
|
||||||
# Disable swoosh api client as it is only required for production adapters.
|
# Disable swoosh api client as it is only required for production adapters.
|
||||||
config :swoosh, :api_client, false
|
config :swoosh, :api_client, false
|
||||||
|
|
||||||
|
@ -26,4 +28,5 @@ config :chronoscope,
|
||||||
registry: Chronoscope.RegistryMock,
|
registry: Chronoscope.RegistryMock,
|
||||||
gen_server: Chronoscope.GenServerMock,
|
gen_server: Chronoscope.GenServerMock,
|
||||||
nts: Chronoscope.NTSMock,
|
nts: Chronoscope.NTSMock,
|
||||||
gemini: Chronoscope.GeminiMock
|
gemini: Chronoscope.GeminiMock,
|
||||||
|
endpoint: Chronoscope.EndpointMock
|
||||||
|
|
|
@ -22,7 +22,9 @@ defmodule Chronoscope.Application do
|
||||||
{Task.Supervisor, name: Chronoscope.Gemini.TaskSupervisor},
|
{Task.Supervisor, name: Chronoscope.Gemini.TaskSupervisor},
|
||||||
{Registry, [keys: :unique, name: Chronoscope.Gemini.Registry]},
|
{Registry, [keys: :unique, name: Chronoscope.Gemini.Registry]},
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry
|
||||||
ChronoscopeWeb.Endpoint
|
ChronoscopeWeb.Endpoint,
|
||||||
|
# Initialize clients used in the main LiveView
|
||||||
|
ChronoscopeWeb.ClientActivator
|
||||||
]
|
]
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
|
|
@ -9,6 +9,8 @@ defmodule Chronoscope.Gemini do
|
||||||
|
|
||||||
alias Chronoscope.Gemini
|
alias Chronoscope.Gemini
|
||||||
|
|
||||||
|
@timeout_in_milliseconds 10_000
|
||||||
|
|
||||||
@registry Application.compile_env(:chronoscope, :registry, Registry)
|
@registry Application.compile_env(:chronoscope, :registry, Registry)
|
||||||
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
|
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
|
||||||
@dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor)
|
@dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor)
|
||||||
|
@ -20,14 +22,14 @@ defmodule Chronoscope.Gemini do
|
||||||
def list() do
|
def list() do
|
||||||
Gemini.DynamicSupervisor
|
Gemini.DynamicSupervisor
|
||||||
|> @dynamic_supervisor.which_children()
|
|> @dynamic_supervisor.which_children()
|
||||||
|> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end)
|
|> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list, @timeout_in_milliseconds) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove(host, port, path) do
|
def remove(host, port, path) do
|
||||||
name = client_name(%{host: host, port: port, path: path})
|
name = client_name(%{host: host, port: port, path: path})
|
||||||
|
|
||||||
case @registry.lookup(Gemini.Registry, name) do
|
case @registry.lookup(Gemini.Registry, name) do
|
||||||
[{pid, _}] -> {:ok, @genserver.call(pid, :terminate)}
|
[{pid, _}] -> {:ok, @genserver.call(pid, :terminate, @timeout_in_milliseconds)}
|
||||||
[] -> {:error, :not_found}
|
[] -> {:error, :not_found}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -36,7 +38,7 @@ defmodule Chronoscope.Gemini do
|
||||||
def connect(host, port, path) do
|
def connect(host, port, path) do
|
||||||
%{host: host, port: port, path: path}
|
%{host: host, port: port, path: path}
|
||||||
|> client_pid()
|
|> client_pid()
|
||||||
|> @genserver.call(:connect)
|
|> @genserver.call(:connect, @timeout_in_milliseconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp client_pid(resource) do
|
defp client_pid(resource) do
|
||||||
|
|
|
@ -5,6 +5,7 @@ defmodule Chronoscope.Gemini.Client do
|
||||||
alias Chronoscope.Gemini.ConnectionClient
|
alias Chronoscope.Gemini.ConnectionClient
|
||||||
|
|
||||||
@interval_in_seconds 30
|
@interval_in_seconds 30
|
||||||
|
@timeout_in_milliseconds 10_000
|
||||||
|
|
||||||
@date_time Application.compile_env(:chronoscope, :date_time, DateTime)
|
@date_time Application.compile_env(:chronoscope, :date_time, DateTime)
|
||||||
|
|
||||||
|
@ -61,7 +62,7 @@ defmodule Chronoscope.Gemini.Client do
|
||||||
defp server_response(%{resource: resource}) do
|
defp server_response(%{resource: resource}) do
|
||||||
Gemini.TaskSupervisor
|
Gemini.TaskSupervisor
|
||||||
|> Task.Supervisor.async(fn -> ConnectionClient.connect(resource) end)
|
|> Task.Supervisor.async(fn -> ConnectionClient.connect(resource) end)
|
||||||
|> Task.await()
|
|> Task.await(@timeout_in_milliseconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp interval_surpassed?(now, last_request) do
|
defp interval_surpassed?(now, last_request) do
|
||||||
|
|
|
@ -9,6 +9,8 @@ defmodule Chronoscope.NTS do
|
||||||
|
|
||||||
alias Chronoscope.NTS
|
alias Chronoscope.NTS
|
||||||
|
|
||||||
|
@timeout_in_milliseconds 10_000
|
||||||
|
|
||||||
@registry Application.compile_env(:chronoscope, :registry, Registry)
|
@registry Application.compile_env(:chronoscope, :registry, Registry)
|
||||||
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
|
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
|
||||||
@dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor)
|
@dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor)
|
||||||
|
@ -23,11 +25,33 @@ defmodule Chronoscope.NTS do
|
||||||
|> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end)
|
|> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_clients(servers) do
|
||||||
|
servers
|
||||||
|
|> Enum.map(&client_pid/1)
|
||||||
|
|> Enum.map(fn pid -> @genserver.call(pid, :list, @timeout_in_milliseconds) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_client(server) do
|
||||||
|
client_pid(server)
|
||||||
|
end
|
||||||
|
|
||||||
|
def auto_refresh(server) do
|
||||||
|
server
|
||||||
|
|> client_pid()
|
||||||
|
|> @genserver.call(:auto_refresh, @timeout_in_milliseconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cancel_auto_refresh(server) do
|
||||||
|
server
|
||||||
|
|> client_pid()
|
||||||
|
|> @genserver.call(:cancel_auto_refresh, @timeout_in_milliseconds)
|
||||||
|
end
|
||||||
|
|
||||||
def remove(host, port) do
|
def remove(host, port) do
|
||||||
name = client_name(%{host: host, port: port})
|
name = client_name(%{host: host, port: port})
|
||||||
|
|
||||||
case @registry.lookup(NTS.Registry, name) do
|
case @registry.lookup(NTS.Registry, name) do
|
||||||
[{pid, _}] -> {:ok, @genserver.call(pid, :terminate)}
|
[{pid, _}] -> {:ok, @genserver.call(pid, :terminate, @timeout_in_milliseconds)}
|
||||||
[] -> {:error, :not_found}
|
[] -> {:error, :not_found}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -36,7 +60,7 @@ defmodule Chronoscope.NTS do
|
||||||
def key_establishment(host, port) do
|
def key_establishment(host, port) do
|
||||||
%{host: host, port: port}
|
%{host: host, port: port}
|
||||||
|> client_pid()
|
|> client_pid()
|
||||||
|> @genserver.call(:key_establishment)
|
|> @genserver.call(:key_establishment, @timeout_in_milliseconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp client_pid(server) do
|
defp client_pid(server) do
|
||||||
|
|
|
@ -5,8 +5,12 @@ defmodule Chronoscope.NTS.Client do
|
||||||
alias Chronoscope.NTS.KeyEstablishmentClient
|
alias Chronoscope.NTS.KeyEstablishmentClient
|
||||||
|
|
||||||
@interval_in_seconds 30
|
@interval_in_seconds 30
|
||||||
|
@timeout_in_milliseconds 10_000
|
||||||
|
@refresh_interval_in_milliseconds :timer.minutes(1)
|
||||||
|
|
||||||
|
@topic Application.compile_env(:chronoscope, :nts_topic)
|
||||||
@date_time Application.compile_env(:chronoscope, :date_time, DateTime)
|
@date_time Application.compile_env(:chronoscope, :date_time, DateTime)
|
||||||
|
@endpoint Application.compile_env(:chronoscope, :endpoint, ChronoscopeWeb.Endpoint)
|
||||||
|
|
||||||
def start_link(server: server, name: name) do
|
def start_link(server: server, name: name) do
|
||||||
GenServer.start_link(__MODULE__, server, name: name)
|
GenServer.start_link(__MODULE__, server, name: name)
|
||||||
|
@ -14,13 +18,13 @@ defmodule Chronoscope.NTS.Client do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(server) do
|
def init(server) do
|
||||||
now = utc_now()
|
@endpoint.broadcast(@topic, "initializing", server)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
server: server,
|
server: server,
|
||||||
key_establishment_response: {:error, "initializing"},
|
key_establishment_response: {:error, "initializing"},
|
||||||
last_key_establishment: DateTime.add(now, -@interval_in_seconds, :second)
|
last_key_establishment: DateTime.add(utc_now(), -@interval_in_seconds, :second)
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -34,6 +38,32 @@ defmodule Chronoscope.NTS.Client do
|
||||||
{:reply, state, state}
|
{:reply, state, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:auto_refresh, _from, %{timer: _timer} = state) do
|
||||||
|
{:reply, :already_started, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:auto_refresh, _from, state) do
|
||||||
|
:timer.send_after(1_000, :key_establishment)
|
||||||
|
{:ok, timer} = :timer.send_interval(@refresh_interval_in_milliseconds, :key_establishment)
|
||||||
|
|
||||||
|
{:reply, :ok, Map.put(state, :timer, timer)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:cancel_auto_refresh, _from, %{timer: timer} = state) do
|
||||||
|
:timer.cancel(timer)
|
||||||
|
@endpoint.broadcast(@topic, "cancel-auto-refresh", state.server)
|
||||||
|
|
||||||
|
{:reply, :ok, Map.delete(state, :timer)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:cancel_auto_refresh, _from, state) do
|
||||||
|
{:reply, :already_cancelled, state}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call(:key_establishment, _from, state) do
|
def handle_call(:key_establishment, _from, state) do
|
||||||
new_state = update_state(state)
|
new_state = update_state(state)
|
||||||
|
@ -41,11 +71,18 @@ defmodule Chronoscope.NTS.Client do
|
||||||
{:reply, new_state.key_establishment_response, new_state}
|
{:reply, new_state.key_establishment_response, new_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:key_establishment, state) do
|
||||||
|
{:noreply, update_state(state)}
|
||||||
|
end
|
||||||
|
|
||||||
defp update_state(state) do
|
defp update_state(state) do
|
||||||
now = utc_now()
|
now = utc_now()
|
||||||
|
|
||||||
if interval_surpassed?(now, state.last_key_establishment) do
|
if interval_surpassed?(now, state.last_key_establishment) do
|
||||||
Map.merge(state, current_data(state, now))
|
state
|
||||||
|
|> Map.merge(current_data(state, now))
|
||||||
|
|> tap(&@endpoint.broadcast(@topic, "key-establishment", Map.delete(&1, :timer)))
|
||||||
else
|
else
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
@ -61,7 +98,7 @@ defmodule Chronoscope.NTS.Client do
|
||||||
defp server_response(%{server: server}) do
|
defp server_response(%{server: server}) do
|
||||||
NTS.TaskSupervisor
|
NTS.TaskSupervisor
|
||||||
|> Task.Supervisor.async(fn -> KeyEstablishmentClient.key_establishment(server) end)
|
|> Task.Supervisor.async(fn -> KeyEstablishmentClient.key_establishment(server) end)
|
||||||
|> Task.await()
|
|> Task.await(@timeout_in_milliseconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp interval_surpassed?(now, last_key_establishment) do
|
defp interval_surpassed?(now, last_key_establishment) do
|
||||||
|
|
|
@ -5,7 +5,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClient do
|
||||||
alias Chronoscope.NTS.KeyEstablishmentRequest
|
alias Chronoscope.NTS.KeyEstablishmentRequest
|
||||||
alias Chronoscope.NTS.KeyEstablishmentResponse
|
alias Chronoscope.NTS.KeyEstablishmentResponse
|
||||||
|
|
||||||
@timeout_in_milliseconds 3000
|
@timeout_in_milliseconds 3500
|
||||||
|
|
||||||
@ssl Application.compile_env(:chronoscope, :ssl, :ssl)
|
@ssl Application.compile_env(:chronoscope, :ssl, :ssl)
|
||||||
|
|
||||||
|
@ -33,12 +33,16 @@ defmodule Chronoscope.NTS.KeyEstablishmentClient do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp perform_key_establishment(socket) do
|
defp perform_key_establishment(socket) do
|
||||||
:ok = @ssl.send(socket, KeyEstablishmentRequest.create())
|
with :ok <- @ssl.send(socket, KeyEstablishmentRequest.create()),
|
||||||
{:ok, peercert} = @ssl.peercert(socket)
|
{:ok, peercert} <- @ssl.peercert(socket) do
|
||||||
|
peercert
|
||||||
peercert
|
|> await_response()
|
||||||
|> await_response()
|
|> tap(fn _ -> @ssl.close(socket) end)
|
||||||
|> tap(fn _ -> @ssl.close(socket) end)
|
else
|
||||||
|
e ->
|
||||||
|
@ssl.close(socket)
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp await_response(peercert) do
|
defp await_response(peercert) do
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule Chronoscope.NTS.KeyEstablishmentResponse do
|
defmodule Chronoscope.NTS.KeyEstablishmentResponse do
|
||||||
import Bitwise
|
import Bitwise
|
||||||
|
|
||||||
@aead_alogorithms %{
|
@aead_algorithms %{
|
||||||
15 => "AEAD_AES_SIV_CMAC_256",
|
15 => "AEAD_AES_SIV_CMAC_256",
|
||||||
30 => "AEAD_AES_128_GCM_SIV"
|
30 => "AEAD_AES_128_GCM_SIV"
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,12 @@ defmodule Chronoscope.NTS.KeyEstablishmentResponse do
|
||||||
2 => "Internal Server Error"
|
2 => "Internal Server Error"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def aead_algorithm_to_id(algorithm) do
|
||||||
|
@aead_algorithms
|
||||||
|
|> Map.new(fn {k, v} -> {v, k} end)
|
||||||
|
|> Map.get(algorithm)
|
||||||
|
end
|
||||||
|
|
||||||
def parse(response) do
|
def parse(response) do
|
||||||
parse_response(response, %{})
|
parse_response(response, %{})
|
||||||
end
|
end
|
||||||
|
@ -127,7 +133,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentResponse do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_parse_aead_algorithm_list([high, low | rest], acc) do
|
defp do_parse_aead_algorithm_list([high, low | rest], acc) do
|
||||||
@aead_alogorithms
|
@aead_algorithms
|
||||||
|> Map.get(combine_octets(high, low), "UNKNOWN")
|
|> Map.get(combine_octets(high, low), "UNKNOWN")
|
||||||
|> then(&do_parse_aead_algorithm_list(rest, [&1 | acc]))
|
|> then(&do_parse_aead_algorithm_list(rest, [&1 | acc]))
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule Chronoscope.NTS.Parse do
|
||||||
|
@default_port 4460
|
||||||
|
|
||||||
|
def parse_nts_server(server) do
|
||||||
|
case String.split(server, ":") do
|
||||||
|
[host, port] -> %{host: host, port: String.to_integer(port)}
|
||||||
|
[host] -> %{host: host, port: @default_port}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,67 @@
|
||||||
|
defmodule ChronoscopeWeb.ClientActivator do
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Chronoscope.NTS
|
||||||
|
alias Chronoscope.NTS.Parse
|
||||||
|
alias ChronoscopeWeb.Endpoint
|
||||||
|
|
||||||
|
@topic Application.compile_env(:chronoscope, :nts_topic)
|
||||||
|
@nts_file Application.compile_env(:chronoscope, :nts_file)
|
||||||
|
|
||||||
|
def start_link(_) do
|
||||||
|
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_) do
|
||||||
|
Endpoint.subscribe(@topic)
|
||||||
|
|
||||||
|
{:ok, %{nts_servers: nts_servers() |> tap(&start_clients/1)}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(%{topic: @topic, event: "initializing", payload: server}, state) do
|
||||||
|
if server in state.nts_servers do
|
||||||
|
Logger.info("activating #{inspect(server)}")
|
||||||
|
NTS.auto_refresh(server)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(%{topic: @topic, event: "cancel-auto-refresh", payload: server}, state) do
|
||||||
|
if server in state.nts_servers do
|
||||||
|
Logger.info("#{inspect(server)} was deactivated")
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(_, state) do
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:get_nts_servers, _from, state) do
|
||||||
|
{:reply, state.nts_servers, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp start_clients(servers) do
|
||||||
|
Enum.each(servers, &NTS.start_client/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp nts_servers() do
|
||||||
|
File.touch(Application.app_dir(:chronoscope, @nts_file))
|
||||||
|
|
||||||
|
Application.app_dir(:chronoscope, @nts_file)
|
||||||
|
|> File.stream!()
|
||||||
|
|> Stream.map(&String.trim/1)
|
||||||
|
|> Stream.filter(&(&1 != ""))
|
||||||
|
|> Stream.map(&Parse.parse_nts_server/1)
|
||||||
|
|> Enum.to_list()
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,28 +1,22 @@
|
||||||
<header class="px-4 sm:px-6 lg:px-8">
|
<header class="px-4 sm:px-6 lg:px-8 sticky top-0 z-10 bg-white dark:bg-zinc-800">
|
||||||
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
|
<div class="flex items-center justify-between border-b border-zinc-100 dark:border-zinc-700 py-3 text-sm">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900 dark:text-zinc-300 pe-4">
|
||||||
<a href="/">
|
<a href="/" class="hover:text-zinc-700 dark:hover:text-zinc-400">
|
||||||
<img src={~p"/images/logo.svg"} width="36" />
|
Chronoscope
|
||||||
</a>
|
</a>
|
||||||
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
|
<p class="bg-brand/10 dark:bg-brand text-brand dark:text-zinc-900 rounded-full px-2 font-medium leading-6">
|
||||||
v<%= Application.spec(:phoenix, :vsn) %>
|
v<%= Application.spec(:chronoscope, :vsn) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
|
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900 dark:text-zinc-300">
|
||||||
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
|
<a href="https://codeberg.org/mike-cifelli" class="hover:text-zinc-700 dark:hover:text-zinc-400">
|
||||||
@elixirphoenix
|
Codeberg
|
||||||
</a>
|
|
||||||
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
<a href="https://hexdocs.pm/phoenix/overview.html" class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80">
|
|
||||||
Get Started <span aria-hidden="true">→</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
||||||
<div class="mx-auto max-w-2xl">
|
<div class="mx-auto max-w-fit">
|
||||||
<.flash_group flash={@flash} />
|
<.flash_group flash={@flash} />
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white antialiased">
|
<body class="bg-white dark:bg-zinc-800 dark:text-zinc-300 antialiased font-sans tracking-wide">
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -13,7 +13,7 @@ defmodule ChronoscopeWeb.API.V1.Gemini.ConnectionController do
|
||||||
|
|
||||||
def get(conn, %{"host" => host, "port" => port, "path" => path}) do
|
def get(conn, %{"host" => host, "port" => port, "path" => path}) do
|
||||||
try do
|
try do
|
||||||
handle_get(conn, %{host: host, port: String.to_integer(port), path: path})
|
handle_get(conn, %{host: String.trim(host), port: String.to_integer(port), path: path})
|
||||||
rescue
|
rescue
|
||||||
ArgumentError -> bad_request_response(conn, "invalid port")
|
ArgumentError -> bad_request_response(conn, "invalid port")
|
||||||
end
|
end
|
||||||
|
@ -25,20 +25,24 @@ defmodule ChronoscopeWeb.API.V1.Gemini.ConnectionController do
|
||||||
|
|
||||||
def get(conn, %{"host" => host, "port" => port}) do
|
def get(conn, %{"host" => host, "port" => port}) do
|
||||||
try do
|
try do
|
||||||
handle_get(conn, %{host: host, port: String.to_integer(port), path: @default_path})
|
handle_get(conn, %{host: String.trim(host), port: String.to_integer(port), path: @default_path})
|
||||||
rescue
|
rescue
|
||||||
ArgumentError -> bad_request_response(conn, "invalid port")
|
ArgumentError -> bad_request_response(conn, "invalid port")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(conn, %{"host" => host}) do
|
def get(conn, %{"host" => host}) do
|
||||||
handle_get(conn, %{host: host, port: @default_port, path: @default_path})
|
handle_get(conn, %{host: String.trim(host), port: @default_port, path: @default_path})
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(conn, _params) do
|
def get(conn, _params) do
|
||||||
bad_request_response(conn, "missing host")
|
bad_request_response(conn, "missing host")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp handle_get(conn, %{host: ""}) do
|
||||||
|
bad_request_response(conn, "empty host")
|
||||||
|
end
|
||||||
|
|
||||||
defp handle_get(conn, %{host: host, port: port, path: path}) when port > 0 and port < 65536 do
|
defp handle_get(conn, %{host: host, port: port, path: path}) when port > 0 and port < 65536 do
|
||||||
case connect(host, port, path) do
|
case connect(host, port, path) do
|
||||||
{:ok, response} ->
|
{:ok, response} ->
|
||||||
|
|
|
@ -12,20 +12,24 @@ defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentController do
|
||||||
|
|
||||||
def get(conn, %{"host" => host, "port" => port}) do
|
def get(conn, %{"host" => host, "port" => port}) do
|
||||||
try do
|
try do
|
||||||
handle_get(conn, %{host: host, port: String.to_integer(port)})
|
handle_get(conn, %{host: String.trim(host), port: String.to_integer(port)})
|
||||||
rescue
|
rescue
|
||||||
ArgumentError -> bad_request_response(conn, "invalid port")
|
ArgumentError -> bad_request_response(conn, "invalid port")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(conn, %{"host" => host}) do
|
def get(conn, %{"host" => host}) do
|
||||||
handle_get(conn, %{host: host, port: @default_port})
|
handle_get(conn, %{host: String.trim(host), port: @default_port})
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(conn, _params) do
|
def get(conn, _params) do
|
||||||
bad_request_response(conn, "missing host")
|
bad_request_response(conn, "missing host")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp handle_get(conn, %{host: ""}) do
|
||||||
|
bad_request_response(conn, "empty host")
|
||||||
|
end
|
||||||
|
|
||||||
defp handle_get(conn, %{host: host, port: port}) when port > 0 and port < 65536 do
|
defp handle_get(conn, %{host: host, port: port}) when port > 0 and port < 65536 do
|
||||||
case key_establishment_response(host, port) do
|
case key_establishment_response(host, port) do
|
||||||
{:ok, response} ->
|
{:ok, response} ->
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
defmodule ChronoscopeWeb.IndexLive do
|
||||||
|
use ChronoscopeWeb, :live_view
|
||||||
|
|
||||||
|
alias Chronoscope.NTS
|
||||||
|
alias Chronoscope.NTS.KeyEstablishmentResponse
|
||||||
|
alias ChronoscopeWeb.ClientActivator
|
||||||
|
alias ChronoscopeWeb.Endpoint
|
||||||
|
|
||||||
|
@topic Application.compile_env(:chronoscope, :nts_topic)
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
Endpoint.subscribe(@topic)
|
||||||
|
{:ok, assign(socket, %{servers: server_list(), clients: client_list()})}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(%{topic: @topic, event: "key-establishment", payload: client}, socket) do
|
||||||
|
if client.server in socket.assigns.servers do
|
||||||
|
{:noreply, update(socket, :clients, &update_client(&1, client))}
|
||||||
|
else
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(_, socket) do
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_client(client_list, client) do
|
||||||
|
Enum.map(client_list, &if(client.server == &1.server, do: client, else: &1))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp server_list() do
|
||||||
|
GenServer.call(ClientActivator, :get_nts_servers)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp client_list() do
|
||||||
|
server_list() |> NTS.list_clients()
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,101 @@
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="mx-auto border-collapse table-auto min-w-full divide-y divide-zinc-200 dark:divide-zinc-700 text-left">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
|
NTS-KE Server
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
|
Algorithm
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
|
Cookies
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
|
Cookie Length
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
|
NTP Host
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
|
NTP Port
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
|
Certificate Expiration
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
|
Last Check
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr :for={client <- @clients} class="hover:bg-zinc-100 dark:hover:bg-zinc-700">
|
||||||
|
<% {status, response} = client.key_establishment_response %>
|
||||||
|
|
||||||
|
<%= if (status == :ok) do %>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%= client.server.host %><span :if={client.server.port != 4460}>:<%= client.server.port %></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%= status %>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<% aead_algorithm = Enum.at(response.aead_algorithms, 0) %>
|
||||||
|
|
||||||
|
<span class="group relative">
|
||||||
|
<%= KeyEstablishmentResponse.aead_algorithm_to_id(aead_algorithm) %>
|
||||||
|
<span class="pointer-events-none absolute -top-9 left-0 w-max p-1 rounded-lg bg-zinc-300 dark:bg-zinc-600 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
<%= aead_algorithm %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%= length(response.cookies) %>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%= response.cookie_length %>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%= Map.get(response, :server, "-") %>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%= Map.get(response, :port, "-") %>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%= response.cert_expiration |> DateTime.from_iso8601 |> then(fn {:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%SZ") end)%>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%= client.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<% else %>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%= client.server.host %><span :if={client.server.port != 4460}>:<%= client.server.port %></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<span class="group relative">
|
||||||
|
<%= status %>
|
||||||
|
<span class="pointer-events-none absolute -top-9 left-0 w-max p-1 rounded-lg bg-zinc-300 dark:bg-zinc-600 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
<%= response %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
|
<%=
|
||||||
|
client.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ")
|
||||||
|
%>
|
||||||
|
</td>
|
||||||
|
<% end %>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
|
@ -17,7 +17,7 @@ defmodule ChronoscopeWeb.Router do
|
||||||
scope "/", ChronoscopeWeb do
|
scope "/", ChronoscopeWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/", PageController, :home
|
live "/", IndexLive
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/api/v1", ChronoscopeWeb.API.V1 do
|
scope "/api/v1", ChronoscopeWeb.API.V1 do
|
||||||
|
|
2
mix.exs
2
mix.exs
|
@ -4,7 +4,7 @@ defmodule Chronoscope.MixProject do
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :chronoscope,
|
app: :chronoscope,
|
||||||
version: "0.1.0",
|
version: "1.0.0",
|
||||||
elixir: "~> 1.16",
|
elixir: "~> 1.16",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
time.cifelli.xyz
|
||||||
|
stratum1.time.cifelli.xyz
|
||||||
|
virginia.time.system76.com
|
||||||
|
paris.time.system76.com
|
||||||
|
oregon.time.system76.com
|
||||||
|
ohio.time.system76.com
|
||||||
|
brazil.time.system76.com
|
||||||
|
time.cloudflare.com
|
||||||
|
nts.netnod.se
|
|
@ -53,7 +53,7 @@ defmodule Chronoscope.Gemini.ClientTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test ":connect - not cached" do
|
test ":connect - not cached" do
|
||||||
response ="20 text/gemini\r\nHello!" |> to_charlist()
|
response = "20 text/gemini\r\nHello!" |> to_charlist()
|
||||||
peercert = peercert()
|
peercert = peercert()
|
||||||
peercert_expiration = Certificate.expiration_date(peercert)
|
peercert_expiration = Certificate.expiration_date(peercert)
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,6 @@ defmodule Chronoscope.GeminiTest do
|
||||||
|
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
setup :verify_on_exit!
|
|
||||||
|
|
||||||
describe "Chronoscope.Gemini.healthy?()" do
|
describe "Chronoscope.Gemini.healthy?()" do
|
||||||
test "is healthy" do
|
test "is healthy" do
|
||||||
assert Gemini.healthy?() == true
|
assert Gemini.healthy?() == true
|
||||||
|
@ -29,8 +27,8 @@ defmodule Chronoscope.GeminiTest do
|
||||||
|> expect(:which_children, fn _ -> [{1, 2, 3, 4}, {5, 6, 7, 8}] end)
|
|> expect(:which_children, fn _ -> [{1, 2, 3, 4}, {5, 6, 7, 8}] end)
|
||||||
|
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 2, :list -> :one end)
|
|> expect(:call, fn 2, :list, 10_000 -> :one end)
|
||||||
|> expect(:call, fn 6, :list -> :two end)
|
|> expect(:call, fn 6, :list, 10_000 -> :two end)
|
||||||
|
|
||||||
assert Gemini.list() == [:one, :two]
|
assert Gemini.list() == [:one, :two]
|
||||||
end
|
end
|
||||||
|
@ -49,7 +47,7 @@ defmodule Chronoscope.GeminiTest do
|
||||||
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
||||||
|
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 1, :terminate -> :terminating end)
|
|> expect(:call, fn 1, :terminate, 10_000 -> :terminating end)
|
||||||
|
|
||||||
assert Gemini.remove("localhost", 1965, "/") == {:ok, :terminating}
|
assert Gemini.remove("localhost", 1965, "/") == {:ok, :terminating}
|
||||||
end
|
end
|
||||||
|
@ -74,7 +72,7 @@ defmodule Chronoscope.GeminiTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 1, :connect -> :result end)
|
|> expect(:call, fn 1, :connect, 10_000 -> :result end)
|
||||||
|
|
||||||
assert Gemini.connect("localhost", 1965, "/") == :result
|
assert Gemini.connect("localhost", 1965, "/") == :result
|
||||||
end
|
end
|
||||||
|
@ -84,7 +82,7 @@ defmodule Chronoscope.GeminiTest do
|
||||||
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
||||||
|
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 1, :connect -> :result end)
|
|> expect(:call, fn 1, :connect, 10_000 -> :result end)
|
||||||
|
|
||||||
assert Gemini.connect("localhost", 1965, "/") == :result
|
assert Gemini.connect("localhost", 1965, "/") == :result
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule Chronoscope.NTS.ClientTest do
|
||||||
|
|
||||||
alias Chronoscope.Certificate
|
alias Chronoscope.Certificate
|
||||||
alias Chronoscope.DateTimeMock
|
alias Chronoscope.DateTimeMock
|
||||||
|
alias Chronoscope.EndpointMock
|
||||||
alias Chronoscope.NTS.Client
|
alias Chronoscope.NTS.Client
|
||||||
alias Chronoscope.SSLMock
|
alias Chronoscope.SSLMock
|
||||||
|
|
||||||
|
@ -19,6 +20,9 @@ defmodule Chronoscope.NTS.ClientTest do
|
||||||
|
|
||||||
describe "Chronoscope.NTS.Client.init()" do
|
describe "Chronoscope.NTS.Client.init()" do
|
||||||
test "initializes successfully" do
|
test "initializes successfully" do
|
||||||
|
EndpointMock
|
||||||
|
|> expect(:broadcast, fn "nts-servers", "initializing", %{host: "localhost", port: 3333} -> :ok end)
|
||||||
|
|
||||||
assert Client.init(%{host: "localhost", port: 3333}) ==
|
assert Client.init(%{host: "localhost", port: 3333}) ==
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
|
@ -38,6 +42,26 @@ defmodule Chronoscope.NTS.ClientTest do
|
||||||
assert Client.handle_call(:list, nil, %{state: true}) == {:reply, %{state: true}, %{state: true}}
|
assert Client.handle_call(:list, nil, %{state: true}) == {:reply, %{state: true}, %{state: true}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test ":auto_refresh" do
|
||||||
|
assert {:reply, :ok, %{server: true, timer: _timer}} = Client.handle_call(:auto_refresh, nil, %{server: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
test ":auto_refresh - already activated" do
|
||||||
|
assert Client.handle_call(:auto_refresh, nil, %{server: true, timer: true}) ==
|
||||||
|
{:reply, :already_started, %{server: true, timer: true}}
|
||||||
|
end
|
||||||
|
|
||||||
|
test ":cancel_auto_refresh" do
|
||||||
|
EndpointMock
|
||||||
|
|> expect(:broadcast, fn "nts-servers", "cancel-auto-refresh", _ -> :ok end)
|
||||||
|
|
||||||
|
assert Client.handle_call(:cancel_auto_refresh, nil, %{server: true, timer: true}) == {:reply, :ok, %{server: true}}
|
||||||
|
end
|
||||||
|
|
||||||
|
test ":cancel_auto_refresh - already cancelled" do
|
||||||
|
assert Client.handle_call(:cancel_auto_refresh, nil, %{server: true}) == {:reply, :already_cancelled, %{server: true}}
|
||||||
|
end
|
||||||
|
|
||||||
test ":key_establishment - cached" do
|
test ":key_establishment - cached" do
|
||||||
assert Client.handle_call(:key_establishment, nil, %{
|
assert Client.handle_call(:key_establishment, nil, %{
|
||||||
server: %{host: "localhost", port: 3333},
|
server: %{host: "localhost", port: 3333},
|
||||||
|
@ -62,6 +86,9 @@ defmodule Chronoscope.NTS.ClientTest do
|
||||||
|> expect(:peercert, fn :socket -> {:ok, peercert} end)
|
|> expect(:peercert, fn :socket -> {:ok, peercert} end)
|
||||||
|> expect(:close, fn :socket -> :ok end)
|
|> expect(:close, fn :socket -> :ok end)
|
||||||
|
|
||||||
|
EndpointMock
|
||||||
|
|> expect(:broadcast, fn "nts-servers", "key-establishment", _ -> :ok end)
|
||||||
|
|
||||||
assert {:reply, {:ok, %{cert_expiration: ^peercert_expiration}},
|
assert {:reply, {:ok, %{cert_expiration: ^peercert_expiration}},
|
||||||
%{
|
%{
|
||||||
server: %{host: "localhost", port: 3333},
|
server: %{host: "localhost", port: 3333},
|
||||||
|
|
|
@ -10,7 +10,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
@timeout 3000
|
@timeout 3500
|
||||||
|
|
||||||
describe "Chronoscope.NTS.KeyEstablishmentClient.key_establishment()" do
|
describe "Chronoscope.NTS.KeyEstablishmentClient.key_establishment()" do
|
||||||
test "sends the correct TLS options" do
|
test "sends the correct TLS options" do
|
||||||
|
|
|
@ -3,6 +3,13 @@ defmodule Chronoscope.NTS.KeyEstablishmentResponseTest do
|
||||||
|
|
||||||
import Chronoscope.NTS.KeyEstablishmentResponse
|
import Chronoscope.NTS.KeyEstablishmentResponse
|
||||||
|
|
||||||
|
describe "Chronoscope.NTS.KeyEstablishmentResponse.aead_algorithm_to_id()" do
|
||||||
|
test "maps names to a numeric identifiers" do
|
||||||
|
assert aead_algorithm_to_id("AEAD_AES_SIV_CMAC_256") == 15
|
||||||
|
assert aead_algorithm_to_id("AEAD_AES_128_GCM_SIV") == 30
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "Chronoscope.NTS.KeyEstablishmentResponse.parse()" do
|
describe "Chronoscope.NTS.KeyEstablishmentResponse.parse()" do
|
||||||
test "handles empty response" do
|
test "handles empty response" do
|
||||||
assert parse([]) == %{}
|
assert parse([]) == %{}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
defmodule Chronoscope.NTS.ParseTest do
|
||||||
|
use Chronoscope.Case, async: true
|
||||||
|
|
||||||
|
import Chronoscope.NTS.Parse
|
||||||
|
|
||||||
|
describe "Chronoscope.NTS.Parse.parse_nts_server()" do
|
||||||
|
test "parses a server with a port" do
|
||||||
|
assert parse_nts_server("test.example.com:1234") == %{host: "test.example.com", port: 1234}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a server without a port" do
|
||||||
|
assert parse_nts_server("test.example.com") == %{host: "test.example.com", port: 4460}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -36,6 +36,136 @@ defmodule Chronoscope.NTSTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "Chronoscope.NTS.list_clients()" do
|
||||||
|
test "shows empty client list" do
|
||||||
|
assert NTS.list_clients([]) == []
|
||||||
|
end
|
||||||
|
|
||||||
|
test "lists the given clients and starts them if they don't exist" do
|
||||||
|
RegistryMock
|
||||||
|
|> expect(:lookup, fn _, "test:1" -> [{1, nil}] end)
|
||||||
|
|> expect(:lookup, fn _, "test2:2" -> [] end)
|
||||||
|
|
||||||
|
DynamicSupervisorMock
|
||||||
|
|> expect(
|
||||||
|
:start_child,
|
||||||
|
fn Chronoscope.NTS.DynamicSupervisor,
|
||||||
|
{Chronoscope.NTS.Client,
|
||||||
|
[
|
||||||
|
server: %{host: "test2", port: 2},
|
||||||
|
name: {:via, RegistryMock, {Chronoscope.NTS.Registry, "test2:2"}}
|
||||||
|
]} ->
|
||||||
|
{:ok, 2}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
GenServerMock
|
||||||
|
|> expect(:call, fn 1, :list, 10_000 -> :one end)
|
||||||
|
|> expect(:call, fn 2, :list, 10_000 -> :two end)
|
||||||
|
|
||||||
|
assert NTS.list_clients([%{host: "test", port: 1}, %{host: "test2", port: 2}]) == [:one, :two]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Chronoscope.NTS.start_client()" do
|
||||||
|
test "starts a client" do
|
||||||
|
RegistryMock
|
||||||
|
|> expect(:lookup, fn _, "test:1" -> [] end)
|
||||||
|
|
||||||
|
DynamicSupervisorMock
|
||||||
|
|> expect(
|
||||||
|
:start_child,
|
||||||
|
fn Chronoscope.NTS.DynamicSupervisor,
|
||||||
|
{Chronoscope.NTS.Client,
|
||||||
|
[
|
||||||
|
server: %{host: "test", port: 1},
|
||||||
|
name: {:via, RegistryMock, {Chronoscope.NTS.Registry, "test:1"}}
|
||||||
|
]} ->
|
||||||
|
{:ok, 2}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
assert NTS.start_client(%{host: "test", port: 1}) == 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an existing client" do
|
||||||
|
RegistryMock
|
||||||
|
|> expect(:lookup, fn _, "test:1" -> [{1, nil}] end)
|
||||||
|
|
||||||
|
assert NTS.start_client(%{host: "test", port: 1}) == 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Chronoscope.NTS.auto_refresh()" do
|
||||||
|
test "sets up auto refresh for a new client" do
|
||||||
|
RegistryMock
|
||||||
|
|> expect(:lookup, fn _, "test:1" -> [] end)
|
||||||
|
|
||||||
|
DynamicSupervisorMock
|
||||||
|
|> expect(
|
||||||
|
:start_child,
|
||||||
|
fn Chronoscope.NTS.DynamicSupervisor,
|
||||||
|
{Chronoscope.NTS.Client,
|
||||||
|
[
|
||||||
|
server: %{host: "test", port: 1},
|
||||||
|
name: {:via, RegistryMock, {Chronoscope.NTS.Registry, "test:1"}}
|
||||||
|
]} ->
|
||||||
|
{:ok, 2}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
GenServerMock
|
||||||
|
|> expect(:call, fn 2, :auto_refresh, 10_000 -> :ok end)
|
||||||
|
|
||||||
|
assert NTS.auto_refresh(%{host: "test", port: 1}) == :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets up auto refresh for an existing client" do
|
||||||
|
RegistryMock
|
||||||
|
|> expect(:lookup, fn _, "test:1" -> [{2, nil}] end)
|
||||||
|
|
||||||
|
GenServerMock
|
||||||
|
|> expect(:call, fn 2, :auto_refresh, 10_000 -> :ok end)
|
||||||
|
|
||||||
|
assert NTS.auto_refresh(%{host: "test", port: 1}) == :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Chronoscope.NTS.cancel_auto_refresh()" do
|
||||||
|
test "cancels auto refresh for a new client" do
|
||||||
|
RegistryMock
|
||||||
|
|> expect(:lookup, fn _, "test:1" -> [] end)
|
||||||
|
|
||||||
|
DynamicSupervisorMock
|
||||||
|
|> expect(
|
||||||
|
:start_child,
|
||||||
|
fn Chronoscope.NTS.DynamicSupervisor,
|
||||||
|
{Chronoscope.NTS.Client,
|
||||||
|
[
|
||||||
|
server: %{host: "test", port: 1},
|
||||||
|
name: {:via, RegistryMock, {Chronoscope.NTS.Registry, "test:1"}}
|
||||||
|
]} ->
|
||||||
|
{:ok, 2}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
GenServerMock
|
||||||
|
|> expect(:call, fn 2, :cancel_auto_refresh, 10_000 -> :ok end)
|
||||||
|
|
||||||
|
assert NTS.cancel_auto_refresh(%{host: "test", port: 1}) == :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cancels auto refresh for an existing client" do
|
||||||
|
RegistryMock
|
||||||
|
|> expect(:lookup, fn _, "test:1" -> [{2, nil}] end)
|
||||||
|
|
||||||
|
GenServerMock
|
||||||
|
|> expect(:call, fn 2, :cancel_auto_refresh, 10_000 -> :ok end)
|
||||||
|
|
||||||
|
assert NTS.cancel_auto_refresh(%{host: "test", port: 1}) == :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "Chronoscope.NTS.remove()" do
|
describe "Chronoscope.NTS.remove()" do
|
||||||
test "does nothing if the client doesn't exist" do
|
test "does nothing if the client doesn't exist" do
|
||||||
RegistryMock
|
RegistryMock
|
||||||
|
@ -49,7 +179,7 @@ defmodule Chronoscope.NTSTest do
|
||||||
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
||||||
|
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 1, :terminate -> :terminating end)
|
|> expect(:call, fn 1, :terminate, 10_000 -> :terminating end)
|
||||||
|
|
||||||
assert NTS.remove("localhost", 1111) == {:ok, :terminating}
|
assert NTS.remove("localhost", 1111) == {:ok, :terminating}
|
||||||
end
|
end
|
||||||
|
@ -74,7 +204,7 @@ defmodule Chronoscope.NTSTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 1, :key_establishment -> :result end)
|
|> expect(:call, fn 1, :key_establishment, 10_000 -> :result end)
|
||||||
|
|
||||||
assert NTS.key_establishment("localhost", 1111) == :result
|
assert NTS.key_establishment("localhost", 1111) == :result
|
||||||
end
|
end
|
||||||
|
@ -84,7 +214,7 @@ defmodule Chronoscope.NTSTest do
|
||||||
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
||||||
|
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 1, :key_establishment -> :result end)
|
|> expect(:call, fn 1, :key_establishment, 10_000 -> :result end)
|
||||||
|
|
||||||
assert NTS.key_establishment("localhost", 1111) == :result
|
assert NTS.key_establishment("localhost", 1111) == :result
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,6 +17,42 @@ defmodule ChronoscopeWeb.API.V1.Gemini.ConnectionControllerTest do
|
||||||
assert %{"error" => "missing host"} == response
|
assert %{"error" => "missing host"} == response
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "requires a host name value", %{conn: conn} do
|
||||||
|
response1 =
|
||||||
|
conn
|
||||||
|
|> get(~p"/api/v1/gemini/connect?host=")
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
response2 =
|
||||||
|
conn
|
||||||
|
|> get(~p"/api/v1/gemini/connect?host=&port=1966")
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
assert %{"error" => "empty host"} == response1
|
||||||
|
assert %{"error" => "empty host"} == response2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "requires a non-blank host name", %{conn: conn} do
|
||||||
|
response1 =
|
||||||
|
conn
|
||||||
|
|> get(~p"/api/v1/gemini/connect?host=%20%20")
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
response2 =
|
||||||
|
conn
|
||||||
|
|> get(~p"/api/v1/gemini/connect?host=%20%20&port=1966")
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
response3 =
|
||||||
|
conn
|
||||||
|
|> get(~p"/api/v1/gemini/connect?host=%20%20&port=1966&path=/test")
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
assert %{"error" => "empty host"} == response1
|
||||||
|
assert %{"error" => "empty host"} == response2
|
||||||
|
assert %{"error" => "empty host"} == response3
|
||||||
|
end
|
||||||
|
|
||||||
test "uses the given host name", %{conn: conn} do
|
test "uses the given host name", %{conn: conn} do
|
||||||
GeminiMock
|
GeminiMock
|
||||||
|> expect(:connect, fn "localhost", 1965, "/" -> {:ok, %{status: :ok}} end)
|
|> expect(:connect, fn "localhost", 1965, "/" -> {:ok, %{status: :ok}} end)
|
||||||
|
|
|
@ -17,6 +17,36 @@ defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentControllerTest do
|
||||||
assert %{"error" => "missing host"} == response
|
assert %{"error" => "missing host"} == response
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "requires a host name value", %{conn: conn} do
|
||||||
|
response1 =
|
||||||
|
conn
|
||||||
|
|> get(~p"/api/v1/nts/key-establishment?host=")
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
response2 =
|
||||||
|
conn
|
||||||
|
|> get(~p"/api/v1/nts/key-establishment?host=&port=4444")
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
assert %{"error" => "empty host"} == response1
|
||||||
|
assert %{"error" => "empty host"} == response2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "requires a non-blank host name", %{conn: conn} do
|
||||||
|
response1 =
|
||||||
|
conn
|
||||||
|
|> get(~p"/api/v1/nts/key-establishment?host=%20%20")
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
response2 =
|
||||||
|
conn
|
||||||
|
|> get(~p"/api/v1/nts/key-establishment?host=%20%20&port=4444")
|
||||||
|
|> json_response(400)
|
||||||
|
|
||||||
|
assert %{"error" => "empty host"} == response1
|
||||||
|
assert %{"error" => "empty host"} == response2
|
||||||
|
end
|
||||||
|
|
||||||
test "uses the given host name", %{conn: conn} do
|
test "uses the given host name", %{conn: conn} do
|
||||||
NTSMock
|
NTSMock
|
||||||
|> expect(:key_establishment, fn "localhost", 4460 -> {:ok, %{status: :ok}} end)
|
|> expect(:key_establishment, fn "localhost", 4460 -> {:ok, %{status: :ok}} end)
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
defmodule ChronoscopeWeb.PageControllerTest do
|
defmodule ChronoscopeWeb.PageControllerTest do
|
||||||
use ChronoscopeWeb.ConnCase, async: true
|
use ChronoscopeWeb.ConnCase, async: true
|
||||||
|
|
||||||
|
import Mox
|
||||||
|
|
||||||
|
setup :verify_on_exit!
|
||||||
|
|
||||||
test "GET /", %{conn: conn} do
|
test "GET /", %{conn: conn} do
|
||||||
conn = get(conn, ~p"/")
|
conn = get(conn, ~p"/")
|
||||||
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
|
assert html_response(conn, 200) =~ "Chronoscope"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,5 +19,10 @@ defmodule Chronoscope.DynamicSupervisor.Behaviour do
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule Chronoscope.GenServer.Behaviour do
|
defmodule Chronoscope.GenServer.Behaviour do
|
||||||
|
@callback call(pid(), any(), any()) :: any()
|
||||||
@callback call(pid(), any()) :: any()
|
@callback call(pid(), any()) :: any()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defmodule ChronoscopeWeb.Endpoint.Behaviour do
|
||||||
|
@callback broadcast(any(), any(), any()) :: any()
|
||||||
|
end
|
||||||
|
|
|
@ -5,3 +5,4 @@ Mox.defmock(Chronoscope.DynamicSupervisorMock, for: Chronoscope.DynamicSuperviso
|
||||||
Mox.defmock(Chronoscope.GenServerMock, for: Chronoscope.GenServer.Behaviour)
|
Mox.defmock(Chronoscope.GenServerMock, for: Chronoscope.GenServer.Behaviour)
|
||||||
Mox.defmock(Chronoscope.NTSMock, for: Chronoscope.NTS.Behaviour)
|
Mox.defmock(Chronoscope.NTSMock, for: Chronoscope.NTS.Behaviour)
|
||||||
Mox.defmock(Chronoscope.GeminiMock, for: Chronoscope.Gemini.Behaviour)
|
Mox.defmock(Chronoscope.GeminiMock, for: Chronoscope.Gemini.Behaviour)
|
||||||
|
Mox.defmock(Chronoscope.EndpointMock, for: ChronoscopeWeb.Endpoint.Behaviour)
|
||||||
|
|
Loading…
Reference in New Issue