Push forwarding
This commit is contained in:
parent
c58d5768ba
commit
47d2334d4d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue