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)
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								lib/chronoscope/nts/parse.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								lib/chronoscope/nts/parse.ex
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
							
								
								
									
										67
									
								
								lib/chronoscope_web/client_activator.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								lib/chronoscope_web/client_activator.ex
									
									
									
									
									
										Normal file
									
								
							@ -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} ->
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								lib/chronoscope_web/live/index_live.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								lib/chronoscope_web/live/index_live.ex
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
							
								
								
									
										101
									
								
								lib/chronoscope_web/live/index_live.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								lib/chronoscope_web/live/index_live.html.heex
									
									
									
									
									
										Normal file
									
								
							@ -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
									
								
								priv/nts-test.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								priv/nts-test.txt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										9
									
								
								priv/nts.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								priv/nts.txt
									
									
									
									
									
										Normal file
									
								
							@ -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([]) == %{}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								test/chronoscope/nts/parse_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								test/chronoscope/nts/parse_test.exs
									
									
									
									
									
										Normal file
									
								
							@ -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
	
	Block a user