Add gemini connection endpoint

This commit is contained in:
Mike Cifelli 2024-04-21 10:30:56 -04:00
parent 6622913d52
commit 53bfdbd75d
Signed by: mike
GPG Key ID: 6B08C6BE47D08E4C
7 changed files with 165 additions and 16 deletions

View File

@ -23,8 +23,8 @@ defmodule Chronoscope.Gemini do
|> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end) |> Enum.map(fn {_, pid, _, _} -> @genserver.call(pid, :list) end)
end end
def remove(host, port) do def remove(host, port, path) do
name = client_name(%{host: host, port: port}) name = client_name(%{host: host, port: port, path: path})
case @registry.lookup(Gemini.Registry, name) do case @registry.lookup(Gemini.Registry, name) do
[{pid, _}] -> {:ok, @genserver.call(pid, :terminate)} [{pid, _}] -> {:ok, @genserver.call(pid, :terminate)}

View File

@ -18,7 +18,7 @@ defmodule Chronoscope.Gemini.Client do
{:ok, {:ok,
%{ %{
resource: resource, resource: resource,
reponse: {:error, "initializing"}, response: {:error, "initializing"},
last_request: DateTime.add(now, -@interval_in_seconds, :second) last_request: DateTime.add(now, -@interval_in_seconds, :second)
}} }}
end end

View File

@ -1,6 +1,6 @@
defmodule Chronoscope.Gemini.Response do defmodule Chronoscope.Gemini.Response do
def parse(response) do def parse(response) do
# TODO # TODO
%{status: 20, type: "text/gemini", body: response} %{status: 20, mime_type: "text/gemini", body: to_string(response)}
end end
end end

View File

@ -0,0 +1,67 @@
defmodule ChronoscopeWeb.API.V1.Gemini.ConnectionController do
use ChronoscopeWeb, :controller
require Logger
alias Chronoscope.Gemini
@default_port 1965
@default_path "/"
@max_host_length 255
@gemini Application.compile_env(:chronoscope, :gemini, Gemini)
def get(conn, %{"host" => host, "port" => port, "path" => path}) do
try do
handle_get(conn, %{host: host, port: String.to_integer(port), path: path})
rescue
ArgumentError -> bad_request_response(conn, "invalid port")
end
end
def get(conn, %{"host" => host, "path" => path}) do
handle_get(conn, %{host: host, port: @default_port, path: path})
end
def get(conn, %{"host" => host, "port" => port}) do
handle_get(conn, %{host: host, port: port, path: @default_path})
end
def get(conn, %{"host" => host}) do
handle_get(conn, %{host: 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: host, port: port, path: path}) when port > 0 and port < 65536 do
case connect(host, port, path) do
{:ok, response} ->
json(conn, %{status: :ok, response: format_response(response)})
{:error, error} ->
json(conn, %{status: :error, reason: to_string(error)})
end
end
defp handle_get(conn, _params) do
bad_request_response(conn, "port out of range")
end
defp connect(host, port, path) do
# TODO - max path length
host
|> String.slice(0, @max_host_length)
|> @gemini.connect(port, path)
end
defp format_response(response) do
Map.take(response, [:status, :mime_type, :body])
end
defp bad_request_response(conn, message) do
conn
|> put_status(:bad_request)
|> json(%{error: message})
end
end

View File

@ -32,6 +32,12 @@ defmodule ChronoscopeWeb.Router do
get "/key-establishment", KeyEstablishmentController, :get get "/key-establishment", KeyEstablishmentController, :get
end end
scope "/api/v1/gemini", ChronoscopeWeb.API.V1.Gemini do
pipe_through :api
get "/connect", ConnectionController, :get
end
# Enable LiveDashboard and Swoosh mailbox preview in development # Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:chronoscope, :dev_routes) do if Application.compile_env(:chronoscope, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put # If you want to use the LiveDashboard in production, you should put

View File

