Docs and cleanup
This commit is contained in:
parent
f0cf03141b
commit
22a7fd9c6d
13 changed files with 159 additions and 74 deletions
|
|
@ -1,4 +1,15 @@
|
|||
defmodule BirdyChat.Identity do
|
||||
@moduledoc """
|
||||
Server identity, populated on startup from the following environment variables:
|
||||
|
||||
- BIRDY_CHAT_IDENTITY: Name of the server, a string that can be formatted into an integer.
|
||||
- BIRDY_CHAT_PEERS: List of peer servers in format of
|
||||
`1::http://localhost:4001;2::http://localhost:4002`
|
||||
|
||||
When the environment variables are not present the server starts in test mode and can only
|
||||
communicate with itself.
|
||||
"""
|
||||
|
||||
use Agent
|
||||
|
||||
defstruct [:identity, :peers, :mode]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
defmodule BirdyChat.Message do
|
||||
@moduledoc """
|
||||
Main module for input validation. I decided to re-use Ecto because of existence of Phoenix.Ecto
|
||||
that clearly integrates the error messages produced from Ecto into HTTP plumbing of Phoenix.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
embedded_schema do
|
||||
|
|
@ -9,12 +14,20 @@ defmodule BirdyChat.Message do
|
|||
field :server, :string
|
||||
end
|
||||
|
||||
def validate(params) do
|
||||
@doc """
|
||||
Validation for inter-server communication. It is essentially the same as validate/1
|
||||
but without the requirement to communicate with home server only.
|
||||
|
||||
This can be also designed as a function that accepts other function or some configuration option
|
||||
but two separately named functions are easier to understand and less prone to misuse.
|
||||
"""
|
||||
def validate_for_inter_server_use(params) do
|
||||
changeset =
|
||||
%__MODULE__{}
|
||||
|> Ecto.Changeset.cast(params, [:from, :to, :message])
|
||||
|> Ecto.Changeset.validate_required([:from, :to, :message])
|
||||
|> put_routing()
|
||||
|> validate_is_local()
|
||||
|
||||
if changeset.valid? do
|
||||
{:ok, changeset}
|
||||
|
|
@ -23,6 +36,45 @@ defmodule BirdyChat.Message do
|
|||
end
|
||||
end
|
||||
|
||||
def validate(params) do
|
||||
changeset =
|
||||
%__MODULE__{}
|
||||
|> Ecto.Changeset.cast(params, [:from, :to, :message])
|
||||
|> Ecto.Changeset.validate_required([:from, :to, :message])
|
||||
|> put_routing()
|
||||
|> validate_home_server()
|
||||
|
||||
if changeset.valid? do
|
||||
{:ok, changeset}
|
||||
else
|
||||
{:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_is_local(%Ecto.Changeset{changes: %{routing: :local}} = changeset) do
|
||||
changeset
|
||||
end
|
||||
|
||||
defp validate_is_local(%Ecto.Changeset{} = changeset) do
|
||||
changeset
|
||||
|> Ecto.Changeset.add_error(:from, "you can only communicate with your home server")
|
||||
end
|
||||
|
||||
defp validate_home_server(%Ecto.Changeset{changes: %{from: from}} = changeset) do
|
||||
identity = BirdyChat.Identity.identity()
|
||||
|
||||
if String.starts_with?(from, identity) do
|
||||
changeset
|
||||
else
|
||||
changeset
|
||||
|> Ecto.Changeset.add_error(:from, "you can only communicate with your home server")
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_home_server(%Ecto.Changeset{} = changeset) do
|
||||
changeset
|
||||
end
|
||||
|
||||
defp put_routing(%Ecto.Changeset{changes: %{to: to}} = changeset) do
|
||||
identity = BirdyChat.Identity.identity()
|
||||
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ defmodule BirdyChatWeb do
|
|||
import BirdyChatWeb.CoreComponents
|
||||
|
||||
# Common modules used in templates
|
||||
alias Phoenix.LiveView.JS
|
||||
alias BirdyChatWeb.Layouts
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
# Routes generation with the ~p sigil
|
||||
unquote(verified_routes())
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
defmodule BirdyChatWeb.Api.Messages.Controller do
|
||||
@moduledoc """
|
||||
The endpoint to be used by users from the "home server".
|
||||
"""
|
||||
|
||||
use BirdyChatWeb, :controller
|
||||
|
||||
def create(conn, params) do
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
defmodule BirdyChatWeb.Api.Messages.JSON do
|
||||
@moduledoc false
|
||||
|
||||
def render("create.json", %{message: message}) do
|
||||
message
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,21 +1,36 @@
|
|||
defmodule BirdyChatWeb.Api.Server.Internal.Controller do
|
||||
@moduledoc """
|
||||
A controller for handling inter-server communication. It started off with using Erlang term
|
||||
format instead of JSON as communication language but then I removed it for the following
|
||||
reasons:
|
||||
|
||||
1. The messages are mostly binaries anyway, there is no big efficiency gain from skipping JSON.
|
||||
2. Testing JSON is much easier than testing erlang term format.
|
||||
3. Erlang term format can give an illusion of extra security but unless the transport is HTTPS
|
||||
then the communication is still inherently unsafe.
|
||||
4. Erlang term format is difficult to handle for unfamiliar developers, you need to remember
|
||||
about safe conversion to avoid atom exhaustion attacks or sending an `rm -rf /` function over
|
||||
the wire.
|
||||
|
||||
The endpoint is protected by simple authentication that requires the secret key of all servers
|
||||
being the same. It is good enough for a demo, but for any real application it would need to be
|
||||
reconsidered.
|
||||
"""
|
||||
|
||||
use BirdyChatWeb, :controller
|
||||
|
||||
def create(conn, params) do
|
||||
if authorised?(conn.req_headers, params) do
|
||||
case BirdyChat.Message.validate(params) do
|
||||
{:ok, changeset} ->
|
||||
case BirdyChat.MessageWriter.write(changeset.changes) do
|
||||
:ok ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> render(:create, message: changeset.changes)
|
||||
end
|
||||
end
|
||||
else
|
||||
with true <- authorised?(conn.req_headers, params),
|
||||
{:ok, changeset} <- BirdyChat.Message.validate_for_inter_server_use(params),
|
||||
:ok <- BirdyChat.MessageWriter.write(changeset.changes) do
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> render(:error, message: "Unauthorised")
|
||||
|> put_status(:created)
|
||||
|> render(:create, message: changeset.changes)
|
||||
else
|
||||
_any ->
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> render(:error, message: "Unauthorised")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
defmodule BirdyChatWeb.Api.Server.Internal.JSON do
|
||||
@moduledoc false
|
||||
|
||||
def render("create.json", %{message: message}) do
|
||||
message
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ defmodule BirdyChatWeb.CoreComponents do
|
|||
use Phoenix.Component
|
||||
use Gettext, backend: BirdyChatWeb.Gettext
|
||||
|
||||
alias Phoenix.HTML.Form
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
@doc """
|
||||
|
|
@ -200,9 +201,7 @@ defmodule BirdyChatWeb.CoreComponents do
|
|||
|
||||
def input(%{type: "checkbox"} = assigns) do
|
||||
assigns =
|
||||
assign_new(assigns, :checked, fn ->
|
||||
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
|
||||
end)
|
||||
assign_new(assigns, :checked, fn -> Form.normalize_value("checkbox", assigns[:value]) end)
|
||||
|
||||
~H"""
|
||||
<div class="fieldset mb-2">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue