From 3786df57c32528216ed2306dabc811b35623aecd Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Tue, 28 May 2024 18:27:02 -0400 Subject: [PATCH 01/21] Create a test liveview page --- lib/chronoscope_web/live/index_live.ex | 21 +++++++++++++++++++ lib/chronoscope_web/live/index_live.html.heex | 6 ++++++ lib/chronoscope_web/router.ex | 6 ++++++ 3 files changed, 33 insertions(+) create mode 100644 lib/chronoscope_web/live/index_live.ex create mode 100644 lib/chronoscope_web/live/index_live.html.heex diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex new file mode 100644 index 0000000..d92d539 --- /dev/null +++ b/lib/chronoscope_web/live/index_live.ex @@ -0,0 +1,21 @@ +defmodule ChronoscopeWeb.IndexLive do + use ChronoscopeWeb, :live_view + + @topic "test" + + def mount(_params, _session, socket) do + ChronoscopeWeb.Endpoint.subscribe(@topic) + ChronoscopeWeb.Endpoint.broadcast_from(self(), @topic, "", %{temperature: 100}) + {:ok, assign(socket, :temperature, 100)} + end + + def handle_event("inc_temperature", _params, socket) do + updated = socket.assigns.temperature + 1 + ChronoscopeWeb.Endpoint.broadcast_from(self(), @topic, "", %{temperature: updated}) + {:noreply, assign(socket, :temperature, updated)} + end + + def handle_info(%{topic: @topic, payload: state}, socket) do + {:noreply, assign(socket, state)} + end +end diff --git a/lib/chronoscope_web/live/index_live.html.heex b/lib/chronoscope_web/live/index_live.html.heex new file mode 100644 index 0000000..c938938 --- /dev/null +++ b/lib/chronoscope_web/live/index_live.html.heex @@ -0,0 +1,6 @@ +
+ Live! +
+ Current temperature: <%= @temperature %>°F + +
diff --git a/lib/chronoscope_web/router.ex b/lib/chronoscope_web/router.ex index 75f0d25..a6b3e48 100644 --- a/lib/chronoscope_web/router.ex +++ b/lib/chronoscope_web/router.ex @@ -14,6 +14,12 @@ defmodule ChronoscopeWeb.Router do plug :accepts, ["json"] end + scope "/test", ChronoscopeWeb do + pipe_through :browser + + live "/", IndexLive + end + scope "/", ChronoscopeWeb do pipe_through :browser From 8b9c3445776aca88f82308b74e1f1c913883b930 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 29 May 2024 15:25:05 -0400 Subject: [PATCH 02/21] Show a table of servers --- .../components/layouts/app.html.heex | 27 ++++---- .../components/layouts/root.html.heex | 2 +- lib/chronoscope_web/live/index_live.ex | 17 ++--- lib/chronoscope_web/live/index_live.html.heex | 66 +++++++++++++++++-- lib/chronoscope_web/router.ex | 8 +-- 5 files changed, 80 insertions(+), 40 deletions(-) diff --git a/lib/chronoscope_web/components/layouts/app.html.heex b/lib/chronoscope_web/components/layouts/app.html.heex index 0997ae9..cc5a745 100644 --- a/lib/chronoscope_web/components/layouts/app.html.heex +++ b/lib/chronoscope_web/components/layouts/app.html.heex @@ -1,28 +1,25 @@
-
-
- - +
+
+ + Chronoscope -

- v<%= Application.spec(:phoenix, :vsn) %> +

+ v<%= Application.spec(:chronoscope, :vsn) %>