@ -0,0 +1,78 @@
defmodule Chronoscope.Gemini.ClientTest do
use Chronoscope.Case, async: true
alias Chronoscope.Certificate
alias Chronoscope.DateTimeMock
alias Chronoscope.Gemini.Client
alias Chronoscope.SSLMock
import Mox
setup :verify_on_exit!
setup _tags do
DateTimeMock
|> stub(:utc_now, fn -> ~U[2024-04-21 01:23:45Z] end)
:ok
end
describe "Chronoscope.Gemini.Client.init()" do
test "initializes successfully" do
assert Client.init(%{host: "localhost", port: 4444, path: "/"}) ==
{:ok,
%{
resource: %{host: "localhost", port: 4444, path: "/"},
response: {:error, "initializing"},
last_request: ~U[2024-04-21 01:23:15Z]
}}
end
end
describe "Chronoscope.Gemini.Client.handle_call()" do
test ":terminate" do
assert Client.handle_call(:terminate, nil, %{state: true}) == {:stop, :normal, self(), %{state: true}}
end
test ":list" do
assert Client.handle_call(:list, nil, %{state: true}) == {:reply, %{state: true}, %{state: true}}
end
test ":connect - cached" do
assert Client.handle_call(:connect, nil, %{
resource: %{host: "localhost", port: 4444, path: "/"},
response: {:error, "initializing"},
last_request: ~U[2024-04-21 01:23:45Z]
}) ==
{:reply, {:error, "initializing"},
%{
resource: %{host: "localhost", port: 4444, path: "/"},
response: {:error, "initializing"},
last_request: ~U[2024-04-21 01:23:45Z]
}}
end
test ":connect - not cached" do
peercert = peercert()
peercert_expiration = Certificate.expiration_date(peercert)
SSLMock
|> expect(:connect, fn ~c"localhost", 4444, _, _ -> {:ok, :socket} end)
|> expect(:send, fn :socket, _ -> send_ssl_response([]) end)
|> expect(:peercert, fn :socket -> {:ok, peercert} end)
|> expect(:close, fn :socket -> :ok end)
assert {:reply, {:ok, %{cert_expiration: ^peercert_expiration}},
%{
resource: %{host: "localhost", port: 4444, path: "/"},
response: {:ok, %{cert_expiration: ^peercert_expiration}},
last_request: ~U[2024-04-21 01:23:45Z]
}} =
Client.handle_call(:connect, nil, %{
resource: %{host: "localhost", port: 4444, path: "/"},
response: {:error, "initializing"},
last_request: ~U[2024-04-21 01:23:00Z]
})
end
end
end

View File

@ -3,9 +3,9 @@ defmodule Chronoscope.NTS.ClientTest do
alias Chronoscope.Certificate alias Chronoscope.Certificate
alias Chronoscope.DateTimeMock alias Chronoscope.DateTimeMock
alias Chronoscope.NTS.Client
alias Chronoscope.SSLMock alias Chronoscope.SSLMock
import Chronoscope.NTS.Client
import Mox import Mox
setup :verify_on_exit! setup :verify_on_exit!
@ -19,7 +19,7 @@ defmodule Chronoscope.NTS.ClientTest do
describe "Chronoscope.NTS.Client.init()" do describe "Chronoscope.NTS.Client.init()" do
test "initializes successfully" do test "initializes successfully" do
assert init(%{host: "localhost", port: 3333}) == assert Client.init(%{host: "localhost", port: 3333}) ==
{:ok, {:ok,
%{ %{
server: %{host: "localhost", port: 3333}, server: %{host: "localhost", port: 3333},
@ -31,26 +31,24 @@ defmodule Chronoscope.NTS.ClientTest do
describe "Chronoscope.NTS.Client.handle_call()" do describe "Chronoscope.NTS.Client.handle_call()" do
test ":terminate" do test ":terminate" do
assert handle_call(:terminate, nil, %{state: true}) == {:stop, :normal, self(), %{state: true}} assert Client.handle_call(:terminate, nil, %{state: true}) == {:stop, :normal, self(), %{state: true}}
end end
test ":list" do test ":list" do
assert handle_call(:list, nil, %{state: true}) == {:reply, %{state: true}, %{state: true}} assert Client.handle_call(:list, nil, %{state: true}) == {:reply, %{state: true}, %{state: true}}
end end
test ":key_establishment - cached" do test ":key_establishment - cached" do
assert handle_call(:key_establishment, nil, %{ assert Client.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:45Z], last_key_establishment: ~U[2024-03-31 01:23:45Z]
port: 3333
}) == }) ==
{:reply, {:error, "initializing"}, {:reply, {:error, "initializing"},
%{ %{
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:45Z], last_key_establishment: ~U[2024-03-31 01:23:45Z]
port: 3333
}} }}
end end
@ -70,7 +68,7 @@ defmodule Chronoscope.NTS.ClientTest do
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]
}} = }} =
handle_call(:key_establishment, nil, %{ Client.handle_call(:key_establishment, nil, %{
server: %{host: "localhost", port: 3333}, 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]