Push forwarding

This commit is contained in:
Shadowfacts 2024-04-06 15:02:18 -04:00
parent c58d5768ba
commit 47d2334d4d
4 changed files with 134 additions and 40 deletions

View File

@ -1,32 +1,87 @@
defmodule TuskerPush.Apns do defmodule TuskerPush.Apns do
alias TuskerPush.Registration alias TuskerPush.Registration
require Logger
@spec send(Registration.t(), Map.t()) :: :ok | {:error, term()} @spec send(Registration.t(), Map.t()) :: :ok | {:error, term()}
def send(registration, payload) do 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), with {:ok, body} <- Jason.encode(payload, pretty_print: false),
{:ok, resp} <- Finch.request(req, TuskerPush.Finch, headers, body) do req <- make_request(registration, body),
nil {:ok, resp} <- Finch.request(req, TuskerPush.Finch) do
handle_response(resp, registration.apns_environment)
else else
{:error, reason} -> {:error, reason} ->
{:error, reason} {:error, reason}
end end
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 def host(:development) do
"api.sandbox.push.apple.com" "api.sandbox.push.apple.com"
end end

View File

@ -1,26 +1,27 @@
defmodule TuskerPush.Apns.Token do defmodule TuskerPush.Apns.Token do
use GenServer use GenServer
require Logger
def start_link(_) do def start_link(_) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__) GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end end
def refresh(expected_token) do
GenServer.call(__MODULE__, {:refresh, expected_token})
end
def current do def current do
GenServer.call(__MODULE__, :current) GenServer.call(__MODULE__, :current)
end end
@impl true @impl true
def init(_) do def init(_) do
Logger.debug("Apns.Token initialized, scheduling update")
Process.send_after(self(), :refresh, 1000 * 60 * 45) Process.send_after(self(), :refresh, 1000 * 60 * 45)
{:ok, generate_token()} {:ok, generate_token()}
end end
@impl true @impl true
def handle_info(:refresh, _old_token) do def handle_info(:refresh, _old_token) do
Logger.debug("Apns.Token regenerated")
Process.send_after(self(), :refresh, 1000 * 60 * 45)
{:noreply, generate_token()} {:noreply, generate_token()}
end end
@ -29,17 +30,6 @@ defmodule TuskerPush.Apns.Token do
{:reply, token, token} {:reply, token, token}
end 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 defp generate_token do
# can't use Joken because it always inserts "typ": "JWT" which APNS doesn't like # can't use Joken because it always inserts "typ": "JWT" which APNS doesn't like

View File

@ -14,7 +14,7 @@ defmodule TuskerPush.Forwarder do
"reg_id" => registration.id, "reg_id" => registration.id,
"data" => Base.encode64(body), "data" => Base.encode64(body),
"salt" => salt, "salt" => salt,
"key" => key "pk" => key
} }
Apns.send(registration, payload) Apns.send(registration, payload)

View File

@ -1,4 +1,5 @@
defmodule TuskerPushWeb.PushController do defmodule TuskerPushWeb.PushController do
alias TuskerPush.Forwarder
use TuskerPushWeb, :controller use TuskerPushWeb, :controller
require Logger require Logger
@ -7,8 +8,11 @@ defmodule TuskerPushWeb.PushController do
with {:registration, registration} when not is_nil(registration) <- with {:registration, registration} when not is_nil(registration) <-
{:registration, TuskerPush.get_registration(id)}, {:registration, TuskerPush.get_registration(id)},
:ok <- TuskerPush.check_registration_expired(registration), :ok <- TuskerPush.check_registration_expired(registration),
{:ok, body, conn} <- read_body(conn) do {:encoding, ["aesgcm"]} <- {:encoding, get_req_header(conn, "content-encoding")},
IO.inspect(body |> byte_size()) {: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") send_resp(conn, 200, "ok")
else else
{:registration, nil} -> {:registration, nil} ->
@ -16,18 +20,63 @@ defmodule TuskerPushWeb.PushController do
{:expired, registration} -> {:expired, registration} ->
TuskerPush.unregister(registration) TuskerPush.unregister(registration)
send_resp(conn, 400, "unregistered") 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") Logger.error("Didn't finish reading")
send_resp(conn, 500, "failed to read body") send_resp(conn, 500, "failed to read body")
{:error, reason} -> {:body, {:error, reason}} ->
Logger.error("Reading body: #{inspect(reason)}") Logger.error("Reading body: #{inspect(reason)}")
send_resp(conn, 500, "failed to read body") 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 end
end end