From 47d2334d4d4e394bce81e91bcf3d5d82d6bbddd4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 6 Apr 2024 15:02:18 -0400 Subject: [PATCH] Push forwarding --- lib/tusker_push/apns.ex | 89 +++++++++++++++---- lib/tusker_push/apns/token.ex | 20 ++--- lib/tusker_push/forwarder.ex | 2 +- .../controllers/push_controller.ex | 63 +++++++++++-- 4 files changed, 134 insertions(+), 40 deletions(-) diff --git a/lib/tusker_push/apns.ex b/lib/tusker_push/apns.ex index 747b0e7..a4540be 100644 --- a/lib/tusker_push/apns.ex +++ b/lib/tusker_push/apns.ex @@ -1,32 +1,87 @@ defmodule TuskerPush.Apns do alias TuskerPush.Registration + require Logger + @spec send(Registration.t(), Map.t()) :: :ok | {:error, term()} def send(registration, payload) do - req = - Finch.build(:post, %URI{ - scheme: "https", - authority: host(registration.apns_environment), - host: host(registration.apns_environment), - port: 443, - path: "/3/device/#{registration.apns_device_token}" - }) - - headers = [ - {"authorization", "bearer #{TuskerPush.Apns.Token.current()}"}, - {"apns-push-type", "alert"}, - {"apns-bundle-id", Application.fetch_env!(:tusker_push, :apns)[:bundle_id]} - ] - with {:ok, body} <- Jason.encode(payload, pretty_print: false), - {:ok, resp} <- Finch.request(req, TuskerPush.Finch, headers, body) do - nil + req <- make_request(registration, body), + {:ok, resp} <- Finch.request(req, TuskerPush.Finch) do + handle_response(resp, registration.apns_environment) else {:error, reason} -> {:error, reason} end end + @spec make_request(Registration.t(), binary()) :: Finch.Request.t() + defp make_request(registration, body) do + bundle_id = Application.fetch_env!(:tusker_push, :apns)[:bundle_id] + + headers = [ + {"authorization", "bearer #{TuskerPush.Apns.Token.current()}"}, + {"apns-push-type", "alert"}, + {"apns-bundle-id", bundle_id}, + {"apns-topic", bundle_id}, + {"apns-expiration", + DateTime.utc_now() |> DateTime.add(1, :day) |> DateTime.to_unix() |> Integer.to_string()} + ] + + Finch.build( + :post, + "https://#{host(registration.apns_environment)}/3/device/#{registration.apns_device_token}", + headers, + body + ) + end + + @spec handle_response(Finch.Response.t(), :development | :production) :: :ok | {:error, term()} + + defp handle_response(resp, env) do + maybe_log_unique_id(resp, env) + + if resp.status in 200..299 do + :ok + else + info = + case Jason.decode(resp.body) do + {:ok, data} -> + inspect(data) + + {:error, _} -> + resp.body + end + + Logger.error("Received #{resp.status} with #{info}") + + {:error, "unexpected status #{resp.status}"} + end + end + + @spec maybe_log_unique_id(Finch.Response.t(), :development | :production) :: :ok + + defp maybe_log_unique_id(resp, :development) do + resp.headers + |> Enum.find(fn + {"apns-unique-id", _} -> true + _ -> false + end) + |> case do + {_, id} -> + Logger.debug("APNS unique id: #{id}") + + _ -> + nil + end + + :ok + end + + defp maybe_log_unique_id(_resp, :production), do: :ok + + @spec host(:development | :production) :: String.t() + def host(:development) do "api.sandbox.push.apple.com" end diff --git a/lib/tusker_push/apns/token.ex b/lib/tusker_push/apns/token.ex index 0bdd68d..05660aa 100644 --- a/lib/tusker_push/apns/token.ex +++ b/lib/tusker_push/apns/token.ex @@ -1,26 +1,27 @@ defmodule TuskerPush.Apns.Token do use GenServer + require Logger + def start_link(_) do GenServer.start_link(__MODULE__, nil, name: __MODULE__) end - def refresh(expected_token) do - GenServer.call(__MODULE__, {:refresh, expected_token}) - end - def current do GenServer.call(__MODULE__, :current) end @impl true def init(_) do + Logger.debug("Apns.Token initialized, scheduling update") Process.send_after(self(), :refresh, 1000 * 60 * 45) {:ok, generate_token()} end @impl true def handle_info(:refresh, _old_token) do + Logger.debug("Apns.Token regenerated") + Process.send_after(self(), :refresh, 1000 * 60 * 45) {:noreply, generate_token()} end @@ -29,17 +30,6 @@ defmodule TuskerPush.Apns.Token do {:reply, token, token} end - @impl true - def handle_call({:refresh, expected_token}, _from, token) when expected_token == token do - new_token = generate_token() - {:reply, new_token, new_token} - end - - @impl true - def handle_call({:refresh, _expected_token}, _from, token) do - {:reply, token, token} - end - defp generate_token do # can't use Joken because it always inserts "typ": "JWT" which APNS doesn't like diff --git a/lib/tusker_push/forwarder.ex b/lib/tusker_push/forwarder.ex index b05ee8d..f3b10c5 100644 --- a/lib/tusker_push/forwarder.ex +++ b/lib/tusker_push/forwarder.ex @@ -14,7 +14,7 @@ defmodule TuskerPush.Forwarder do "reg_id" => registration.id, "data" => Base.encode64(body), "salt" => salt, - "key" => key + "pk" => key } Apns.send(registration, payload) diff --git a/lib/tusker_push_web/controllers/push_controller.ex b/lib/tusker_push_web/controllers/push_controller.ex index d865489..cb510ed 100644 --- a/lib/tusker_push_web/controllers/push_controller.ex +++ b/lib/tusker_push_web/controllers/push_controller.ex @@ -1,4 +1,5 @@ defmodule TuskerPushWeb.PushController do + alias TuskerPush.Forwarder use TuskerPushWeb, :controller require Logger @@ -7,8 +8,11 @@ defmodule TuskerPushWeb.PushController do with {:registration, registration} when not is_nil(registration) <- {:registration, TuskerPush.get_registration(id)}, :ok <- TuskerPush.check_registration_expired(registration), - {:ok, body, conn} <- read_body(conn) do - IO.inspect(body |> byte_size()) + {:encoding, ["aesgcm"]} <- {:encoding, get_req_header(conn, "content-encoding")}, + {:body, {:ok, body, conn}} <- {:body, read_body(conn)}, + {:salt, salt} when not is_nil(salt) <- get_salt(conn), + {:key, key} when not is_nil(key) <- get_key(conn), + {:forward, :ok} <- {:forward, Forwarder.forward(registration, body, salt, key)} do send_resp(conn, 200, "ok") else {:registration, nil} -> @@ -16,18 +20,63 @@ defmodule TuskerPushWeb.PushController do {:expired, registration} -> TuskerPush.unregister(registration) - send_resp(conn, 400, "unregistered") - {:more, _, conn} -> + {:encoding, encoding} -> + Logger.warning("Unexpected encoding: #{inspect(encoding)}") + send_resp(conn, 400, "bad encoding") + + {:body, {:more, _, conn}} -> Logger.error("Didn't finish reading") - send_resp(conn, 500, "failed to read body") - {:error, reason} -> + {:body, {:error, reason}} -> Logger.error("Reading body: #{inspect(reason)}") - send_resp(conn, 500, "failed to read body") + + {:salt, nil} -> + Logger.warning("Missing salt") + send_resp(conn, 400, "missing salt") + + {:key, nil} -> + Logger.warning("Missing key") + send_resp(conn, 400, "missing key") + + {:forward, {:error, reason}} -> + Logger.error("Sending notification: #{inspect(reason)}") + send_resp(conn, 500, "failed to send") + end + end + + defp get_salt(conn) do + conn + |> get_req_header("encryption") + |> case do + ["salt=" <> salt] -> + {:salt, salt} + + _ -> + {:salt, nil} + end + end + + defp get_key(conn) do + conn + |> get_req_header("crypto-key") + |> case do + [value] -> + dh = + value + |> String.split(";") + |> Enum.find_value(fn + "dh=" <> val -> val + _ -> false + end) + + {:key, dh} + + _ -> + {:key, nil} end end end