Add gemini client
This commit is contained in:
parent
3e017e6c78
commit
6622913d52
|
@ -19,10 +19,11 @@ config :logger, level: :warning
|
||||||
# Initialize plugs at runtime for faster test compilation
|
# Initialize plugs at runtime for faster test compilation
|
||||||
config :phoenix, :plug_init_mode, :runtime
|
config :phoenix, :plug_init_mode, :runtime
|
||||||
|
|
||||||
config :chronoscope, Chronoscope.NTS,
|
config :chronoscope,
|
||||||
behaviour: Chronoscope.NTS.BehaviourMock,
|
date_time: Chronoscope.DateTimeMock,
|
||||||
date_time: Chronoscope.NTS.DateTimeMock,
|
ssl: Chronoscope.SSLMock,
|
||||||
ssl: Chronoscope.NTS.SSLMock,
|
dynamic_supervisor: Chronoscope.DynamicSupervisorMock,
|
||||||
registry: Chronoscope.NTS.RegistryMock,
|
registry: Chronoscope.RegistryMock,
|
||||||
dynamic_supervisor: Chronoscope.NTS.DynamicSupervisorMock,
|
gen_server: Chronoscope.GenServerMock,
|
||||||
gen_server: Chronoscope.NTS.GenServerMock
|
nts: Chronoscope.NTSMock,
|
||||||
|
gemini: Chronoscope.GeminiMock
|
||||||
|
|
|
@ -18,6 +18,9 @@ defmodule Chronoscope.Application do
|
||||||
{DynamicSupervisor, name: Chronoscope.NTS.DynamicSupervisor, strategy: :one_for_one},
|
{DynamicSupervisor, name: Chronoscope.NTS.DynamicSupervisor, strategy: :one_for_one},
|
||||||
{Task.Supervisor, name: Chronoscope.NTS.TaskSupervisor},
|
{Task.Supervisor, name: Chronoscope.NTS.TaskSupervisor},
|
||||||
{Registry, [keys: :unique, name: Chronoscope.NTS.Registry]},
|
{Registry, [keys: :unique, name: Chronoscope.NTS.Registry]},
|
||||||
|
{DynamicSupervisor, name: Chronoscope.Gemini.DynamicSupervisor, strategy: :one_for_one},
|
||||||
|
{Task.Supervisor, name: Chronoscope.Gemini.TaskSupervisor},
|
||||||
|
{Registry, [keys: :unique, name: Chronoscope.Gemini.Registry]},
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry
|
||||||
ChronoscopeWeb.Endpoint
|
ChronoscopeWeb.Endpoint
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
defmodule Chronoscope.NTS.Certificate do
|
defmodule Chronoscope.Certificate do
|
||||||
@date_time Application.compile_env(:chronoscope, Chronoscope.NTS)[:date_time] || DateTime
|
@date_time Application.compile_env(:chronoscope, :date_time, DateTime)
|
||||||
|
|
||||||
def expiration_date(certificate) do
|
def expiration_date(certificate) do
|
||||||
{:Validity, _, {:utcTime, expiration}} =
|
case(
|
||||||
certificate
|
certificate
|
||||||
|> X509.Certificate.from_der!()
|
|> X509.Certificate.from_der!()
|
||||||
|> X509.Certificate.validity()
|
|> X509.Certificate.validity()
|
||||||
|
) do
|
||||||
|
{:Validity, _, {:utcTime, expiration}} ->
|
||||||
|
cert_time_to_iso8601(expiration)
|
||||||
|
|
||||||
cert_time_to_iso8601(expiration)
|
{:Validity, {:utcTime, expiration}, _} ->
|
||||||
|
cert_time_to_iso8601(expiration)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def cert_time_to_iso8601(cert_time) do
|
def cert_time_to_iso8601(cert_time) do
|
|
@ -1,2 +1,60 @@
|
||||||
defmodule Chronoscope.Gemini do
|
defmodule Chronoscope.Gemini.Behaviour do
|
||||||
|
@callback(connect(host :: String.t(), port :: integer(), path :: String.t()) :: {:ok, Map.t()}, {:error, any()})
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Chronoscope.Gemini do
|
||||||
|
@behaviour Chronoscope.Gemini.Behaviour
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Chronoscope.Gemini
|
||||||
|
|
||||||
|
@registry Application.compile_env(:chronoscope, :registry, Registry)
|
||||||
|
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
|
||||||
|
@dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor)
|
||||||
|
|
||||||
|
def healthy?() do
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def list() do
|
||||||
|
Gemini.DynamicSupervisor
|
||||||
|
|> @dynamic_supervisor.which_children()
|
||||||
|
|> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove(host, port) do
|
||||||
|
name = client_name(%{host: host, port: port})
|
||||||
|
|
||||||
|
case @registry.lookup(Gemini.Registry, name) do
|
||||||
|
[{pid, _}] -> {:ok, @genserver.call(pid, :terminate)}
|
||||||
|
[] -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def connect(host, port, path) do
|
||||||
|
%{host: host, port: port, path: path}
|
||||||
|
|> client_pid()
|
||||||
|
|> @genserver.call(:connect)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp client_pid(resource) do
|
||||||
|
name = client_name(resource)
|
||||||
|
|
||||||
|
case @registry.lookup(Gemini.Registry, name) do
|
||||||
|
[{pid, _}] -> pid
|
||||||
|
[] -> start_client(resource, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp client_name(%{host: host, port: port, path: path}) do
|
||||||
|
"#{host}:#{port}#{path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp start_client(resource, name) do
|
||||||
|
Gemini.DynamicSupervisor
|
||||||
|
|> @dynamic_supervisor.start_child({Gemini.Client, resource: resource, name: {:via, @registry, {Gemini.Registry, name}}})
|
||||||
|
|> then(fn {:ok, pid} -> pid end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
defmodule Chronoscope.Gemini.Client do
|
||||||
|
use GenServer, restart: :transient
|
||||||
|
|
||||||
|
alias Chronoscope.Gemini
|
||||||
|
alias Chronoscope.Gemini.ConnectionClient
|
||||||
|
|
||||||
|
@interval_in_seconds 30
|
||||||
|
@date_time Application.compile_env(:chronoscope, :date_time, DateTime)
|
||||||
|
|
||||||
|
def start_link(resource: resource, name: name) do
|
||||||
|
GenServer.start_link(__MODULE__, resource, name: name)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(resource) do
|
||||||
|
now = utc_now()
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
resource: resource,
|
||||||
|
reponse: {:error, "initializing"},
|
||||||
|
last_request: DateTime.add(now, -@interval_in_seconds, :second)
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:terminate, _from, state) do
|
||||||
|
{:stop, :normal, self(), state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:list, _from, state) do
|
||||||
|
{:reply, state, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:connect, _from, state) do
|
||||||
|
new_state = update_state(state)
|
||||||
|
|
||||||
|
{:reply, new_state.response, new_state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_state(state) do
|
||||||
|
now = utc_now()
|
||||||
|
|
||||||
|
if interval_surpassed?(now, state.last_request) do
|
||||||
|
Map.merge(state, current_data(state, now))
|
||||||
|
else
|
||||||
|
state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp current_data(state, now) do
|
||||||
|
%{
|
||||||
|
response: server_response(state),
|
||||||
|
last_request: now
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp server_response(%{resource: resource}) do
|
||||||
|
Gemini.TaskSupervisor
|
||||||
|
|> Task.Supervisor.async(fn -> ConnectionClient.connect(resource) end)
|
||||||
|
|> Task.await()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp interval_surpassed?(now, last_request) do
|
||||||
|
DateTime.diff(now, last_request, :second) >= @interval_in_seconds
|
||||||
|
end
|
||||||
|
|
||||||
|
defp utc_now() do
|
||||||
|
@date_time.utc_now()
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,60 @@
|
||||||
|
defmodule Chronoscope.Gemini.ConnectionClient do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Chronoscope.Certificate
|
||||||
|
alias Chronoscope.Gemini.Request
|
||||||
|
alias Chronoscope.Gemini.Response
|
||||||
|
|
||||||
|
@timeout_in_milliseconds 3000
|
||||||
|
@ssl Application.compile_env(:chronoscope, :ssl, :ssl)
|
||||||
|
|
||||||
|
def connect(%{host: host, port: port, path: _} = resource) do
|
||||||
|
case ssl_connect(host, port) do
|
||||||
|
{:ok, socket} -> make_request(socket, resource)
|
||||||
|
{:error, {:tls_alert, {:handshake_failure, error}}} -> {:error, String.trim("#{error}")}
|
||||||
|
{:error, {:tls_alert, {:no_application_protocol, error}}} -> {:error, String.trim("#{error}")}
|
||||||
|
{:error, :timeout} -> {:error, :timeout}
|
||||||
|
{:error, error} -> {:error, inspect(error)}
|
||||||
|
error -> {:error, inspect(error)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ssl_connect(host, port) do
|
||||||
|
host
|
||||||
|
|> String.to_charlist()
|
||||||
|
|> @ssl.connect(port, tls_options(host), @timeout_in_milliseconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp tls_options(_host) do
|
||||||
|
# TODO
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp make_request(socket, url) do
|
||||||
|
:ok = @ssl.send(socket, Request.create(url))
|
||||||
|
{:ok, peercert} = @ssl.peercert(socket)
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:ssl, _socket, response} ->
|
||||||
|
@ssl.close(socket)
|
||||||
|
parse_response(response, peercert)
|
||||||
|
|
||||||
|
msg ->
|
||||||
|
@ssl.close(socket)
|
||||||
|
Logger.error("received unexpected message: #{inspect(msg)}")
|
||||||
|
{:error, :no_response}
|
||||||
|
after
|
||||||
|
@timeout_in_milliseconds ->
|
||||||
|
@ssl.close(socket)
|
||||||
|
Logger.error("timed out waiting for response")
|
||||||
|
{:error, :timeout}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_response(response, peercert) do
|
||||||
|
response
|
||||||
|
|> Response.parse()
|
||||||
|
|> Map.put(:cert_expiration, Certificate.expiration_date(peercert))
|
||||||
|
|> then(&{:ok, &1})
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
defmodule Chronoscope.Gemini.Request do
|
||||||
|
def create(resource) do
|
||||||
|
"gemini://#{resource.host}:#{resource.port}#{resource.path}\r\n"
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
defmodule Chronoscope.Gemini.Response do
|
||||||
|
def parse(response) do
|
||||||
|
# TODO
|
||||||
|
%{status: 20, type: "text/gemini", body: response}
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,9 +9,9 @@ defmodule Chronoscope.NTS do
|
||||||
|
|
||||||
alias Chronoscope.NTS
|
alias Chronoscope.NTS
|
||||||
|
|
||||||
@registry Application.compile_env(:chronoscope, Chronoscope.NTS)[:registry] || Registry
|
@registry Application.compile_env(:chronoscope, :registry, Registry)
|
||||||
@genserver Application.compile_env(:chronoscope, Chronoscope.NTS)[:gen_server] || GenServer
|
@genserver Application.compile_env(:chronoscope, :gen_server, GenServer)
|
||||||
@dynamic_supervisor Application.compile_env(:chronoscope, Chronoscope.NTS)[:dynamic_supervisor] || DynamicSupervisor
|
@dynamic_supervisor Application.compile_env(:chronoscope, :dynamic_supervisor, DynamicSupervisor)
|
||||||
|
|
||||||
def healthy?() do
|
def healthy?() do
|
||||||
true
|
true
|
||||||
|
|
|
@ -5,20 +5,19 @@ defmodule Chronoscope.NTS.Client do
|
||||||
alias Chronoscope.NTS.KeyEstablishmentClient
|
alias Chronoscope.NTS.KeyEstablishmentClient
|
||||||
|
|
||||||
@interval_in_seconds 30
|
@interval_in_seconds 30
|
||||||
@date_time Application.compile_env(:chronoscope, Chronoscope.NTS)[:date_time] || DateTime
|
@date_time Application.compile_env(:chronoscope, :date_time, DateTime)
|
||||||
|
|
||||||
def start_link(server: server, name: name) do
|
def start_link(server: server, name: name) do
|
||||||
GenServer.start_link(__MODULE__, server, name: name)
|
GenServer.start_link(__MODULE__, server, name: name)
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(%{host: host, port: port}) do
|
def init(server) do
|
||||||
now = utc_now()
|
now = utc_now()
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
host: host,
|
server: server,
|
||||||
port: port,
|
|
||||||
key_establishment_response: {:error, "initializing"},
|
key_establishment_response: {:error, "initializing"},
|
||||||
last_key_establishment: DateTime.add(now, -@interval_in_seconds, :second)
|
last_key_establishment: DateTime.add(now, -@interval_in_seconds, :second)
|
||||||
}}
|
}}
|
||||||
|
@ -58,9 +57,7 @@ defmodule Chronoscope.NTS.Client do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp server_response(state) do
|
defp server_response(%{server: server}) do
|
||||||
server = Map.take(state, [:host, :port])
|
|
||||||
|
|
||||||
NTS.TaskSupervisor
|
NTS.TaskSupervisor
|
||||||
|> Task.Supervisor.async(fn -> KeyEstablishmentClient.key_establishment(server) end)
|
|> Task.Supervisor.async(fn -> KeyEstablishmentClient.key_establishment(server) end)
|
||||||
|> Task.await()
|
|> Task.await()
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
defmodule Chronoscope.NTS.KeyEstablishmentClient do
|
defmodule Chronoscope.NTS.KeyEstablishmentClient do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias Chronoscope.NTS.Certificate
|
alias Chronoscope.Certificate
|
||||||
alias Chronoscope.NTS.KeyEstablishmentRequest
|
alias Chronoscope.NTS.KeyEstablishmentRequest
|
||||||
alias Chronoscope.NTS.KeyEstablishmentResponse
|
alias Chronoscope.NTS.KeyEstablishmentResponse
|
||||||
|
|
||||||
@timeout_in_milliseconds 3000
|
@timeout_in_milliseconds 3000
|
||||||
@ssl Application.compile_env(:chronoscope, Chronoscope.NTS)[:ssl] || :ssl
|
@ssl Application.compile_env(:chronoscope, :ssl, :ssl)
|
||||||
|
|
||||||
def key_establishment(%{host: host, port: port}) do
|
def key_establishment(%{host: host, port: port}) do
|
||||||
case ssl_connect(host, port) do
|
case ssl_connect(host, port) do
|
||||||
|
|
|
@ -7,7 +7,7 @@ defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentController do
|
||||||
|
|
||||||
@default_port 4460
|
@default_port 4460
|
||||||
@max_host_length 255
|
@max_host_length 255
|
||||||
@nts Application.compile_env(:chronoscope, NTS)[:behaviour] || NTS
|
@nts Application.compile_env(:chronoscope, :nts, NTS)
|
||||||
|
|
||||||
def get(conn, %{"host" => host, "port" => port}) do
|
def get(conn, %{"host" => host, "port" => port}) do
|
||||||
try do
|
try do
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
defmodule Chronoscope.NTS.CertificateTest do
|
defmodule Chronoscope.CertificateTest do
|
||||||
use Chronoscope.Case, async: true
|
use Chronoscope.Case, async: true
|
||||||
|
|
||||||
alias Chronoscope.NTS.DateTimeMock
|
alias Chronoscope.Certificate
|
||||||
|
alias Chronoscope.DateTimeMock
|
||||||
|
|
||||||
import Chronoscope.NTS.Certificate
|
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
describe "Chronoscope.NTS.Certificate.expiration_date()" do
|
describe "Chronoscope.Certificate.expiration_date()" do
|
||||||
test "parses the expiration date of a certificate" do
|
test "parses the expiration date of a certificate" do
|
||||||
{:ok, expiration, _} =
|
{:ok, expiration, _} =
|
||||||
:secp256r1
|
:secp256r1
|
||||||
|> X509.PrivateKey.new_ec()
|
|> X509.PrivateKey.new_ec()
|
||||||
|> X509.Certificate.self_signed("/C=US/ST=CA/L=San Francisco/O=Acme/CN=Test", validity: 12)
|
|> X509.Certificate.self_signed("/C=US/ST=CA/L=San Francisco/O=Acme/CN=Test", validity: 12)
|
||||||
|> X509.Certificate.to_der()
|
|> X509.Certificate.to_der()
|
||||||
|> expiration_date()
|
|> Certificate.expiration_date()
|
||||||
|> DateTime.from_iso8601()
|
|> DateTime.from_iso8601()
|
||||||
|
|
||||||
expiration_time_in_minutes = DateTime.diff(expiration, DateTime.utc_now(), :minute)
|
expiration_time_in_minutes = DateTime.diff(expiration, DateTime.utc_now(), :minute)
|
||||||
|
@ -25,26 +25,26 @@ defmodule Chronoscope.NTS.CertificateTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Chronoscope.NTS.Certificate.cert_time_to_iso8601()" do
|
describe "Chronoscope.Certificate.cert_time_to_iso8601()" do
|
||||||
test "converts certificate datetime to iso8601" do
|
test "converts certificate datetime to iso8601" do
|
||||||
DateTimeMock
|
DateTimeMock
|
||||||
|> expect(:utc_now, fn -> ~U[2024-03-31 01:23:45Z] end)
|
|> expect(:utc_now, fn -> ~U[2024-03-31 01:23:45Z] end)
|
||||||
|
|
||||||
assert cert_time_to_iso8601("240326110000Z") == "2024-03-26T11:00:00Z"
|
assert Certificate.cert_time_to_iso8601("240326110000Z") == "2024-03-26T11:00:00Z"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles century rollover" do
|
test "handles century rollover" do
|
||||||
DateTimeMock
|
DateTimeMock
|
||||||
|> expect(:utc_now, fn -> ~U[2024-03-31 01:23:45Z] end)
|
|> expect(:utc_now, fn -> ~U[2024-03-31 01:23:45Z] end)
|
||||||
|
|
||||||
assert cert_time_to_iso8601("010326110000Z") == "2101-03-26T11:00:00Z"
|
assert Certificate.cert_time_to_iso8601("010326110000Z") == "2101-03-26T11:00:00Z"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles millenium rollover" do
|
test "handles millenium rollover" do
|
||||||
DateTimeMock
|
DateTimeMock
|
||||||
|> expect(:utc_now, fn -> ~U[2999-03-31 01:23:45Z] end)
|
|> expect(:utc_now, fn -> ~U[2999-03-31 01:23:45Z] end)
|
||||||
|
|
||||||
assert cert_time_to_iso8601("010326110000Z") == "3001-03-26T11:00:00Z"
|
assert Certificate.cert_time_to_iso8601("010326110000Z") == "3001-03-26T11:00:00Z"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule Chronoscope.NTS.ClientTest do
|
defmodule Chronoscope.NTS.ClientTest do
|
||||||
use Chronoscope.Case, async: true
|
use Chronoscope.Case, async: true
|
||||||
|
|
||||||
alias Chronoscope.NTS.Certificate
|
alias Chronoscope.Certificate
|
||||||
alias Chronoscope.NTS.DateTimeMock
|
alias Chronoscope.DateTimeMock
|
||||||
alias Chronoscope.NTS.SSLMock
|
alias Chronoscope.SSLMock
|
||||||
|
|
||||||
import Chronoscope.NTS.Client
|
import Chronoscope.NTS.Client
|
||||||
import Mox
|
import Mox
|
||||||
|
@ -22,10 +22,9 @@ defmodule Chronoscope.NTS.ClientTest do
|
||||||
assert init(%{host: "localhost", port: 3333}) ==
|
assert init(%{host: "localhost", port: 3333}) ==
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
host: "localhost",
|
server: %{host: "localhost", port: 3333},
|
||||||
key_establishment_response: {:error, "initializing"},
|
key_establishment_response: {:error, "initializing"},
|
||||||
last_key_establishment: ~U[2024-03-31 01:23:15Z],
|
last_key_establishment: ~U[2024-03-31 01:23:15Z]
|
||||||
port: 3333
|
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -67,16 +66,14 @@ defmodule Chronoscope.NTS.ClientTest do
|
||||||
|
|
||||||
assert {:reply, {:ok, %{cert_expiration: ^peercert_expiration}},
|
assert {:reply, {:ok, %{cert_expiration: ^peercert_expiration}},
|
||||||
%{
|
%{
|
||||||
host: "localhost",
|
server: %{host: "localhost", port: 3333},
|
||||||
key_establishment_response: {:ok, %{cert_expiration: ^peercert_expiration}},
|
key_establishment_response: {:ok, %{cert_expiration: ^peercert_expiration}},
|
||||||
last_key_establishment: ~U[2024-03-31 01:23:45Z],
|
last_key_establishment: ~U[2024-03-31 01:23:45Z]
|
||||||
port: 3333
|
|
||||||
}} =
|
}} =
|
||||||
handle_call(:key_establishment, nil, %{
|
handle_call(:key_establishment, nil, %{
|
||||||
host: "localhost",
|
server: %{host: "localhost", port: 3333},
|
||||||
key_establishment_response: {:error, "initializing"},
|
key_establishment_response: {:error, "initializing"},
|
||||||
last_key_establishment: ~U[2024-03-31 01:23:00Z],
|
last_key_establishment: ~U[2024-03-31 01:23:00Z]
|
||||||
port: 3333
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
||||||
use Chronoscope.Case, async: true
|
use Chronoscope.Case, async: true
|
||||||
|
|
||||||
alias Chronoscope.NTS.Certificate
|
alias Chronoscope.Certificate
|
||||||
|
alias Chronoscope.NTS.KeyEstablishmentClient
|
||||||
alias Chronoscope.NTS.KeyEstablishmentRequest
|
alias Chronoscope.NTS.KeyEstablishmentRequest
|
||||||
alias Chronoscope.NTS.SSLMock
|
alias Chronoscope.SSLMock
|
||||||
|
|
||||||
import Chronoscope.NTS.KeyEstablishmentClient
|
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
@ -23,7 +23,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
||||||
|> expect(:peercert, fn :socket -> {:ok, peercert()} end)
|
|> expect(:peercert, fn :socket -> {:ok, peercert()} end)
|
||||||
|> expect(:close, fn :socket -> :ok end)
|
|> expect(:close, fn :socket -> :ok end)
|
||||||
|
|
||||||
key_establishment(%{host: "localhost", port: 2222})
|
KeyEstablishmentClient.key_establishment(%{host: "localhost", port: 2222})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles an empty response" do
|
test "handles an empty response" do
|
||||||
|
@ -37,7 +37,8 @@ defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
||||||
|> expect(:peercert, fn :socket -> {:ok, peercert} end)
|
|> expect(:peercert, fn :socket -> {:ok, peercert} end)
|
||||||
|> expect(:close, fn :socket -> :ok end)
|
|> expect(:close, fn :socket -> :ok end)
|
||||||
|
|
||||||
assert {:ok, %{cert_expiration: ^peercert_expiration}} = key_establishment(%{host: "localhost", port: 2222})
|
assert {:ok, %{cert_expiration: ^peercert_expiration}} =
|
||||||
|
KeyEstablishmentClient.key_establishment(%{host: "localhost", port: 2222})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles a full response" do
|
test "handles a full response" do
|
||||||
|
@ -69,7 +70,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
||||||
next_protocols: ["NTPv4"],
|
next_protocols: ["NTPv4"],
|
||||||
port: 1230,
|
port: 1230,
|
||||||
server: "127.0.0.1"
|
server: "127.0.0.1"
|
||||||
}} = key_establishment(%{host: "localhost", port: 2222})
|
}} = KeyEstablishmentClient.key_establishment(%{host: "localhost", port: 2222})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles a bad certificate hostname failure" do
|
test "handles a bad certificate hostname failure" do
|
||||||
|
@ -78,7 +79,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
||||||
{:error, {:tls_alert, {:handshake_failure, "connection failed {bad_cert,hostname_check_failed}"}}}
|
{:error, {:tls_alert, {:handshake_failure, "connection failed {bad_cert,hostname_check_failed}"}}}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert key_establishment(%{host: "localhost", port: 2222}) ==
|
assert KeyEstablishmentClient.key_establishment(%{host: "localhost", port: 2222}) ==
|
||||||
{:error, "The certificate is NOT trusted. The name in the certificate does not match the expected."}
|
{:error, "The certificate is NOT trusted. The name in the certificate does not match the expected."}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
||||||
{:error, {:tls_alert, {:handshake_failure, "unsatisfactory handshake"}}}
|
{:error, {:tls_alert, {:handshake_failure, "unsatisfactory handshake"}}}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert key_establishment(%{host: "localhost", port: 2222}) == {:error, "unsatisfactory handshake"}
|
assert KeyEstablishmentClient.key_establishment(%{host: "localhost", port: 2222}) == {:error, "unsatisfactory handshake"}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles a no application protocol failure" do
|
test "handles a no application protocol failure" do
|
||||||
|
@ -97,28 +98,30 @@ defmodule Chronoscope.NTS.KeyEstablishmentClientTest do
|
||||||
{:error, {:tls_alert, {:no_application_protocol, "unsatisfactory protocol"}}}
|
{:error, {:tls_alert, {:no_application_protocol, "unsatisfactory protocol"}}}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert key_establishment(%{host: "localhost", port: 2222}) == {:error, "unsatisfactory protocol"}
|
assert KeyEstablishmentClient.key_establishment(%{host: "localhost", port: 2222}) == {:error, "unsatisfactory protocol"}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles a timeout failure" do
|
test "handles a timeout failure" do
|
||||||
SSLMock
|
SSLMock
|
||||||
|> expect(:connect, fn ~c"localhost", 2222, _tls_options, @timeout -> {:error, :timeout} end)
|
|> expect(:connect, fn ~c"localhost", 2222, _tls_options, @timeout -> {:error, :timeout} end)
|
||||||
|
|
||||||
assert key_establishment(%{host: "localhost", port: 2222}) == {:error, :timeout}
|
assert KeyEstablishmentClient.key_establishment(%{host: "localhost", port: 2222}) == {:error, :timeout}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles an unknown error" do
|
test "handles an unknown error" do
|
||||||
SSLMock
|
SSLMock
|
||||||
|> expect(:connect, fn ~c"localhost", 2222, _tls_options, @timeout -> {:error, {:unsatisfactory, "client"}} end)
|
|> expect(:connect, fn ~c"localhost", 2222, _tls_options, @timeout -> {:error, {:unsatisfactory, "client"}} end)
|
||||||
|
|
||||||
assert key_establishment(%{host: "localhost", port: 2222}) == {:error, "{:unsatisfactory, \"client\"}"}
|
assert KeyEstablishmentClient.key_establishment(%{host: "localhost", port: 2222}) ==
|
||||||
|
{:error, "{:unsatisfactory, \"client\"}"}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles an unexpected error" do
|
test "handles an unexpected error" do
|
||||||
SSLMock
|
SSLMock
|
||||||
|> expect(:connect, fn ~c"localhost", 2222, _tls_options, @timeout -> {:unsatisfactory, "client"} end)
|
|> expect(:connect, fn ~c"localhost", 2222, _tls_options, @timeout -> {:unsatisfactory, "client"} end)
|
||||||
|
|
||||||
assert key_establishment(%{host: "localhost", port: 2222}) == {:error, "{:unsatisfactory, \"client\"}"}
|
assert KeyEstablishmentClient.key_establishment(%{host: "localhost", port: 2222}) ==
|
||||||
|
{:error, "{:unsatisfactory, \"client\"}"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
defmodule Chronoscope.NTSTest do
|
defmodule Chronoscope.NTSTest do
|
||||||
use Chronoscope.Case, async: true
|
use Chronoscope.Case, async: true
|
||||||
|
|
||||||
|
alias Chronoscope.DynamicSupervisorMock
|
||||||
|
alias Chronoscope.GenServerMock
|
||||||
alias Chronoscope.NTS
|
alias Chronoscope.NTS
|
||||||
alias Chronoscope.NTS.DynamicSupervisorMock
|
alias Chronoscope.RegistryMock
|
||||||
alias Chronoscope.NTS.GenServerMock
|
|
||||||
alias Chronoscope.NTS.RegistryMock
|
|
||||||
|
|
||||||
import Chronoscope.NTS
|
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
describe "Chronoscope.NTS.healthy?()" do
|
describe "Chronoscope.NTS.healthy?()" do
|
||||||
test "is healthy" do
|
test "is healthy" do
|
||||||
assert healthy?() == true
|
assert NTS.healthy?() == true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ defmodule Chronoscope.NTSTest do
|
||||||
DynamicSupervisorMock
|
DynamicSupervisorMock
|
||||||
|> expect(:which_children, fn _ -> [] end)
|
|> expect(:which_children, fn _ -> [] end)
|
||||||
|
|
||||||
assert list() == []
|
assert NTS.list() == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "lists all children" do
|
test "lists all children" do
|
||||||
|
@ -33,7 +32,7 @@ defmodule Chronoscope.NTSTest do
|
||||||
|> expect(:call, fn 2, :list -> :one end)
|
|> expect(:call, fn 2, :list -> :one end)
|
||||||
|> expect(:call, fn 6, :list -> :two end)
|
|> expect(:call, fn 6, :list -> :two end)
|
||||||
|
|
||||||
assert list() == [:one, :two]
|
assert NTS.list() == [:one, :two]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,7 +41,7 @@ defmodule Chronoscope.NTSTest do
|
||||||
RegistryMock
|
RegistryMock
|
||||||
|> expect(:lookup, fn _, _ -> [] end)
|
|> expect(:lookup, fn _, _ -> [] end)
|
||||||
|
|
||||||
assert remove("localhost", 1111) == {:error, :not_found}
|
assert NTS.remove("localhost", 1111) == {:error, :not_found}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "removes a client" do
|
test "removes a client" do
|
||||||
|
@ -52,7 +51,7 @@ defmodule Chronoscope.NTSTest do
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 1, :terminate -> :terminating end)
|
|> expect(:call, fn 1, :terminate -> :terminating end)
|
||||||
|
|
||||||
assert remove("localhost", 1111) == {:ok, :terminating}
|
assert NTS.remove("localhost", 1111) == {:ok, :terminating}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -62,11 +61,11 @@ defmodule Chronoscope.NTSTest do
|
||||||
|> expect(:lookup, fn _, _ -> [] end)
|
|> expect(:lookup, fn _, _ -> [] end)
|
||||||
|
|
||||||
DynamicSupervisorMock
|
DynamicSupervisorMock
|
||||||
|> expect(:start_child, fn NTS.DynamicSupervisor,
|
|> expect(:start_child, fn Chronoscope.NTS.DynamicSupervisor,
|
||||||
{NTS.Client,
|
{Chronoscope.NTS.Client,
|
||||||
[
|
[
|
||||||
server: %{host: "localhost", port: 1111},
|
server: %{host: "localhost", port: 1111},
|
||||||
name: {:via, RegistryMock, {NTS.Registry, "localhost:1111"}}
|
name: {:via, RegistryMock, {Chronoscope.NTS.Registry, "localhost:1111"}}
|
||||||
]} ->
|
]} ->
|
||||||
{:ok, 1}
|
{:ok, 1}
|
||||||
end)
|
end)
|
||||||
|
@ -74,7 +73,7 @@ defmodule Chronoscope.NTSTest do
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 1, :key_establishment -> :result end)
|
|> expect(:call, fn 1, :key_establishment -> :result end)
|
||||||
|
|
||||||
assert key_establishment("localhost", 1111) == :result
|
assert NTS.key_establishment("localhost", 1111) == :result
|
||||||
end
|
end
|
||||||
|
|
||||||
test "reuses an existing client" do
|
test "reuses an existing client" do
|
||||||
|
@ -84,7 +83,7 @@ defmodule Chronoscope.NTSTest do
|
||||||
GenServerMock
|
GenServerMock
|
||||||
|> expect(:call, fn 1, :key_establishment -> :result end)
|
|> expect(:call, fn 1, :key_establishment -> :result end)
|
||||||
|
|
||||||
assert key_establishment("localhost", 1111) == :result
|
assert NTS.key_establishment("localhost", 1111) == :result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentControllerTest do
|
defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentControllerTest do
|
||||||
use ChronoscopeWeb.ConnCase, async: true
|
use ChronoscopeWeb.ConnCase, async: true
|
||||||
|
|
||||||
|
alias Chronoscope.NTSMock
|
||||||
|
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
@ -16,7 +18,7 @@ defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentControllerTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "truncates the host name", %{conn: conn} do
|
test "truncates the host name", %{conn: conn} do
|
||||||
Chronoscope.NTS.BehaviourMock
|
NTSMock
|
||||||
|> expect(
|
|> expect(
|
||||||
:key_establishment,
|
:key_establishment,
|
||||||
fn "test.example.com.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456",
|
fn "test.example.com.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.0123456",
|
||||||
|
@ -36,7 +38,7 @@ defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentControllerTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "uses the given port number", %{conn: conn} do
|
test "uses the given port number", %{conn: conn} do
|
||||||
Chronoscope.NTS.BehaviourMock
|
NTSMock
|
||||||
|> expect(:key_establishment, fn "localhost", 4461 -> {:ok, %{status: :ok}} end)
|
|> expect(:key_establishment, fn "localhost", 4461 -> {:ok, %{status: :ok}} end)
|
||||||
|
|
||||||
response =
|
response =
|
||||||
|
@ -75,7 +77,7 @@ defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentControllerTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns an empty successful response", %{conn: conn} do
|
test "returns an empty successful response", %{conn: conn} do
|
||||||
Chronoscope.NTS.BehaviourMock
|
NTSMock
|
||||||
|> expect(:key_establishment, fn "localhost", 4460 -> {:ok, %{status: :ok}} end)
|
|> expect(:key_establishment, fn "localhost", 4460 -> {:ok, %{status: :ok}} end)
|
||||||
|
|
||||||
response =
|
response =
|
||||||
|
@ -87,7 +89,7 @@ defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentControllerTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns a full successful response", %{conn: conn} do
|
test "returns a full successful response", %{conn: conn} do
|
||||||
Chronoscope.NTS.BehaviourMock
|
NTSMock
|
||||||
|> expect(:key_establishment, fn "localhost", 4460 ->
|
|> expect(:key_establishment, fn "localhost", 4460 ->
|
||||||
{:ok, %{cookies: [[], [], []], cookie_length: 300}}
|
{:ok, %{cookies: [[], [], []], cookie_length: 300}}
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -8,7 +8,7 @@ defmodule Chronoscope.Case do
|
||||||
end
|
end
|
||||||
|
|
||||||
setup _tags do
|
setup _tags do
|
||||||
Mox.stub_with(Chronoscope.NTS.DateTimeMock, Chronoscope.DateTime.Stub)
|
Mox.stub_with(Chronoscope.DateTimeMock, Chronoscope.DateTimeStub)
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ defmodule ChronoscopeWeb.ConnCase do
|
||||||
end
|
end
|
||||||
|
|
||||||
setup _tags do
|
setup _tags do
|
||||||
Mox.stub_with(Chronoscope.NTS.DateTimeMock, Chronoscope.DateTime.Stub)
|
Mox.stub_with(Chronoscope.DateTimeMock, Chronoscope.DateTimeStub)
|
||||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
Mox.defmock(Chronoscope.NTS.BehaviourMock, for: Chronoscope.NTS.Behaviour)
|
Mox.defmock(Chronoscope.DateTimeMock, for: Chronoscope.DateTime.Behaviour)
|
||||||
Mox.defmock(Chronoscope.NTS.DateTimeMock, for: Chronoscope.DateTime.Behaviour)
|
Mox.defmock(Chronoscope.SSLMock, for: Chronoscope.SSL.Behaviour)
|
||||||
Mox.defmock(Chronoscope.NTS.SSLMock, for: Chronoscope.SSL.Behaviour)
|
Mox.defmock(Chronoscope.RegistryMock, for: Chronoscope.Registry.Behaviour)
|
||||||
Mox.defmock(Chronoscope.NTS.RegistryMock, for: Chronoscope.Registry.Behaviour)
|
Mox.defmock(Chronoscope.DynamicSupervisorMock, for: Chronoscope.DynamicSupervisor.Behaviour)
|
||||||
Mox.defmock(Chronoscope.NTS.DynamicSupervisorMock, for: Chronoscope.DynamicSupervisor.Behaviour)
|
Mox.defmock(Chronoscope.GenServerMock, for: Chronoscope.GenServer.Behaviour)
|
||||||
Mox.defmock(Chronoscope.NTS.GenServerMock, for: Chronoscope.GenServer.Behaviour)
|
Mox.defmock(Chronoscope.NTSMock, for: Chronoscope.NTS.Behaviour)
|
||||||
|
Mox.defmock(Chronoscope.GeminiMock, for: Chronoscope.Gemini.Behaviour)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Chronoscope.DateTime.Stub do
|
defmodule Chronoscope.DateTimeStub do
|
||||||
@behaviour Chronoscope.DateTime.Behaviour
|
@behaviour Chronoscope.DateTime.Behaviour
|
||||||
def utc_now(), do: DateTime.utc_now()
|
def utc_now(), do: DateTime.utc_now()
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue