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