Show a dynamically updated list of NTS servers #1
|
@ -37,3 +37,6 @@ npm-debug.log
|
||||||
|
|
||||||
# Ignore IDE files.
|
# Ignore IDE files.
|
||||||
.elixir_ls/
|
.elixir_ls/
|
||||||
|
|
||||||
|
# Ignore server lists.
|
||||||
|
/priv/nts.txt
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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, "test/priv/nts.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
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,8 @@ 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,
|
||||||
|
Chronoscope.Monitor
|
||||||
]
|
]
|
||||||
|
|
||||||
# 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
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
defmodule Chronoscope.Monitor do
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Chronoscope.NTS
|
||||||
|
alias Chronoscope.NTS.Parse
|
||||||
|
|
||||||
|
@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
|
||||||
|
File.touch(@nts_file)
|
||||||
|
activate_nts_clients()
|
||||||
|
{:ok, %{nts_servers: activate_nts_clients()}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:activate_clients, _state) do
|
||||||
|
{:noreply, %{nts_servers: activate_nts_clients()}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:get_nts_servers, _from, state) do
|
||||||
|
{:reply, state.nts_servers, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp activate_nts_clients() do
|
||||||
|
@nts_file
|
||||||
|
|> File.stream!()
|
||||||
|
|> Stream.map(&String.trim/1)
|
||||||
|
|> Stream.map(&Parse.parse_nts_server/1)
|
||||||
|
|> Enum.to_list()
|
||||||
|
|> tap(fn server -> Enum.each(server, &NTS.auto_refresh/1) end)
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,10 +9,11 @@ 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)
|
||||||
@topic "nts-servers"
|
|
||||||
|
|
||||||
def healthy?() do
|
def healthy?() do
|
||||||
true
|
true
|
||||||
|
@ -24,11 +25,29 @@ 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(clients) do
|
||||||
|
clients
|
||||||
|
|> Enum.map(&client_pid/1)
|
||||||
|
|> Enum.map(fn pid -> @genserver.call(pid, :list, @timeout_in_milliseconds) end)
|
||||||
|
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
|
||||||
|
@ -37,8 +56,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)
|
||||||
|> tap(fn _ -> ChronoscopeWeb.Endpoint.broadcast(@topic, "", "") end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp client_pid(server) do
|
defp client_pid(server) do
|
||||||
|
|
|
@ -5,7 +5,10 @@ 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)
|
||||||
|
|
||||||
def start_link(server: server, name: name) do
|
def start_link(server: server, name: name) do
|
||||||
|
@ -34,6 +37,28 @@ 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
|
||||||
|
{: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)
|
||||||
|
{: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 +66,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(fn _ -> ChronoscopeWeb.Endpoint.broadcast(@topic, "", "") end)
|
||||||
else
|
else
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
@ -61,7 +93,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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
||||||
<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 dark:border-zinc-700 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 font-semibold leading-6 text-zinc-900 dark:text-zinc-300 pe-4">
|
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900 dark:text-zinc-300 pe-4">
|
||||||
<a href="/" class="hover:text-zinc-700 dark:hover:text-zinc-400">
|
<a href="/" class="hover:text-zinc-700 dark:hover:text-zinc-400">
|
||||||
|
|
|
@ -4,14 +4,20 @@ defmodule ChronoscopeWeb.IndexLive do
|
||||||
alias Chronoscope.NTS
|
alias Chronoscope.NTS
|
||||||
alias Chronoscope.NTS.KeyEstablishmentResponse
|
alias Chronoscope.NTS.KeyEstablishmentResponse
|
||||||
|
|
||||||
@topic "nts-servers"
|
@topic Application.compile_env(:chronoscope, :nts_topic)
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
ChronoscopeWeb.Endpoint.subscribe(@topic)
|
ChronoscopeWeb.Endpoint.subscribe(@topic)
|
||||||
{:ok, assign(socket, %{servers: NTS.list()})}
|
{:ok, assign(socket, %{servers: server_list()})}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(%{topic: @topic}, socket) do
|
def handle_info(%{topic: @topic}, socket) do
|
||||||
{:noreply, assign(socket, %{servers: NTS.list()})}
|
{:noreply, assign(socket, %{servers: server_list()})}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp server_list() do
|
||||||
|
Chronoscope.Monitor
|
||||||
|
|> GenServer.call(:get_nts_servers)
|
||||||
|
|> NTS.list_clients()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,31 +2,31 @@
|
||||||
<table class="mx-auto border-collapse table-auto min-w-full divide-y divide-zinc-200 dark:divide-zinc-700 text-left">
|
<table class="mx-auto border-collapse table-auto min-w-full divide-y divide-zinc-200 dark:divide-zinc-700 text-left">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="py-3 px-6 whitespace-nowrap">
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
NTS-KE Server
|
NTS-KE Server
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="py-3 px-6 whitespace-nowrap">
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="py-3 px-6 whitespace-nowrap">
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
Algorithm
|
Algorithm
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="py-3 px-6 whitespace-nowrap">
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
Cookies
|
Cookies
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="py-3 px-6 whitespace-nowrap">
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
Cookie Length
|
Cookie Length
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="py-3 px-6 whitespace-nowrap">
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
NTP Host
|
NTP Host
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="py-3 px-6 whitespace-nowrap">
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
NTP Port
|
NTP Port
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="py-3 px-6 whitespace-nowrap">
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
Certificate Expiration
|
Certificate Expiration
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="py-3 px-6 whitespace-nowrap">
|
<th scope="col" class="py-2 px-6 whitespace-nowrap">
|
||||||
Last Check
|
Last Check
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -36,13 +36,13 @@
|
||||||
<% {status, response} = server.key_establishment_response %>
|
<% {status, response} = server.key_establishment_response %>
|
||||||
|
|
||||||
<%= if (status == :ok) do %>
|
<%= if (status == :ok) do %>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<%= server.server.host %>:<%= server.server.port %>
|
<%= server.server.host %><span :if={server.server.port != 4460}>:<%= server.server.port %></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<%= status %>
|
<%= status %>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<% aead_algorithm = Enum.at(response.aead_algorithms, 0) %>
|
<% aead_algorithm = Enum.at(response.aead_algorithms, 0) %>
|
||||||
|
|
||||||
<span class="group relative">
|
<span class="group relative">
|
||||||
|
@ -52,30 +52,30 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<%= length(response.cookies) %>
|
<%= length(response.cookies) %>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<%= response.cookie_length %>
|
<%= response.cookie_length %>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<%= Map.get(response, :server, "-") %>
|
<%= Map.get(response, :server, "-") %>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<%= Map.get(response, :port, "-") %>
|
<%= Map.get(response, :port, "-") %>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<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)%>
|
<%= response.cert_expiration |> DateTime.from_iso8601 |> then(fn {:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%SZ") end)%>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<%= server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %>
|
<%= server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<%= server.server.host %>:<%= server.server.port %>
|
<%= server.server.host %><span :if={server.server.port != 4460}>:<%= server.server.port %></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<span class="group relative">
|
<span class="group relative">
|
||||||
<%= status %>
|
<%= 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">
|
<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">
|
||||||
|
@ -83,13 +83,13 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap"> - </td>
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap"> - </td>
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap"> - </td>
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap"> - </td>
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap"> - </td>
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap"> - </td>
|
<td class="py-2 px-6 whitespace-nowrap"> - </td>
|
||||||
<td class="py-4 px-6 whitespace-nowrap">
|
<td class="py-2 px-6 whitespace-nowrap">
|
||||||
<%=
|
<%=
|
||||||
server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ")
|
server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ")
|
||||||
%>
|
%>
|
||||||
|
|
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,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,37 @@ 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 -> :one end)
|
||||||
|
|> expect(:call, fn 2, :list -> :two end)
|
||||||
|
|
||||||
|
assert NTS.list_clients([%{host: "test", port: 1}, %{host: "test2", port: 2}]) == [:one, :two]
|
||||||
|
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
|
||||||
|
@ -74,7 +105,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 +115,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
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
defmodule ChronoscopeWeb.PageControllerTest do
|
defmodule ChronoscopeWeb.PageControllerTest do
|
||||||
use ChronoscopeWeb.ConnCase, async: true
|
use ChronoscopeWeb.ConnCase, async: true
|
||||||
|
|
||||||
alias Chronoscope.DynamicSupervisorMock
|
|
||||||
|
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
test "GET /", %{conn: conn} do
|
test "GET /", %{conn: conn} do
|
||||||
DynamicSupervisorMock
|
|
||||||
|> expect(:which_children, fn _ -> [] end)
|
|
||||||
|
|
||||||
conn = get(conn, ~p"/")
|
conn = get(conn, ~p"/")
|
||||||
assert html_response(conn, 200) =~ "Chronoscope"
|
assert html_response(conn, 200) =~ "Chronoscope"
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,5 +19,6 @@ 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
|
||||||
|
|
Loading…
Reference in New Issue