Add gemini connection endpoint
This commit is contained in:
		
							parent
							
								
									6622913d52
								
							
						
					
					
						commit
						53bfdbd75d
					
				@ -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)}
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										78
									
								
								test/chronoscope/gemini/client_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								test/chronoscope/gemini/client_test.exs
									
									
									
									
									
										Normal 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
 | 
				
			||||||
@ -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]
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user