Current section
Files
Jump to
Current section
Files
lib/ex_aws/sns/public_key_cache.ex
defmodule ExAws.SNS.PublicKeyCache do
use GenServer
alias ExAws.SNS.CertUtility
@valid_sns_hosts_default ~r/^sns\.[a-z0-9-]+\.amazonaws\.com(\.cn)?$|^sns\.[a-z0-9-]+\.api\.aws$/
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, :ok, opts)
end
def get(cert_url) do
case :ets.lookup(__MODULE__, cert_url) do
[{_cert_url, entry}] -> {:ok, entry}
[] -> GenServer.call(__MODULE__, {:get_public_key, cert_url})
end
end
## Callbacks
def init(:ok) do
ets = :ets.new(__MODULE__, [:named_table, read_concurrency: true])
{:ok, ets}
end
def handle_call({:get_public_key, cert_url}, _from, ets) do
http_client = Application.get_env(:ex_aws, :http_client, ExAws.Request.Hackney)
with :ok <- validate_cert_url(cert_url),
{:ok, pem_entry} <- fetch_certificate(cert_url, http_client),
{:ok, otp_cert} <- CertUtility.decode_otp(pem_entry),
:ok <- CertUtility.validate_subject(pem_entry),
{:ok, not_before, not_after} <- CertUtility.get_validity(otp_cert),
:ok <- CertUtility.build_and_validate_chain(pem_entry, http_client),
{:ok, public_key} <- CertUtility.get_public_key(pem_entry) do
entry = {public_key, not_before, not_after}
:ets.insert(__MODULE__, {cert_url, entry})
{:reply, {:ok, entry}, ets}
else
error -> {:reply, error, ets}
end
end
defp fetch_certificate(cert_url, http_client) do
with {:ok, %{status_code: 200, body: cert_binary}} <- http_client.request(:get, cert_url) do
case :public_key.pem_decode(cert_binary) do
[entry] -> {:ok, entry}
entries -> {:error, "Invalid PEM entries: #{inspect(entries)}"}
end
else
{:ok, %{status_code: status_code}} ->
{:error,
"Could not fetch certificate from #{cert_url}, expected http code 200, got: #{status_code}"}
{:error, %{reason: reason}} ->
{:error,
"Unexpected error, could not fetch certificate from #{cert_url}, got #{inspect(reason)}"}
end
end
defp validate_cert_url(cert_url) do
allowed_cert_schemes = Application.get_env(:ex_aws_sns, :allowed_cert_schemes, ["https"])
valid_hosts = Application.get_env(:ex_aws_sns, :valid_hosts, @valid_sns_hosts_default)
uri = URI.parse(cert_url)
cond do
uri.scheme not in allowed_cert_schemes ->
{:error, "Invalid certificate URL scheme: #{cert_url}"}
not String.ends_with?(uri.path || "", ".pem") ->
{:error, "Invalid SNS certificate URL path, expected .pem extension: #{uri.path}"}
!(uri.host =~ valid_hosts) ->
{:error, "Invalid SNS certificate URL host: #{uri.host}"}
true ->
:ok
end
end
end