From c58d5768ba3cfeff51a1cda4cf20c73d7ffc0f91 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 5 Apr 2024 23:50:28 -0400 Subject: [PATCH] APNS tokens --- lib/tusker_push/apns.ex | 37 ++++++++++++++++++ lib/tusker_push/apns/token.ex | 67 +++++++++++++++++++++++++++++++++ lib/tusker_push/application.ex | 7 ++++ lib/tusker_push/forwarder.ex | 22 +++++++++++ lib/tusker_push/registration.ex | 1 + mix.exs | 4 +- mix.lock | 6 +++ 7 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 lib/tusker_push/apns.ex create mode 100644 lib/tusker_push/apns/token.ex create mode 100644 lib/tusker_push/forwarder.ex diff --git a/lib/tusker_push/apns.ex b/lib/tusker_push/apns.ex new file mode 100644 index 0000000..747b0e7 --- /dev/null +++ b/lib/tusker_push/apns.ex @@ -0,0 +1,37 @@ +defmodule TuskerPush.Apns do + alias TuskerPush.Registration + + @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 + else + {:error, reason} -> + {:error, reason} + end + end + + def host(:development) do + "api.sandbox.push.apple.com" + end + + def host(:production) do + "api.push.apple.com" + end +end diff --git a/lib/tusker_push/apns/token.ex b/lib/tusker_push/apns/token.ex new file mode 100644 index 0000000..0bdd68d --- /dev/null +++ b/lib/tusker_push/apns/token.ex @@ -0,0 +1,67 @@ +defmodule TuskerPush.Apns.Token do + use GenServer + + 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 + Process.send_after(self(), :refresh, 1000 * 60 * 45) + {:ok, generate_token()} + end + + @impl true + def handle_info(:refresh, _old_token) do + {:noreply, generate_token()} + end + + @impl true + def handle_call(:current, _from, 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 + + config = Application.fetch_env!(:tusker_push, :apns) + + jwk = JOSE.JWK.from_pem(config[:key_pem]) + + jws = + JOSE.JWS.from_map(%{ + "kid" => config[:key_id], + "alg" => "ES256" + }) + + claims = %{ + "iss" => config[:team_id], + "iat" => DateTime.utc_now() |> DateTime.to_unix() + } + + {_, result} = + JOSE.JWT.sign(jwk, jws, claims) + |> JOSE.JWS.compact() + + result + end +end diff --git a/lib/tusker_push/application.ex b/lib/tusker_push/application.ex index fe4c513..9e09b1b 100644 --- a/lib/tusker_push/application.ex +++ b/lib/tusker_push/application.ex @@ -14,6 +14,13 @@ defmodule TuskerPush.Application do {Phoenix.PubSub, name: TuskerPush.PubSub}, # Start a worker by calling: TuskerPush.Worker.start_link(arg) # {TuskerPush.Worker, arg}, + {Finch, + name: TuskerPush.Finch, + pools: %{ + ("https://" <> TuskerPush.Apns.host(:development)) => [protocols: [:http2], count: 1], + ("https://" <> TuskerPush.Apns.host(:production)) => [protocols: [:http2], count: 1] + }}, + TuskerPush.Apns.Token, # Start to serve requests, typically the last entry TuskerPushWeb.Endpoint ] diff --git a/lib/tusker_push/forwarder.ex b/lib/tusker_push/forwarder.ex new file mode 100644 index 0000000..b05ee8d --- /dev/null +++ b/lib/tusker_push/forwarder.ex @@ -0,0 +1,22 @@ +defmodule TuskerPush.Forwarder do + alias TuskerPush.Apns + alias TuskerPush.Registration + + @spec forward(Registration.t(), binary(), String.t(), String.t()) :: :ok | {:error, term()} + def forward(registration, body, salt, key) do + payload = %{ + "aps" => %{ + "alert" => %{ + "loc-key" => "apns_enc" + }, + "mutable-content" => 1 + }, + "reg_id" => registration.id, + "data" => Base.encode64(body), + "salt" => salt, + "key" => key + } + + Apns.send(registration, payload) + end +end diff --git a/lib/tusker_push/registration.ex b/lib/tusker_push/registration.ex index 781ca1b..50fa698 100644 --- a/lib/tusker_push/registration.ex +++ b/lib/tusker_push/registration.ex @@ -18,6 +18,7 @@ defmodule TuskerPush.Registration do field :storekit_original_transaction_id, :string field :apns_environment, Ecto.Enum, values: [:production, :development] + # hex-encoded field :apns_device_token, :string timestamps() diff --git a/mix.exs b/mix.exs index cf76b4d..8c47497 100644 --- a/mix.exs +++ b/mix.exs @@ -41,7 +41,9 @@ defmodule TuskerPush.MixProject do {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.1.1"}, {:bandit, "~> 1.2"}, - {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false} + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, + {:finch, "~> 0.18"}, + {:jose, "~> 1.11"} ] end diff --git a/mix.lock b/mix.lock index cb4eb21..99ef863 100644 --- a/mix.lock +++ b/mix.lock @@ -8,9 +8,15 @@ "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"}, + "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "phoenix": {:hex, :phoenix, "1.7.11", "1d88fc6b05ab0c735b250932c4e6e33bfa1c186f76dcf623d8dd52f07d6379c7", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "b1ec57f2e40316b306708fe59b92a16b9f6f4bf50ccfa41aa8c7feb79e0ec02a"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},