Skip to content

Commit

Permalink
fix: pass dialyzer (#170)
Browse files Browse the repository at this point in the history
* chore: update deps

* fix: pass dialyzer

* fix: more specific apns notification payload typespec

* fix: proper APNS notification payload spec
  • Loading branch information
hpopp authored May 25, 2020
1 parent 527b305 commit 9a248b3
Show file tree
Hide file tree
Showing 14 changed files with 183 additions and 181 deletions.
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
elixir 1.10.2
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

# v1.5.1
* Fixed various typespecs.

## v1.5.0
* Bumped minimum Elixir version to 1.6
* Raise `Pigeon.ConfigError` when booting invalid config structs.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Add pigeon and kadabra as `mix.exs` dependencies:
```elixir
def deps do
[
{:pigeon, "~> 1.5.0"},
{:pigeon, "~> 1.5.1"},
{:kadabra, "~> 0.4.4"}
]
end
Expand Down
2 changes: 1 addition & 1 deletion lib/pigeon/adm/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ defmodule Pigeon.ADM.Config do

defp valid_item?(item), do: is_binary(item) and String.length(item) > 0

@spec validate!(any) :: :ok
@spec validate!(any) :: :ok | no_return
def validate!(config) do
if valid?(config) do
:ok
Expand Down
87 changes: 38 additions & 49 deletions lib/pigeon/apns/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ defmodule Pigeon.APNS.Config do
Configuration for APNS Workers using certificates.
"""

defstruct name: nil,
reconnect: true,
cert: nil,
defstruct cert: nil,
certfile: nil,
key: nil,
keyfile: nil,
uri: nil,
name: nil,
port: 443,
ping_period: 600_000
ping_period: 600_000,
reconnect: true,
uri: nil

@typedoc ~S"""
Certificate APNS configuration struct
Expand Down Expand Up @@ -152,9 +152,14 @@ end
defimpl Pigeon.Configurable, for: Pigeon.APNS.Config do
@moduledoc false

alias Pigeon.APNS.Shared
alias Pigeon.APNS.{
Config,
ConfigParser,
Shared
}

@type sock :: {:sslsocket, any, pid | {any, any}}
@type socket_opts :: maybe_improper_list(atom, integer | boolean)

# Configurable Callbacks

Expand All @@ -166,13 +171,8 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.Config do
def connect(%{uri: uri} = config) do
uri = to_charlist(uri)

case connect_socket_options(config) do
{:ok, options} ->
Pigeon.Http2.Client.default().connect(uri, :https, options)

error ->
error
end
options = connect_socket_options(config)
Pigeon.Http2.Client.default().connect(uri, :https, options)
end

defdelegate push_headers(config, notification, opts), to: Shared
Expand All @@ -186,53 +186,42 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.Config do

defdelegate close(config), to: Shared

def validate!(config) do
case config do
%{cert: {:error, _}, certfile: {:error, _}} ->
raise Pigeon.ConfigError,
reason: "attempted to start without valid certificate",
config: redact(config)

%{key: {:error, _}, keyfile: {:error, _}} ->
raise Pigeon.ConfigError,
reason: "attempted to start without valid key",
config: redact(config)
@spec validate!(Config.t()) :: :ok | no_return
def validate!(%Config{cert: {:error, _}, certfile: {:error, _}} = config) do
raise Pigeon.ConfigError,
reason: "attempted to start without valid certificate",
config: ConfigParser.redact(config)
end

_ ->
:ok
end
def validate!(%Config{key: {:error, _}, keyfile: {:error, _}} = config) do
raise Pigeon.ConfigError,
reason: "attempted to start without valid key",
config: ConfigParser.redact(config)
end

defp redact(config) do
[:cert, :key]
|> Enum.reduce(config, fn key, acc ->
case Map.get(acc, key) do
bin when is_binary(bin) -> Map.put(acc, key, "[FILTERED]")
{:RSAPrivateKey, _bin} -> Map.put(acc, key, "[FILTERED]")
_ -> acc
end
end)
def validate!(%Config{}) do
:ok
end

@spec connect_socket_options(Config.t()) :: socket_opts
def connect_socket_options(config) do
options =
[
cert_option(config),
key_option(config),
{:password, ''},
{:packet, 0},
{:reuseaddr, true},
{:active, true},
:binary
]
|> Shared.add_port(config)

{:ok, options}
[
cert_option(config),
key_option(config),
{:password, ''},
{:packet, 0},
{:reuseaddr, true},
{:active, true},
:binary
]
|> Shared.add_port(config)
end

@spec cert_option(Config.t()) :: {:cert, binary} | {:certfile, binary}
def cert_option(%{cert: cert, certfile: nil}), do: {:cert, cert}
def cert_option(%{cert: nil, certfile: file}), do: {:certfile, file}

@spec key_option(Config.t()) :: {:key, binary} | {:keyfile, binary}
def key_option(%{key: key, keyfile: nil}), do: {:key, key}
def key_option(%{key: nil, keyfile: file}), do: {:keyfile, file}
end
15 changes: 15 additions & 0 deletions lib/pigeon/apns/config_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ defmodule Pigeon.APNS.ConfigParser do
|> parse()
end

@spec config_type(any) :: module | :error
defp config_type(%{cert: _cert, key_identifier: _key_id}), do: :error
defp config_type(%{cert: _cert}), do: Config
defp config_type(%{key_identifier: _jwt_key}), do: JWTConfig
defp config_type(_else), do: :error

@doc false
@spec file_path(binary) :: binary | {:error, {:nofile, binary}}
def file_path(path) when is_binary(path) do
if :filelib.is_file(path) do
Path.expand(path)
Expand All @@ -58,6 +60,18 @@ defmodule Pigeon.APNS.ConfigParser do

def file_path(other), do: {:error, {:nofile, other}}

@doc false
def redact(config) when is_map(config) do
[:cert, :key, :keyfile]
|> Enum.reduce(config, fn key, acc ->
case Map.get(acc, key) do
bin when is_binary(bin) -> Map.put(acc, key, "[FILTERED]")
{:RSAPrivateKey, _bin} -> Map.put(acc, key, "[FILTERED]")
_ -> acc
end
end)
end

@doc false
def strip_errors(config, key1, key2) do
case {Map.get(config, key1), Map.get(config, key2)} do
Expand All @@ -67,6 +81,7 @@ defmodule Pigeon.APNS.ConfigParser do
end
end

@spec uri_for_mode(atom) :: binary | nil
def uri_for_mode(:dev), do: @apns_development_api_uri
def uri_for_mode(:prod), do: @apns_production_api_uri
def uri_for_mode(_else), do: nil
Expand Down
123 changes: 57 additions & 66 deletions lib/pigeon/apns/jwt_config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ defmodule Pigeon.APNS.JWTConfig do
Configuration for APNS Workers using JWT.
"""

defstruct name: nil,
reconnect: true,
uri: nil,
port: 443,
ping_period: 600_000,
key: nil,
defstruct key: nil,
keyfile: nil,
key_identifier: nil,
team_id: nil
name: nil,
ping_period: 600_000,
port: 443,
reconnect: true,
team_id: nil,
uri: nil

alias Pigeon.APNS.{Config, ConfigParser}
alias Pigeon.APNS.ConfigParser

@type headers :: [{binary, binary}]

@typedoc ~S"""
JWT APNS configuration struct
Expand Down Expand Up @@ -41,8 +43,8 @@ defmodule Pigeon.APNS.JWTConfig do
uri: binary | nil,
port: pos_integer,
ping_period: pos_integer,
key: binary | nil,
keyfile: binary | nil,
key: binary | nil | {:error, term},
keyfile: binary | nil | {:error, term},
key_identifier: binary | nil,
team_id: binary | nil
}
Expand Down Expand Up @@ -142,9 +144,16 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do

import Joken.Config

alias Pigeon.APNS.{Config, JWTConfig, Notification, Shared}
alias Pigeon.APNS.{
ConfigParser,
JWTConfig,
Notification,
Shared
}

@type headers :: [{binary, binary}]
@type sock :: {:sslsocket, any, pid | {any, any}}
@type socket_opts :: maybe_improper_list(atom, integer | boolean)

# Seconds
@token_max_age 3_590
Expand All @@ -155,22 +164,16 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do

defdelegate max_demand(any), to: Shared

@spec connect(any) :: {:ok, sock} | {:error, String.t()}
@spec connect(any) :: {:ok, sock} | {:error, binary}
def connect(%{uri: uri} = config) do
uri = to_charlist(uri)

case connect_socket_options(config) do
{:ok, options} ->
Pigeon.Http2.Client.default().connect(uri, :https, options)

error ->
error
end
options = connect_socket_options(config)
Pigeon.Http2.Client.default().connect(uri, :https, options)
end

@spec push_headers(JWTConfig.t(), Notification.t(), Keyword.t()) ::
Shared.headers()
def push_headers(config, notification, opts) do
@spec push_headers(JWTConfig.t(), Notification.t(), Keyword.t()) :: headers | no_return
def push_headers(%JWTConfig{} = config, notification, opts) do
config
|> Shared.push_headers(notification, opts)
|> put_bearer_token(config)
Expand All @@ -185,54 +188,42 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do

defdelegate close(config), to: Shared

def validate!(config) do
case config do
%{team_id: nil} ->
raise Pigeon.ConfigError,
reason: "attempted to start without valid team_id",
config: redact(config)

%{key_identifier: nil} ->
raise Pigeon.ConfigError,
reason: "attempted to start without valid key_identifier",
config: redact(config)

%{key: {:error, _}, keyfile: {:error, _}} ->
raise Pigeon.ConfigError,
reason: "attempted to start without valid key",
config: redact(config)

_ ->
:ok
end
@spec validate!(JWTConfig.t()) :: :ok | no_return
def validate!(%JWTConfig{team_id: nil} = config) do
raise Pigeon.ConfigError,
reason: "attempted to start without valid team_id",
config: ConfigParser.redact(config)
end

defp redact(config) do
[:key, :keyfile]
|> Enum.reduce(config, fn key, acc ->
case Map.get(acc, key) do
bin when is_binary(bin) -> Map.put(acc, key, "[FILTERED]")
{:RSAPrivateKey, _bin} -> Map.put(acc, key, "[FILTERED]")
_ -> acc
end
end)
def validate!(%JWTConfig{key_identifier: nil} = config) do
raise Pigeon.ConfigError,
reason: "attempted to start without valid key_identifier",
config: ConfigParser.redact(config)
end

def validate!(%JWTConfig{key: {:error, _}, keyfile: {:error, _}} = config) do
raise Pigeon.ConfigError,
reason: "attempted to start without valid key",
config: ConfigParser.redact(config)
end

def validate!(%JWTConfig{}) do
:ok
end

@spec connect_socket_options(JWTConfig.t()) :: socket_opts
def connect_socket_options(%{key: _jwt_key} = config) do
options =
[
{:packet, 0},
{:reuseaddr, true},
{:active, true},
:binary
]
|> Shared.add_port(config)

{:ok, options}
[
{:packet, 0},
{:reuseaddr, true},
{:active, true},
:binary
]
|> Shared.add_port(config)
end

@spec put_bearer_token(Config.headers(), JWTConfig.t()) :: Config.headers()
defp put_bearer_token(headers, config) do
@spec put_bearer_token(headers, JWTConfig.t()) :: headers
defp put_bearer_token(headers, %JWTConfig{} = config) when is_list(headers) do
token_storage_key = config.key_identifier <> ":" <> config.team_id
{timestamp, saved_token} = Pigeon.APNS.Token.get(token_storage_key)
now = :os.system_time(:seconds)
Expand All @@ -246,7 +237,7 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do
[{"authorization", "bearer " <> token} | headers]
end

@spec generate_apns_jwt(JWTConfig.t(), String.t()) :: String.t()
@spec generate_apns_jwt(JWTConfig.t(), binary) :: binary
defp generate_apns_jwt(config, token_storage_key) do
key = get_token_key(config)
now = :os.system_time(:seconds)
Expand All @@ -255,14 +246,14 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do

{:ok, token, _claims} =
default_claims(iss: config.team_id, iat: now)
|> Joken.generate_and_sign(nil, signer)
|> Joken.generate_and_sign(%{}, signer)

:ok = Pigeon.APNS.Token.update(token_storage_key, {now, token})

token
end

@spec get_token_key(JWTConfig.t()) :: JOSE.JWK.t()
@spec get_token_key(JWTConfig.t()) :: %{binary => binary}
defp get_token_key(%JWTConfig{keyfile: nil} = config) do
%{"pem" => config.key}
end
Expand Down
Loading

0 comments on commit 9a248b3

Please sign in to comment.