Show a dynamically updated list of NTS servers #1
|
@ -14,7 +14,7 @@ module.exports = {
|
|||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#FD4F00",
|
||||
brand: "#DC7556",
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -30,6 +30,9 @@ config :chronoscope, ChronoscopeWeb.Endpoint,
|
|||
# at the `config/runtime.exs`.
|
||||
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)
|
||||
config :esbuild,
|
||||
version: "0.17.11",
|
||||
|
|
|
@ -33,13 +33,15 @@ if config_env() == :prod do
|
|||
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")
|
||||
|
||||
config :chronoscope, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
||||
|
||||
config :chronoscope, ChronoscopeWeb.Endpoint,
|
||||
url: [host: host, port: 443, scheme: "https"],
|
||||
check_origin: ["https://" <> host, "https://" <> aux_host],
|
||||
http: [
|
||||
# Enable IPv6 and bind on all interfaces.
|
||||
# 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.
|
||||
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.
|
||||
config :swoosh, :api_client, false
|
||||
|
||||
|
@ -26,4 +28,5 @@ config :chronoscope,
|
|||
registry: Chronoscope.RegistryMock,
|
||||
gen_server: Chronoscope.GenServerMock,
|
||||
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},
|
||||
{Registry, [keys: :unique, name: Chronoscope.Gemini.Registry]},
|
||||
# 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
|
||||
|
|
|
@ -9,6 +9,8 @@ defmodule Chronoscope.Gemini do
|
|||
|
||||
alias Chronoscope.Gemini
|
||||
|
||||
@timeout_in_milliseconds 10_000
|
||||
|
||||
@registry Application.compile_env(:chronoscope, :registry, Registry)
|
||||
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
|
||||
@dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor)
|
||||
|
@ -20,14 +22,14 @@ defmodule Chronoscope.Gemini do
|
|||
def list() do
|
||||
Gemini.DynamicSupervisor
|
||||
|> @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
|
||||
|
||||
def remove(host, port, path) do
|
||||
name = client_name(%{host: host, port: port, path: path})
|
||||
|
||||
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}
|
||||
end
|
||||
end
|
||||
|
@ -36,7 +38,7 @@ defmodule Chronoscope.Gemini do
|
|||
def connect(host, port, path) do
|
||||
%{host: host, port: port, path: path}
|
||||
|> client_pid()
|
||||
|> @genserver.call(:connect)
|
||||
|> @genserver.call(:connect, @timeout_in_milliseconds)
|
||||
end
|
||||
|
||||
defp client_pid(resource) do
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule Chronoscope.Gemini.Client do
|
|||
alias Chronoscope.Gemini.ConnectionClient
|
||||
|
||||
@interval_in_seconds 30
|
||||
@timeout_in_milliseconds 10_000
|
||||
|
||||
@date_time Application.compile_env(:chronoscope, :date_time, DateTime)
|
||||
|
||||
|
@ -61,7 +62,7 @@ defmodule Chronoscope.Gemini.Client do
|
|||
defp server_response(%{resource: resource}) do
|
||||
Gemini.TaskSupervisor
|
||||
|> Task.Supervisor.async(fn -> ConnectionClient.connect(resource) end)
|
||||
|> Task.await()
|
||||
|> Task.await(@timeout_in_milliseconds)
|
||||
end
|
||||
|
||||
defp interval_surpassed?(now, last_request) do
|
||||
|
|
|
@ -9,6 +9,8 @@ defmodule Chronoscope.NTS do
|
|||
|
||||
alias Chronoscope.NTS
|
||||
|
||||
@timeout_in_milliseconds 10_000
|
||||
|
||||
@registry Application.compile_env(:chronoscope, :registry, Registry)
|
||||
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
|
||||
@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)
|
||||
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
|
||||
name = client_name(%{host: host, port: port})
|
||||
|
||||
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}
|
||||
end
|
||||
end
|
||||
|
@ -36,7 +60,7 @@ defmodule Chronoscope.NTS do
|
|||
def key_establishment(host, port) do
|
||||
%{host: host, port: port}
|
||||
|> client_pid()
|
||||
|> @genserver.call(:key_establishment)
|
||||
|> @genserver.call(:key_establishment, @timeout_in_milliseconds)
|
||||
end
|
||||
|
||||
defp client_pid(server) do
|
||||
|
|
|
@ -5,8 +5,12 @@ defmodule Chronoscope.NTS.Client do
|
|||
alias Chronoscope.NTS.KeyEstablishmentClient
|
||||
|
||||
@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)
|
||||
@endpoint Application.compile_env(:chronoscope, :endpoint, ChronoscopeWeb.Endpoint)
|
||||
|
||||
def start_link(server: server, name: name) do
|
||||
GenServer.start_link(__MODULE__, server, name: name)
|
||||
|
@ -14,13 +18,13 @@ defmodule Chronoscope.NTS.Client do
|
|||
|
||||
@impl true
|
||||
def init(server) do
|
||||
now = utc_now()
|
||||
@endpoint.broadcast(@topic, "initializing", server)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
server: server,
|
||||
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
|
||||
|
||||
|
@ -34,6 +38,32 @@ defmodule Chronoscope.NTS.Client do
|
|||
{:reply, state, state}
|
||||
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
|
||||
def handle_call(:key_establishment, _from, state) do
|
||||
new_state = update_state(state)
|
||||
|
@ -41,11 +71,18 @@ defmodule Chronoscope.NTS.Client do
|
|||
{:reply, new_state.key_establishment_response, new_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:key_establishment, state) do
|
||||
{:noreply, update_state(state)}
|
||||
end
|
||||
|
||||
defp update_state(state) do
|
||||
now = utc_now()
|
||||
|
||||
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
|
||||
state
|
||||
end
|
||||
|
@ -61,7 +98,7 @@ defmodule Chronoscope.NTS.Client do
|
|||
defp server_response(%{server: server}) do
|
||||
NTS.TaskSupervisor
|
||||
|> Task.Supervisor.async(fn -> KeyEstablishmentClient.key_establishment(server) end)
|
||||
|> Task.await()
|
||||
|> Task.await(@timeout_in_milliseconds)
|
||||
end
|
||||
|
||||
defp interval_surpassed?(now, last_key_establishment) do
|
||||
|
|
|
@ -5,7 +5,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClient do
|
|||
alias Chronoscope.NTS.KeyEstablishmentRequest
|
||||
alias Chronoscope.NTS.KeyEstablishmentResponse
|
||||
|
||||
@timeout_in_milliseconds 3000
|
||||
@timeout_in_milliseconds 3500
|
||||
|
||||
@ssl Application.compile_env(:chronoscope, :ssl, :ssl)
|
||||
|
||||
|
@ -33,12 +33,16 @@ defmodule Chronoscope.NTS.KeyEstablishmentClient do
|
|||
end
|
||||
|
||||
defp perform_key_establishment(socket) do
|
||||
:ok = @ssl.send(socket, KeyEstablishmentRequest.create())
|
||||
{:ok, peercert} = @ssl.peercert(socket)
|
||||
|
||||
with :ok <- @ssl.send(socket, KeyEstablishmentRequest.create()),
|
||||
{:ok, peercert} <- @ssl.peercert(socket) do
|
||||
peercert
|
||||
|> await_response()
|
||||
|> tap(fn _ -> @ssl.close(socket) end)
|
||||
else
|
||||
e ->
|
||||
@ssl.close(socket)
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
defp await_response(peercert) do
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Chronoscope.NTS.KeyEstablishmentResponse do
|
||||
import Bitwise
|
||||
|
||||
@aead_alogorithms %{
|
||||
@aead_algorithms %{
|
||||
15 => "AEAD_AES_SIV_CMAC_256",
|
||||
30 => "AEAD_AES_128_GCM_SIV"
|
||||
}
|
||||
|
@ -16,6 +16,12 @@ defmodule Chronoscope.NTS.KeyEstablishmentResponse do
|
|||
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
|
||||
parse_response(response, %{})
|
||||
end
|
||||
|
@ -127,7 +133,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentResponse do
|
|||
end
|
||||
|
||||
defp do_parse_aead_algorithm_list([high, low | rest], acc) do
|
||||
@aead_alogorithms
|
||||
@aead_algorithms
|
||||
|> Map.get(combine_octets(high, low), "UNKNOWN")
|
||||
|> then(&do_parse_aead_algorithm_list(rest, [&1 | acc]))
|
||||
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">
|
||||
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/">
|
||||
<img src={~p"/images/logo.svg"} width="36" />
|
||||
<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 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">
|
||||
Chronoscope
|
||||
</a>
|
||||
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
|
||||
v<%= Application.spec(:phoenix, :vsn) %>
|
||||
<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(:chronoscope, :vsn) %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
|
||||
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
|
||||
@elixirphoenix
|
||||
</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>
|
||||
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900 dark:text-zinc-300">
|
||||
<a href="https://codeberg.org/mike-cifelli" class="hover:text-zinc-700 dark:hover:text-zinc-400">
|
||||
Codeberg
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<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} />
|
||||
<%= @inner_content %>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||
</script>
|
||||
</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 %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -13,7 +13,7 @@ defmodule ChronoscopeWeb.API.V1.Gemini.ConnectionController do
|
|||
|
||||
def get(conn, %{"host" => host, "port" => port, "path" => path}) 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
|
||||
ArgumentError -> bad_request_response(conn, "invalid port")
|
||||
end
|
||||
|
@ -25,20 +25,24 @@ defmodule ChronoscopeWeb.API.V1.Gemini.ConnectionController do
|
|||
|
||||
def get(conn, %{"host" => host, "port" => port}) 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
|
||||
ArgumentError -> bad_request_response(conn, "invalid port")
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def get(conn, _params) do
|
||||
bad_request_response(conn, "missing host")
|
||||
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
|
||||
case connect(host, port, path) do
|
||||
{:ok, response} ->
|
||||
|
|
|
@ -12,20 +12,24 @@ defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentController do
|
|||
|
||||
def get(conn, %{"host" => host, "port" => port}) 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
|
||||
ArgumentError -> bad_request_response(conn, "invalid port")
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def get(conn, _params) do
|
||||
bad_request_response(conn, "missing host")
|
||||
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
|
||||
case key_establishment_response(host, port) do
|
||||
{: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
|
||||
pipe_through :browser
|
||||
|
||||
get "/", PageController, :home
|
||||
live "/", IndexLive
|
||||
end
|
||||
|
||||
scope "/api/v1", ChronoscopeWeb.API.V1 do
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -4,7 +4,7 @@ defmodule Chronoscope.MixProject do
|
|||
def project do
|
||||
[
|
||||
app: :chronoscope,
|
||||
version: "0.1.0",
|
||||
version: "1.0.0",
|
||||
elixir: "~> 1.16",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
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
|
||||
|
||||
test ":connect - not cached" do
|
||||
response ="20 text/gemini\r\nHello!" |> to_charlist()
|
||||
response = "20 text/gemini\r\nHello!" |> to_charlist()
|
||||
peercert = peercert()
|
||||
peercert_expiration = Certificate.expiration_date(peercert)
|
||||
|
||||
|
|
|
@ -8,8 +8,6 @@ defmodule Chronoscope.GeminiTest do
|
|||
|
||||
import Mox
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "Chronoscope.Gemini.healthy?()" do
|
||||
test "is healthy" do
|
||||
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)
|
||||
|
||||
GenServerMock
|
||||
|> expect(:call, fn 2, :list -> :one end)
|
||||
|> expect(:call, fn 6, :list -> :two end)
|
||||
|> expect(:call, fn 2, :list, 10_000 -> :one end)
|
||||
|> expect(:call, fn 6, :list, 10_000 -> :two end)
|
||||
|
||||
assert Gemini.list() == [:one, :two]
|
||||
end
|
||||
|
@ -49,7 +47,7 @@ defmodule Chronoscope.GeminiTest do
|
|||
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
||||
|
||||
GenServerMock
|
||||
|> expect(:call, fn 1, :terminate -> :terminating end)
|
||||
|> expect(:call, fn 1, :terminate, 10_000 -> :terminating end)
|
||||
|
||||
assert Gemini.remove("localhost", 1965, "/") == {:ok, :terminating}
|
||||
end
|
||||
|
@ -74,7 +72,7 @@ defmodule Chronoscope.GeminiTest do
|
|||
)
|
||||
|
||||
GenServerMock
|
||||
|> expect(:call, fn 1, :connect -> :result end)
|
||||
|> expect(:call, fn 1, :connect, 10_000 -> :result end)
|
||||
|
||||
assert Gemini.connect("localhost", 1965, "/") == :result
|
||||
end
|
||||
|
@ -84,7 +82,7 @@ defmodule Chronoscope.GeminiTest do
|
|||
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
||||
|
||||
GenServerMock
|
||||
|> expect(:call, fn 1, :connect -> :result end)
|
||||
|> expect(:call, fn 1, :connect, 10_000 -> :result end)
|
||||
|
||||
assert Gemini.connect("localhost", 1965, "/") == :result
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule Chronoscope.NTS.ClientTest do
|
|||
|
||||
alias Chronoscope.Certificate
|
||||
alias Chronoscope.DateTimeMock
|
||||
alias Chronoscope.EndpointMock
|
||||
alias Chronoscope.NTS.Client
|
||||
alias Chronoscope.SSLMock
|
||||
|
||||
|
@ -19,6 +20,9 @@ defmodule Chronoscope.NTS.ClientTest do
|
|||
|
||||
describe "Chronoscope.NTS.Client.init()" 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}) ==
|
||||
{:ok,
|
||||
%{
|
||||
|
@ -38,6 +42,26 @@ defmodule Chronoscope.NTS.ClientTest do
|
|||
assert Client.handle_call(:list, nil, %{state: true}) == {:reply, %{state: true}, %{state: true}}
|
||||
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
|
||||
assert Client.handle_call(:key_establishment, nil, %{
|
||||
server: %{host: "localhost", port: 3333},
|
||||
|
@ -62,6 +86,9 @@ defmodule Chronoscope.NTS.ClientTest do
|
|||
|> expect(:peercert, fn :socket -> {:ok, peercert} end)
|
||||
|> expect(:close, fn :socket -> :ok end)
|
||||
|
||||
EndpointMock
|
||||
|> expect(:broadcast, fn "nts-servers", "key-establishment", _ -> :ok end)
|
||||
|
||||
assert {:reply, {:ok, %{cert_expiration: ^peercert_expiration}},
|
||||
%{
|
||||
server: %{host: "localhost", port: 3333},
|
||||
|
|
|
@ -10,7 +10,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
|||
|
||||
setup :verify_on_exit!
|
||||
|
||||
@timeout 3000
|
||||
@timeout 3500
|
||||
|
||||
describe "Chronoscope.NTS.KeyEstablishmentClient.key_establishment()" do
|
||||
test "sends the correct TLS options" do
|
||||
|
|
|
@ -3,6 +3,13 @@ defmodule Chronoscope.NTS.KeyEstablishmentResponseTest do
|
|||
|
||||
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
|
||||
test "handles empty response" do
|
||||
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
|
||||
|
||||
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
|
||||
test "does nothing if the client doesn't exist" do
|
||||
RegistryMock
|
||||
|
@ -49,7 +179,7 @@ defmodule Chronoscope.NTSTest do
|
|||
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
||||
|
||||
GenServerMock
|
||||
|> expect(:call, fn 1, :terminate -> :terminating end)
|
||||
|> expect(:call, fn 1, :terminate, 10_000 -> :terminating end)
|
||||
|
||||
assert NTS.remove("localhost", 1111) == {:ok, :terminating}
|
||||
end
|
||||
|
@ -74,7 +204,7 @@ defmodule Chronoscope.NTSTest do
|
|||
)
|
||||
|
||||
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
|
||||
end
|
||||
|
@ -84,7 +214,7 @@ defmodule Chronoscope.NTSTest do
|
|||
|> expect(:lookup, fn _, _ -> [{1, 2}] end)
|
||||
|
||||
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
|
||||
end
|
||||
|
|
|
@ -17,6 +17,42 @@ defmodule ChronoscopeWeb.API.V1.Gemini.ConnectionControllerTest do
|
|||
assert %{"error" => "missing host"} == response
|
||||
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
|
||||
GeminiMock
|
||||
|> 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
|
||||
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
|
||||
NTSMock
|
||||
|> expect(:key_establishment, fn "localhost", 4460 -> {:ok, %{status: :ok}} end)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
defmodule ChronoscopeWeb.PageControllerTest do
|
||||
use ChronoscopeWeb.ConnCase, async: true
|
||||
|
||||
import Mox
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
test "GET /", %{conn: conn} do
|
||||
conn = get(conn, ~p"/")
|
||||
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
|
||||
assert html_response(conn, 200) =~ "Chronoscope"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,5 +19,10 @@ defmodule Chronoscope.DynamicSupervisor.Behaviour do
|
|||
end
|
||||
|
||||
defmodule Chronoscope.GenServer.Behaviour do
|
||||
@callback call(pid(), any(), any()) :: any()
|
||||
@callback call(pid(), any()) :: any()
|
||||
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.NTSMock, for: Chronoscope.NTS.Behaviour)
|
||||
Mox.defmock(Chronoscope.GeminiMock, for: Chronoscope.Gemini.Behaviour)
|
||||
Mox.defmock(Chronoscope.EndpointMock, for: ChronoscopeWeb.Endpoint.Behaviour)
|
||||
|
|
Loading…
Reference in New Issue