Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New api for web hosting #337

Merged
9 commits merged into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions lib/archethic_web/controllers/api/web_hosting_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
defmodule ArchethicWeb.API.WebHostingController do
@moduledoc false

use ArchethicWeb, :controller

alias Archethic

alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData

alias Archethic.Crypto

use Pathex

require Logger

@spec web_hosting(Plug.Conn.t(), map) :: Plug.Conn.t()
def web_hosting(conn, %{"address" => address, "url_path" => url_path}) do
with {:ok, address} <- Base.decode16(address, case: :mixed),
true <- Crypto.valid_address?(address),
{:ok, %Transaction{address: last_address, data: %TransactionData{content: content}}} <-
Archethic.get_last_transaction(address),
{:ok, json_content} <- Jason.decode(content),
{:ok, file, mime_type} <- get_file(json_content, url_path),
{cached?, etag} <- get_cache(conn, last_address, url_path),
{:ok, file_content, encodage} <- get_file_content(file, cached?) do
conn =
conn
|> put_resp_content_type(mime_type, "utf-8")
|> put_resp_header("content-encoding", "gzip")
|> put_resp_header("cache-control", "public")
|> put_resp_header("etag", etag)

if cached? do
send_resp(conn, 304, "")
else
case encodage do
"gzip" ->
send_resp(conn, 200, file_content)

_ ->
send_resp(conn, 200, :zlib.gzip(file_content))
end
end
else
# Base.decode16 || Crypto.valid_address
er when er in [:error, false] ->
send_resp(conn, 400, "Invalid address")

# Jason.decode
{:error, %Jason.DecodeError{}} ->
send_resp(conn, 400, "Invalid transaction content")

# Archethic.get_last_transaction
{:error, _} ->
send_resp(conn, 400, "Invalid address")

{:file_not_found, url} ->
send_resp(conn, 404, "File #{url} does not exist")

:encodage_error ->
send_resp(conn, 400, "Invalid file encodage")

:file_error ->
send_resp(conn, 400, "Cannot find file content")

_reason ->
send_resp(conn, 404, "Not Found")
end
end

# API without path returns default index.html file
@spec get_file(json_content :: map(), url_path :: list()) ::
{:ok, map(), binary()} | {:file_not_found, binary()}
defp get_file(json_content, url_path) do
{json_path, url} =
case Enum.count(url_path) do
0 ->
json_path = path("index.html")
{json_path, "index.html"}

1 ->
file_name = Enum.at(url_path, 0)
json_path = path(file_name)
{json_path, file_name}

_ ->
json_path = get_json_path(url_path)
url = Path.join(url_path)
{json_path, url}
end

case Pathex.view(json_content, json_path) do
{:ok, file} ->
{:ok, file, MIME.from_path(url)}

:error ->
{:file_not_found, url}
end
end

defp get_json_path(url_path) do
Enum.reduce(url_path, nil, fn value, acc ->
if acc == nil do
path(value)
else
acc ~> path(value)
end
end)
end

@spec get_cache(conn :: Plug.Conn.t(), last_address :: binary(), url_path :: list()) ::
{boolean(), binary()}
defp get_cache(conn, last_address, url_path) do
etag =
case Enum.empty?(url_path) do
true ->
Base.encode16(last_address, case: :lower)

false ->
Base.encode16(last_address, case: :lower) <> Path.join(url_path)
end

cached? =
case List.first(get_req_header(conn, "if-none-match")) do
got_etag when got_etag == etag ->
true

_ ->
false
end

{cached?, etag}
end

# All file are encoded in base64 in JSON content
@spec get_file_content(file :: map(), cached? :: boolean()) ::
{:ok, binary(), binary() | nil} | :encodage_error | :file_error
defp get_file_content(_file, _cached? = true), do: {:ok, nil, nil}

defp get_file_content(file = %{"content" => content}, _cached = false) do
try do
file_content = Base.url_decode64!(content, padding: false)
encodage = Map.get(file, "encodage")
{:ok, file_content, encodage}
rescue
_ ->
:encodage_error
end
end

defp get_file_content(_, _), do: :file_error
end
2 changes: 2 additions & 0 deletions lib/archethic_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ defmodule ArchethicWeb.Router do
:last_transaction_content
)

get("/web_hosting/:address/*url_path", ArchethicWeb.API.WebHostingController, :web_hosting)

post("/origin_key", ArchethicWeb.API.OriginKeyController, :origin_key)

post("/transaction", ArchethicWeb.API.TransactionController, :new)
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ defmodule Archethic.MixProject do
{:flow, "~> 1.0"},
{:broadway, "~> 1.0"},
{:knigge, "~> 1.4"},
{:ex_json_schema, "~> 0.9.1", override: true}
{:ex_json_schema, "~> 0.9.1", override: true},
{:pathex, "~> 2.0"}
]
end

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"nimble_options": {:hex, :nimble_options, "0.3.7", "1e52dd7673d36138b1a5dede183b5d86dff175dc46d104a8e98e396b85b04670", [:mix], [], "hexpm", "2086907e6665c6b6579be54ef5001928df5231f355f71ed258f80a55e9f63633"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"},
"observer_cli": {:hex, :observer_cli, "1.7.1", "c9ca1f623a3ef0158283a3c37cd7b7235bfe85927ad6e26396dd247e2057f5a1", [:mix, :rebar3], [{:recon, "~>2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "4ccafaaa2ce01b85ddd14591f4d5f6731b4e13b610a70fb841f0701178478280"},
"pathex": {:hex, :pathex, "2.0.0", "b129359b22bf2005d5ed6bf41beb7aed64ca83d8510a0624a596a3fa23d34a1c", [:mix], [], "hexpm", "ffe038484ad8313fd91bec3ed51d0f821a4ba3eb6f2a1b14d9ab2685c5c3c876"},
"phoenix": {:hex, :phoenix, "1.5.13", "d4e0805ec0973bed80d67302631130fb47d75b1a0b7335a0b23c4432b6ce55ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1a7c4f1900e6e60bb60ae6680e48418e3f7c360d58bcb9f812487b6d0d281a0f"},
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"},
Expand Down
Loading