-
-
+
<.flash_group flash={@flash} /> <%= @inner_content %>
diff --git a/lib/chronoscope_web/components/layouts/root.html.heex b/lib/chronoscope_web/components/layouts/root.html.heex index 9032ef4..156dd6f 100644 --- a/lib/chronoscope_web/components/layouts/root.html.heex +++ b/lib/chronoscope_web/components/layouts/root.html.heex @@ -11,7 +11,7 @@ - + <%= @inner_content %> diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex index d92d539..de8ec66 100644 --- a/lib/chronoscope_web/live/index_live.ex +++ b/lib/chronoscope_web/live/index_live.ex @@ -1,21 +1,16 @@ defmodule ChronoscopeWeb.IndexLive do use ChronoscopeWeb, :live_view - @topic "test" + alias Chronoscope.NTS + + @topic "nts-servers" def mount(_params, _session, socket) do ChronoscopeWeb.Endpoint.subscribe(@topic) - ChronoscopeWeb.Endpoint.broadcast_from(self(), @topic, "", %{temperature: 100}) - {:ok, assign(socket, :temperature, 100)} + {:ok, assign(socket, %{servers: NTS.list()})} end - def handle_event("inc_temperature", _params, socket) do - updated = socket.assigns.temperature + 1 - ChronoscopeWeb.Endpoint.broadcast_from(self(), @topic, "", %{temperature: updated}) - {:noreply, assign(socket, :temperature, updated)} - end - - def handle_info(%{topic: @topic, payload: state}, socket) do - {:noreply, assign(socket, state)} + def handle_info(%{topic: @topic, payload: servers}, socket) do + {:noreply, assign(socket, %{servers: servers})} end end diff --git a/lib/chronoscope_web/live/index_live.html.heex b/lib/chronoscope_web/live/index_live.html.heex index c938938..cc7b4c5 100644 --- a/lib/chronoscope_web/live/index_live.html.heex +++ b/lib/chronoscope_web/live/index_live.html.heex @@ -1,6 +1,60 @@ -
- Live! -
- Current temperature: <%= @temperature %>°F - -
+ + + + + + + + + + + + + <% {status, response} = server.key_establishment_response %> + + <%= if (status == :ok) do %> + + + + + + + <% else %> + + + + + + <% end %> + + +
+ Host + + Status + + Algorithm + + Cookies + + Cookie Length +
+ <%= server.server.host %>:<%= server.server.port %> + + <%= status %> + + <%= Enum.at(response.aead_algorithms, 0) %> + + <%= length(response.cookies) %> + + <%= response.cookie_length %> + + <%= server.server.host %>:<%= server.server.port %> + + + <%= status %> + + <%= response %> + + + - - -
diff --git a/lib/chronoscope_web/router.ex b/lib/chronoscope_web/router.ex index a6b3e48..d6cac2d 100644 --- a/lib/chronoscope_web/router.ex +++ b/lib/chronoscope_web/router.ex @@ -14,16 +14,10 @@ defmodule ChronoscopeWeb.Router do plug :accepts, ["json"] end - scope "/test", ChronoscopeWeb do - pipe_through :browser - - live "/", IndexLive - end - scope "/", ChronoscopeWeb do pipe_through :browser - get "/", PageController, :home + live "/", IndexLive end scope "/api/v1", ChronoscopeWeb.API.V1 do From 86d3808e544c7d9035a93573843c4b5f4605c023 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 29 May 2024 16:25:27 -0400 Subject: [PATCH 03/21] Update server list dynamically --- lib/chronoscope/nts.ex | 2 ++ lib/chronoscope_web/live/index_live.ex | 4 +-- lib/chronoscope_web/live/index_live.html.heex | 27 +++++++++++++++---- test/chronoscope/gemini_test.exs | 1 - .../controllers/page_controller_test.exs | 11 +++++++- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/lib/chronoscope/nts.ex b/lib/chronoscope/nts.ex index 5866dde..09193a7 100644 --- a/lib/chronoscope/nts.ex +++ b/lib/chronoscope/nts.ex @@ -12,6 +12,7 @@ defmodule Chronoscope.NTS do @registry Application.compile_env(:chronoscope, :registry, Registry) @genserver Application.compile_env(:chronoscope, :gen_server, GenServer) @dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor) + @topic "nts-servers" def healthy?() do true @@ -37,6 +38,7 @@ defmodule Chronoscope.NTS do %{host: host, port: port} |> client_pid() |> @genserver.call(:key_establishment) + |> tap(fn _ -> ChronoscopeWeb.Endpoint.broadcast(@topic, "", "") end) end defp client_pid(server) do diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex index de8ec66..995671d 100644 --- a/lib/chronoscope_web/live/index_live.ex +++ b/lib/chronoscope_web/live/index_live.ex @@ -10,7 +10,7 @@ defmodule ChronoscopeWeb.IndexLive do {:ok, assign(socket, %{servers: NTS.list()})} end - def handle_info(%{topic: @topic, payload: servers}, socket) do - {:noreply, assign(socket, %{servers: servers})} + def handle_info(%{topic: @topic}, socket) do + {:noreply, assign(socket, %{servers: NTS.list()})} end end diff --git a/lib/chronoscope_web/live/index_live.html.heex b/lib/chronoscope_web/live/index_live.html.heex index cc7b4c5..b4f3152 100644 --- a/lib/chronoscope_web/live/index_live.html.heex +++ b/lib/chronoscope_web/live/index_live.html.heex @@ -1,21 +1,27 @@ - - - - - + + @@ -38,6 +44,12 @@ + + <% else %> + + + <% end %> diff --git a/test/chronoscope/gemini_test.exs b/test/chronoscope/gemini_test.exs index 9cb0a79..af0ac15 100644 --- a/test/chronoscope/gemini_test.exs +++ b/test/chronoscope/gemini_test.exs @@ -8,7 +8,6 @@ defmodule Chronoscope.GeminiTest do import Mox - setup :verify_on_exit! describe "Chronoscope.Gemini.healthy?()" do test "is healthy" do diff --git a/test/chronoscope_web/controllers/page_controller_test.exs b/test/chronoscope_web/controllers/page_controller_test.exs index e11c7c9..6b920f1 100644 --- a/test/chronoscope_web/controllers/page_controller_test.exs +++ b/test/chronoscope_web/controllers/page_controller_test.exs @@ -1,8 +1,17 @@ defmodule ChronoscopeWeb.PageControllerTest do use ChronoscopeWeb.ConnCase, async: true + alias Chronoscope.DynamicSupervisorMock + + import Mox + + setup :verify_on_exit! + test "GET /", %{conn: conn} do + DynamicSupervisorMock + |> expect(:which_children, fn _ -> [] end) + conn = get(conn, ~p"/") - assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + assert html_response(conn, 200) =~ "Chronoscope" end end From dc481bd6aa7668ac3db9e75b2fbdf590690d4f3e Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 29 May 2024 19:02:10 -0400 Subject: [PATCH 04/21] Allow websockets through multiple hostnames --- config/runtime.exs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/runtime.exs b/config/runtime.exs index 4f5bfd6..7f2dca0 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -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. From 4f5b886e9e0cff3c1037bf544b706aa2927a9190 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Fri, 31 May 2024 17:42:32 -0400 Subject: [PATCH 05/21] Center the main table --- assets/tailwind.config.js | 2 +- lib/chronoscope_web/components/layouts/app.html.heex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index bd5c376..01a7c57 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -14,7 +14,7 @@ module.exports = { theme: { extend: { colors: { - brand: "#FD4F00", + brand: "#DC7556", } }, }, diff --git a/lib/chronoscope_web/components/layouts/app.html.heex b/lib/chronoscope_web/components/layouts/app.html.heex index cc5a745..8223b45 100644 --- a/lib/chronoscope_web/components/layouts/app.html.heex +++ b/lib/chronoscope_web/components/layouts/app.html.heex @@ -4,7 +4,7 @@ Chronoscope -

+

v<%= Application.spec(:chronoscope, :vsn) %>

@@ -19,7 +19,7 @@
-
+
<.flash_group flash={@flash} /> <%= @inner_content %>
From 7e218dfb5da82f78b16ee978e0e3469ba9b52450 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Sat, 1 Jun 2024 09:53:32 -0400 Subject: [PATCH 06/21] Scroll table on small screen sizes --- lib/chronoscope_web/live/index_live.html.heex | 150 +++++++++--------- 1 file changed, 76 insertions(+), 74 deletions(-) diff --git a/lib/chronoscope_web/live/index_live.html.heex b/lib/chronoscope_web/live/index_live.html.heex index b4f3152..144a507 100644 --- a/lib/chronoscope_web/live/index_live.html.heex +++ b/lib/chronoscope_web/live/index_live.html.heex @@ -1,77 +1,79 @@ -
+ Host + Status + Algorithm + Cookies + Cookie Length + Certificate Expiration + + Last Check +
<%= response.cookie_length %> + <%= response.cert_expiration %> + + <%= server.last_key_establishment %> + @@ -54,6 +66,11 @@ - - - - + <%= server.last_key_establishment %> +
- - - - - - - - - - - - - - <% {status, response} = server.key_establishment_response %> +
+
- Host - - Status - - Algorithm - - Cookies - - Cookie Length - - Certificate Expiration - - Last Check -
+ + + + + + + + + + + + + + <% {status, response} = server.key_establishment_response %> - <%= if (status == :ok) do %> - - - - - - - - - <% else %> - - + - - - - + + + + + + - - <% end %> - - -
+ Host + + Status + + Algorithm + + Cookies + + Cookie Length + + Certificate Expiration + + Last Check +
- <%= server.server.host %>:<%= server.server.port %> - - <%= status %> - - <%= Enum.at(response.aead_algorithms, 0) %> - - <%= length(response.cookies) %> - - <%= response.cookie_length %> - - <%= response.cert_expiration %> - - <%= server.last_key_establishment %> - - <%= server.server.host %>:<%= server.server.port %> - - + <%= if (status == :ok) do %> + + <%= server.server.host %>:<%= server.server.port %> + <%= status %> - - <%= response %> - - - - - - - + <%= Enum.at(response.aead_algorithms, 0) %> + + <%= length(response.cookies) %> + + <%= response.cookie_length %> + + <%= response.cert_expiration %> + + <%= server.last_key_establishment %> + - <%= server.last_key_establishment %> -
+ <% else %> + + <%= server.server.host %>:<%= server.server.port %> + + + + <%= status %> + + <%= response %> + + + + - + - + - + - + + + <%= server.last_key_establishment %> + + <% end %> + + + +
From 4dee9f61dadc7f3661566755a63df43592dd70e1 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Sat, 1 Jun 2024 10:30:35 -0400 Subject: [PATCH 07/21] Simplify table values --- .../nts/key_establishment_response.ex | 10 ++++++++-- lib/chronoscope_web/live/index_live.ex | 1 + lib/chronoscope_web/live/index_live.html.heex | 16 ++++++++++++---- .../nts/key_establishment_response_test.exs | 7 +++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/chronoscope/nts/key_establishment_response.ex b/lib/chronoscope/nts/key_establishment_response.ex index 5a4bff6..5130b07 100644 --- a/lib/chronoscope/nts/key_establishment_response.ex +++ b/lib/chronoscope/nts/key_establishment_response.ex @@ -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 diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex index 995671d..0497db8 100644 --- a/lib/chronoscope_web/live/index_live.ex +++ b/lib/chronoscope_web/live/index_live.ex @@ -2,6 +2,7 @@ defmodule ChronoscopeWeb.IndexLive do use ChronoscopeWeb, :live_view alias Chronoscope.NTS + alias Chronoscope.NTS.KeyEstablishmentResponse @topic "nts-servers" diff --git a/lib/chronoscope_web/live/index_live.html.heex b/lib/chronoscope_web/live/index_live.html.heex index 144a507..319e135 100644 --- a/lib/chronoscope_web/live/index_live.html.heex +++ b/lib/chronoscope_web/live/index_live.html.heex @@ -37,7 +37,13 @@ <%= status %> - <%= Enum.at(response.aead_algorithms, 0) %> + <% aead_algorithm = Enum.at(response.aead_algorithms, 0) %> + + <%= KeyEstablishmentResponse.aead_algorithm_to_id(aead_algorithm) %> + + <%= aead_algorithm %> + + <%= length(response.cookies) %> @@ -46,10 +52,10 @@ <%= response.cookie_length %> - <%= response.cert_expiration %> + <%= response.cert_expiration |> DateTime.from_iso8601 |> then(fn {:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%SZ") end)%> - <%= server.last_key_establishment %> + <%= server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %> <% else %> @@ -70,7 +76,9 @@ - - <%= server.last_key_establishment %> + <%= + server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") + %> <% end %> diff --git a/test/chronoscope/nts/key_establishment_response_test.exs b/test/chronoscope/nts/key_establishment_response_test.exs index 03b2a3f..53f392a 100644 --- a/test/chronoscope/nts/key_establishment_response_test.exs +++ b/test/chronoscope/nts/key_establishment_response_test.exs @@ -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([]) == %{} From 7226742a6b5e7ef84feb2847717210ec8777debc Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Sat, 1 Jun 2024 10:47:12 -0400 Subject: [PATCH 08/21] Add NTP information to table --- lib/chronoscope_web/live/index_live.html.heex | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/chronoscope_web/live/index_live.html.heex b/lib/chronoscope_web/live/index_live.html.heex index 319e135..c7c5e58 100644 --- a/lib/chronoscope_web/live/index_live.html.heex +++ b/lib/chronoscope_web/live/index_live.html.heex @@ -3,7 +3,7 @@ - Host + NTS-KE Server Status @@ -17,6 +17,12 @@ Cookie Length + + NTP Host + + + NTP Port + Certificate Expiration @@ -38,6 +44,7 @@ <% aead_algorithm = Enum.at(response.aead_algorithms, 0) %> + <%= KeyEstablishmentResponse.aead_algorithm_to_id(aead_algorithm) %> @@ -51,6 +58,12 @@ <%= response.cookie_length %> + + <%= Map.get(response, :server, "-") %> + + + <%= Map.get(response, :port, "-") %> + <%= response.cert_expiration |> DateTime.from_iso8601 |> then(fn {:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%SZ") end)%> @@ -74,7 +87,8 @@ - - - - + - + - <%= server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") From 5109722a667048f431059ee07af8aed53b263721 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Tue, 4 Jun 2024 20:03:02 -0400 Subject: [PATCH 09/21] Show a list of NTS servers --- .gitignore | 3 + config/config.exs | 3 + config/test.exs | 2 + lib/chronoscope/application.ex | 3 +- lib/chronoscope/gemini.ex | 8 ++- lib/chronoscope/gemini/client.ex | 3 +- lib/chronoscope/monitor.ex | 40 +++++++++++++ lib/chronoscope/nts.ex | 26 +++++++-- lib/chronoscope/nts/client.ex | 36 +++++++++++- .../nts/key_establishment_client.ex | 2 +- lib/chronoscope/nts/parse.ex | 10 ++++ .../components/layouts/app.html.heex | 2 +- lib/chronoscope_web/live/index_live.ex | 12 +++- lib/chronoscope_web/live/index_live.html.heex | 58 +++++++++---------- mix.exs | 2 +- test/chronoscope/nts/parse_test.exs | 15 +++++ test/chronoscope/nts_test.exs | 35 ++++++++++- .../controllers/page_controller_test.exs | 5 -- test/priv/nts.txt | 0 test/support/behaviours.ex | 1 + 20 files changed, 213 insertions(+), 53 deletions(-) create mode 100644 lib/chronoscope/monitor.ex create mode 100644 lib/chronoscope/nts/parse.ex create mode 100644 test/chronoscope/nts/parse_test.exs create mode 100644 test/priv/nts.txt diff --git a/.gitignore b/.gitignore index c1b92e1..ca8fdd9 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ npm-debug.log # Ignore IDE files. .elixir_ls/ + +# Ignore server lists. +/priv/nts.txt diff --git a/config/config.exs b/config/config.exs index f4905e5..a76f30a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -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", diff --git a/config/test.exs b/config/test.exs index e445d94..79eae7f 100644 --- a/config/test.exs +++ b/config/test.exs @@ -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, "test/priv/nts.txt" + # Disable swoosh api client as it is only required for production adapters. config :swoosh, :api_client, false diff --git a/lib/chronoscope/application.ex b/lib/chronoscope/application.ex index 770f140..f297711 100644 --- a/lib/chronoscope/application.ex +++ b/lib/chronoscope/application.ex @@ -22,7 +22,8 @@ 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, + Chronoscope.Monitor ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/chronoscope/gemini.ex b/lib/chronoscope/gemini.ex index abbd14f..e858a06 100644 --- a/lib/chronoscope/gemini.ex +++ b/lib/chronoscope/gemini.ex @@ -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 diff --git a/lib/chronoscope/gemini/client.ex b/lib/chronoscope/gemini/client.ex index 13fd46c..bcfe11e 100644 --- a/lib/chronoscope/gemini/client.ex +++ b/lib/chronoscope/gemini/client.ex @@ -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 diff --git a/lib/chronoscope/monitor.ex b/lib/chronoscope/monitor.ex new file mode 100644 index 0000000..9ed2392 --- /dev/null +++ b/lib/chronoscope/monitor.ex @@ -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 diff --git a/lib/chronoscope/nts.ex b/lib/chronoscope/nts.ex index 09193a7..4e4995d 100644 --- a/lib/chronoscope/nts.ex +++ b/lib/chronoscope/nts.ex @@ -9,10 +9,11 @@ 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) - @topic "nts-servers" def healthy?() do true @@ -24,11 +25,29 @@ defmodule Chronoscope.NTS do |> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) 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 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 @@ -37,8 +56,7 @@ defmodule Chronoscope.NTS do def key_establishment(host, port) do %{host: host, port: port} |> client_pid() - |> @genserver.call(:key_establishment) - |> tap(fn _ -> ChronoscopeWeb.Endpoint.broadcast(@topic, "", "") end) + |> @genserver.call(:key_establishment, @timeout_in_milliseconds) end defp client_pid(server) do diff --git a/lib/chronoscope/nts/client.ex b/lib/chronoscope/nts/client.ex index d5b3a84..2500e6e 100644 --- a/lib/chronoscope/nts/client.ex +++ b/lib/chronoscope/nts/client.ex @@ -5,7 +5,10 @@ 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) def start_link(server: server, name: name) do @@ -34,6 +37,28 @@ 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 + {: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 def handle_call(:key_establishment, _from, state) do new_state = update_state(state) @@ -41,11 +66,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(fn _ -> ChronoscopeWeb.Endpoint.broadcast(@topic, "", "") end) else state end @@ -61,7 +93,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 diff --git a/lib/chronoscope/nts/key_establishment_client.ex b/lib/chronoscope/nts/key_establishment_client.ex index 102a62d..1ab989e 100644 --- a/lib/chronoscope/nts/key_establishment_client.ex +++ b/lib/chronoscope/nts/key_establishment_client.ex @@ -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) diff --git a/lib/chronoscope/nts/parse.ex b/lib/chronoscope/nts/parse.ex new file mode 100644 index 0000000..d4bb794 --- /dev/null +++ b/lib/chronoscope/nts/parse.ex @@ -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 diff --git a/lib/chronoscope_web/components/layouts/app.html.heex b/lib/chronoscope_web/components/layouts/app.html.heex index 8223b45..eda1e69 100644 --- a/lib/chronoscope_web/components/layouts/app.html.heex +++ b/lib/chronoscope_web/components/layouts/app.html.heex @@ -1,4 +1,4 @@ -
+
diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex index 0497db8..b385b06 100644 --- a/lib/chronoscope_web/live/index_live.ex +++ b/lib/chronoscope_web/live/index_live.ex @@ -4,14 +4,20 @@ defmodule ChronoscopeWeb.IndexLive do alias Chronoscope.NTS alias Chronoscope.NTS.KeyEstablishmentResponse - @topic "nts-servers" + @topic Application.compile_env(:chronoscope, :nts_topic) def mount(_params, _session, socket) do ChronoscopeWeb.Endpoint.subscribe(@topic) - {:ok, assign(socket, %{servers: NTS.list()})} + {:ok, assign(socket, %{servers: server_list()})} end 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 diff --git a/lib/chronoscope_web/live/index_live.html.heex b/lib/chronoscope_web/live/index_live.html.heex index c7c5e58..9ca85be 100644 --- a/lib/chronoscope_web/live/index_live.html.heex +++ b/lib/chronoscope_web/live/index_live.html.heex @@ -2,31 +2,31 @@ - - - - - - - - - @@ -36,13 +36,13 @@ <% {status, response} = server.key_establishment_response %> <%= if (status == :ok) do %> - - - - - - - - - <% else %> - - - - - - - - - + + + + + + - - <% {status, response} = server.key_establishment_response %> + + <% {status, response} = client.key_establishment_response %> <%= if (status == :ok) do %> <% else %> <% end %> From 92251c4084f308b0bc0f78ceefb66b8604849487 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 5 Jun 2024 13:45:01 -0400 Subject: [PATCH 12/21] Only look for listed nts servers in live view --- lib/chronoscope/application.ex | 1 + lib/chronoscope/view_activator.ex | 1 + .../api/v1/gemini/connection_controller.ex | 10 ++++-- .../v1/nts/key_establishment_controller.ex | 8 +++-- lib/chronoscope_web/live/index_live.ex | 17 ++++++--- .../v1/gemini/connection_controller_test.exs | 36 +++++++++++++++++++ .../nts/key_establishment_controller_test.exs | 30 ++++++++++++++++ 7 files changed, 93 insertions(+), 10 deletions(-) diff --git a/lib/chronoscope/application.ex b/lib/chronoscope/application.ex index 1f35d3d..b984f28 100644 --- a/lib/chronoscope/application.ex +++ b/lib/chronoscope/application.ex @@ -23,6 +23,7 @@ defmodule Chronoscope.Application do {Registry, [keys: :unique, name: Chronoscope.Gemini.Registry]}, # Start to serve requests, typically the last entry ChronoscopeWeb.Endpoint, + # Initialize clients for the main LiveView Chronoscope.ViewActivator ] diff --git a/lib/chronoscope/view_activator.ex b/lib/chronoscope/view_activator.ex index 54b7fb5..5ca763a 100644 --- a/lib/chronoscope/view_activator.ex +++ b/lib/chronoscope/view_activator.ex @@ -33,6 +33,7 @@ defmodule Chronoscope.ViewActivator do @nts_file |> File.stream!() |> Stream.map(&String.trim/1) + |> Stream.filter(&(&1 != "")) |> Stream.map(&Parse.parse_nts_server/1) |> Enum.to_list() |> tap(fn server -> Enum.each(server, &NTS.auto_refresh/1) end) diff --git a/lib/chronoscope_web/controllers/api/v1/gemini/connection_controller.ex b/lib/chronoscope_web/controllers/api/v1/gemini/connection_controller.ex index 39d8861..208b4b0 100644 --- a/lib/chronoscope_web/controllers/api/v1/gemini/connection_controller.ex +++ b/lib/chronoscope_web/controllers/api/v1/gemini/connection_controller.ex @@ -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} -> diff --git a/lib/chronoscope_web/controllers/api/v1/nts/key_establishment_controller.ex b/lib/chronoscope_web/controllers/api/v1/nts/key_establishment_controller.ex index 7fa4944..3c5cc5c 100644 --- a/lib/chronoscope_web/controllers/api/v1/nts/key_establishment_controller.ex +++ b/lib/chronoscope_web/controllers/api/v1/nts/key_establishment_controller.ex @@ -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} -> diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex index 1b72ddb..7e7fb8d 100644 --- a/lib/chronoscope_web/live/index_live.ex +++ b/lib/chronoscope_web/live/index_live.ex @@ -3,26 +3,33 @@ defmodule ChronoscopeWeb.IndexLive do alias Chronoscope.NTS alias Chronoscope.NTS.KeyEstablishmentResponse + alias Chronoscope.ViewActivator @topic Application.compile_env(:chronoscope, :nts_topic) def mount(_params, _session, socket) do ChronoscopeWeb.Endpoint.subscribe(@topic) - {:ok, assign(socket, %{clients: client_list()})} + {:ok, assign(socket, %{servers: server_list(), clients: client_list()})} end def handle_info(%{topic: @topic, event: "key-exchange", payload: client}, socket) do - {:noreply, update(socket, :clients, &update_client(&1, client))} + if client.server in socket.assigns.servers do + {:noreply, update(socket, :clients, &update_client(&1, client))} + else + {:noreply, socket} + end end defp update_client(client_list, client) do - # todo - use a map instead of list for fast lookups? Enum.map(client_list, &if(client.server == &1.server, do: client, else: &1)) end + defp server_list() do + GenServer.call(ViewActivator, :get_nts_servers) + end + defp client_list() do - Chronoscope.ViewActivator - |> GenServer.call(:get_nts_servers) + server_list() |> NTS.list_clients() end end diff --git a/test/chronoscope_web/controllers/api/v1/gemini/connection_controller_test.exs b/test/chronoscope_web/controllers/api/v1/gemini/connection_controller_test.exs index e932087..c0a00d3 100644 --- a/test/chronoscope_web/controllers/api/v1/gemini/connection_controller_test.exs +++ b/test/chronoscope_web/controllers/api/v1/gemini/connection_controller_test.exs @@ -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) diff --git a/test/chronoscope_web/controllers/api/v1/nts/key_establishment_controller_test.exs b/test/chronoscope_web/controllers/api/v1/nts/key_establishment_controller_test.exs index 0c88fc6..a360003 100644 --- a/test/chronoscope_web/controllers/api/v1/nts/key_establishment_controller_test.exs +++ b/test/chronoscope_web/controllers/api/v1/nts/key_establishment_controller_test.exs @@ -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) From 2690449fa972dd2cceab6b7b72cde75436fcee47 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 5 Jun 2024 14:21:26 -0400 Subject: [PATCH 13/21] Fix deployment --- .gitignore | 3 --- config/test.exs | 2 +- lib/chronoscope/application.ex | 4 ++-- .../client_activator.ex} | 7 +++---- lib/chronoscope_web/live/index_live.ex | 4 ++-- test/priv/nts.txt => priv/nts-test.txt | 0 priv/nts.txt | 7 +++++++ 7 files changed, 15 insertions(+), 12 deletions(-) rename lib/{chronoscope/view_activator.ex => chronoscope_web/client_activator.ex} (84%) rename test/priv/nts.txt => priv/nts-test.txt (100%) create mode 100644 priv/nts.txt diff --git a/.gitignore b/.gitignore index ca8fdd9..c1b92e1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,3 @@ npm-debug.log # Ignore IDE files. .elixir_ls/ - -# Ignore server lists. -/priv/nts.txt diff --git a/config/test.exs b/config/test.exs index 79eae7f..f997ecd 100644 --- a/config/test.exs +++ b/config/test.exs @@ -10,7 +10,7 @@ config :chronoscope, ChronoscopeWeb.Endpoint, # In test we don't send emails. config :chronoscope, Chronoscope.Mailer, adapter: Swoosh.Adapters.Test -config :chronoscope, :nts_file, "test/priv/nts.txt" +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 diff --git a/lib/chronoscope/application.ex b/lib/chronoscope/application.ex index b984f28..2ed3a3a 100644 --- a/lib/chronoscope/application.ex +++ b/lib/chronoscope/application.ex @@ -23,8 +23,8 @@ defmodule Chronoscope.Application do {Registry, [keys: :unique, name: Chronoscope.Gemini.Registry]}, # Start to serve requests, typically the last entry ChronoscopeWeb.Endpoint, - # Initialize clients for the main LiveView - Chronoscope.ViewActivator + # Initialize clients used in the main LiveView + ChronoscopeWeb.ClientActivator ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/chronoscope/view_activator.ex b/lib/chronoscope_web/client_activator.ex similarity index 84% rename from lib/chronoscope/view_activator.ex rename to lib/chronoscope_web/client_activator.ex index 5ca763a..8ea4ae8 100644 --- a/lib/chronoscope/view_activator.ex +++ b/lib/chronoscope_web/client_activator.ex @@ -1,4 +1,4 @@ -defmodule Chronoscope.ViewActivator do +defmodule ChronoscopeWeb.ClientActivator do use GenServer require Logger @@ -14,8 +14,7 @@ defmodule Chronoscope.ViewActivator do @impl true def init(_) do - File.touch(@nts_file) - activate_nts_clients() + File.touch(Application.app_dir(:chronoscope, @nts_file)) {:ok, %{nts_servers: activate_nts_clients()}} end @@ -30,7 +29,7 @@ defmodule Chronoscope.ViewActivator do end defp activate_nts_clients() do - @nts_file + Application.app_dir(:chronoscope, @nts_file) |> File.stream!() |> Stream.map(&String.trim/1) |> Stream.filter(&(&1 != "")) diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex index 7e7fb8d..a6b43e5 100644 --- a/lib/chronoscope_web/live/index_live.ex +++ b/lib/chronoscope_web/live/index_live.ex @@ -3,7 +3,7 @@ defmodule ChronoscopeWeb.IndexLive do alias Chronoscope.NTS alias Chronoscope.NTS.KeyEstablishmentResponse - alias Chronoscope.ViewActivator + alias ChronoscopeWeb.ClientActivator @topic Application.compile_env(:chronoscope, :nts_topic) @@ -25,7 +25,7 @@ defmodule ChronoscopeWeb.IndexLive do end defp server_list() do - GenServer.call(ViewActivator, :get_nts_servers) + GenServer.call(ClientActivator, :get_nts_servers) end defp client_list() do diff --git a/test/priv/nts.txt b/priv/nts-test.txt similarity index 100% rename from test/priv/nts.txt rename to priv/nts-test.txt diff --git a/priv/nts.txt b/priv/nts.txt new file mode 100644 index 0000000..45016b4 --- /dev/null +++ b/priv/nts.txt @@ -0,0 +1,7 @@ +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 From 3c3d078c4d08c38e63ba8565c8cc0e8f5967811a Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 5 Jun 2024 16:40:38 -0400 Subject: [PATCH 14/21] Handle intermittent failures --- lib/chronoscope/nts/key_establishment_client.ex | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/chronoscope/nts/key_establishment_client.ex b/lib/chronoscope/nts/key_establishment_client.ex index 1ab989e..8391e06 100644 --- a/lib/chronoscope/nts/key_establishment_client.ex +++ b/lib/chronoscope/nts/key_establishment_client.ex @@ -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) - - peercert - |> await_response() - |> tap(fn _ -> @ssl.close(socket) end) + 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 From 13dd4002d0397792f8e5975aa8b49fba5159a963 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 5 Jun 2024 17:01:57 -0400 Subject: [PATCH 15/21] Re-activate clients after a failure --- lib/chronoscope/nts/client.ex | 2 ++ lib/chronoscope_web/client_activator.ex | 29 ++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/chronoscope/nts/client.ex b/lib/chronoscope/nts/client.ex index b39fec9..0acb101 100644 --- a/lib/chronoscope/nts/client.ex +++ b/lib/chronoscope/nts/client.ex @@ -44,7 +44,9 @@ defmodule Chronoscope.NTS.Client do @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 diff --git a/lib/chronoscope_web/client_activator.ex b/lib/chronoscope_web/client_activator.ex index 8ea4ae8..381bee1 100644 --- a/lib/chronoscope_web/client_activator.ex +++ b/lib/chronoscope_web/client_activator.ex @@ -6,6 +6,8 @@ defmodule ChronoscopeWeb.ClientActivator do alias Chronoscope.NTS alias Chronoscope.NTS.Parse + @activate_interval_in_milliseconds 300_000 + @nts_file Application.compile_env(:chronoscope, :nts_file) def start_link(_) do @@ -14,13 +16,29 @@ defmodule ChronoscopeWeb.ClientActivator do @impl true def init(_) do - File.touch(Application.app_dir(:chronoscope, @nts_file)) - {:ok, %{nts_servers: activate_nts_clients()}} + nts_servers = + nts_servers() + |> tap(fn servers -> Enum.each(servers, &NTS.auto_refresh/1) end) + + :timer.send_interval(@activate_interval_in_milliseconds, :ensure_activated) + + {:ok, %{nts_servers: nts_servers}} + end + + @impl true + def handle_info(:ensure_activated, state) do + Enum.each(state.nts_servers, &NTS.auto_refresh/1) + + {:noreply, state} end @impl true def handle_info(:activate_clients, _state) do - {:noreply, %{nts_servers: activate_nts_clients()}} + nts_servers = + nts_servers() + |> tap(fn servers -> Enum.each(servers, &NTS.auto_refresh/1) end) + + {:noreply, %{nts_servers: nts_servers}} end @impl true @@ -28,13 +46,14 @@ defmodule ChronoscopeWeb.ClientActivator do {:reply, state.nts_servers, state} end - defp activate_nts_clients() do + 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() - |> tap(fn server -> Enum.each(server, &NTS.auto_refresh/1) end) end end From 7b91ca224515357953ef221c4e8c9884fb5a4c04 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Thu, 6 Jun 2024 10:56:23 -0400 Subject: [PATCH 16/21] Re-activate clients if they are restarted --- lib/chronoscope/nts.ex | 8 ++++-- lib/chronoscope/nts/client.ex | 9 ++++-- lib/chronoscope_web/client_activator.ex | 38 +++++++++++++++---------- lib/chronoscope_web/live/index_live.ex | 13 +++++++-- priv/nts.txt | 2 ++ 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/lib/chronoscope/nts.ex b/lib/chronoscope/nts.ex index 4e4995d..e9a1686 100644 --- a/lib/chronoscope/nts.ex +++ b/lib/chronoscope/nts.ex @@ -25,12 +25,16 @@ defmodule Chronoscope.NTS do |> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end) end - def list_clients(clients) do - clients + 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() diff --git a/lib/chronoscope/nts/client.ex b/lib/chronoscope/nts/client.ex index 0acb101..bd4ba3a 100644 --- a/lib/chronoscope/nts/client.ex +++ b/lib/chronoscope/nts/client.ex @@ -3,6 +3,7 @@ defmodule Chronoscope.NTS.Client do alias Chronoscope.NTS alias Chronoscope.NTS.KeyEstablishmentClient + alias ChronoscopeWeb.Endpoint @interval_in_seconds 30 @timeout_in_milliseconds 10_000 @@ -17,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 @@ -53,6 +54,8 @@ defmodule Chronoscope.NTS.Client do @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 @@ -79,7 +82,7 @@ defmodule Chronoscope.NTS.Client do if interval_surpassed?(now, state.last_key_establishment) do state |> Map.merge(current_data(state, now)) - |> tap(&ChronoscopeWeb.Endpoint.broadcast(@topic, "key-exchange", Map.delete(&1, :timer))) + |> tap(&Endpoint.broadcast(@topic, "key-exchange", Map.delete(&1, :timer))) else state end diff --git a/lib/chronoscope_web/client_activator.ex b/lib/chronoscope_web/client_activator.ex index 381bee1..32ee207 100644 --- a/lib/chronoscope_web/client_activator.ex +++ b/lib/chronoscope_web/client_activator.ex @@ -5,9 +5,9 @@ defmodule ChronoscopeWeb.ClientActivator do alias Chronoscope.NTS alias Chronoscope.NTS.Parse + alias ChronoscopeWeb.Endpoint - @activate_interval_in_milliseconds 300_000 - + @topic Application.compile_env(:chronoscope, :nts_topic) @nts_file Application.compile_env(:chronoscope, :nts_file) def start_link(_) do @@ -16,29 +16,33 @@ defmodule ChronoscopeWeb.ClientActivator do @impl true def init(_) do - nts_servers = - nts_servers() - |> tap(fn servers -> Enum.each(servers, &NTS.auto_refresh/1) end) + Endpoint.subscribe(@topic) - :timer.send_interval(@activate_interval_in_milliseconds, :ensure_activated) - - {:ok, %{nts_servers: nts_servers}} + {:ok, %{nts_servers: nts_servers() |> tap(&start_clients/1)}} end @impl true - def handle_info(:ensure_activated, state) do - Enum.each(state.nts_servers, &NTS.auto_refresh/1) + 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(:activate_clients, _state) do - nts_servers = - nts_servers() - |> tap(fn servers -> Enum.each(servers, &NTS.auto_refresh/1) end) + 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, %{nts_servers: nts_servers}} + {:noreply, state} + end + + @impl true + def handle_info(_, state) do + {:noreply, state} end @impl true @@ -46,6 +50,10 @@ defmodule ChronoscopeWeb.ClientActivator 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)) diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex index a6b43e5..74e4f7d 100644 --- a/lib/chronoscope_web/live/index_live.ex +++ b/lib/chronoscope_web/live/index_live.ex @@ -4,14 +4,17 @@ defmodule ChronoscopeWeb.IndexLive do 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 - ChronoscopeWeb.Endpoint.subscribe(@topic) + Endpoint.subscribe(@topic) {:ok, assign(socket, %{servers: server_list(), clients: client_list()})} end + @impl true def handle_info(%{topic: @topic, event: "key-exchange", payload: client}, socket) do if client.server in socket.assigns.servers do {:noreply, update(socket, :clients, &update_client(&1, client))} @@ -20,6 +23,11 @@ defmodule ChronoscopeWeb.IndexLive do 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 @@ -29,7 +37,6 @@ defmodule ChronoscopeWeb.IndexLive do end defp client_list() do - server_list() - |> NTS.list_clients() + server_list() |> NTS.list_clients() end end diff --git a/priv/nts.txt b/priv/nts.txt index 45016b4..562c952 100644 --- a/priv/nts.txt +++ b/priv/nts.txt @@ -5,3 +5,5 @@ paris.time.system76.com oregon.time.system76.com ohio.time.system76.com brazil.time.system76.com +time.cloudflare.com +nts.netnod.se From fbd29d394636242ce549cb50a18c6a920b5d3ce8 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Thu, 4 Jul 2024 11:09:16 -0400 Subject: [PATCH 17/21] Add tests --- test/chronoscope/nts_test.exs | 99 +++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/test/chronoscope/nts_test.exs b/test/chronoscope/nts_test.exs index 8924b64..29ae414 100644 --- a/test/chronoscope/nts_test.exs +++ b/test/chronoscope/nts_test.exs @@ -67,6 +67,105 @@ defmodule Chronoscope.NTSTest do 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 From 79340221eb722d617c3e0da2060c0983f982eb30 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Sat, 6 Jul 2024 11:46:24 -0400 Subject: [PATCH 18/21] Add tests --- test/chronoscope/nts/client_test.exs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/chronoscope/nts/client_test.exs b/test/chronoscope/nts/client_test.exs index f092704..b478248 100644 --- a/test/chronoscope/nts/client_test.exs +++ b/test/chronoscope/nts/client_test.exs @@ -38,6 +38,23 @@ 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 + 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}, From bb05e5fad8f806d41351247724f3f08370c3587f Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Sat, 6 Jul 2024 17:08:49 -0400 Subject: [PATCH 19/21] Format code --- test/chronoscope/gemini/client_test.exs | 2 +- test/chronoscope/gemini_test.exs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/chronoscope/gemini/client_test.exs b/test/chronoscope/gemini/client_test.exs index 76b373f..a728f62 100644 --- a/test/chronoscope/gemini/client_test.exs +++ b/test/chronoscope/gemini/client_test.exs @@ -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) diff --git a/test/chronoscope/gemini_test.exs b/test/chronoscope/gemini_test.exs index e508201..cb94b4f 100644 --- a/test/chronoscope/gemini_test.exs +++ b/test/chronoscope/gemini_test.exs @@ -8,7 +8,6 @@ defmodule Chronoscope.GeminiTest do import Mox - describe "Chronoscope.Gemini.healthy?()" do test "is healthy" do assert Gemini.healthy?() == true From 548ef87bc48c2cd60baa205525288a0a859d94a6 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Fri, 26 Jul 2024 09:29:42 -0400 Subject: [PATCH 20/21] Add tests --- config/test.exs | 3 ++- lib/chronoscope/nts/client.ex | 8 ++++---- lib/chronoscope_web/live/index_live.ex | 2 +- test/chronoscope/nts/client_test.exs | 10 ++++++++++ test/support/behaviours.ex | 4 ++++ test/support/mocks.ex | 1 + 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/config/test.exs b/config/test.exs index f997ecd..1f482af 100644 --- a/config/test.exs +++ b/config/test.exs @@ -28,4 +28,5 @@ config :chronoscope, registry: Chronoscope.RegistryMock, gen_server: Chronoscope.GenServerMock, nts: Chronoscope.NTSMock, - gemini: Chronoscope.GeminiMock + gemini: Chronoscope.GeminiMock, + endpoint: Chronoscope.EndpointMock diff --git a/lib/chronoscope/nts/client.ex b/lib/chronoscope/nts/client.ex index bd4ba3a..a1083c3 100644 --- a/lib/chronoscope/nts/client.ex +++ b/lib/chronoscope/nts/client.ex @@ -3,7 +3,6 @@ defmodule Chronoscope.NTS.Client do alias Chronoscope.NTS alias Chronoscope.NTS.KeyEstablishmentClient - alias ChronoscopeWeb.Endpoint @interval_in_seconds 30 @timeout_in_milliseconds 10_000 @@ -11,6 +10,7 @@ defmodule Chronoscope.NTS.Client do @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) @@ -18,7 +18,7 @@ defmodule Chronoscope.NTS.Client do @impl true def init(server) do - Endpoint.broadcast(@topic, "initializing", server) + @endpoint.broadcast(@topic, "initializing", server) {:ok, %{ @@ -54,7 +54,7 @@ defmodule Chronoscope.NTS.Client do @impl true def handle_call(:cancel_auto_refresh, _from, %{timer: timer} = state) do :timer.cancel(timer) - Endpoint.broadcast(@topic, "cancel-auto-refresh", state.server) + @endpoint.broadcast(@topic, "cancel-auto-refresh", state.server) {:reply, :ok, Map.delete(state, :timer)} end @@ -82,7 +82,7 @@ defmodule Chronoscope.NTS.Client do if interval_surpassed?(now, state.last_key_establishment) do state |> Map.merge(current_data(state, now)) - |> tap(&Endpoint.broadcast(@topic, "key-exchange", Map.delete(&1, :timer))) + |> tap(&@endpoint.broadcast(@topic, "key-establishment", Map.delete(&1, :timer))) else state end diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex index 74e4f7d..685b16d 100644 --- a/lib/chronoscope_web/live/index_live.ex +++ b/lib/chronoscope_web/live/index_live.ex @@ -15,7 +15,7 @@ defmodule ChronoscopeWeb.IndexLive do end @impl true - def handle_info(%{topic: @topic, event: "key-exchange", payload: client}, socket) do + 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 diff --git a/test/chronoscope/nts/client_test.exs b/test/chronoscope/nts/client_test.exs index b478248..94a870c 100644 --- a/test/chronoscope/nts/client_test.exs +++ b/test/chronoscope/nts/client_test.exs @@ -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, %{ @@ -48,6 +52,9 @@ defmodule Chronoscope.NTS.ClientTest do 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 @@ -79,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}, diff --git a/test/support/behaviours.ex b/test/support/behaviours.ex index 4856f5a..b3e1e65 100644 --- a/test/support/behaviours.ex +++ b/test/support/behaviours.ex @@ -22,3 +22,7 @@ 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 diff --git a/test/support/mocks.ex b/test/support/mocks.ex index dcc3552..8a56fdf 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -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) From 860603ee722c3c0370affe637b63d7d7e89beee4 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Fri, 26 Jul 2024 09:33:01 -0400 Subject: [PATCH 21/21] Remove add server button --- lib/chronoscope_web/components/layouts/app.html.heex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/chronoscope_web/components/layouts/app.html.heex b/lib/chronoscope_web/components/layouts/app.html.heex index eda1e69..bc49530 100644 --- a/lib/chronoscope_web/components/layouts/app.html.heex +++ b/lib/chronoscope_web/components/layouts/app.html.heex @@ -12,9 +12,6 @@ Codeberg - - Add a Server -
+ NTS-KE Server + Status + Algorithm + Cookies + Cookie Length + NTP Host + NTP Port + Certificate Expiration + Last Check
- <%= server.server.host %>:<%= server.server.port %> + + <%= server.server.host %>:<%= server.server.port %> + <%= status %> + <% aead_algorithm = Enum.at(response.aead_algorithms, 0) %> @@ -52,30 +52,30 @@ + <%= length(response.cookies) %> + <%= response.cookie_length %> + <%= Map.get(response, :server, "-") %> + <%= Map.get(response, :port, "-") %> + <%= response.cert_expiration |> DateTime.from_iso8601 |> then(fn {:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%SZ") end)%> + <%= server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %> - <%= server.server.host %>:<%= server.server.port %> + + <%= server.server.host %>:<%= server.server.port %> + <%= status %> @@ -83,13 +83,13 @@ - - - - - - + - - - - - - <%= server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %> diff --git a/mix.exs b/mix.exs index 99b1d72..ac249e9 100644 --- a/mix.exs +++ b/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, diff --git a/test/chronoscope/nts/parse_test.exs b/test/chronoscope/nts/parse_test.exs new file mode 100644 index 0000000..bfcfaf2 --- /dev/null +++ b/test/chronoscope/nts/parse_test.exs @@ -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 diff --git a/test/chronoscope/nts_test.exs b/test/chronoscope/nts_test.exs index 67c75b7..a5bb6aa 100644 --- a/test/chronoscope/nts_test.exs +++ b/test/chronoscope/nts_test.exs @@ -36,6 +36,37 @@ 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 -> :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 test "does nothing if the client doesn't exist" do RegistryMock @@ -74,7 +105,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 +115,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 diff --git a/test/chronoscope_web/controllers/page_controller_test.exs b/test/chronoscope_web/controllers/page_controller_test.exs index 6b920f1..ad4c0cf 100644 --- a/test/chronoscope_web/controllers/page_controller_test.exs +++ b/test/chronoscope_web/controllers/page_controller_test.exs @@ -1,16 +1,11 @@ defmodule ChronoscopeWeb.PageControllerTest do use ChronoscopeWeb.ConnCase, async: true - alias Chronoscope.DynamicSupervisorMock - import Mox setup :verify_on_exit! test "GET /", %{conn: conn} do - DynamicSupervisorMock - |> expect(:which_children, fn _ -> [] end) - conn = get(conn, ~p"/") assert html_response(conn, 200) =~ "Chronoscope" end diff --git a/test/priv/nts.txt b/test/priv/nts.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/support/behaviours.ex b/test/support/behaviours.ex index b5003f6..4856f5a 100644 --- a/test/support/behaviours.ex +++ b/test/support/behaviours.ex @@ -19,5 +19,6 @@ defmodule Chronoscope.DynamicSupervisor.Behaviour do end defmodule Chronoscope.GenServer.Behaviour do + @callback call(pid(), any(), any()) :: any() @callback call(pid(), any()) :: any() end From 1efc6419853a395ed7317449bcb30e5c993c7198 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 5 Jun 2024 09:00:28 -0400 Subject: [PATCH 10/21] Fix tests --- test/chronoscope/gemini_test.exs | 10 +++++----- test/chronoscope/nts/key_establishment_client_test.exs | 2 +- test/chronoscope/nts_test.exs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/chronoscope/gemini_test.exs b/test/chronoscope/gemini_test.exs index af0ac15..e508201 100644 --- a/test/chronoscope/gemini_test.exs +++ b/test/chronoscope/gemini_test.exs @@ -28,8 +28,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 @@ -48,7 +48,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 @@ -73,7 +73,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 @@ -83,7 +83,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 diff --git a/test/chronoscope/nts/key_establishment_client_test.exs b/test/chronoscope/nts/key_establishment_client_test.exs index 4a5758f..ae9b64c 100644 --- a/test/chronoscope/nts/key_establishment_client_test.exs +++ b/test/chronoscope/nts/key_establishment_client_test.exs @@ -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 diff --git a/test/chronoscope/nts_test.exs b/test/chronoscope/nts_test.exs index a5bb6aa..8924b64 100644 --- a/test/chronoscope/nts_test.exs +++ b/test/chronoscope/nts_test.exs @@ -60,8 +60,8 @@ defmodule Chronoscope.NTSTest do ) GenServerMock - |> expect(:call, fn 1, :list -> :one end) - |> expect(:call, fn 2, :list -> :two end) + |> 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 @@ -80,7 +80,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 From af1f893db5f09f69fa437f2741f3a8b8a666c450 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 5 Jun 2024 12:23:25 -0400 Subject: [PATCH 11/21] Update individual clients in the live view --- lib/chronoscope/application.ex | 2 +- lib/chronoscope/nts/client.ex | 2 +- lib/chronoscope/{monitor.ex => view_activator.ex} | 2 +- lib/chronoscope_web/live/index_live.ex | 15 ++++++++++----- lib/chronoscope_web/live/index_live.html.heex | 12 ++++++------ 5 files changed, 19 insertions(+), 14 deletions(-) rename lib/chronoscope/{monitor.ex => view_activator.ex} (95%) diff --git a/lib/chronoscope/application.ex b/lib/chronoscope/application.ex index f297711..1f35d3d 100644 --- a/lib/chronoscope/application.ex +++ b/lib/chronoscope/application.ex @@ -23,7 +23,7 @@ defmodule Chronoscope.Application do {Registry, [keys: :unique, name: Chronoscope.Gemini.Registry]}, # Start to serve requests, typically the last entry ChronoscopeWeb.Endpoint, - Chronoscope.Monitor + Chronoscope.ViewActivator ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/chronoscope/nts/client.ex b/lib/chronoscope/nts/client.ex index 2500e6e..b39fec9 100644 --- a/lib/chronoscope/nts/client.ex +++ b/lib/chronoscope/nts/client.ex @@ -77,7 +77,7 @@ defmodule Chronoscope.NTS.Client do if interval_surpassed?(now, state.last_key_establishment) do state |> Map.merge(current_data(state, now)) - |> tap(fn _ -> ChronoscopeWeb.Endpoint.broadcast(@topic, "", "") end) + |> tap(&ChronoscopeWeb.Endpoint.broadcast(@topic, "key-exchange", Map.delete(&1, :timer))) else state end diff --git a/lib/chronoscope/monitor.ex b/lib/chronoscope/view_activator.ex similarity index 95% rename from lib/chronoscope/monitor.ex rename to lib/chronoscope/view_activator.ex index 9ed2392..54b7fb5 100644 --- a/lib/chronoscope/monitor.ex +++ b/lib/chronoscope/view_activator.ex @@ -1,4 +1,4 @@ -defmodule Chronoscope.Monitor do +defmodule Chronoscope.ViewActivator do use GenServer require Logger diff --git a/lib/chronoscope_web/live/index_live.ex b/lib/chronoscope_web/live/index_live.ex index b385b06..1b72ddb 100644 --- a/lib/chronoscope_web/live/index_live.ex +++ b/lib/chronoscope_web/live/index_live.ex @@ -8,15 +8,20 @@ defmodule ChronoscopeWeb.IndexLive do def mount(_params, _session, socket) do ChronoscopeWeb.Endpoint.subscribe(@topic) - {:ok, assign(socket, %{servers: server_list()})} + {:ok, assign(socket, %{clients: client_list()})} end - def handle_info(%{topic: @topic}, socket) do - {:noreply, assign(socket, %{servers: server_list()})} + def handle_info(%{topic: @topic, event: "key-exchange", payload: client}, socket) do + {:noreply, update(socket, :clients, &update_client(&1, client))} end - defp server_list() do - Chronoscope.Monitor + defp update_client(client_list, client) do + # todo - use a map instead of list for fast lookups? + Enum.map(client_list, &if(client.server == &1.server, do: client, else: &1)) + end + + defp client_list() do + Chronoscope.ViewActivator |> GenServer.call(:get_nts_servers) |> NTS.list_clients() end diff --git a/lib/chronoscope_web/live/index_live.html.heex b/lib/chronoscope_web/live/index_live.html.heex index 9ca85be..de4f785 100644 --- a/lib/chronoscope_web/live/index_live.html.heex +++ b/lib/chronoscope_web/live/index_live.html.heex @@ -32,12 +32,12 @@
- <%= server.server.host %>:<%= server.server.port %> + <%= client.server.host %>:<%= client.server.port %> <%= status %> @@ -68,12 +68,12 @@ <%= response.cert_expiration |> DateTime.from_iso8601 |> then(fn {:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%SZ") end)%> - <%= server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %> + <%= client.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %> - <%= server.server.host %>:<%= server.server.port %> + <%= client.server.host %>:<%= client.server.port %> @@ -91,7 +91,7 @@ - <%= - server.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") + client.last_key_establishment |> Calendar.strftime("%Y-%m-%d %H:%M:%SZ") %>