diff --git a/lib/account/websocket/channel/account/requests/bootstrap.ex b/lib/account/websocket/channel/account/requests/bootstrap.ex index 325b16aa..e7f753a3 100644 --- a/lib/account/websocket/channel/account/requests/bootstrap.ex +++ b/lib/account/websocket/channel/account/requests/bootstrap.ex @@ -1,31 +1,27 @@ -defmodule Helix.Account.Websocket.Channel.Account.Requests.Bootstrap do +import Helix.Websocket.Request - require Helix.Websocket.Request +request Helix.Account.Websocket.Channel.Account.Requests.Bootstrap do - Helix.Websocket.Request.register() + alias Helix.Account.Public.Account, as: AccountPublic - defimpl Helix.Websocket.Requestable do + def check_params(request, _socket), + do: reply_ok(request) - alias Helix.Websocket.Utils, as: WebsocketUtils - alias Helix.Account.Public.Account, as: AccountPublic + def check_permissions(request, _socket), + do: reply_ok(request) - def check_params(request, _socket), - do: {:ok, request} + def handle_request(request, socket) do + entity_id = socket.assigns.entity_id - def check_permissions(request, _socket), - do: {:ok, request} + meta = %{ + bootstrap: AccountPublic.bootstrap(entity_id) + } - def handle_request(request, socket) do - entity_id = socket.assigns.entity_id - - meta = %{bootstrap: AccountPublic.bootstrap(entity_id)} - - {:ok, %{request| meta: meta}} - end + update_meta(request, meta, reply: true) + end - def reply(request, socket) do - data = AccountPublic.render_bootstrap(request.meta.bootstrap) - WebsocketUtils.reply_ok(data, socket) - end + render(request, _socket) do + data = AccountPublic.render_bootstrap(request.meta.bootstrap) + {:ok, data} end end diff --git a/lib/account/websocket/channel/account/requests/email_reply.ex b/lib/account/websocket/channel/account/requests/email_reply.ex index ae28e8a7..02ec05fe 100644 --- a/lib/account/websocket/channel/account/requests/email_reply.ex +++ b/lib/account/websocket/channel/account/requests/email_reply.ex @@ -1,53 +1,46 @@ -defmodule Helix.Account.Websocket.Channel.Account.Requests.EmailReply do +import Helix.Websocket.Request + +request Helix.Account.Websocket.Channel.Account.Requests.EmailReply do @moduledoc """ Implementation of the `EmailReply` request, which allows the player to send an (storyline) email reply to the Contact (story character) """ - require Helix.Websocket.Request - - Helix.Websocket.Request.register() - - defimpl Helix.Websocket.Requestable do - - alias Helix.Websocket.Utils, as: WebsocketUtils - alias Helix.Story.Public.Story, as: StoryPublic + alias Helix.Story.Public.Story, as: StoryPublic - def check_params(request, _socket) do - with \ - true <- is_binary(request.unsafe["reply_id"]) - do - params = %{ - reply_id: request.unsafe["reply_id"] - } + def check_params(request, _socket) do + with \ + true <- is_binary(request.unsafe["reply_id"]) + do + params = %{ + reply_id: request.unsafe["reply_id"] + } - {:ok, %{request| params: params}} - else - _ -> - {:error, %{message: "bad_request"}} - end + update_params(request, params, reply: true) + else + _ -> + bad_request() end + end - @doc """ - Permissions whether that reply is valid within the player's current context - are checked at StoryPublic- and StoryAction-level - """ - def check_permissions(request, _socket), - do: {:ok, request} - - def handle_request(request, socket) do - entity_id = socket.assigns.entity_id - reply_id = request.params.reply_id - - case StoryPublic.send_reply(entity_id, reply_id) do - :ok -> - {:ok, request} - error -> - error - end + @doc """ + Permissions whether that reply is valid within the player's current context + are checked at StoryPublic- and StoryAction-level + """ + def check_permissions(request, _socket), + do: reply_ok(request) + + def handle_request(request, socket) do + entity_id = socket.assigns.entity_id + reply_id = request.params.reply_id + + case StoryPublic.send_reply(entity_id, reply_id) do + :ok -> + reply_ok(request) + error -> + reply_error(error) end - - def reply(_request, socket), - do: WebsocketUtils.reply_ok(%{}, socket) end + + render_empty() end diff --git a/lib/hell/hell/utils.ex b/lib/hell/hell/utils.ex index 8d0fb6c5..bacdf3df 100644 --- a/lib/hell/hell/utils.ex +++ b/lib/hell/hell/utils.ex @@ -28,25 +28,35 @@ defmodule HELL.Utils do def ensure_list(value), do: [value] + @spec concat_atom(atom | String.t, atom | String.t) :: + atom @doc """ Concatenates two elements, returning an atom. """ - def concat_atom(a, b) when is_atom(a) and is_atom(b), - do: concat_atom(Atom.to_string(a), Atom.to_string(b)) - def concat_atom(a, b) when is_binary(a) and is_atom(b), - do: concat_atom(a, Atom.to_string(b)) - def concat_atom(a, b) when is_atom(a) and is_binary(b), + def concat_atom(a, b) when is_atom(a), do: concat_atom(Atom.to_string(a), b) + def concat_atom(a, b) when is_atom(b), + do: concat_atom(a, Atom.to_string(b)) def concat_atom(a, b) when is_binary(a) and is_binary(b), do: String.to_atom(a <> b) + @spec concat(atom | String.t, atom | String.t) :: + String.t @doc """ Concatenates two strings. It's a more readable option to Kernel.<>/2, intended - to be used on pipes. + to be used on pipes. It can also handle concatenation of atoms, in which case + this function will always return a string. See also `concat_atom/2`. """ + def concat(a, b) when is_atom(a), + do: concat(Atom.to_string(a), b) + def concat(a, b) when is_atom(b), + do: concat(a, Atom.to_string(b)) def concat(a, b) when is_binary(a) and is_binary(b), do: a <> b + def concat(a, b, c), + do: concat(a, b) |> concat(c) + def atom_contains?(a, value) when is_atom(a) do a |> Atom.to_string() diff --git a/lib/server/websocket/channel/server.ex b/lib/server/websocket/channel/server.ex index 423a0ec8..1020f15f 100644 --- a/lib/server/websocket/channel/server.ex +++ b/lib/server/websocket/channel/server.ex @@ -13,6 +13,9 @@ defmodule Helix.Server.Websocket.Channel.Server do alias Helix.Websocket.Socket, as: Websocket + alias Helix.Software.Websocket.Requests.PFTP.Server.Enable, + as: PFTPServerEnableRequest + alias Helix.Server.Websocket.Channel.Server.Join, as: ServerJoin alias Helix.Server.Websocket.Channel.Server.Requests.Bootstrap, as: BootstrapRequest @@ -88,6 +91,16 @@ defmodule Helix.Server.Websocket.Channel.Server do Websocket.handle_request(request, socket) end + @doc """ + Activates/enables the PublicFTP server of the player. + Params: + None + """ + def handle_in("pftp.server.enable", params, socket) do + request = PFTPServerEnableRequest.new(params) + Websocket.handle_request(request, socket) + end + @doc """ Browses to the specified address, which may be an IPv4 or domain name. diff --git a/lib/server/websocket/channel/server/requests/bootstrap.ex b/lib/server/websocket/channel/server/requests/bootstrap.ex index b8a547c2..2549f44e 100644 --- a/lib/server/websocket/channel/server/requests/bootstrap.ex +++ b/lib/server/websocket/channel/server/requests/bootstrap.ex @@ -1,38 +1,41 @@ -defmodule Helix.Server.Websocket.Channel.Server.Requests.Bootstrap do +import Helix.Websocket.Request - require Helix.Websocket.Request +request Helix.Server.Websocket.Channel.Server.Requests.Bootstrap do + @moduledoc """ + ServerBootstrapRequest is used to allow the client to resync its local data + with the Helix server. - Helix.Websocket.Request.register() + It returns the ServerBootstrap, which is the exact same struct returned after + joining a local or remote server Channel. + """ - defimpl Helix.Websocket.Requestable do + alias Helix.Server.Public.Server, as: ServerPublic - alias Helix.Websocket.Utils, as: WebsocketUtils - alias Helix.Server.Public.Server, as: ServerPublic + def check_params(request, _socket), + do: {:ok, request} - def check_params(request, _socket), - do: {:ok, request} + def check_permissions(request, socket) do - def check_permissions(request, socket) do - - if socket.assigns.meta.access_type == :remote do - {:ok, request} - else - {:error, %{message: "own_server_bootstrap"}} - end + if socket.assigns.meta.access_type == :remote do + reply_ok(request) + else + reply_error("own_server_bootstrap") end + end - def handle_request(request, socket) do - entity_id = socket.assigns.entity_id - server_id = socket.assigns.destination.server_id + def handle_request(request, socket) do + entity_id = socket.assigns.entity_id + server_id = socket.assigns.destination.server_id - meta = %{bootstrap: ServerPublic.bootstrap(server_id, entity_id)} + meta = %{ + bootstrap: ServerPublic.bootstrap(server_id, entity_id) + } - {:ok, %{request| meta: meta}} - end + update_meta(request, meta, reply: true) + end - def reply(request, socket) do - data = ServerPublic.render_bootstrap(request.meta.bootstrap) - WebsocketUtils.reply_ok(data, socket) - end + render(request, _socket) do + data = ServerPublic.render_bootstrap(request.meta.bootstrap) + {:ok, data} end end diff --git a/lib/server/websocket/channel/server/requests/browse.ex b/lib/server/websocket/channel/server/requests/browse.ex index 4341a49e..a858c584 100644 --- a/lib/server/websocket/channel/server/requests/browse.ex +++ b/lib/server/websocket/channel/server/requests/browse.ex @@ -1,91 +1,84 @@ -defmodule Helix.Server.Websocket.Channel.Server.Requests.Browse do +import Helix.Websocket.Request - require Helix.Websocket.Request +request Helix.Server.Websocket.Channel.Server.Requests.Browse do - Helix.Websocket.Request.register() + alias Helix.Network.Model.Network + alias Helix.Network.Henforcer.Network, as: NetworkHenforcer + alias Helix.Server.Model.Server + alias Helix.Server.Public.Server, as: ServerPublic - defimpl Helix.Websocket.Requestable do + def check_params(request, socket) do + gateway_id = socket.assigns.gateway.server_id + destination_id = socket.assigns.destination.server_id - alias Helix.Websocket.Utils, as: WebsocketUtils - alias Helix.Network.Model.Network - alias Helix.Network.Henforcer.Network, as: NetworkHenforcer - alias Helix.Server.Model.Server - alias Helix.Server.Public.Server, as: ServerPublic - - def check_params(request, socket) do - gateway_id = socket.assigns.gateway.server_id - destination_id = socket.assigns.destination.server_id - - origin_id = - if Map.has_key?(request.unsafe, "origin") do - request.unsafe["origin"] - else - socket.assigns.destination.server_id - end - - with \ - {:ok, network_id} <- - Network.ID.cast(request.unsafe["network_id"]), - {:ok, origin_id} <- Server.ID.cast(origin_id), - true <- - NetworkHenforcer.valid_origin?(origin_id, gateway_id, destination_id) - || :badorigin - do - validated_params = %{ - network_id: network_id, - address: request.unsafe["address"], - origin: origin_id - } - - {:ok, %{request| params: validated_params}} + origin_id = + if Map.has_key?(request.unsafe, "origin") do + request.unsafe["origin"] else - :badorigin -> - {:error, %{message: "bad_origin"}} - _ -> - {:error, %{message: "bad_request"}} + socket.assigns.destination.server_id end + + with \ + {:ok, network_id} <- + Network.ID.cast(request.unsafe["network_id"]), + {:ok, origin_id} <- Server.ID.cast(origin_id), + true <- + NetworkHenforcer.valid_origin?(origin_id, gateway_id, destination_id) + || :badorigin + do + validated_params = %{ + network_id: network_id, + address: request.unsafe["address"], + origin: origin_id + } + + update_params(request, validated_params, reply: true) + else + :badorigin -> + reply_error("bad_origin") + _ -> + bad_request() end + end - def check_permissions(request, _socket), - do: {:ok, request} + def check_permissions(request, _socket), + do: {:ok, request} - def handle_request(request, _socket) do - network_id = request.params.network_id - origin_id = request.params.origin - address = request.params.address + def handle_request(request, _socket) do + network_id = request.params.network_id + origin_id = request.params.origin + address = request.params.address - case ServerPublic.network_browse(network_id, address, origin_id) do - {:ok, web} -> - meta = %{web: web} + case ServerPublic.network_browse(network_id, address, origin_id) do + {:ok, web} -> + update_meta(request, %{web: web}, reply: true) - {:ok, %{request| meta: meta}} - error = {:error, %{message: _}} -> - error - end + {:error, %{message: reason}} -> + reply_error(reason) end + end - def reply(request, socket) do - web = request.meta.web + render(request, _socket) do + web = request.meta.web - [network_id, ip] = web.nip + [network_id, ip] = web.nip - type = - if web.subtype do - to_string(web.type) <> "_" <> to_string(web.subtype) - else - to_string(web.type) - end + type = + if web.subtype do + to_string(web.type) <> "_" <> to_string(web.subtype) + else + to_string(web.type) + end - data = %{ - content: web.content, - type: type, - meta: %{ - nip: [to_string(network_id), to_string(ip)], - password: web.password - } + data = %{ + content: web.content, + type: type, + meta: %{ + nip: [to_string(network_id), to_string(ip)], + password: web.password } + } - WebsocketUtils.reply_ok(data, socket) - end + {:ok, data} end end diff --git a/lib/server/websocket/channel/server/requests/bruteforce.ex b/lib/server/websocket/channel/server/requests/bruteforce.ex index 9365807d..65df4971 100644 --- a/lib/server/websocket/channel/server/requests/bruteforce.ex +++ b/lib/server/websocket/channel/server/requests/bruteforce.ex @@ -1,90 +1,81 @@ -defmodule Helix.Server.Websocket.Channel.Server.Requests.Bruteforce do +import Helix.Websocket.Request - require Helix.Websocket.Request +request Helix.Server.Websocket.Channel.Server.Requests.Bruteforce do - Helix.Websocket.Request.register() + alias HELL.IPv4 + alias Helix.Network.Model.Network + alias Helix.Software.Henforcer.File.Cracker, as: CrackerHenforcer + alias Helix.Server.Model.Server + alias Helix.Server.Public.Server, as: ServerPublic - defimpl Helix.Websocket.Requestable do + def check_params(request, socket) do + with \ + {:ok, network_id} <- + Network.ID.cast(request.unsafe["network_id"]), + true <- IPv4.valid?(request.unsafe["ip"]), + {:ok, bounces} = cast_bounces(request.unsafe["bounces"]), + true <- socket.assigns.meta.access_type == :local || :bad_attack_src + do + params = %{ + bounces: bounces, + network_id: network_id, + ip: request.unsafe["ip"] + } - alias HELL.IPv4 - alias Helix.Websocket.Utils, as: WebsocketUtils - alias Helix.Network.Model.Network - alias Helix.Software.Henforcer.File.Cracker, as: CrackerHenforcer - alias Helix.Server.Model.Server - alias Helix.Server.Public.Server, as: ServerPublic - - def check_params(request, socket) do - with \ - {:ok, network_id} <- - Network.ID.cast(request.unsafe["network_id"]), - true <- IPv4.valid?(request.unsafe["ip"]), - {:ok, bounces} = cast_bounces(request.unsafe["bounces"]), - true <- socket.assigns.meta.access_type == :local || :bad_attack_src - do - params = %{ - bounces: bounces, - network_id: network_id, - ip: request.unsafe["ip"] - } - - {:ok, %{request| params: params}} - else - :bad_attack_src -> - {:error, %{message: "bad_attack_src"}} - _ -> - {:error, %{message: "bad_request"}} - end + update_params(request, params, reply: true) + else + :bad_attack_src -> + reply_error("bad_attack_src") + _ -> + bad_request() end + end - def check_permissions(request, socket) do - network_id = request.params.network_id - source_id = socket.assigns.gateway.server_id - source_ip = socket.assigns.gateway.ip - ip = request.params.ip + def check_permissions(request, socket) do + network_id = request.params.network_id + source_id = socket.assigns.gateway.server_id + source_ip = socket.assigns.gateway.ip + ip = request.params.ip - can_bruteforce = - CrackerHenforcer.can_bruteforce(source_id, source_ip, network_id, ip) + can_bruteforce = + CrackerHenforcer.can_bruteforce(source_id, source_ip, network_id, ip) - case can_bruteforce do - :ok -> - {:ok, request} - {:error, {:target, :self}} -> - {:error, %{message: "target_self"}} - end + case can_bruteforce do + :ok -> + reply_ok(request) + {:error, {:target, :self}} -> + reply_error("target_self") end + end - def handle_request(request, socket) do - source_id = socket.assigns.gateway.server_id - network_id = request.params.network_id - ip = request.params.ip - bounces = request.params.bounces - - case ServerPublic.bruteforce(source_id, network_id, ip, bounces) do - {:ok, process} -> - meta = %{process: process} + def handle_request(request, socket) do + source_id = socket.assigns.gateway.server_id + network_id = request.params.network_id + ip = request.params.ip + bounces = request.params.bounces - {:ok, %{request| meta: meta}} + case ServerPublic.bruteforce(source_id, network_id, ip, bounces) do + {:ok, process} -> + update_meta(request, %{process: process}, reply: true) - # HACK: Workaround for https://github.com/elixir-lang/elixir/issues/6426 - error = {_, m} -> - if Map.has_key?(m, :message) do - error - else - {:error, %{message: "internal"}} - end - # error = {:error, %{message: _}} -> - # error - # _ -> - # {:error, %{message: "internal"}} - end + # HACK: Workaround for https://github.com/elixir-lang/elixir/issues/6426 + error = {_, m} -> + if Map.has_key?(m, :message) do + error + else + internal_error() + end + # error = {:error, %{message: _}} -> + # error + # _ -> + # {:error, %{message: "internal"}} end + end - def reply(request, socket), - do: WebsocketUtils.reply_process(request.meta.process, socket) + render_process() - defp cast_bounces(bounces) when is_list(bounces), - do: {:ok, Enum.map(bounces, &(Server.ID.cast!(&1)))} - defp cast_bounces(_), - do: :error - end + defp cast_bounces(bounces) when is_list(bounces), + do: {:ok, Enum.map(bounces, &(Server.ID.cast!(&1)))} + defp cast_bounces(_), + do: :error end diff --git a/lib/server/websocket/channel/server/requests/file_download.ex b/lib/server/websocket/channel/server/requests/file_download.ex index 367869f3..8fc184cb 100644 --- a/lib/server/websocket/channel/server/requests/file_download.ex +++ b/lib/server/websocket/channel/server/requests/file_download.ex @@ -1,131 +1,114 @@ -defmodule Helix.Server.Websocket.Channel.Server.Requests.FileDownload do - - require Helix.Websocket.Request - - Helix.Websocket.Request.register() - - defimpl Helix.Websocket.Requestable do - - import HELL.Macros - - alias Helix.Websocket.Utils, as: WebsocketUtils - alias Helix.Cache.Query.Cache, as: CacheQuery - alias Helix.Software.Model.File - alias Helix.Software.Model.Storage - alias Helix.Software.Henforcer.File.Transfer, as: FileTransferHenforcer - alias Helix.Server.Model.Server - alias Helix.Server.Public.Server, as: ServerPublic - - # Hack for elixir-lang issue #6577 - @dialyzer({:nowarn_function, get_error: 1}) - - def check_params(request, socket) do - # Fetches the server's main storage if none were specified - unsafe_storage_id = - if Map.has_key?(request.unsafe, "storage_id") do - request.unsafe["storage_id"] - else - get_download_storage(socket.assigns.gateway.server_id) - end - - with \ - true <- socket.assigns.meta.access_type == :remote || :bad_access, - {:ok, file_id} <- File.ID.cast(request.unsafe["file_id"]), - {:ok, storage_id} <- Storage.ID.cast(unsafe_storage_id) - do - params = %{ - file_id: file_id, - storage_id: storage_id - } +import Helix.Websocket.Request + +request Helix.Server.Websocket.Channel.Server.Requests.FileDownload do + + import HELL.Macros + + alias Helix.Cache.Query.Cache, as: CacheQuery + alias Helix.Software.Model.File + alias Helix.Software.Model.Storage + alias Helix.Software.Henforcer.File.Transfer, as: FileTransferHenforcer + alias Helix.Server.Model.Server + alias Helix.Server.Public.Server, as: ServerPublic - {:ok, %{request| params: params}} + # Hack for elixir-lang issue #6577 + @dialyzer {:nowarn_function, get_error: 1} + + def check_params(request, socket) do + # Fetches the server's main storage if none were specified + unsafe_storage_id = + if Map.has_key?(request.unsafe, "storage_id") do + request.unsafe["storage_id"] else - :bad_access -> - {:error, %{message: "download_self"}} - _ -> - {:error, %{message: "bad_request"}} + get_download_storage(socket.assigns.gateway.server_id) end - end - @doc """ - Verifies the permission for the download. Most of the permission logic - has been delegated to `FileTransferHenforcer.can_transfer?`, check it out. - - This is where we verify the file being download exists, belongs to the - correct server, the storage belongs to the server, the user has access to - the storage, etc. - """ - def check_permissions(request, socket) do - gateway_id = socket.assigns.gateway.server_id - destination_id = socket.assigns.destination.server_id - file_id = request.params.file_id - storage_id = request.params.storage_id - - can_transfer? = - FileTransferHenforcer.can_transfer?( - :download, - gateway_id, - destination_id, - storage_id, - file_id - ) - - case can_transfer? do - {true, relay} -> - meta = %{ - file: relay.file, - storage: relay.storage - } - - {:ok, %{request| meta: meta}} - - {false, reason, _} -> - {:error, %{message: get_error(reason)}} - end + with \ + true <- socket.assigns.meta.access_type == :remote || :bad_access, + {:ok, file_id} <- File.ID.cast(request.unsafe["file_id"]), + {:ok, storage_id} <- Storage.ID.cast(unsafe_storage_id) + do + params = %{ + file_id: file_id, + storage_id: storage_id + } + + update_params(request, params, reply: true) + else + :bad_access -> + reply_error("download_self") + _ -> + bad_request() end + end - def handle_request(request, socket) do - file = request.meta.file - storage = request.meta.storage - tunnel = socket.assigns.tunnel - - case ServerPublic.file_download(tunnel, storage, file) do - {:ok, process} -> - meta = %{process: process} + @doc """ + Verifies the permission for the download. Most of the permission logic + has been delegated to `FileTransferHenforcer.can_transfer?`, check it out. + + This is where we verify the file being download exists, belongs to the + correct server, the storage belongs to the server, the user has access to + the storage, etc. + """ + def check_permissions(request, socket) do + gateway_id = socket.assigns.gateway.server_id + destination_id = socket.assigns.destination.server_id + file_id = request.params.file_id + storage_id = request.params.storage_id + + can_transfer? = + FileTransferHenforcer.can_transfer?( + :download, + gateway_id, + destination_id, + storage_id, + file_id + ) + + case can_transfer? do + {true, relay} -> + meta = %{ + file: relay.file, + storage: relay.storage + } - {:ok, %{request| meta: meta}} + update_meta(request, meta, reply: true) - {:error, reason} -> - {:error, %{message: get_error(reason)}} - end + {false, reason, _} -> + reply_error(reason) end + end - def reply(request, socket), - do: WebsocketUtils.reply_process(request.meta.process, socket) + def handle_request(request, socket) do + file = request.meta.file + storage = request.meta.storage + tunnel = socket.assigns.tunnel - @spec get_download_storage(Server.id) :: - Storage.id - defp get_download_storage(gateway_id) do - gateway_id - |> CacheQuery.from_server_get_storages() - |> elem(1) - |> List.first() + case ServerPublic.file_download(tunnel, storage, file) do + {:ok, process} -> + update_meta(request, %{process: process}, reply: true) + + {:error, reason} -> + reply_error(reason) end + end - @spec get_error(reason :: {term, term} | term) :: - String.t - docp """ - Error handler for FileDownloadRequest. Should handle all possible returns. - """ - defp get_error({:file, :not_found}), - do: "file_not_found" - defp get_error({:file, :not_belongs}), - do: "file_not_found" - defp get_error({:storage, :full}), - do: "storage_full" - defp get_error({:storage, :not_found}), - do: "storage_not_found" - defp get_error(:internal), - do: "internal" + render_process() + + @spec get_download_storage(Server.id) :: + Storage.id + defp get_download_storage(gateway_id) do + gateway_id + |> CacheQuery.from_server_get_storages() + |> elem(1) + |> List.first() end + + @spec get_error(reason :: {term, term} | term) :: + String.t + docp """ + Error handler for FileDownloadRequest. Should handle all possible returns. + """ + defp get_error({:file, :not_belongs}), + do: "file_not_found" end diff --git a/lib/software/action/public_ftp.ex b/lib/software/action/public_ftp.ex new file mode 100644 index 00000000..3db5e6de --- /dev/null +++ b/lib/software/action/public_ftp.ex @@ -0,0 +1,86 @@ +defmodule Helix.Software.Action.PublicFTP do + + alias Helix.Server.Model.Server + alias Helix.Software.Internal.PublicFTP, as: PublicFTPInternal + alias Helix.Software.Model.File + alias Helix.Software.Model.PublicFTP + alias Helix.Software.Query.PublicFTP, as: PublicFTPQuery + + @spec enable_server(Server.t) :: + {:ok, PublicFTP.t, [term]} + | {:error, {:pftp, :already_enabled}} + def enable_server(server = %Server{}) do + result = + case PublicFTPQuery.fetch_server(server.server_id) do + pftp = %PublicFTP{is_active: false} -> + PublicFTPInternal.enable_server(pftp) + + %PublicFTP{is_active: true} -> + {:error, {:pftp, :already_enabled}} + + nil -> + PublicFTPInternal.setup_server(server.server_id) + end + + with {:ok, pftp} <- result do + {:ok, pftp, []} + end + end + + @spec disable_server(PublicFTP.t) :: + {:ok, PublicFTP.t, [term]} + | {:error, :internal} + def disable_server(pftp = %PublicFTP{is_active: true}) do + case PublicFTPInternal.disable_server(pftp) do + {:ok, pftp} -> + {:ok, pftp, []} + + {:error, _} -> + {:error, :internal} + end + end + + @spec add_file(PublicFTP.t, File.t) :: + {:ok, PublicFTP.File.t, [term]} + | {:error, :internal} + def add_file(pftp = %PublicFTP{is_active: true}, file = %File{}) do + case PublicFTPInternal.add_file(pftp, file.file_id) do + {:ok, pftp_file} -> + # event = PFTPFileAddedEvent.new(pftp_file) + {:ok, pftp_file, []} + + {:error, _} -> + {:error, :internal} + end + end + + @spec remove_file(PublicFTP.t, PublicFTP.File.t) :: + {:ok, PublicFTP.File.t, [term]} + | {:error, :internal} + def remove_file(%PublicFTP{is_active: true}, pftp_file = %PublicFTP.File{}) do + case PublicFTPInternal.remove_file(pftp_file) do + {:ok, pftp_file} -> + {:ok, pftp_file, []} + {:error, _} -> + {:error, :internal} + end + end +end + +defmodule Helix.Software.Query.PublicFTP do + + alias Helix.Software.Internal.PublicFTP, as: PublicFTPInternal + + def fetch_file(server_id, file_id) do + PublicFTPInternal.fetch_file(server_id, file_id) + end + + def fetch_server(server_id) do + PublicFTPInternal.fetch(server_id) + end + + def list_files(server_id) do + PublicFTPInternal.list_files(server_id) + end + +end diff --git a/lib/software/henforcer/file.ex b/lib/software/henforcer/file.ex index a828819f..1dba6b2b 100644 --- a/lib/software/henforcer/file.ex +++ b/lib/software/henforcer/file.ex @@ -67,330 +67,4 @@ defmodule Helix.Software.Henforcer.File do end end end - - defmodule PublicFTP do - - import Helix.Henforcer - - alias Helix.Entity.Model.Entity - alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer - alias Helix.Server.Model.Server - alias Helix.Server.Henforcer.Server, as: ServerHenforcer - alias Helix.Software.Model.PublicFTP - alias Helix.Software.Model.File - alias Helix.Software.Model.Storage - alias Helix.Software.Henforcer.File, as: FileHenforcer - alias Helix.Software.Query.PublicFTP, as: PublicFTPQuery - - @type pftp_exists_relay :: %{pftp: PublicFTP.t, server: Server.t} - @type pftp_exists_relay_partial :: %{server: Server.t} - @type pftp_exists_error :: - {true, {:pftp, :not_found}, pftp_exists_relay_partial} - | ServerHenforcer.server_exists_error - - @spec pftp_exists?(Server.idt) :: - {false, pftp_exists_relay} - | pftp_exists_error - def pftp_exists?(server_id = %Server.ID{}) do - henforce(ServerHenforcer.server_exists?(server_id)) do - pftp_exists?(relay.server) - end - end - - def pftp_exists?(server = %Server{}) do - with pftp = %{} <- PublicFTPQuery.fetch_server(server.server_id) do - reply_ok(%{pftp: pftp}) - else - _ -> - reply_error({:pftp, :not_found}) - end - end - - @type file_exists_relay :: - %{file: File.t, server: Server.t, pftp_file: PublicFTP.Files.t} - @type file_exists_relay_partial :: %{file: File.t, server: Server.t} - @type file_exists_error :: - {false, {:pftp_file, :not_found}, file_exists_relay_partial} - | FileHenforcer.file_exists_error - | ServerHenforcer.server_exists_error - - @spec file_exists?(Server.idt, File.idt) :: - {true, file_exists_relay} - | file_exists_error - def file_exists?(server, file_id = %File.ID{}) do - henforce(FileHenforcer.file_exists?(file_id)) do - file_exists?(server, relay.file) - end - end - - def file_exists?(server_id = %Server.ID{}, file) do - henforce(ServerHenforcer.server_exists?(server_id)) do - file_exists?(relay.server, file) - end - end - - def file_exists?(server = %Server{}, file = %File{}) do - case PublicFTPQuery.fetch_file(server.server_id, file.file_id) do - pftp_file = %PublicFTP.Files{} -> - reply_ok(%{pftp_file: pftp_file}) - - _ -> - reply_error({:pftp_file, :not_found}) - end - |> wrap_relay(%{server: server, file: file}) - end - - @type not_file_exists_relay :: file_exists_relay_partial - @type not_file_exists_relay_partial :: file_exists_relay - @type not_file_exists_error :: - {false, {:file, :exists}, not_file_exists_relay_partial} - | file_exists_error - - @spec not_file_exists?(Server.idt, File.idt) :: - {true, not_file_exists_relay} - | not_file_exists_error - def not_file_exists?(server, file) do - henforce_not(file_exists?(server, file), {:file, :exists}) - end - - @type pftp_enabled_relay :: %{pftp: PublicFTP.t, server: Server.t} - @type pftp_enabled_relay_partial :: pftp_enabled_relay - @type pftp_enabled_error :: - {false, {:pftp, :disabled}, pftp_enabled_relay_partial} - | pftp_exists_error - | ServerHenforcer.server_exists_error - - @spec pftp_enabled?(Server.idt | PublicFTP.t) :: - {true, pftp_enabled_relay} - | pftp_enabled_error - def pftp_enabled?(server_id = %Server.ID{}) do - henforce ServerHenforcer.server_exists?(server_id) do - pftp_enabled?(relay.server) - end - end - - def pftp_enabled?(server = %Server{}) do - henforce pftp_exists?(server) do - pftp_enabled?(relay.pftp) - end - end - - def pftp_enabled?(%PublicFTP{is_active: true}), - do: reply_ok() - def pftp_enabled?(%PublicFTP{is_active: false}), - do: reply_error({:pftp, :disabled}) - - @type pftp_disabled_relay :: - %{pftp: PublicFTP.t, server: Server.t} - @type pftp_disabled_relay_partial :: %{server: Server.t, pftp: PublicFTP.t} - @type pftp_disabled_error :: - {false, {:pftp, :enabled}, pftp_disabled_relay_partial} - | pftp_exists_error - | ServerHenforcer.server_exists_error - - @spec pftp_disabled?(Server.idt | PublicFTP.t) :: - {true, pftp_disabled_relay} - | pftp_disabled_error - @doc """ - Verifies whether a PublicFTP server is disabled. - - It may be disabled if: - - There's an entry on the database, but the `is_active` field is `false` - - There's no entry on the database. - """ - def pftp_disabled?(server_id = %Server.ID{}) do - henforce ServerHenforcer.server_exists?(server_id) do - pftp_disabled?(relay.server) - end - end - - def pftp_disabled?(server = %Server{}) do - case pftp_exists?(server) do - {true, relay} -> - wrap_relay(pftp_disabled?(relay.pftp), relay) - {false, _, relay} -> - reply_ok(relay) - end - end - - def pftp_disabled?(%PublicFTP{is_active: false}), - do: reply_ok() - def pftp_disabled?(%PublicFTP{is_active: true}), - do: reply_error({:pftp, :enabled}) - - @type can_add_file_relay :: - %{ - file: File.t, - pftp: PublicFTP.t, - server: Server.t, - entity: Entity.t, - storage: Storage.t - } - @type can_add_file_error :: - pftp_enabled_error - | not_file_exists_error - | EntityHenforcer.owns_server_error - | FileHenforcer.belongs_to_server_error - - @spec can_add_file?(Entity.id, Server.id, File.id) :: - {true, can_add_file_relay} - | can_add_file_error - @doc """ - Verifies whether a file can be added to the server's PublicFTP. - Among other things, verifies that: - - The PublicFTP server is enabled - - The file is not already on the PublicFTP - - The entity owns that server - - The file belongs to that server. - """ - def can_add_file?(entity_id, server_id, file_id) do - with \ - {true, r1} <- pftp_enabled?(server_id), - server = r1.server, - {true, r2} <- not_file_exists?(server, file_id), - file = r2.file, - {true, r3} <- EntityHenforcer.owns_server?(entity_id, server), - {true, r4} <- FileHenforcer.belongs_to_server?(file, server) - do - reply_ok(relay([r1, r2, r3, r4])) - end - end - - @type can_remove_file_relay :: - %{ - file: File.t, - pftp: PublicFTP.t, - pftp_file: PublicFTP.Files.t, - server: Server.t, - entity: Entity.t - } - @type can_remove_file_error :: - pftp_enabled_error - | file_exists_error - | EntityHenforcer.owns_server_error - - @spec can_remove_file?(Entity.id, Server.id, File.id) :: - {true, can_remove_file_relay} - | can_remove_file_error - @doc """ - Verifies whether a file can be removed from a PublicFTP server. - Among other things, verifies that: - - The PublicFTP server is enabled - - The file exists on the PublicFTP - - The PublicFTP server belongs to the entity - """ - def can_remove_file?(entity_id, server_id, file_id) do - with \ - {true, r1} <- pftp_enabled?(server_id), - server = r1.server, - {true, r2} <- file_exists?(server, file_id), - {true, r3} <- EntityHenforcer.owns_server?(entity_id, server) - do - reply_ok(relay([r1, r2, r3])) - end - end - - @type can_enable_server_relay :: %{entity: Entity.t, server: Server.t} - @type can_enable_server_error :: - pftp_exists_error - | pftp_disabled_error - - @spec can_enable_server?(Entity.id, Server.id) :: - {true, can_enable_server_relay} - | can_enable_server_error - @doc """ - Henforces an Entity can enable a PublicFTP server. This is the case if: - - - The PublicFTP server is disabled; - - The Entity owns that server - """ - def can_enable_server?(entity_id, server_id) do - with \ - {true, r1} <- pftp_disabled?(server_id), - server = r1.server, - {true, r2} <- EntityHenforcer.owns_server?(entity_id, server) - do - reply_ok(relay(r1, r2)) - end - end - - @type can_disable_server_relay :: - %{pftp: PublicFTP.t, entity: Entity.t, server: Server.t} - @type can_disable_server_error :: - pftp_enabled_error - | EntityHenforcer.owns_server_error - - @spec can_disable_server?(Entity.id, Server.id) :: - {true, can_disable_server_relay} - | can_disable_server_error - @doc """ - Henforcers an Entity can disable a PublicFTP server. This is the case if: - - - The PublicFTP server is enabled; - - The Entity owns that server. - """ - def can_disable_server?(entity_id, server_id) do - with \ - {true, r1} <- pftp_enabled?(server_id), - server = r1.server, - {true, r2} <- EntityHenforcer.owns_server?(entity_id, server) - do - reply_ok(relay(r1, r2)) - end - end - end - - defmodule Transfer do - @moduledoc """ - Henforcers related to file transfer. - """ - - import Helix.Henforcer - - alias Helix.Server.Model.Server - alias Helix.Software.Model.File - alias Helix.Software.Model.Storage - alias Helix.Software.Henforcer.File, as: FileHenforcer - alias Helix.Software.Henforcer.Storage, as: StorageHenforcer - - @type transfer :: :download | :upload - - @spec can_transfer?(transfer, Server.id, Server.id, Storage.id, File.id) :: - {true, %{file: File.t, storage: Storage.t}} - | {false, {:file, :not_belongs | :not_found}, term} - | {false, {:storage, :full | :not_found}, term} - @doc """ - Verifies the FileTransfer can be made. - - Checks: - - File being transferred must come/go from/to a different server. - - The file belongs to the origin server - - The target storage can accommodate the file size - + indirect checks along the way - """ - def can_transfer?(type, gateway_id, endpoint_id, storage_id, file_id) do - {origin_id, target_id} = - if type == :download do - {endpoint_id, gateway_id} - else - {gateway_id, endpoint_id} - end - - with \ - true <- gateway_id != endpoint_id || :self_target, - {true, %{file: file}} <- - FileHenforcer.belongs_to_server?(file_id, origin_id), - {true, %{storage: storage}} <- - StorageHenforcer.belongs_to_server?(storage_id, target_id), - {true, _} <- StorageHenforcer.has_enough_space?(storage, file) - do - {true, relay(%{storage: storage, file: file})} - else - :self_target -> - {false, {:target, :self}, %{}} - error -> - error - end - end - end end diff --git a/lib/software/henforcer/file/public_ftp.ex b/lib/software/henforcer/file/public_ftp.ex new file mode 100644 index 00000000..7e0d9fc1 --- /dev/null +++ b/lib/software/henforcer/file/public_ftp.ex @@ -0,0 +1,274 @@ +defmodule Helix.Software.Henforcer.File.PublicFTP do + @moduledoc """ + Henforcers for PublicFTP operations. + """ + + import Helix.Henforcer + + alias Helix.Entity.Model.Entity + alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer + alias Helix.Server.Model.Server + alias Helix.Server.Henforcer.Server, as: ServerHenforcer + alias Helix.Software.Model.PublicFTP + alias Helix.Software.Model.File + alias Helix.Software.Model.Storage + alias Helix.Software.Henforcer.File, as: FileHenforcer + alias Helix.Software.Query.PublicFTP, as: PublicFTPQuery + + @type pftp_exists_relay :: %{pftp: PublicFTP.t, server: Server.t} + @type pftp_exists_relay_partial :: %{server: Server.t} + @type pftp_exists_error :: + {true, {:pftp, :not_found}, pftp_exists_relay_partial} + | ServerHenforcer.server_exists_error + + @spec pftp_exists?(Server.idt) :: + {false, pftp_exists_relay} + | pftp_exists_error + def pftp_exists?(server_id = %Server.ID{}) do + henforce(ServerHenforcer.server_exists?(server_id)) do + pftp_exists?(relay.server) + end + end + + def pftp_exists?(server = %Server{}) do + with pftp = %{} <- PublicFTPQuery.fetch_server(server.server_id) do + reply_ok(%{pftp: pftp}) + else + _ -> + reply_error({:pftp, :not_found}) + end + end + + @type file_exists_relay :: + %{file: File.t, server: Server.t, pftp_file: PublicFTP.File.t} + @type file_exists_relay_partial :: %{file: File.t, server: Server.t} + @type file_exists_error :: + {false, {:pftp_file, :not_found}, file_exists_relay_partial} + | FileHenforcer.file_exists_error + | ServerHenforcer.server_exists_error + + @spec file_exists?(Server.idt, File.idt) :: + {true, file_exists_relay} + | file_exists_error + def file_exists?(server, file_id = %File.ID{}) do + henforce(FileHenforcer.file_exists?(file_id)) do + file_exists?(server, relay.file) + end + end + + def file_exists?(server_id = %Server.ID{}, file) do + henforce(ServerHenforcer.server_exists?(server_id)) do + file_exists?(relay.server, file) + end + end + + def file_exists?(server = %Server{}, file = %File{}) do + case PublicFTPQuery.fetch_file(server.server_id, file.file_id) do + pftp_file = %PublicFTP.File{} -> + reply_ok(%{pftp_file: pftp_file}) + + _ -> + reply_error({:pftp_file, :not_found}) + end + |> wrap_relay(%{server: server, file: file}) + end + + @type not_file_exists_relay :: file_exists_relay_partial + @type not_file_exists_relay_partial :: file_exists_relay + @type not_file_exists_error :: + {false, {:file, :exists}, not_file_exists_relay_partial} + | file_exists_error + + @spec not_file_exists?(Server.idt, File.idt) :: + {true, not_file_exists_relay} + | not_file_exists_error + def not_file_exists?(server, file) do + henforce_not(file_exists?(server, file), {:file, :exists}) + end + + @type pftp_enabled_relay :: %{pftp: PublicFTP.t, server: Server.t} + @type pftp_enabled_relay_partial :: pftp_enabled_relay + @type pftp_enabled_error :: + {false, {:pftp, :disabled}, pftp_enabled_relay_partial} + | pftp_exists_error + | ServerHenforcer.server_exists_error + + @spec pftp_enabled?(Server.idt | PublicFTP.t) :: + {true, pftp_enabled_relay} + | pftp_enabled_error + def pftp_enabled?(server_id = %Server.ID{}) do + henforce ServerHenforcer.server_exists?(server_id) do + pftp_enabled?(relay.server) + end + end + + def pftp_enabled?(server = %Server{}) do + henforce pftp_exists?(server) do + pftp_enabled?(relay.pftp) + end + end + + def pftp_enabled?(%PublicFTP{is_active: true}), + do: reply_ok() + def pftp_enabled?(%PublicFTP{is_active: false}), + do: reply_error({:pftp, :disabled}) + + @type pftp_disabled_relay :: + %{pftp: PublicFTP.t, server: Server.t} + @type pftp_disabled_relay_partial :: %{server: Server.t, pftp: PublicFTP.t} + @type pftp_disabled_error :: + {false, {:pftp, :enabled}, pftp_disabled_relay_partial} + | pftp_exists_error + | ServerHenforcer.server_exists_error + + @spec pftp_disabled?(Server.idt | PublicFTP.t) :: + {true, pftp_disabled_relay} + | pftp_disabled_error + @doc """ + Verifies whether a PublicFTP server is disabled. + + It may be disabled if: + - There's an entry on the database, but the `is_active` field is `false` + - There's no entry on the database. + """ + def pftp_disabled?(server_id = %Server.ID{}) do + henforce ServerHenforcer.server_exists?(server_id) do + pftp_disabled?(relay.server) + end + end + + def pftp_disabled?(server = %Server{}) do + case pftp_exists?(server) do + {true, relay} -> + wrap_relay(pftp_disabled?(relay.pftp), relay) + {false, _, relay} -> + reply_ok(relay) + end + end + + def pftp_disabled?(%PublicFTP{is_active: false}), + do: reply_ok() + def pftp_disabled?(%PublicFTP{is_active: true}), + do: reply_error({:pftp, :enabled}) + + @type can_add_file_relay :: + %{ + file: File.t, + pftp: PublicFTP.t, + server: Server.t, + entity: Entity.t, + storage: Storage.t + } + @type can_add_file_error :: + pftp_enabled_error + | not_file_exists_error + | EntityHenforcer.owns_server_error + | FileHenforcer.belongs_to_server_error + + @spec can_add_file?(Entity.id, Server.id, File.id) :: + {true, can_add_file_relay} + | can_add_file_error + @doc """ + Verifies whether a file can be added to the server's PublicFTP. + Among other things, verifies that: + - The PublicFTP server is enabled + - The file is not already on the PublicFTP + - The entity owns that server + - The file belongs to that server. + """ + def can_add_file?(entity_id, server_id, file_id) do + with \ + {true, r1} <- pftp_enabled?(server_id), + server = r1.server, + {true, r2} <- not_file_exists?(server, file_id), + file = r2.file, + {true, r3} <- EntityHenforcer.owns_server?(entity_id, server), + {true, r4} <- FileHenforcer.belongs_to_server?(file, server) + do + reply_ok(relay([r1, r2, r3, r4])) + end + end + + @type can_remove_file_relay :: + %{ + file: File.t, + pftp: PublicFTP.t, + pftp_file: PublicFTP.File.t, + server: Server.t, + entity: Entity.t + } + @type can_remove_file_error :: + pftp_enabled_error + | file_exists_error + | EntityHenforcer.owns_server_error + + @spec can_remove_file?(Entity.id, Server.id, File.id) :: + {true, can_remove_file_relay} + | can_remove_file_error + @doc """ + Verifies whether a file can be removed from a PublicFTP server. + Among other things, verifies that: + - The PublicFTP server is enabled + - The file exists on the PublicFTP + - The PublicFTP server belongs to the entity + """ + def can_remove_file?(entity_id, server_id, file_id) do + with \ + {true, r1} <- pftp_enabled?(server_id), + server = r1.server, + {true, r2} <- file_exists?(server, file_id), + {true, r3} <- EntityHenforcer.owns_server?(entity_id, server) + do + reply_ok(relay([r1, r2, r3])) + end + end + + @type can_enable_server_relay :: %{entity: Entity.t, server: Server.t} + @type can_enable_server_error :: + pftp_exists_error + | pftp_disabled_error + + @spec can_enable_server?(Entity.id, Server.id) :: + {true, can_enable_server_relay} + | can_enable_server_error + @doc """ + Henforces an Entity can enable a PublicFTP server. This is the case if: + + - The PublicFTP server is disabled; + - The Entity owns that server + """ + def can_enable_server?(entity_id, server_id) do + with \ + {true, r1} <- pftp_disabled?(server_id), + server = r1.server, + {true, r2} <- EntityHenforcer.owns_server?(entity_id, server) + do + reply_ok(relay(r1, r2)) + end + end + + @type can_disable_server_relay :: + %{pftp: PublicFTP.t, entity: Entity.t, server: Server.t} + @type can_disable_server_error :: + pftp_enabled_error + | EntityHenforcer.owns_server_error + + @spec can_disable_server?(Entity.id, Server.id) :: + {true, can_disable_server_relay} + | can_disable_server_error + @doc """ + Henforcers an Entity can disable a PublicFTP server. This is the case if: + + - The PublicFTP server is enabled; + - The Entity owns that server. + """ + def can_disable_server?(entity_id, server_id) do + with \ + {true, r1} <- pftp_enabled?(server_id), + server = r1.server, + {true, r2} <- EntityHenforcer.owns_server?(entity_id, server) + do + reply_ok(relay(r1, r2)) + end + end +end diff --git a/lib/software/henforcer/file/transfer.ex b/lib/software/henforcer/file/transfer.ex new file mode 100644 index 00000000..092a9b55 --- /dev/null +++ b/lib/software/henforcer/file/transfer.ex @@ -0,0 +1,53 @@ +defmodule Helix.Software.Henforcer.File.Transfer do + @moduledoc """ + Henforcers related to file transfer. + """ + + import Helix.Henforcer + + alias Helix.Server.Model.Server + alias Helix.Software.Model.File + alias Helix.Software.Model.Storage + alias Helix.Software.Henforcer.File, as: FileHenforcer + alias Helix.Software.Henforcer.Storage, as: StorageHenforcer + + @type transfer :: :download | :upload + + @spec can_transfer?(transfer, Server.id, Server.id, Storage.id, File.id) :: + {true, %{file: File.t, storage: Storage.t}} + | {false, {:file, :not_belongs | :not_found}, term} + | {false, {:storage, :full | :not_found}, term} + @doc """ + Verifies the FileTransfer can be made. + + Checks: + - File being transferred must come/go from/to a different server. + - The file belongs to the origin server + - The target storage can accommodate the file size + + indirect checks along the way + """ + def can_transfer?(type, gateway_id, endpoint_id, storage_id, file_id) do + {origin_id, target_id} = + if type == :download do + {endpoint_id, gateway_id} + else + {gateway_id, endpoint_id} + end + + with \ + true <- gateway_id != endpoint_id || :self_target, + {true, %{file: file}} <- + FileHenforcer.belongs_to_server?(file_id, origin_id), + {true, %{storage: storage}} <- + StorageHenforcer.belongs_to_server?(storage_id, target_id), + {true, _} <- StorageHenforcer.has_enough_space?(storage, file) + do + {true, relay(%{storage: storage, file: file})} + else + :self_target -> + {false, {:target, :self}, %{}} + error -> + error + end + end +end diff --git a/lib/software/internal/file.ex b/lib/software/internal/file.ex index 62dd34dc..45b47707 100644 --- a/lib/software/internal/file.ex +++ b/lib/software/internal/file.ex @@ -29,7 +29,6 @@ defmodule Helix.Software.Internal.File do case best do [file_id] -> - # HACK: https://stackoverflow.com/q/46651888/1454986 fetch(file_id) [] -> nil diff --git a/lib/software/internal/public_ftp.ex b/lib/software/internal/public_ftp.ex index 84d77db0..cb8fb4bc 100644 --- a/lib/software/internal/public_ftp.ex +++ b/lib/software/internal/public_ftp.ex @@ -5,54 +5,78 @@ defmodule Helix.Software.Internal.PublicFTP do alias Helix.Software.Model.PublicFTP alias Helix.Software.Repo + @typep update_pftp_repo :: + {:ok, PublicFTP.t} + | {:error, PublicFTP.changeset} + + @spec fetch(Server.id) :: + PublicFTP.t + | nil def fetch(server_id) do server_id |> PublicFTP.Query.by_server() |> Repo.one() end - def fetch_file(server_id, file_id) do - server_id - |> PublicFTP.Files.Query.by_file(file_id) + @spec fetch_file(File.id) :: + PublicFTP.File.t + | nil + def fetch_file(file_id) do + file_id + |> PublicFTP.File.Query.by_file() |> Repo.one() end + @spec list_files(Server.id) :: + [File.t] def list_files(server_id) do query = PublicFTP.Query.list_files(server_id) - Hector.get(Repo, query, load: File) + Hector.get!(Repo, query, load: File) end + @spec setup_server(Server.id) :: + {:ok, PublicFTP.t} + | {:error, PublicFTP.changeset} def setup_server(server_id) do %{server_id: server_id} |> PublicFTP.create_changeset() |> Repo.insert() end + @spec enable_server(PublicFTP.t) :: + update_pftp_repo def enable_server(pftp = %PublicFTP{}) do pftp |> PublicFTP.enable_server() |> update() end + @spec disable_server(PublicFTP.t) :: + update_pftp_repo def disable_server(pftp = %PublicFTP{}) do pftp |> PublicFTP.disable_server() |> update() end + @spec add_file(PublicFTP.t, File.id) :: + {:ok, PublicFTP.File.t} + | {:error, PublicFTP.File.changeset} def add_file(pftp = %PublicFTP{}, file_id) do pftp.server_id - |> PublicFTP.Files.add_file(file_id) + |> PublicFTP.File.add_file(file_id) |> Repo.insert() end - def remove_file(pftp = %PublicFTP{}, file_id) do - pftp.server_id - |> PublicFTP.Files.Query.by_file(file_id) - |> Repo.delete() - end + @spec remove_file(File.id) :: + {:ok, PublicFTP.File.t} + | {:error, PublicFTP.File.changeset} + def remove_file(pftp_file = %PublicFTP.File{}), + do: Repo.delete(pftp_file) + @spec update(PublicFTP.changeset | PublicFTP.File.changeset) :: + update_pftp_repo defp update(changeset), do: Repo.update(changeset) end diff --git a/lib/software/model/file.ex b/lib/software/model/file.ex index 51220560..3ba2b5f4 100644 --- a/lib/software/model/file.ex +++ b/lib/software/model/file.ex @@ -146,6 +146,12 @@ defmodule Helix.Software.Model.File do end def hector_loader(repo, {columns, row}) do + IO.puts "#####################" + IO.puts "GOT ROW" + IO.inspect(row) + IO.puts "WITH COLUMNS" + IO.inspect(columns) + IO.puts "#####################" file = apply(repo, :load, [File, {columns, row}]) apply(repo, :preload, [file, :modules]) diff --git a/lib/software/model/public_ftp.ex b/lib/software/model/public_ftp.ex index 04949327..8ea47e9d 100644 --- a/lib/software/model/public_ftp.ex +++ b/lib/software/model/public_ftp.ex @@ -7,15 +7,17 @@ defmodule Helix.Software.Model.PublicFTP do alias Ecto.Changeset alias Hector alias Helix.Server.Model.Server - alias Helix.Software.Model.File alias __MODULE__, as: PublicFTP - @type t :: term + @type t :: %__MODULE__{ + server_id: Server.id, + is_active: boolean + } @type changeset :: %Changeset{data: %__MODULE__{}} @type creation_params :: %{ - server_id: Server.id + server_id: Server.idtb } @creation_fields [:server_id] @@ -28,7 +30,7 @@ defmodule Helix.Software.Model.PublicFTP do field :is_active, :boolean - has_many :files, PublicFTP.Files, + has_many :files, PublicFTP.File, foreign_key: :server_id, references: :server_id end @@ -72,18 +74,17 @@ defmodule Helix.Software.Model.PublicFTP do def by_server(query \\ PublicFTP, server_id), do: where(query, [pf], pf.server_id == ^server_id) - def list_files(query \\ PublicFTP, server_id) do + def list_files(server_id) do sql = " SELECT files.* - FROM public_ftp_files AS pftp_files + FROM pftp_files LEFT JOIN files ON files.file_id = pftp_files.file_id WHERE server_id = ##1::server_id AND ( SELECT is_active - FROM public_ftps + FROM pftps WHERE server_id = ##2::server_id - ) = TRUE - " + ) = TRUE" # TODO: Implement repeated params support on Hector Hector.query!(sql, [server_id, server_id], &HELL.Hector.caster/2) diff --git a/lib/software/model/public_ftp/files.ex b/lib/software/model/public_ftp/file.ex similarity index 67% rename from lib/software/model/public_ftp/files.ex rename to lib/software/model/public_ftp/file.ex index 82607a4e..fbc3fccb 100644 --- a/lib/software/model/public_ftp/files.ex +++ b/lib/software/model/public_ftp/file.ex @@ -1,9 +1,10 @@ -defmodule Helix.Software.Model.PublicFTP.Files do +defmodule Helix.Software.Model.PublicFTP.File do use Ecto.Schema import Ecto.Changeset + alias Ecto.Changeset alias Helix.Server.Model.Server alias Helix.Software.Model.File alias Helix.Software.Model.PublicFTP @@ -14,6 +15,12 @@ defmodule Helix.Software.Model.PublicFTP.Files do inserted_at: DateTime.t } + @type changeset :: %Changeset{data: %__MODULE__{}} + + @type creation_params :: %{ + server_id: Server.idtb + } + @creation_fields [:server_id, :file_id] @required_fields [:server_id, :file_id, :inserted_at] @@ -36,11 +43,15 @@ defmodule Helix.Software.Model.PublicFTP.Files do define_field: false end + @spec add_file(Server.id, File.id) :: + changeset def add_file(server_id, file_id) do %{server_id: server_id, file_id: file_id} |> create_changeset() end + @spec create_changeset(creation_params) :: + changeset defp create_changeset(params) do %__MODULE__{} |> cast(params, @creation_fields) @@ -48,11 +59,15 @@ defmodule Helix.Software.Model.PublicFTP.Files do |> validate_changeset() end + @spec validate_changeset(changeset) :: + changeset defp validate_changeset(changeset) do changeset |> validate_required(@required_fields) end + @spec add_timestamp(changeset) :: + changeset defp add_timestamp(changeset), do: put_change(changeset, :inserted_at, DateTime.utc_now()) @@ -60,11 +75,13 @@ defmodule Helix.Software.Model.PublicFTP.Files do import Ecto.Query + alias Ecto.Queryable + alias Helix.Software.Model.File alias Helix.Software.Model.PublicFTP - def by_file(query \\ PublicFTP.Files, server_id, file_id) do - where(query, [pf], pf.server_id == ^server_id and pf.file_id == ^file_id) - end - + @spec by_file(Queryable.t, File.idtb) :: + Queryable.t + def by_file(query \\ PublicFTP.File, file_id), + do: where(query, [pf], pf.file_id == ^file_id) end end diff --git a/lib/software/public/file.ex b/lib/software/public/file.ex index 3ce9db66..141345da 100644 --- a/lib/software/public/file.ex +++ b/lib/software/public/file.ex @@ -50,10 +50,29 @@ defmodule Helix.Software.Public.File do gateway_id: tunnel.gateway_id, destination_id: tunnel.destination_id, network_id: tunnel.network_id, - bounces: [] # TODO + bounces: [] # TODO 256 } - case FileTransferFlow.transfer(:download, file, storage, network_info) do + download(:download, file, storage, network_info) + end + + def pftp_download(network_id, gateway_id, pftp, storage, file) do + network_info = + %{ + gateway_id: gateway_id, + destination_id: pftp.server_id, + network_id: network_id, + bounces: [] # TODO 256 + } + + download(:pftp_download, file, storage, network_info) + end + + # def pftp_add_file(pftp = %PublicFTP{}, file = %File{}) do + # end + + defp download(type, file, storage, network_info) do + case FileTransferFlow.transfer(type, file, storage, network_info) do {:ok, process} -> {:ok, process} diff --git a/lib/software/public/pftp.ex b/lib/software/public/pftp.ex new file mode 100644 index 00000000..8c303f26 --- /dev/null +++ b/lib/software/public/pftp.ex @@ -0,0 +1,59 @@ +defmodule Helix.Software.Action.Flow.PublicFTP do + + import HELF.Flow + + alias Helix.Event + alias Helix.Server.Model.Server + alias Helix.Software.Action.PublicFTP, as: PublicFTPAction + alias Helix.Software.Model.PublicFTP + + @spec enable_server(Server.t) :: + {:ok, PublicFTP.t} + | {:error, {:pftp, :already_enabled}} + def enable_server(server = %Server{}) do + flowing do + with \ + {:ok, pftp, events} <- PublicFTPAction.enable_server(server), + on_success(fn -> Event.emit(events) end) + do + {:ok, pftp} + end + end + end + + @spec disable_server(PublicFTP.t) :: + {:ok, PublicFTP.t} + | {:error, :internal} + def disable_server(pftp = %PublicFTP{}) do + flowing do + with \ + {:ok, pftp, events} <- PublicFTPAction.disable_server(pftp), + on_success(fn -> Event.emit(events) end) + do + {:ok, pftp} + end + end + end +end + +defmodule Helix.Software.Public.PFTP do + + alias Helix.Server.Model.Server + alias Helix.Software.Action.Flow.PublicFTP, as: PublicFTPFlow + alias Helix.Software.Model.File + alias Helix.Software.Model.PublicFTP + + def enable_server(server = %Server{}), + do: PublicFTPFlow.enable_server(server) + + def disable_server(pftp = %PublicFTP{}), + do: PublicFTPFlow.disable_server(pftp) + + def add_file(pftp = %PublicFTP{}, file = %File{}) do + # TODO + end + + def remove_file(pftp = %PublicFTP{}, pftp_file = %PublicFTP.File{}) do + # TODO + end +end diff --git a/lib/software/websocket/requests/pftp/server/enable.ex b/lib/software/websocket/requests/pftp/server/enable.ex new file mode 100644 index 00000000..9a6218be --- /dev/null +++ b/lib/software/websocket/requests/pftp/server/enable.ex @@ -0,0 +1,62 @@ +import Helix.Websocket.Request + +request Helix.Software.Websocket.Requests.PFTP.Server.Enable do + @moduledoc """ + PFTPServerEnableRequest is called when the player wants to activate/enable his + PublicFTP server. + """ + + alias Helix.Software.Henforcer.File, as: FileHenforcer + alias Helix.Software.Public.PFTP, as: PFTPPublic + + @doc """ + All PFTP requests, including `pftp.file.download`, must be performed on the + local socket. + """ + def check_params(request, socket) do + if socket.assigns.meta.access_type == :local do + reply_ok(request) + else + reply_error("pftp_must_be_local") + end + end + + @doc """ + Most or all permissions are delegated to PFTPHenforcer. + """ + def check_permissions(request, socket) do + server_id = socket.assigns.gateway.server_id + entity_id = socket.assigns.gateway.entity_id + + case FileHenforcer.PublicFTP.can_enable_server?(entity_id, server_id) do + {true, relay} -> + meta = %{server: relay.server} + update_meta(request, meta, reply: true) + + {false, reason, _} -> + reply_error(reason) + end + end + + def handle_request(request, _socket) do + server = request.meta.server + + case PFTPPublic.enable_server(server) do + {:ok, _pftp} -> + reply_ok(request) + + {:error, reason} -> + reply_error(reason) + end + end + + @doc """ + Renders an empty response. Client will receive only a successful return code. + + Client shall soon receive a PFTPServerEnabledEvent. + """ + render_empty() + + defp get_error({:pftp, :enabled}), + do: "pftp_already_enabled" +end diff --git a/lib/websocket/request.ex b/lib/websocket/request.ex index dcfdc48a..4b080226 100644 --- a/lib/websocket/request.ex +++ b/lib/websocket/request.ex @@ -1,7 +1,7 @@ defmodule Helix.Websocket.Request do @moduledoc """ - `Request` is a generic data type that represents any Channel request that is - handled by `Requestable`. + `WebsocketRequest` is a generic data type that represents any Channel request + that is handled by `Requestable`. A module that wants to process channel requests should: @@ -9,8 +9,13 @@ defmodule Helix.Websocket.Request do - Implement the Requestable protocol For usage example, see `lib/server/websocket/server/requests/bruteforce.ex` + and `lib/software/websocket/requests/pftp/server/enable.ex` """ + import HELL.Macros + + alias Helix.Websocket.Utils, as: WebsocketUtils + @type t(struct) :: %{ __struct__: struct, unsafe: map, @@ -18,28 +23,163 @@ defmodule Helix.Websocket.Request do meta: map } - defmacro register do - type = - quote do + @doc """ + Top-level macro for creating a Websocket Request, which can be handled by any + channel. It must implement the Requestable protocol. + """ + defmacro request(name, do: block) do + quote do + + defmodule unquote(name) do + @moduledoc false + @type t :: Helix.Websocket.Request.t(__MODULE__) - end - struct = - quote do @enforce_keys [:unsafe] defstruct [:unsafe, params: %{}, meta: %{}] - end - new = - quote do - @spec new(term) :: t + @spec new(term) :: + t def new(params \\ %{}) do %__MODULE__{ unsafe: params } end + + defimpl Helix.Websocket.Requestable do + @moduledoc false + + unquote(block) + + docp """ + Fallbacks to WebsocketUtils' general purpose error code translator. + """ + defp get_error(error), + do: WebsocketUtils.get_error(error) + end + end + + end + end + + @doc """ + Interrupts the Request flow with an error message. + + If the passed message is an atom, we assume it hasn't been translated to the + external format yet, so we call `get_error/1`. + """ + defmacro reply_error(msg) when is_binary(msg) do + quote do + {:error, %{message: unquote(msg)}} + end + end + + defmacro reply_error(reason) when is_atom(reason) or is_tuple(reason) do + quote do + {:error, %{message: get_error(unquote(reason))}} + end + end + + @doc """ + Shorthand for the `bad_request` error. + """ + defmacro bad_request do + quote do + reply_error("bad_request") + end + end + + @doc """ + Shorthand for the `internal` error. + """ + defmacro internal_error do + quote do + reply_error("internal") + end + end + + @doc """ + Proceeds with the Request flow by signaling everything is OK. + """ + defmacro reply_ok(request) do + quote do + {:ok, unquote(request)} + end + end + + @doc """ + Updates the meta field with the given value. If `reply` opt is specified, + automatically return the expected OK response. + """ + defmacro update_meta(request, meta, reply: true) do + quote do + reply_ok(%{unquote(request)| meta: unquote(meta)}) + end + end + + defmacro update_meta(request, meta) do + quote do + var!(request) = %{unquote(request)| meta: unquote(meta)} + end + end + + @doc """ + Updates the params field with the given value. If `reply` opt is specified, + automatically return the expected OK response. + """ + defmacro update_params(request, params, reply: true) do + quote do + reply_ok(%{unquote(request)| params: unquote(params)}) + end + end + + defmacro update_params(request, params) do + quote do + var!(request) = %{unquote(request)| params: unquote(params)} + end + end + + @doc """ + Macro used to render the output that will be sent to the client. It's simply a + wrapper to `Requestable.reply/2`. The advantage of wrapping it through this + macro, though, is that we can apply system-wide patches/parsers if/when the + time comes. + """ + defmacro render(request, socket, do: block) do + quote do + + def reply(unquote(request), unquote(socket)) do + unquote(block) + end + + end + end + + @doc """ + Shorthand for requests that render a process as response. It assumes the + process is at the Request meta field, under the `process` key + """ + defmacro render_process do + quote do + + def reply(request, socket) do + {:ok, WebsocketUtils.render_process(request.meta.process, socket)} + end + + end + end + + @doc """ + Shorthand for requests that want to reply with an empty data field. In these + cases, all the client gets is a successful return code. + """ + defmacro render_empty do + quote do + + def reply(_, _) do + {:ok, %{}} end - [type, struct, new] + end end end diff --git a/lib/websocket/socket.ex b/lib/websocket/socket.ex index 960226a5..0d76e82a 100644 --- a/lib/websocket/socket.ex +++ b/lib/websocket/socket.ex @@ -2,6 +2,7 @@ defmodule Helix.Websocket.Socket do use Phoenix.Socket + alias Phoenix.Socket alias Helix.Event.Notificable alias Helix.Websocket.Joinable alias Helix.Websocket.Requestable @@ -9,6 +10,8 @@ defmodule Helix.Websocket.Socket do alias Helix.Account.Action.Session, as: SessionAction alias Helix.Entity.Query.Entity, as: EntityQuery + @typep socket :: Socket.t + transport :websocket, Phoenix.Transports.WebSocket channel "requests", Helix.Websocket.RequestsChannel @@ -67,7 +70,9 @@ defmodule Helix.Websocket.Socket do {:ok, request} <- Requestable.check_permissions(request, socket), {:ok, request} <- Requestable.handle_request(request, socket) do - Requestable.reply(request, socket) + request + |> Requestable.reply(socket) + |> reply_request(socket) else {:error, %{message: msg}} -> WebsocketUtils.reply_error(msg, socket) @@ -76,6 +81,20 @@ defmodule Helix.Websocket.Socket do end end + # TODO: se fode ai nerdao + @spec reply_request({:ok | :reply | :error, term} | :noreply, socket) :: + {:reply, {:ok, %{data: term}}, socket} + | {:reply, {:error, %{data: term}}, socket} + | {:noreply, socket} + defp reply_request({:ok, data}, socket), + do: WebsocketUtils.reply_ok(data, socket) + defp reply_request({:reply, data}, socket), + do: WebsocketUtils.reply_ok(data, socket) + defp reply_request({:error, data}, socket), + do: WebsocketUtils.reply_error(data, socket) + defp reply_request({:noreply, _}, socket), + do: WebsocketUtils.no_reply(socket) + @doc """ Generic notification ("event going out") handler. It guides the notification through the Notificable flow, making sure the payload sent to the client is diff --git a/lib/websocket/utils.ex b/lib/websocket/utils.ex index 6071a9c9..e7513ffe 100644 --- a/lib/websocket/utils.ex +++ b/lib/websocket/utils.ex @@ -1,5 +1,6 @@ defmodule Helix.Websocket.Utils do + alias HELL.Utils alias Helix.Process.Model.Process alias Helix.Process.Public.View.Process, as: ProcessView @@ -19,19 +20,19 @@ defmodule Helix.Websocket.Utils do def no_reply(socket), do: {:noreply, socket} - @spec reply_process(Process.t, socket) :: + @spec render_process(Process.t, socket) :: reply_ok @doc """ Helper that automatically renders the reply with the recently created process. """ - def reply_process(process = %Process{}, socket) do + def render_process(process = %Process{}, socket) do process_data = process.process_data server_id = socket.assigns.gateway.server_id entity_id = socket.assigns.entity_id pview = ProcessView.render(process_data, process, server_id, entity_id) - reply_ok(%{data: pview}, socket) + %{data: pview} end @spec reply_ok(term, socket) :: @@ -59,4 +60,21 @@ defmodule Helix.Websocket.Utils do reply_error def internal_error(socket), do: reply_error("internal", socket) + + @doc """ + General purpose error code translator. If you want to specify or handle a + custom return for the errors below, make sure to add a pattern match before + calling this function. For an example, see FileDownloadRequest. + + Most common translation pattern: {:error, :reason} => "error_reason". + + For instance, {:storage, :full} or {:server, :not_found} are translated to + "storage_full" and "server_not_found", respectively. + """ + def get_error(msg) when is_binary(msg), + do: msg + def get_error(:internal), + do: "internal" + def get_error({a, b}), + do: Utils.concat(a, "_", b) end diff --git a/test/software/henforcer/file_public_ftp_test.exs b/test/software/henforcer/file/public_ftp_test.exs similarity index 100% rename from test/software/henforcer/file_public_ftp_test.exs rename to test/software/henforcer/file/public_ftp_test.exs diff --git a/test/software/henforcer/file_transfer_test.exs b/test/software/henforcer/file/transfer_test.exs similarity index 100% rename from test/software/henforcer/file_transfer_test.exs rename to test/software/henforcer/file/transfer_test.exs diff --git a/test/software/internal/public_ftp_test.exs b/test/software/internal/public_ftp_test.exs new file mode 100644 index 00000000..373595a6 --- /dev/null +++ b/test/software/internal/public_ftp_test.exs @@ -0,0 +1,131 @@ +defmodule Helix.Software.Internal.PublicFTPTest do + + use Helix.Test.Case.Integration + + alias Helix.Server.Model.Server + alias Helix.Software.Internal.PublicFTP, as: PublicFTPInternal + alias Helix.Software.Model.PublicFTP + + alias Helix.Test.Software.Setup, as: SoftwareSetup + + describe "fetch/1" do + test "returns the pftp server if found" do + {pftp, _} = SoftwareSetup.pftp() + + entry = PublicFTPInternal.fetch(pftp.server_id) + assert entry == pftp + end + + test "fetches pftp server even if it is disabled" do + {pftp, _} = SoftwareSetup.pftp(active: false) + refute pftp.is_active + + entry = PublicFTPInternal.fetch(pftp.server_id) + assert entry == pftp + end + + test "returns nil if nothing was found" do + refute PublicFTPInternal.fetch(Server.ID.generate()) + end + end + + describe "fetch_file/1" do + test "returns the corresponding PublicFTP.File entry if found" do + {pftp_file, _} = SoftwareSetup.pftp_file() + + entry = PublicFTPInternal.fetch_file(pftp_file.file_id) + assert entry == pftp_file + end + + @tag :pending + # Provavelmente nao, `list_files` jah filtra tb + test "discuss: should I return PFTP.File and PFTP if server is disabled?" + end + + describe "list_files/1" do + test "returns all files as File.t" do + {pftp, _} = SoftwareSetup.pftp(active: true, real_server: true) + + server_id = pftp.server_id + + {file1, _} = SoftwareSetup.file(server_id: server_id) + {file2, _} = SoftwareSetup.file(server_id: server_id) + {file3, _} = SoftwareSetup.file(server_id: server_id) + + SoftwareSetup.pftp_file(server_id: server_id, file_id: file1.file_id) + SoftwareSetup.pftp_file(server_id: server_id, file_id: file2.file_id) + SoftwareSetup.pftp_file(server_id: server_id, file_id: file3.file_id) + + files = PublicFTPInternal.list_files(pftp.server_id) + + assert is_list(files) and length(files) == 3 + assert Enum.sort(files) == Enum.sort([file1, file2, file3]) + end + + test "returns nothing if server is disabled" do + {pftp, _} = SoftwareSetup.pftp(active: false, real_server: true) + {_, _} = SoftwareSetup.pftp_file(server_id: pftp.server_id) + + # Got nothing, even though there is a file there. + assert [] == PublicFTPInternal.list_files(pftp.server_id) + end + end + + describe "setup_server/1" do + test "creates the PublicFTP entry" do + assert {:ok, entry} = PublicFTPInternal.setup_server(Server.ID.generate()) + assert %PublicFTP{} = entry + assert entry.is_active + end + end + + describe "enable_server/1" do + test "enables an otherwise disabled server" do + {pftp, _} = SoftwareSetup.pftp(active: false) + refute pftp.is_active + + assert {:ok, new_pftp} = PublicFTPInternal.enable_server(pftp) + assert new_pftp.is_active + assert new_pftp.server_id == pftp.server_id + end + end + + describe "disable_server/1" do + test "disables an otherwise enabled server" do + {pftp, _} = SoftwareSetup.pftp(active: true) + assert pftp.is_active + + assert {:ok, new_pftp} = PublicFTPInternal.disable_server(pftp) + refute new_pftp.is_active + assert new_pftp.server_id == pftp.server_id + end + end + + describe "add_file/2" do + test "adds file to the server" do + {pftp, _} = SoftwareSetup.pftp(active: true, real_server: true) + {file, _} = SoftwareSetup.file(server_id: pftp.server_id) + + assert {:ok, pftp_file} = PublicFTPInternal.add_file(pftp, file.file_id) + assert pftp_file.file_id == file.file_id + assert pftp_file.server_id == pftp.server_id + end + end + + describe "remove_file/2" do + test "removes file from the server" do + {pftp_file, _} = SoftwareSetup.pftp_file() + + total = length(PublicFTPInternal.list_files(pftp_file.server_id)) + assert total == 1 + + assert {:ok, removed} = PublicFTPInternal.remove_file(pftp_file) + assert removed.file_id == pftp_file.file_id + assert removed.server_id == pftp_file.server_id + assert removed.__meta__.state == :deleted + + total = length(PublicFTPInternal.list_files(pftp_file.server_id)) + assert total == 0 + end + end +end diff --git a/test/software/websocket/requests/pftp/server/enable_test.exs b/test/software/websocket/requests/pftp/server/enable_test.exs new file mode 100644 index 00000000..1f7adf1a --- /dev/null +++ b/test/software/websocket/requests/pftp/server/enable_test.exs @@ -0,0 +1,69 @@ +defmodule Helix.Software.Websocket.Requests.PFTP.Server.EnableTest do + + use Helix.Test.Case.Integration + + alias Helix.Websocket.Requestable + alias Helix.Software.Websocket.Requests.PFTP.Server.Enable, + as: PFTPServerEnableRequest + + alias Helix.Test.Channel.Setup, as: ChannelSetup + alias Helix.Test.Software.Setup, as: SoftwareSetup + + describe "check_params/2" do + test "does not allow pftp_server_enable on remote channel" do + request = PFTPServerEnableRequest.new(%{}) + remote_socket = ChannelSetup.mock_server_socket() + + assert {:error, %{message: reason}} = + Requestable.check_params(request, remote_socket) + + assert reason == "pftp_must_be_local" + end + end + + describe "check_permission/2" do + test "henforces the request through PFTPHenforcer.can_enable_server" do + # Note: this is not intended as an extensive test. For an extended + # permission test, see `FileHenforcer.PublicFTPTest`. + request = PFTPServerEnableRequest.new(%{}) + {socket, _} = ChannelSetup.join_server() + + assert {:ok, request} = Requestable.check_permissions(request, socket) + assert request.meta.server.server_id == socket.assigns.gateway.server_id + + # Now we'll enable pftp on that server, so the request should fail + SoftwareSetup.pftp(server_id: socket.assigns.gateway.server_id) + + assert {:error, %{message: reason}} = + Requestable.check_permissions(request, socket) + + assert reason == "pftp_already_enabled" + end + end + + describe "handle_request/2" do + test "it uses the `server` returned on the `permissions` step" do + request = PFTPServerEnableRequest.new(%{}) + {socket, _} = ChannelSetup.join_server() + + assert {:ok, request} = Requestable.check_permissions(request, socket) + assert {:ok, _request} = Requestable.handle_request(request, socket) + end + end + + describe "reply/2" do + test "response is empty when successful" do + pftp = SoftwareSetup.fake_pftp() + + request = + %{} + |> PFTPServerEnableRequest.new() + |> Map.put(:meta, %{pftp: pftp}) + + mock_socket = ChannelSetup.mock_server_socket(own_server: true) + assert {:ok, response} = Requestable.reply(request, mock_socket) + + assert response == %{} + end + end +end diff --git a/test/support/channel/setup.ex b/test/support/channel/setup.ex index 6a8b91d8..0d96cad6 100644 --- a/test/support/channel/setup.ex +++ b/test/support/channel/setup.ex @@ -222,7 +222,7 @@ defmodule Helix.Test.Channel.Setup do - destination_entity_id - network_id - access_type: Inferred if not set - - own_server: Force socket to represent own server channel + - own_server: Force socket to represent own server channel. Defaults to false. - counter: Defaults to 0. """ def mock_server_socket(opts \\ []) do diff --git a/test/support/henforcer/macros.ex b/test/support/henforcer/macros.ex index 829e6864..f227fe71 100644 --- a/test/support/henforcer/macros.ex +++ b/test/support/henforcer/macros.ex @@ -1,5 +1,10 @@ defmodule Helix.Test.Henforcer.Macros do + @doc """ + Basically, this macro ensures that the returned relay has the given keys and + only them. It did not return any extra keys. Useful to test that the relay + accumulation worked as expected. + """ defmacro assert_relay(relay, keys) do quote do acc_relay = diff --git a/test/support/software/setup.ex b/test/support/software/setup.ex index 49be717a..e9ca3389 100644 --- a/test/support/software/setup.ex +++ b/test/support/software/setup.ex @@ -199,7 +199,7 @@ defmodule Helix.Test.Software.Setup do Opts: - server_id: Specify the server id. Defaults to generating a fake server id. - active: Whether the generated pftp should be active. Defaults to true. - - real_server: Whether to generate real server. Defaults to false. + - real_server: Whether to generate a real server (desktop). Defaults to false. Related: Server.t if `real_server` """ @@ -251,7 +251,7 @@ defmodule Helix.Test.Software.Setup do @doc """ - file_id: Specify file id. Generates a real file if not specified. - real_file: Whether to generate a real file. Defaults to true. Overwrites the - `file_id` option + `file_id` option when set. - server_id: Which pftp server to link to. Generates a real pftp by default Related: @@ -267,7 +267,7 @@ defmodule Helix.Test.Software.Setup do {nil, nil, File.ID.generate()} opts[:file_id] -> - {nil, nil, File.ID.generate()} + {nil, nil, opts[:file_id]} true -> {file, related} = file() @@ -278,12 +278,9 @@ defmodule Helix.Test.Software.Setup do if file do Keyword.get(opts, :server_id, file_related.server_id) else - Server.ID.generate() + Keyword.get(opts, :server_id, Server.ID.generate()) end - if opts[:server_id] do - end - pftp = if opts[:server_id] do nil @@ -293,7 +290,7 @@ defmodule Helix.Test.Software.Setup do pftp_file = server_id - |> PublicFTP.Files.add_file(file_id) + |> PublicFTP.File.add_file(file_id) |> Changeset.apply_changes() related = %{