Rename repository
This commit is contained in:
parent
4aeb2d2cd8
commit
232df26b85
71 changed files with 348 additions and 345 deletions
35
lib/architex_web/client/authenticate_client.ex
Normal file
35
lib/architex_web/client/authenticate_client.ex
Normal file
|
@ -0,0 +1,35 @@
|
|||
defmodule ArchitexWeb.Client.Plug.AuthenticateClient do
|
||||
import ArchitexWeb.Error
|
||||
import Plug.Conn
|
||||
|
||||
alias Architex.Account
|
||||
alias Plug.Conn
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(%Conn{params: %{"access_token" => access_token}} = conn, _opts) do
|
||||
authenticate(conn, access_token)
|
||||
end
|
||||
|
||||
def call(%Conn{req_headers: headers} = conn, _opts) do
|
||||
case List.keyfind(headers, "authorization", 0) do
|
||||
{_, "Bearer " <> access_token} ->
|
||||
authenticate(conn, access_token)
|
||||
|
||||
_ ->
|
||||
put_error(conn, :missing_token)
|
||||
end
|
||||
end
|
||||
|
||||
defp authenticate(conn, access_token) do
|
||||
case Account.by_access_token(access_token) do
|
||||
{account, device} ->
|
||||
conn
|
||||
|> assign(:account, account)
|
||||
|> assign(:device, device)
|
||||
|
||||
nil ->
|
||||
put_error(conn, :unknown_token)
|
||||
end
|
||||
end
|
||||
end
|
72
lib/architex_web/client/controllers/account_controller.ex
Normal file
72
lib/architex_web/client/controllers/account_controller.ex
Normal file
|
@ -0,0 +1,72 @@
|
|||
defmodule ArchitexWeb.Client.AccountController do
|
||||
use ArchitexWeb, :controller
|
||||
|
||||
import Architex
|
||||
import ArchitexWeb.Error
|
||||
|
||||
alias Architex.{Account, Repo}
|
||||
alias Plug.Conn
|
||||
|
||||
@doc """
|
||||
Checks to see if a username is available, and valid, for the server.
|
||||
|
||||
Action for GET /_matrix/client/r0/register/available.
|
||||
"""
|
||||
def available(conn, params) do
|
||||
localpart = Map.get(params, "username", "")
|
||||
|
||||
case Account.available?(localpart) do
|
||||
:ok ->
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{available: true})
|
||||
|
||||
{:error, error} ->
|
||||
put_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets information about the owner of a given access token.
|
||||
|
||||
Action for GET /_matrix/client/r0/account/whoami.
|
||||
"""
|
||||
def whoami(%Conn{assigns: %{account: %Account{localpart: localpart}}} = conn, _params) do
|
||||
data = %{user_id: get_mxid(localpart)}
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invalidates an existing access token, so that it can no longer be used for authorization.
|
||||
|
||||
Action for POST /_matrix/client/r0/logout.
|
||||
"""
|
||||
def logout(%Conn{assigns: %{device: device}} = conn, _params) do
|
||||
case Repo.delete(device) do
|
||||
{:ok, _} ->
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{})
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invalidates all access tokens for a user, so that they can no longer be used
|
||||
for authorization.
|
||||
|
||||
Action for POST /_matrix/client/r0/logout/all.
|
||||
"""
|
||||
def logout_all(%Conn{assigns: %{account: account}} = conn, _params) do
|
||||
Repo.delete_all(Ecto.assoc(account, :devices))
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{})
|
||||
end
|
||||
end
|
27
lib/architex_web/client/controllers/aliases_controller.ex
Normal file
27
lib/architex_web/client/controllers/aliases_controller.ex
Normal file
|
@ -0,0 +1,27 @@
|
|||
defmodule ArchitexWeb.Client.AliasesController do
|
||||
use ArchitexWeb, :controller
|
||||
|
||||
import ArchitexWeb.Error
|
||||
|
||||
alias Architex.Alias
|
||||
|
||||
@doc """
|
||||
Create a new mapping from room alias to room ID.
|
||||
|
||||
Action for PUT /_matrix/client/r0/directory/room/{roomAlias}.
|
||||
"""
|
||||
def create(conn, %{"alias" => alias, "room_id" => room_id}) do
|
||||
case Alias.create(alias, room_id) do
|
||||
{:ok, _} ->
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{})
|
||||
|
||||
{:error, cs} ->
|
||||
put_error(conn, Alias.get_error(cs))
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: create error view for this?
|
||||
def create(conn, _), do: put_error(conn, :bad_json)
|
||||
end
|
24
lib/architex_web/client/controllers/info_controller.ex
Normal file
24
lib/architex_web/client/controllers/info_controller.ex
Normal file
|
@ -0,0 +1,24 @@
|
|||
defmodule ArchitexWeb.Client.InfoController do
|
||||
use ArchitexWeb, :controller
|
||||
|
||||
import ArchitexWeb.Error
|
||||
|
||||
@supported_versions ["r0.6.1"]
|
||||
|
||||
@doc """
|
||||
Gets the versions of the specification supported by the server.
|
||||
|
||||
Action for GET /_matrix/client/versions.
|
||||
"""
|
||||
def versions(conn, _params) do
|
||||
data = %{versions: @supported_versions}
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(data)
|
||||
end
|
||||
|
||||
def unrecognized(conn, _params) do
|
||||
put_error(conn, :unrecognized)
|
||||
end
|
||||
end
|
70
lib/architex_web/client/controllers/login_controller.ex
Normal file
70
lib/architex_web/client/controllers/login_controller.ex
Normal file
|
@ -0,0 +1,70 @@
|
|||
defmodule ArchitexWeb.Client.LoginController do
|
||||
use ArchitexWeb, :controller
|
||||
|
||||
import ArchitexWeb.Error
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Architex.{Repo, Account, Device}
|
||||
alias ArchitexWeb.Client.Request.Login
|
||||
alias Ecto.Changeset
|
||||
|
||||
@login_type "m.login.password"
|
||||
|
||||
@doc """
|
||||
Gets the homeserver's supported login types to authenticate users.
|
||||
|
||||
Action for GET /_matrix/client/r0/login.
|
||||
"""
|
||||
def login_types(conn, _params) do
|
||||
data = %{flows: [%{type: @login_type}]}
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Authenticates the user, and issues an access token they can use to
|
||||
authorize themself in subsequent requests.
|
||||
|
||||
Action for POST /_matrix/client/r0/login.
|
||||
"""
|
||||
def login(
|
||||
conn,
|
||||
%{"type" => @login_type, "identifier" => %{"type" => "m.id.user"}} = params
|
||||
) do
|
||||
case Login.changeset(params) do
|
||||
%Changeset{valid?: true} = cs ->
|
||||
input = apply_changes(cs)
|
||||
|
||||
case Account.login(input) |> Repo.transaction() do
|
||||
{:ok,
|
||||
{%Account{localpart: localpart},
|
||||
%Device{access_token: access_token, device_id: device_id}}} ->
|
||||
data = %{
|
||||
user_id: Architex.get_mxid(localpart),
|
||||
access_token: access_token,
|
||||
device_id: device_id
|
||||
}
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(data)
|
||||
|
||||
{:error, error} when is_atom(error) ->
|
||||
put_error(conn, error)
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
|
||||
_ ->
|
||||
put_error(conn, :bad_json)
|
||||
end
|
||||
end
|
||||
|
||||
def login(conn, _params) do
|
||||
# Other login types and identifiers are unsupported for now.
|
||||
put_error(conn, :unrecognized, "Only m.login.password is supported currently.")
|
||||
end
|
||||
end
|
69
lib/architex_web/client/controllers/register_controller.ex
Normal file
69
lib/architex_web/client/controllers/register_controller.ex
Normal file
|
@ -0,0 +1,69 @@
|
|||
defmodule ArchitexWeb.Client.RegisterController do
|
||||
use ArchitexWeb, :controller
|
||||
|
||||
import ArchitexWeb.Error
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Architex.{Repo, Account, Device}
|
||||
alias ArchitexWeb.Client.Request.Register
|
||||
alias Ecto.Changeset
|
||||
|
||||
@register_type "m.login.dummy"
|
||||
|
||||
@doc """
|
||||
Register for an account on this homeserver.
|
||||
|
||||
Action for POST /_matrix/client/r0/register.
|
||||
"""
|
||||
def register(conn, %{"auth" => %{"type" => @register_type}} = params) do
|
||||
case Register.changeset(params) do
|
||||
%Changeset{valid?: true} = cs ->
|
||||
%Register{inhibit_login: inhibit_login} = input = apply_changes(cs)
|
||||
|
||||
case Account.register(input) |> Repo.transaction() do
|
||||
{:ok,
|
||||
%{
|
||||
account: %Account{localpart: localpart},
|
||||
device: %Device{device_id: device_id, access_token: access_token}
|
||||
}} ->
|
||||
data = %{user_id: Architex.get_mxid(localpart)}
|
||||
|
||||
data =
|
||||
if not inhibit_login do
|
||||
data
|
||||
|> Map.put(:device_id, device_id)
|
||||
|> Map.put(:access_token, access_token)
|
||||
else
|
||||
data
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(data)
|
||||
|
||||
{:error, _, cs, _} ->
|
||||
put_error(conn, Register.get_error(cs))
|
||||
end
|
||||
|
||||
_ ->
|
||||
put_error(conn, :bad_json)
|
||||
end
|
||||
end
|
||||
|
||||
def register(conn, %{"auth" => _}) do
|
||||
# Other login types are unsupported for now.
|
||||
put_error(conn, :unrecognized, "Only m.login.dummy is supported currently.")
|
||||
end
|
||||
|
||||
def register(conn, _params) do
|
||||
# User has not started an auth flow.
|
||||
data = %{
|
||||
flows: [%{stages: [@register_type]}],
|
||||
params: %{}
|
||||
}
|
||||
|
||||
conn
|
||||
|> put_status(401)
|
||||
|> json(data)
|
||||
end
|
||||
end
|
235
lib/architex_web/client/controllers/room_controller.ex
Normal file
235
lib/architex_web/client/controllers/room_controller.ex
Normal file
|
@ -0,0 +1,235 @@
|
|||
defmodule ArchitexWeb.Client.RoomController do
|
||||
use ArchitexWeb, :controller
|
||||
|
||||
import ArchitexWeb.Error
|
||||
import Ecto.{Changeset, Query}
|
||||
|
||||
alias Architex.{Repo, Room, RoomServer}
|
||||
alias Architex.Types.UserId
|
||||
alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban}
|
||||
alias Ecto.Changeset
|
||||
alias Plug.Conn
|
||||
|
||||
@doc """
|
||||
Create a new room with various configuration options.
|
||||
|
||||
Action for POST /_matrix/client/r0/createRoom.
|
||||
"""
|
||||
def create(%Conn{assigns: %{account: account}} = conn, params) do
|
||||
case CreateRoom.changeset(params) do
|
||||
%Changeset{valid?: true} = cs ->
|
||||
input = apply_changes(cs)
|
||||
|
||||
case Room.create(account, input) do
|
||||
{:ok, room_id} ->
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{room_id: room_id})
|
||||
|
||||
{:error, :authorization} ->
|
||||
put_error(conn, :invalid_room_state)
|
||||
|
||||
{:error, :unknown} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
|
||||
_ ->
|
||||
put_error(conn, :bad_json)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
This API returns a list of the user's current rooms.
|
||||
|
||||
Action for GET /_matrix/client/r0/joined_rooms.
|
||||
"""
|
||||
def joined_rooms(%Conn{assigns: %{account: account}} = conn, _params) do
|
||||
joined_room_ids =
|
||||
account
|
||||
|> Ecto.assoc(:joined_rooms)
|
||||
|> select([jr], jr.id)
|
||||
|> Repo.all()
|
||||
|
||||
data = %{
|
||||
joined_rooms: joined_room_ids
|
||||
}
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
This API invites a user to participate in a particular room.
|
||||
|
||||
Action for POST /_matrix/client/r0/rooms/{roomId}/invite.
|
||||
"""
|
||||
def invite(%Conn{assigns: %{account: account}} = conn, %{
|
||||
"room_id" => room_id,
|
||||
"user_id" => user_id
|
||||
}) do
|
||||
with {:ok, _} <- UserId.cast(user_id),
|
||||
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
||||
case RoomServer.invite(pid, account, user_id) do
|
||||
:ok ->
|
||||
conn
|
||||
|> send_resp(200, [])
|
||||
|> halt()
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
else
|
||||
:error -> put_error(conn, :invalid_param, "Given user ID is invalid.")
|
||||
{:error, :not_found} -> put_error(conn, :not_found, "The given room was not found.")
|
||||
end
|
||||
end
|
||||
|
||||
def invite(conn, _), do: put_error(conn, :missing_param)
|
||||
|
||||
@doc """
|
||||
This API starts a user participating in a particular room, if that user is allowed to participate in that room.
|
||||
|
||||
Action for POST /_matrix/client/r0/rooms/{roomId}/join.
|
||||
TODO: third_party_signed
|
||||
"""
|
||||
def join(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id}) do
|
||||
case RoomServer.get_room_server(room_id) do
|
||||
{:ok, pid} ->
|
||||
case RoomServer.join(pid, account) do
|
||||
{:ok, room_id} ->
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{room_id: room_id})
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
|
||||
{:error, :not_found} ->
|
||||
put_error(conn, :not_found, "The given room was not found.")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
This API stops a user participating in a particular room.
|
||||
|
||||
Action for POST /_matrix/client/r0/rooms/{roomId}/leave.
|
||||
"""
|
||||
def leave(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id}) do
|
||||
case RoomServer.get_room_server(room_id) do
|
||||
{:ok, pid} ->
|
||||
case RoomServer.leave(pid, account) do
|
||||
:ok ->
|
||||
conn
|
||||
|> send_resp(200, [])
|
||||
|> halt()
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
|
||||
{:error, :not_found} ->
|
||||
put_error(conn, :not_found, "The given room was not found.")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Kick a user from the room.
|
||||
|
||||
Action for POST /_matrix/client/r0/rooms/{roomId}/kick.
|
||||
"""
|
||||
def kick(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do
|
||||
with {:ok, request} <- Kick.parse(params),
|
||||
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
||||
case RoomServer.kick(pid, account, request) do
|
||||
:ok ->
|
||||
conn
|
||||
|> send_resp(200, [])
|
||||
|> halt()
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
else
|
||||
{:error, %Ecto.Changeset{}} -> put_error(conn, :bad_json)
|
||||
{:error, :not_found} -> put_error(conn, :not_found, "Room not found.")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ban a user in the room.
|
||||
|
||||
Action for POST /_matrix/client/r0/rooms/{roomId}/ban.
|
||||
"""
|
||||
def ban(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do
|
||||
with {:ok, request} <- Ban.parse(params),
|
||||
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
||||
case RoomServer.ban(pid, account, request) do
|
||||
:ok ->
|
||||
conn
|
||||
|> send_resp(200, [])
|
||||
|> halt()
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
else
|
||||
{:error, %Ecto.Changeset{}} -> put_error(conn, :bad_json)
|
||||
{:error, :not_found} -> put_error(conn, :not_found, "Room not found.")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unban a user from the room.
|
||||
|
||||
Action for POST /_matrix/client/r0/rooms/{roomId}/unban.
|
||||
"""
|
||||
def unban(%Conn{assigns: %{account: account}} = conn, %{
|
||||
"room_id" => room_id,
|
||||
"user_id" => user_id
|
||||
}) do
|
||||
case RoomServer.get_room_server(room_id) do
|
||||
{:ok, pid} ->
|
||||
case RoomServer.unban(pid, account, user_id) do
|
||||
:ok ->
|
||||
conn
|
||||
|> send_resp(200, [])
|
||||
|> halt()
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
|
||||
{:error, :not_found} ->
|
||||
put_error(conn, :not_found, "The given room was not found.")
|
||||
end
|
||||
end
|
||||
|
||||
def unban(conn, _), do: put_error(conn, :missing_param)
|
||||
|
||||
def send_message(
|
||||
%Conn{assigns: %{account: account, device: device}, body_params: body_params} = conn,
|
||||
%{
|
||||
"room_id" => room_id,
|
||||
"event_type" => event_type,
|
||||
"txn_id" => txn_id
|
||||
}
|
||||
) do
|
||||
case RoomServer.get_room_server(room_id) do
|
||||
{:ok, pid} ->
|
||||
case RoomServer.send_message(pid, account, device, event_type, body_params, txn_id) do
|
||||
{:ok, event_id} ->
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{event_id: event_id})
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
|
||||
{:error, :not_found} ->
|
||||
put_error(conn, :not_found, "The given room was not found.")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,58 @@
|
|||
defmodule ArchitexWeb.Client.RoomDirectoryController do
|
||||
use ArchitexWeb, :controller
|
||||
|
||||
import ArchitexWeb.Error
|
||||
import Ecto.Query
|
||||
|
||||
alias Architex.{Repo, Room, RoomServer}
|
||||
alias Plug.Conn
|
||||
|
||||
@doc """
|
||||
Gets the visibility of a given room on the server's public room directory.
|
||||
|
||||
Action for GET /_matrix/client/r0/directory/list/room/{roomId}.
|
||||
"""
|
||||
def get_visibility(conn, %{"room_id" => room_id}) do
|
||||
case Repo.one(from r in Room, where: r.id == ^room_id) do
|
||||
%Room{visibility: visibility} ->
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{visibility: visibility})
|
||||
|
||||
nil ->
|
||||
put_error(conn, :not_found, "The room was not found.")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the visibility of a given room in the server's public room directory.
|
||||
|
||||
Only allow the creator of the room to change visibility.
|
||||
Action for PUT /_matrix/client/r0/directory/list/room/{roomId}.
|
||||
"""
|
||||
def set_visibility(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do
|
||||
visibility = Map.get(params, "visibility", "public")
|
||||
|
||||
if visibility in ["public", "private"] do
|
||||
visibility = String.to_atom(visibility)
|
||||
|
||||
with {:ok, pid} <- RoomServer.get_room_server(room_id),
|
||||
:ok <- RoomServer.set_visibility(pid, account, visibility) do
|
||||
conn
|
||||
|> send_resp(200, [])
|
||||
|> halt()
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
put_error(conn, :not_found, "The given room was not found.")
|
||||
|
||||
{:error, :unauthorized} ->
|
||||
put_error(conn, :unauthorized, "Only the room's creator can change visibility.")
|
||||
|
||||
{:error, _} ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
else
|
||||
put_error(conn, :invalid_param, "Invalid visibility.")
|
||||
end
|
||||
end
|
||||
end
|
20
lib/architex_web/client/request/ban.ex
Normal file
20
lib/architex_web/client/request/ban.ex
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule ArchitexWeb.Client.Request.Ban do
|
||||
use ArchitexWeb.Request
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
user_id: String.t(),
|
||||
reason: String.t() | nil
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :user_id, :string
|
||||
field :reason, :string
|
||||
end
|
||||
|
||||
def changeset(data, params) do
|
||||
data
|
||||
|> cast(params, [:user_id, :reason])
|
||||
|> validate_required([:user_id])
|
||||
end
|
||||
end
|
48
lib/architex_web/client/request/create_room.ex
Normal file
48
lib/architex_web/client/request/create_room.ex
Normal file
|
@ -0,0 +1,48 @@
|
|||
defmodule ArchitexWeb.Client.Request.CreateRoom do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Ecto.Changeset
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
visibility: String.t(),
|
||||
room_alias_name: String.t(),
|
||||
name: String.t(),
|
||||
topic: String.t(),
|
||||
invite: list(String.t()),
|
||||
room_version: String.t(),
|
||||
preset: String.t()
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :visibility, :string
|
||||
field :room_alias_name, :string
|
||||
field :name, :string
|
||||
field :topic, :string
|
||||
field :invite, {:array, :string}
|
||||
field :room_version, :string
|
||||
field :preset, :string
|
||||
# TODO: unimplemented:
|
||||
# creation_content, initial_state, invite_3pid, initial_state,
|
||||
# is_direct, power_level_content_override
|
||||
end
|
||||
|
||||
def changeset(params) do
|
||||
%__MODULE__{}
|
||||
|> cast(params, [
|
||||
:visibility,
|
||||
:room_alias_name,
|
||||
:name,
|
||||
:topic,
|
||||
:invite,
|
||||
:room_version,
|
||||
:preset
|
||||
])
|
||||
|> validate_inclusion(:preset, ["private_chat", "public_chat", "trusted_private_chat"])
|
||||
end
|
||||
|
||||
def get_error(%Changeset{errors: [error | _]}), do: get_error(error)
|
||||
def get_error(_), do: :bad_json
|
||||
end
|
20
lib/architex_web/client/request/kick.ex
Normal file
20
lib/architex_web/client/request/kick.ex
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule ArchitexWeb.Client.Request.Kick do
|
||||
use ArchitexWeb.Request
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
user_id: String.t(),
|
||||
reason: String.t() | nil
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :user_id, :string
|
||||
field :reason, :string
|
||||
end
|
||||
|
||||
def changeset(data, params) do
|
||||
data
|
||||
|> cast(params, [:user_id, :reason])
|
||||
|> validate_required([:user_id])
|
||||
end
|
||||
end
|
38
lib/architex_web/client/request/login.ex
Normal file
38
lib/architex_web/client/request/login.ex
Normal file
|
@ -0,0 +1,38 @@
|
|||
defmodule ArchitexWeb.Client.Request.Login do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: String.t(),
|
||||
password: String.t(),
|
||||
device_id: String.t(),
|
||||
initial_device_display_name: String.t()
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :type, :string
|
||||
field :password, :string
|
||||
field :device_id, :string
|
||||
field :initial_device_display_name, :string
|
||||
|
||||
embeds_one :identifier, Identifier, primary_key: false do
|
||||
field :type, :string
|
||||
field :user, :string
|
||||
end
|
||||
end
|
||||
|
||||
def changeset(params) do
|
||||
%__MODULE__{}
|
||||
|> cast(params, [:type, :password, :device_id, :initial_device_display_name])
|
||||
|> cast_embed(:identifier, with: &identifier_changeset/2, required: true)
|
||||
|> validate_required([:type, :password])
|
||||
end
|
||||
|
||||
def identifier_changeset(identifier, params) do
|
||||
identifier
|
||||
|> cast(params, [:type, :user])
|
||||
|> validate_required([:type, :user])
|
||||
end
|
||||
end
|
41
lib/architex_web/client/request/register.ex
Normal file
41
lib/architex_web/client/request/register.ex
Normal file
|
@ -0,0 +1,41 @@
|
|||
defmodule ArchitexWeb.Client.Request.Register do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Ecto.Changeset
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
device_id: String.t(),
|
||||
initial_device_display_name: String.t(),
|
||||
password: String.t(),
|
||||
username: String.t(),
|
||||
inhibit_login: boolean()
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :device_id, :string
|
||||
field :initial_device_display_name, :string
|
||||
field :password, :string
|
||||
field :username, :string
|
||||
field :inhibit_login, :boolean, default: false
|
||||
end
|
||||
|
||||
def changeset(params) do
|
||||
%__MODULE__{}
|
||||
|> cast(params, [
|
||||
:device_id,
|
||||
:initial_device_display_name,
|
||||
:password,
|
||||
:username,
|
||||
:inhibit_login
|
||||
])
|
||||
|> validate_required([:password])
|
||||
end
|
||||
|
||||
def get_error(%Changeset{errors: [error | _]}), do: get_error(error)
|
||||
def get_error({:localpart, {_, [{:constraint, :unique} | _]}}), do: :user_in_use
|
||||
def get_error({:localpart, {_, [{:validation, _} | _]}}), do: :invalid_username
|
||||
def get_error(_), do: :bad_json
|
||||
end
|
44
lib/architex_web/endpoint.ex
Normal file
44
lib/architex_web/endpoint.ex
Normal file
|
@ -0,0 +1,44 @@
|
|||
defmodule ArchitexWeb.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :architex
|
||||
|
||||
# The session will be stored in the cookie and signed,
|
||||
# this means its contents can be read but not tampered with.
|
||||
# Set :encryption_salt if you would also like to encrypt it.
|
||||
@session_options [
|
||||
store: :cookie,
|
||||
key: "_architex_key",
|
||||
signing_salt: "IGPHtnAo"
|
||||
]
|
||||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
#
|
||||
# You should set gzip to true if you are running phx.digest
|
||||
# when deploying your static files in production.
|
||||
plug Plug.Static,
|
||||
at: "/",
|
||||
from: :architex,
|
||||
gzip: false,
|
||||
only: ~w(css fonts images js favicon.ico robots.txt)
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
if code_reloading? do
|
||||
plug Phoenix.CodeReloader
|
||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :architex
|
||||
end
|
||||
|
||||
plug Plug.RequestId
|
||||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Phoenix.json_library()
|
||||
|
||||
plug Plug.MethodOverride
|
||||
plug Plug.Head
|
||||
plug Plug.Session, @session_options
|
||||
plug CORSPlug
|
||||
|
||||
plug ArchitexWeb.Router
|
||||
end
|
34
lib/architex_web/error.ex
Normal file
34
lib/architex_web/error.ex
Normal file
|
@ -0,0 +1,34 @@
|
|||
defmodule ArchitexWeb.Error do
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller, only: [json: 2]
|
||||
|
||||
@error_map %{
|
||||
bad_json: {400, "M_BAD_JSON", "Bad request."},
|
||||
user_in_use: {400, "M_USER_IN_USE", "Username is already taken."},
|
||||
invalid_username: {400, "M_INVALID_USERNAME", "Invalid username."},
|
||||
forbidden: {400, "M_FORBIDDEN", "The requested action is forbidden."},
|
||||
unrecognized: {400, "M_UNRECOGNIZED", "Unrecognized request."},
|
||||
unknown: {400, "M_UNKNOWN", "An unknown error occurred."},
|
||||
invalid_room_state:
|
||||
{400, "M_INVALID_ROOM_STATE", "The request would leave the room in an invalid state."},
|
||||
unauthorized: {400, "M_UNAUTHORIZED", "The request was unauthorized."},
|
||||
invalid_param: {400, "M_INVALID_PARAM", "A request parameter was invalid."},
|
||||
missing_param: {400, "M_MISSING_PARAM", "A request parameter is missing."},
|
||||
unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."},
|
||||
missing_token: {401, "M_MISSING_TOKEN", "Access token required."},
|
||||
not_found: {404, "M_NOT_FOUND", "The requested resource was not found."},
|
||||
room_alias_exists: {409, "M_UNKNOWN", "The given room alias already exists."}
|
||||
}
|
||||
|
||||
def put_error(conn, {error, msg}), do: put_error(conn, error, msg)
|
||||
|
||||
def put_error(conn, error, msg \\ nil) do
|
||||
{status, errcode, default_msg} = @error_map[error]
|
||||
data = %{errcode: errcode, error: msg || default_msg}
|
||||
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(data)
|
||||
|> halt()
|
||||
end
|
||||
end
|
98
lib/architex_web/federation/authenticate_server.ex
Normal file
98
lib/architex_web/federation/authenticate_server.ex
Normal file
|
@ -0,0 +1,98 @@
|
|||
defmodule ArchitexWeb.Federation.AuthenticateServer do
|
||||
import ArchitexWeb.Error
|
||||
|
||||
alias Architex.{SigningKey, ServerKeyInfo}
|
||||
|
||||
@auth_header_regex ~r/^X-Matrix origin=(?<origin>.*),key="(?<key>.*)",sig="(?<sig>.*)"$/
|
||||
|
||||
def authenticate(%Plug.Conn{
|
||||
body_params: body_params,
|
||||
req_headers: headers,
|
||||
request_path: path,
|
||||
method: method,
|
||||
query_string: query_string
|
||||
}) do
|
||||
# TODO: This will break if request ends with '?'.
|
||||
uri = URI.decode_www_form(path)
|
||||
|
||||
uri =
|
||||
if String.length(query_string) > 0 do
|
||||
uri <> "?" <> URI.decode_www_form(query_string)
|
||||
else
|
||||
uri
|
||||
end
|
||||
|
||||
object_to_sign = %{
|
||||
uri: uri,
|
||||
method: method,
|
||||
destination: Architex.server_name()
|
||||
}
|
||||
|
||||
object_to_sign =
|
||||
if method != "GET", do: Map.put(object_to_sign, :content, body_params), else: object_to_sign
|
||||
|
||||
object_fun = &Map.put(object_to_sign, :origin, &1)
|
||||
|
||||
authenticate_with_headers(headers, object_fun)
|
||||
end
|
||||
|
||||
defp authenticate_with_headers(headers, object_fun) do
|
||||
# TODO: Only query once per origin.
|
||||
headers
|
||||
|> parse_authorization_headers()
|
||||
|> Enum.find(:error, fn {origin, _, sig} ->
|
||||
object = object_fun.(origin)
|
||||
|
||||
with {:ok, raw_sig} <- Architex.decode_base64(sig),
|
||||
{:ok, encoded_object} <- Architex.encode_canonical_json(object),
|
||||
{:ok, %ServerKeyInfo{signing_keys: keys}} <-
|
||||
ServerKeyInfo.with_fresh_signing_keys(origin) do
|
||||
Enum.find_value(keys, false, fn %SigningKey{signing_key: signing_key} ->
|
||||
with {:ok, decoded_key} <- Architex.decode_base64(signing_key) do
|
||||
Architex.sign_verify(raw_sig, encoded_object, decoded_key)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def parse_authorization_headers(headers) do
|
||||
headers
|
||||
|> Enum.filter(&(elem(&1, 0) == "authorization"))
|
||||
|> Enum.map(fn {_, auth_header} ->
|
||||
Regex.named_captures(@auth_header_regex, auth_header)
|
||||
end)
|
||||
|> Enum.reject(&Kernel.is_nil/1)
|
||||
|> Enum.map(fn %{"origin" => origin, "key" => key, "sig" => sig} ->
|
||||
{origin, key, sig}
|
||||
end)
|
||||
|> Enum.filter(fn {_, key, _} -> String.starts_with?(key, "ed25519:") end)
|
||||
end
|
||||
|
||||
defmacro __using__(opts) do
|
||||
except = Keyword.get(opts, :except) || []
|
||||
|
||||
quote do
|
||||
def action(conn, _) do
|
||||
action = action_name(conn)
|
||||
|
||||
if action not in unquote(except) do
|
||||
case ArchitexWeb.Federation.AuthenticateServer.authenticate(conn) do
|
||||
{origin, _key, _sig} ->
|
||||
conn = Plug.Conn.assign(conn, :origin, origin)
|
||||
apply(__MODULE__, action, [conn, conn.params])
|
||||
|
||||
:error ->
|
||||
put_error(conn, :unauthorized, "Signature verification failed.")
|
||||
end
|
||||
else
|
||||
apply(__MODULE__, action, [conn, conn.params])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
119
lib/architex_web/federation/controllers/event_controller.ex
Normal file
119
lib/architex_web/federation/controllers/event_controller.ex
Normal file
|
@ -0,0 +1,119 @@
|
|||
defmodule ArchitexWeb.Federation.EventController do
|
||||
use ArchitexWeb, :controller
|
||||
use ArchitexWeb.Federation.AuthenticateServer
|
||||
|
||||
import ArchitexWeb.Error
|
||||
import Ecto.Query
|
||||
|
||||
alias Architex.{Repo, Event, RoomServer}
|
||||
alias ArchitexWeb.Federation.Transaction
|
||||
|
||||
@doc """
|
||||
Retrieves a single event.
|
||||
|
||||
Action for GET /_matrix/federation/v1/event/{eventId}.
|
||||
"""
|
||||
def event(%Plug.Conn{assigns: %{origin: origin}} = conn, %{"event_id" => event_id}) do
|
||||
query =
|
||||
Event
|
||||
|> where([e], e.event_id == ^event_id)
|
||||
|> preload(:room)
|
||||
|
||||
case Repo.one(query) do
|
||||
%Event{room: room} = event ->
|
||||
case RoomServer.get_room_server(room) do
|
||||
{:ok, pid} ->
|
||||
if RoomServer.server_in_room?(pid, origin) do
|
||||
data = Transaction.new([event])
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(data)
|
||||
else
|
||||
put_error(conn, :unauthorized, "Origin server is not participating in room.")
|
||||
end
|
||||
|
||||
_ ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
|
||||
nil ->
|
||||
put_error(conn, :not_found, "Event or room not found.")
|
||||
end
|
||||
end
|
||||
|
||||
def event(conn, _), do: put_error(conn, :missing_param)
|
||||
|
||||
@doc """
|
||||
Retrieves a snapshot of a room's state at a given event.
|
||||
|
||||
Action for GET /_matrix/federation/v1/state/{roomId}.
|
||||
"""
|
||||
def state(%Plug.Conn{assigns: %{origin: origin}} = conn, %{
|
||||
"event_id" => event_id,
|
||||
"room_id" => room_id
|
||||
}) do
|
||||
get_state_or_state_ids(conn, :state, origin, event_id, room_id)
|
||||
end
|
||||
|
||||
def state(conn, _), do: put_error(conn, :missing_param)
|
||||
|
||||
@doc """
|
||||
Retrieves a snapshot of a room's state at a given event, in the form of event IDs.
|
||||
|
||||
Action for GET /_matrix/federation/v1/state_ids/{roomId}.
|
||||
"""
|
||||
def state_ids(%Plug.Conn{assigns: %{origin: origin}} = conn, %{
|
||||
"event_id" => event_id,
|
||||
"room_id" => room_id
|
||||
}) do
|
||||
get_state_or_state_ids(conn, :state_ids, origin, event_id, room_id)
|
||||
end
|
||||
|
||||
def state_ids(conn, _), do: put_error(conn, :missing_param)
|
||||
|
||||
@spec get_state_or_state_ids(
|
||||
Plug.Conn.t(),
|
||||
:state | :state_ids,
|
||||
String.t(),
|
||||
String.t(),
|
||||
String.t()
|
||||
) :: Plug.Conn.t()
|
||||
defp get_state_or_state_ids(conn, state_or_state_ids, origin, event_id, room_id) do
|
||||
query =
|
||||
Event
|
||||
|> where([e], e.event_id == ^event_id and e.room_id == ^room_id)
|
||||
|> preload(:room)
|
||||
|
||||
case Repo.one(query) do
|
||||
%Event{room: room} = event ->
|
||||
case RoomServer.get_room_server(room) do
|
||||
{:ok, pid} ->
|
||||
if RoomServer.server_in_room?(pid, origin) do
|
||||
{state_events, auth_chain} =
|
||||
case state_or_state_ids do
|
||||
:state -> RoomServer.get_state_at_event(pid, event)
|
||||
:state_ids -> RoomServer.get_state_ids_at_event(pid, event)
|
||||
end
|
||||
|
||||
data = %{
|
||||
auth_chain: auth_chain,
|
||||
pdus: state_events
|
||||
}
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(data)
|
||||
else
|
||||
put_error(conn, :unauthorized, "Origin server is not participating in room.")
|
||||
end
|
||||
|
||||
_ ->
|
||||
put_error(conn, :unknown)
|
||||
end
|
||||
|
||||
nil ->
|
||||
put_error(conn, :not_found, "Event or room not found.")
|
||||
end
|
||||
end
|
||||
end
|
43
lib/architex_web/federation/controllers/key_controller.ex
Normal file
43
lib/architex_web/federation/controllers/key_controller.ex
Normal file
|
@ -0,0 +1,43 @@
|
|||
defmodule ArchitexWeb.Federation.KeyController do
|
||||
use ArchitexWeb, :controller
|
||||
|
||||
import ArchitexWeb.Error
|
||||
|
||||
alias Architex.KeyServer
|
||||
|
||||
@doc """
|
||||
Gets the homeserver's published signing keys.
|
||||
|
||||
Action for GET /_matrix/key/v2/server/{keyId}.
|
||||
"""
|
||||
def get_signing_keys(conn, _params) do
|
||||
keys =
|
||||
KeyServer.get_own_signing_keys()
|
||||
|> Enum.into(%{}, fn {key_id, key} ->
|
||||
{key_id, %{"key" => key}}
|
||||
end)
|
||||
|
||||
# TODO: Consider using TimeX.
|
||||
# Valid for one month.
|
||||
valid_until = DateTime.utc_now() |> DateTime.add(60 * 60 * 24 * 30, :second)
|
||||
|
||||
data = %{
|
||||
server_name: Architex.server_name(),
|
||||
verify_keys: keys,
|
||||
old_verify_keys: %{},
|
||||
valid_until_ts: DateTime.to_unix(valid_until, :millisecond)
|
||||
}
|
||||
|
||||
case KeyServer.sign_object(data) do
|
||||
{:ok, sig, key_id} ->
|
||||
signed_data = Architex.add_signature(data, key_id, sig)
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(signed_data)
|
||||
|
||||
:error ->
|
||||
put_error(conn, :unknown, "Error signing object.")
|
||||
end
|
||||
end
|
||||
end
|
61
lib/architex_web/federation/controllers/query_controller.ex
Normal file
61
lib/architex_web/federation/controllers/query_controller.ex
Normal file
|
@ -0,0 +1,61 @@
|
|||
defmodule ArchitexWeb.Federation.QueryController do
|
||||
use ArchitexWeb, :controller
|
||||
use ArchitexWeb.Federation.AuthenticateServer
|
||||
|
||||
import ArchitexWeb.Error
|
||||
import Ecto.Query
|
||||
|
||||
alias Architex.{Repo, Account}
|
||||
alias Architex.Types.UserId
|
||||
|
||||
defmodule ProfileRequest do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :user_id, UserId
|
||||
field :field, :string
|
||||
end
|
||||
|
||||
def validate(params) do
|
||||
%__MODULE__{}
|
||||
|> cast(params, [:user_id, :field])
|
||||
|> validate_required([:user_id])
|
||||
|> validate_inclusion(:field, ["displayname", "avatar_url"])
|
||||
|> then(fn
|
||||
%Ecto.Changeset{valid?: true} = cs -> {:ok, apply_changes(cs)}
|
||||
_ -> :error
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a query to get profile information, such as a display name or avatar,
|
||||
for a given user.
|
||||
|
||||
Action for GET /_matrix/federation/v1/query/profile.
|
||||
"""
|
||||
def profile(conn, params) do
|
||||
with {:ok, %ProfileRequest{user_id: %UserId{localpart: localpart, domain: domain}}} <-
|
||||
ProfileRequest.validate(params) do
|
||||
if domain == Architex.server_name() do
|
||||
case Repo.one(from a in Account, where: a.localpart == ^localpart) do
|
||||
%Account{} ->
|
||||
# TODO: Return displayname and avatar_url when we implement them.
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{})
|
||||
|
||||
nil ->
|
||||
put_error(conn, :not_found, "User does not exist.")
|
||||
end
|
||||
else
|
||||
put_error(conn, :not_found, "Wrong server name.")
|
||||
end
|
||||
else
|
||||
_ -> put_error(conn, :bad_json)
|
||||
end
|
||||
end
|
||||
end
|
92
lib/architex_web/federation/http_client.ex
Normal file
92
lib/architex_web/federation/http_client.ex
Normal file
|
@ -0,0 +1,92 @@
|
|||
defmodule ArchitexWeb.Federation.HTTPClient do
|
||||
use Tesla
|
||||
|
||||
alias ArchitexWeb.Endpoint
|
||||
alias ArchitexWeb.Federation.Request.GetSigningKeys
|
||||
alias ArchitexWeb.Federation.Middleware.SignRequest
|
||||
alias ArchitexWeb.Router.Helpers, as: RouteHelpers
|
||||
|
||||
# TODO: Maybe create database-backed homeserver struct to pass to client function.
|
||||
# TODO: Fix error propagation.
|
||||
|
||||
@adapter {Tesla.Adapter.Finch, name: ArchitexWeb.HTTPClient}
|
||||
|
||||
def client(server_name) do
|
||||
Tesla.client(
|
||||
[
|
||||
{Tesla.Middleware.Opts, [server_name: server_name]},
|
||||
SignRequest,
|
||||
{Tesla.Middleware.BaseUrl, "http://" <> server_name},
|
||||
Tesla.Middleware.JSON
|
||||
],
|
||||
@adapter
|
||||
)
|
||||
end
|
||||
|
||||
def get_signing_keys(client) do
|
||||
path = RouteHelpers.key_path(Endpoint, :get_signing_keys)
|
||||
|
||||
with {:ok,
|
||||
%GetSigningKeys{server_name: server_name, verify_keys: verify_keys, signatures: sigs} =
|
||||
response} <- tesla_request(:get, client, path, GetSigningKeys),
|
||||
serializable_response <- Architex.to_serializable_map(response),
|
||||
serializable_response <- Map.drop(serializable_response, [:signatures]),
|
||||
{:ok, encoded_body} <- Architex.encode_canonical_json(serializable_response),
|
||||
server_sigs when not is_nil(server_sigs) <- sigs[server_name] do
|
||||
# For each verify key, check if there is a matching signature.
|
||||
# If not, invalidate the whole response.
|
||||
Enum.all?(verify_keys, fn {key_id, %{"key" => key}} ->
|
||||
with true <- Map.has_key?(server_sigs, key_id),
|
||||
{:ok, decoded_key} <- Architex.decode_base64(key),
|
||||
{:ok, decoded_sig} <- Architex.decode_base64(server_sigs[key_id]) do
|
||||
Architex.sign_verify(decoded_sig, encoded_body, decoded_key)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
|> then(fn
|
||||
true -> {:ok, response}
|
||||
false -> :error
|
||||
end)
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def query_profile(client, user_id, field \\ nil) do
|
||||
path = RouteHelpers.query_path(Endpoint, :profile) |> Tesla.build_url(user_id: user_id)
|
||||
path = if field, do: Tesla.build_url(path, field: field), else: path
|
||||
|
||||
Tesla.get(client, path)
|
||||
end
|
||||
|
||||
def get_event(client, event_id) do
|
||||
path = RouteHelpers.event_path(Endpoint, :event, event_id)
|
||||
|
||||
Tesla.get(client, path)
|
||||
end
|
||||
|
||||
def get_state(client, room_id, event_id) do
|
||||
path =
|
||||
RouteHelpers.event_path(Endpoint, :state, room_id) |> Tesla.build_url(event_id: event_id)
|
||||
|
||||
Tesla.get(client, path)
|
||||
end
|
||||
|
||||
def get_state_ids(client, room_id, event_id) do
|
||||
path =
|
||||
RouteHelpers.event_path(Endpoint, :state_ids, room_id)
|
||||
|> Tesla.build_url(event_id: event_id)
|
||||
|
||||
Tesla.get(client, path)
|
||||
end
|
||||
|
||||
defp tesla_request(method, client, path, request_schema) do
|
||||
with {:ok, %Tesla.Env{body: body}} <- Tesla.request(client, url: path, method: method),
|
||||
%Ecto.Changeset{valid?: true} = cs <- apply(request_schema, :changeset, [body]) do
|
||||
{:ok, Ecto.Changeset.apply_changes(cs)}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
end
|
36
lib/architex_web/federation/request/get_signing_keys.ex
Normal file
36
lib/architex_web/federation/request/get_signing_keys.ex
Normal file
|
@ -0,0 +1,36 @@
|
|||
defmodule ArchitexWeb.Federation.Request.GetSigningKeys do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :server_name, :string
|
||||
field :verify_keys, {:map, {:map, :string}}
|
||||
field :old_verify_keys, {:map, :map}
|
||||
field :signatures, {:map, {:map, :string}}
|
||||
field :valid_until_ts, :integer
|
||||
end
|
||||
|
||||
def changeset(params) do
|
||||
# TODO: There must be a better way to validate embedded maps?
|
||||
%__MODULE__{}
|
||||
|> cast(params, [:server_name, :verify_keys, :old_verify_keys, :signatures, :valid_until_ts])
|
||||
|> validate_required([:server_name, :verify_keys, :valid_until_ts])
|
||||
|> Architex.validate_change_simple(:verify_keys, fn map ->
|
||||
Enum.all?(map, fn {_, map} ->
|
||||
is_map_key(map, "key")
|
||||
end)
|
||||
end)
|
||||
|> Architex.validate_change_simple(:old_verify_keys, fn map ->
|
||||
Enum.all?(map, fn
|
||||
{_, %{"key" => key, "expired_ts" => expired_ts}}
|
||||
when is_binary(key) and is_integer(expired_ts) ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
41
lib/architex_web/federation/sign_request_middleware.ex
Normal file
41
lib/architex_web/federation/sign_request_middleware.ex
Normal file
|
@ -0,0 +1,41 @@
|
|||
defmodule ArchitexWeb.Federation.Middleware.SignRequest do
|
||||
@behaviour Tesla.Middleware
|
||||
|
||||
def call(%Tesla.Env{opts: opts} = env, next, _opts) do
|
||||
sign = Keyword.get(opts, :sign, true)
|
||||
|
||||
case sign_request(env, sign) do
|
||||
%Tesla.Env{} = env -> Tesla.run(env, next)
|
||||
:error -> {:error, :sign_request}
|
||||
end
|
||||
end
|
||||
|
||||
defp sign_request(env, false), do: env
|
||||
|
||||
defp sign_request(%Tesla.Env{method: method, url: path, opts: opts, body: body} = env, true) do
|
||||
origin = Architex.server_name()
|
||||
|
||||
object_to_sign = %{
|
||||
method: Atom.to_string(method) |> String.upcase(),
|
||||
origin: origin,
|
||||
uri: URI.decode_www_form(path),
|
||||
destination: Keyword.fetch!(opts, :server_name)
|
||||
}
|
||||
|
||||
object_to_sign =
|
||||
if not is_nil(body), do: Map.put(object_to_sign, :content, body), else: object_to_sign
|
||||
|
||||
with {:ok, sig, key_id} <- Architex.KeyServer.sign_object(object_to_sign) do
|
||||
sigs = %{origin => %{key_id => sig}}
|
||||
auth_headers = create_signature_authorization_headers(sigs, origin)
|
||||
|
||||
Tesla.put_headers(env, auth_headers)
|
||||
end
|
||||
end
|
||||
|
||||
defp create_signature_authorization_headers(signatures, origin) do
|
||||
Enum.map(signatures[origin], fn {key, sig} ->
|
||||
{"Authorization", "X-Matrix origin=#{origin},key=\"#{key}\",sig=\"#{sig}\""}
|
||||
end)
|
||||
end
|
||||
end
|
36
lib/architex_web/federation/transaction.ex
Normal file
36
lib/architex_web/federation/transaction.ex
Normal file
|
@ -0,0 +1,36 @@
|
|||
defmodule ArchitexWeb.Federation.Transaction do
|
||||
alias Architex.Event
|
||||
alias ArchitexWeb.Federation.Transaction
|
||||
|
||||
# TODO
|
||||
@type edu :: any()
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
origin: String.t(),
|
||||
origin_server_ts: integer(),
|
||||
pdus: [Event.t()],
|
||||
edus: [edu()] | nil
|
||||
}
|
||||
|
||||
defstruct [:origin, :origin_server_ts, :pdus, :edus]
|
||||
|
||||
defimpl Jason.Encoder, for: Transaction do
|
||||
@fields [:origin, :origin_server_ts, :pdus, :edus]
|
||||
|
||||
def encode(transaction, opts) do
|
||||
transaction
|
||||
|> Map.take(@fields)
|
||||
|> Jason.Encode.map(opts)
|
||||
end
|
||||
end
|
||||
|
||||
@spec new([Event.t()], [edu()] | nil) :: t()
|
||||
def new(pdu_events, edus \\ nil) do
|
||||
%Transaction{
|
||||
origin: Architex.server_name(),
|
||||
origin_server_ts: System.os_time(:millisecond),
|
||||
pdus: Enum.map(pdu_events, &Architex.to_serializable_map/1),
|
||||
edus: edus
|
||||
}
|
||||
end
|
||||
end
|
26
lib/architex_web/request.ex
Normal file
26
lib/architex_web/request.ex
Normal file
|
@ -0,0 +1,26 @@
|
|||
defmodule ArchitexWeb.Request do
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Ecto.Changeset
|
||||
|
||||
@spec parse(module(), map()) :: {:ok, struct()} | {:error, Changeset.t()}
|
||||
def parse(module, params) do
|
||||
case apply(module, :changeset, [struct(module), params]) do
|
||||
%Ecto.Changeset{valid?: true} = cs -> {:ok, apply_changes(cs)}
|
||||
cs -> {:error, cs}
|
||||
end
|
||||
end
|
||||
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@spec parse(map()) :: {:ok, struct()} | {:error, Changeset.t()}
|
||||
def parse(params) do
|
||||
ArchitexWeb.Request.parse(__MODULE__, params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
89
lib/architex_web/router.ex
Normal file
89
lib/architex_web/router.ex
Normal file
|
@ -0,0 +1,89 @@
|
|||
defmodule ArchitexWeb.Router do
|
||||
use ArchitexWeb, :router
|
||||
|
||||
alias ArchitexWeb.Client.Plug.AuthenticateClient
|
||||
|
||||
# TODO: might be able to handle malformed JSON with custom body reader:
|
||||
# https://elixirforum.com/t/write-malformed-json-in-the-body-plug/30578/13
|
||||
|
||||
# TODO: Split endpoint into client and federation?
|
||||
|
||||
pipeline :public do
|
||||
plug :accepts, ["json"]
|
||||
end
|
||||
|
||||
pipeline :authenticate_client do
|
||||
plug :accepts, ["json"]
|
||||
plug AuthenticateClient
|
||||
end
|
||||
|
||||
pipeline :authenticate_server do
|
||||
plug :accepts, ["json"]
|
||||
end
|
||||
|
||||
# Public client endpoint.
|
||||
scope "/_matrix/client", ArchitexWeb.Client do
|
||||
pipe_through :public
|
||||
|
||||
scope "/r0" do
|
||||
post "/register", RegisterController, :register
|
||||
get "/register/available", AccountController, :available
|
||||
get "/login", LoginController, :login_types
|
||||
post "/login", LoginController, :login
|
||||
get "/directory/list/room/:room_id", RoomDirectoryController, :get_visibility
|
||||
end
|
||||
|
||||
get "/versions", InfoController, :versions
|
||||
end
|
||||
|
||||
# Public federation endpoint.
|
||||
scope "/_matrix", ArchitexWeb.Federation do
|
||||
scope "/key/v2" do
|
||||
get "/server", KeyController, :get_signing_keys
|
||||
end
|
||||
end
|
||||
|
||||
# Authenticated client endpoint.
|
||||
scope "/_matrix/client", ArchitexWeb.Client do
|
||||
pipe_through :authenticate_client
|
||||
|
||||
scope "/r0" do
|
||||
get "/account/whoami", AccountController, :whoami
|
||||
post "/logout", AccountController, :logout
|
||||
post "/logout/all", AccountController, :logout_all
|
||||
post "/createRoom", RoomController, :create
|
||||
get "/joined_rooms", RoomController, :joined_rooms
|
||||
|
||||
scope "/directory" do
|
||||
put "/room/:alias", AliasesController, :create
|
||||
put "/list/room/:room_id", RoomDirectoryController, :set_visibility
|
||||
end
|
||||
|
||||
scope "/rooms/:room_id" do
|
||||
post "/invite", RoomController, :invite
|
||||
post "/join", RoomController, :join
|
||||
post "/leave", RoomController, :leave
|
||||
post "/kick", RoomController, :kick
|
||||
post "/ban", RoomController, :ban
|
||||
post "/unban", RoomController, :unban
|
||||
put "/send/:event_type/:txn_id", RoomController, :send_message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Authenticated federation endpoint.
|
||||
scope "/_matrix/federation", ArchitexWeb.Federation do
|
||||
pipe_through :authenticate_server
|
||||
|
||||
scope "/v1" do
|
||||
get "/query/profile", QueryController, :profile
|
||||
get "/event/:event_id", EventController, :event
|
||||
get "/state/:room_id", EventController, :state
|
||||
get "/state_ids/:room_id", EventController, :state_ids
|
||||
end
|
||||
end
|
||||
|
||||
scope "/", ArchitexWeb.Client do
|
||||
match :*, "/*path", InfoController, :unrecognized
|
||||
end
|
||||
end
|
55
lib/architex_web/telemetry.ex
Normal file
55
lib/architex_web/telemetry.ex
Normal file
|
@ -0,0 +1,55 @@
|
|||
defmodule ArchitexWeb.Telemetry do
|
||||
use Supervisor
|
||||
import Telemetry.Metrics
|
||||
|
||||
def start_link(arg) do
|
||||
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_arg) do
|
||||
children = [
|
||||
# Telemetry poller will execute the given period measurements
|
||||
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
|
||||
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
|
||||
# Add reporters as children of your supervision tree.
|
||||
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
def metrics do
|
||||
[
|
||||
# Phoenix Metrics
|
||||
summary("phoenix.endpoint.stop.duration",
|
||||
unit: {:native, :millisecond}
|
||||
),
|
||||
summary("phoenix.router_dispatch.stop.duration",
|
||||
tags: [:route],
|
||||
unit: {:native, :millisecond}
|
||||
),
|
||||
|
||||
# Database Metrics
|
||||
summary("architex.repo.query.total_time", unit: {:native, :millisecond}),
|
||||
summary("architex.repo.query.decode_time", unit: {:native, :millisecond}),
|
||||
summary("architex.repo.query.query_time", unit: {:native, :millisecond}),
|
||||
summary("architex.repo.query.queue_time", unit: {:native, :millisecond}),
|
||||
summary("architex.repo.query.idle_time", unit: {:native, :millisecond}),
|
||||
|
||||
# VM Metrics
|
||||
summary("vm.memory.total", unit: {:byte, :kilobyte}),
|
||||
summary("vm.total_run_queue_lengths.total"),
|
||||
summary("vm.total_run_queue_lengths.cpu"),
|
||||
summary("vm.total_run_queue_lengths.io")
|
||||
]
|
||||
end
|
||||
|
||||
defp periodic_measurements do
|
||||
[
|
||||
# A module, function and arguments to be invoked periodically.
|
||||
# This function must call :telemetry.execute/3 and a metric must be added above.
|
||||
# {ArchitexWeb, :count_users, []}
|
||||
]
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue