Packages
cldr_utils
2.26.0
2.29.7
2.29.6
2.29.5
2.29.4
2.29.3
2.29.2
2.29.1
2.29.0
2.28.3
2.28.2
2.28.1
2.28.0
2.27.0
2.26.0
2.25.0
2.24.2
2.24.1
2.24.0
2.23.1
2.23.0
2.22.0
2.21.0
2.20.0
2.19.2
2.19.1
2.19.0
2.18.0
2.17.2
2.17.1
2.17.0
2.17.0-rc.0
2.16.0
2.15.1
2.15.0
2.14.1
2.14.0
2.13.3
retired
2.13.2
2.13.1
2.13.0
retired
2.12.0
2.11.0
2.10.0
2.9.1
2.9.0
retired
2.8.0
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
Map, Calendar, Digits, Decimal, HTTP, Macro, Math, and String helpers for ex_cldr.
Current section
Files
Jump to
Current section
Files
lib/cldr/http/http.ex
defmodule Cldr.Http do
@moduledoc """
Supports securely downloading https content.
"""
@cldr_unsafe_https "CLDR_UNSAFE_HTTPS"
@cldr_default_timeout "120000"
@cldr_default_connection_timeout "60000"
@doc """
Securely download https content from
a URL.
This function uses the built-in `:httpc`
client but enables certificate verification
which is not enabled by `:httc` by default.
See also https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl
### Arguments
* `url` is a binary URL or a `{url, list_of_headers}` tuple. If
provided the headers are a list of `{'header_name', 'header_value'}`
tuples. Note that the name and value are both charlists, not
strings.
* `options` is a keyword list of options.
### Options
* `:verify_peer` is a boolean value indicating
if peer verification should be done for this request.
The default is `true` in which case the default
`:ssl` options follow the [erlef guidelines](https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl)
noted above.
* `:timeout` is the number of milliseconds available
for the request to complete. The default is
#{inspect @cldr_default_timeout}. This option may also be
set with the `CLDR_HTTP_TIMEOUT` environment variable.
* `:connection_timeout` is the number of milliseconds
available for the a connection to be estabklished to
the remote host. The default is #{inspect @cldr_default_connection_timeout}.
This option may also be set with the
`CLDR_HTTP_CONNECTION_TIMEOUT` environment variable.
### Returns
* `{:ok, body}` if the return is successful.
* `{:not_modified, headers}` if the request would result in
returning the same results as one matching an etag.
* `{:error, error}` if the download is
unsuccessful. An error will also be logged
in these cases.
### Unsafe HTTPS
If the environment variable `CLDR_UNSAFE_HTTPS` is
set to anything other than `FALSE`, `false`, `nil`
or `NIL` then no peer verification of certificates
is performed. Setting this variable is not recommended
but may be required is where peer verification for
unidentified reasons. Please [open an issue](https://github.com/elixir-cldr/cldr/issues)
if this occurs.
### Certificate stores
In order to keep dependencies to a minimum,
`get/1` attempts to locate an already installed
certificate store. It will try to locate a
store in the following order which is intended
to satisfy most host systems. The certificate
store is expected to be a path name on the
host system.
```elixir
# A certificate store configured by the
# developer
Application.get_env(:ex_cldr, :cacertfile)
# Populated if hex package `CAStore` is configured
CAStore.file_path()
# Populated if hex package `certfi` is configured
:certifi.cacertfile()
# Debian/Ubuntu/Gentoo etc.
"/etc/ssl/certs/ca-certificates.crt",
# Fedora/RHEL 6
"/etc/pki/tls/certs/ca-bundle.crt",
# OpenSUSE
"/etc/ssl/ca-bundle.pem",
# OpenELEC
"/etc/pki/tls/cacert.pem",
# CentOS/RHEL 7
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
# Open SSL on MacOS
"/usr/local/etc/openssl/cert.pem",
# MacOS & Alpine Linux
"/etc/ssl/cert.pem"
```
"""
@spec get(String.t | {String.t, list()}, options :: Keyword.t) ::
{:ok, binary} | {:not_modified, any()} | {:error, any}
def get(url, options \\ [])
def get(url, options) when is_binary(url) and is_list(options) do
case get_with_headers(url, options) do
{:ok, _headers, body} -> {:ok, body}
other -> other
end
end
def get({url, headers}, options) when is_binary(url) and is_list(headers) and is_list(options) do
case get_with_headers({url, headers}, options) do
{:ok, _headers, body} -> {:ok, body}
other -> other
end
end
@doc """
Securely download https content from
a URL.
This function uses the built-in `:httpc`
client but enables certificate verification
which is not enabled by `:httc` by default.
See also https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl
### Arguments
* `url` is a binary URL or a `{url, list_of_headers}` tuple. If
provided the headers are a list of `{'header_name', 'header_value'}`
tuples. Note that the name and value are both charlists, not
strings.
* `options` is a keyword list of options.
### Options
* `:verify_peer` is a boolean value indicating
if peer verification should be done for this request.
The default is `true` in which case the default
`:ssl` options follow the [erlef guidelines](https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl)
noted above.
* `:timeout` is the number of milliseconds available
for the request to complete. The default is
#{inspect @cldr_default_timeout}. This option may also be
set with the `CLDR_HTTP_TIMEOUT` environment variable.
* `:connection_timeout` is the number of milliseconds
available for the a connection to be estabklished to
the remote host. The default is #{inspect @cldr_default_connection_timeout}.
This option may also be set with the
`CLDR_HTTP_CONNECTION_TIMEOUT` environment variable.
* `:https_proxy` is the URL of an https proxy to be used. The
default is `nil`.
### Returns
* `{:ok, body, headers}` if the return is successful.
* `{:not_modified, headers}` if the request would result in
returning the same results as one matching an etag.
* `{:error, error}` if the download is
unsuccessful. An error will also be logged
in these cases.
### Unsafe HTTPS
If the environment variable `CLDR_UNSAFE_HTTPS` is
set to anything other than `FALSE`, `false`, `nil`
or `NIL` then no peer verification of certificates
is performed. Setting this variable is not recommended
but may be required is where peer verification for
unidentified reasons. Please [open an issue](https://github.com/elixir-cldr/cldr/issues)
if this occurs.
### Https Proxy
`Cldr.Http.get/2` will look for a proxy URL in the following
locales in the order presented:
* `options[:https_proxy]`
* `ex_cldr` compile-time configuration under the
key `:ex_cldr[:https_proxy]`
* The environment variable `HTTPS_PROXY`
* The environment variable `https_proxy`
### Certificate stores
In order to keep dependencies to a minimum,
`get/1` attempts to locate an already installed
certificate store. It will try to locate a
store in the following order which is intended
to satisfy most host systems. The certificate
store is expected to be a path name on the
host system.
```elixir
# A certificate store configured by the
# developer
Application.get_env(:ex_cldr, :cacertfile)
# Populated if hex package `CAStore` is configured
CAStore.file_path()
# Populated if hex package `certfi` is configured
:certifi.cacertfile()
# Debian/Ubuntu/Gentoo etc.
"/etc/ssl/certs/ca-certificates.crt",
# Fedora/RHEL 6
"/etc/pki/tls/certs/ca-bundle.crt",
# OpenSUSE
"/etc/ssl/ca-bundle.pem",
# OpenELEC
"/etc/pki/tls/cacert.pem",
# CentOS/RHEL 7
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
# Open SSL on MacOS
"/usr/local/etc/openssl/cert.pem",
# MacOS & Alpine Linux
"/etc/ssl/cert.pem"
```
"""
@doc since: "2.21.0"
@spec get_with_headers(String.t | {String.t, list()}, options :: Keyword.t) ::
{:ok, list(), binary} | {:not_modified, any()} | {:error, any}
def get_with_headers(request, options \\ [])
def get_with_headers(url, options) when is_binary(url) do
get_with_headers({url, []}, options)
end
def get_with_headers({url, headers}, options) when is_binary(url) and is_list(headers) and is_list(options) do
require Logger
hostname = String.to_charlist(URI.parse(url).host)
url = String.to_charlist(url)
http_options = http_opts(hostname, options)
https_proxy = https_proxy(options)
if https_proxy do
case URI.parse(https_proxy) do
%{host: host, port: port} when is_binary(host) and is_integer(port) ->
:httpc.set_options([{:https_proxy, {{String.to_charlist(host), port}, []}}])
_other ->
Logger.bare_log(:warning, "https_proxy was set to an invalid value. Found #{inspect https_proxy}.")
end
end
case :httpc.request(:get, {url, headers}, http_options, []) do
{:ok, {{_version, 200, _}, headers, body}} ->
{:ok, headers, body}
{:ok, {{_version, 304, _}, headers, _body}} ->
{:not_modified, headers}
{_, {{_version, code, message}, _headers, _body}} ->
Logger.bare_log(
:error,
"Failed to download #{url}. " <>
"HTTP Error: (#{code}) #{inspect(message)}"
)
{:error, code}
{:error, {:failed_connect, [{_, {host, _port}}, {_, _, sys_message}]}} ->
if sys_message == :timeout do
Logger.bare_log(
:error,
"Timeout connecting to #{inspect(host)} to download #{inspect url}. " <>
"Connection time exceeded #{http_options[:connect_timeout]}ms."
)
{:error, :connection_timeout}
else
Logger.bare_log(
:error,
"Failed to connect to #{inspect(host)} to download #{inspect url}"
)
{:error, sys_message}
end
{:error, {other}} ->
Logger.bare_log(
:error,
"Failed to download #{inspect url}. Error #{inspect other}"
)
{:error, other}
{:error, :timeout} ->
Logger.bare_log(
:error,
"Timeout downloading from #{inspect url}. " <>
"Request exceeded #{http_options[:timeout]}ms."
)
{:error, :timeout}
end
end
@static_certificate_locations [
# Debian/Ubuntu/Gentoo etc.
"/etc/ssl/certs/ca-certificates.crt",
# Fedora/RHEL 6
"/etc/pki/tls/certs/ca-bundle.crt",
# OpenSUSE
"/etc/ssl/ca-bundle.pem",
# OpenELEC
"/etc/pki/tls/cacert.pem",
# CentOS/RHEL 7
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
# Open SSL on MacOS
"/usr/local/etc/openssl/cert.pem",
# MacOS & Alpine Linux
"/etc/ssl/cert.pem"
]
defp dynamic_certificate_locations do
[
# Configured cacertfile
Application.get_env(:ex_cldr, :cacertfile),
# Populated if hex package CAStore is configured
if(Code.ensure_loaded?(CAStore), do: apply(CAStore, :file_path, [])),
# Populated if hex package certfi is configured
if(Code.ensure_loaded?(:certifi), do: apply(:certifi, :cacertfile, []) |> List.to_string())
]
|> Enum.reject(&is_nil/1)
end
def certificate_locations() do
dynamic_certificate_locations() ++ @static_certificate_locations
end
@doc false
defp certificate_store do
certificate_locations()
|> Enum.find(&File.exists?/1)
|> raise_if_no_cacertfile!
|> :erlang.binary_to_list()
end
defp raise_if_no_cacertfile!(nil) do
raise RuntimeError, """
No certificate trust store was found.
Tried looking for: #{inspect(certificate_locations())}
A certificate trust store is required in
order to download locales for your configuration.
Since ex_cldr could not detect a system
installed certificate trust store one of the
following actions may be taken:
1. Install the hex package `castore`. It will
be automatically detected after recompilation.
2. Install the hex package `certifi`. It will
be automatically detected after recomilation.
3. Specify the location of a certificate trust store
by configuring it in `config.exs` or `runtime.exs`:
config :ex_cldr,
cacertfile: "/path/to/cacertfile",
...
"""
end
defp raise_if_no_cacertfile!(file) do
file
end
defp http_opts(hostname, options) do
default_timeout =
"CLDR_HTTP_TIMEOUT"
|> System.get_env(@cldr_default_timeout)
|> String.to_integer()
default_connection_timeout =
"CLDR_HTTP_CONNECTION_TIMEOUT"
|> System.get_env(@cldr_default_connection_timeout)
|> String.to_integer()
verify_peer? = Keyword.get(options, :verify_peer, true)
ssl_options = https_ssl_opts(hostname, verify_peer?)
timeout = Keyword.get(options, :timeout, default_timeout)
connection_timeout = Keyword.get(options, :connection_timeout, default_connection_timeout)
[timeout: timeout, connect_timeout: connection_timeout, ssl: ssl_options]
end
defp https_ssl_opts(hostname, verify_peer?) do
if secure_ssl?() and verify_peer? do
[
verify: :verify_peer,
cacertfile: certificate_store(),
depth: 4,
ciphers: preferred_ciphers(),
versions: protocol_versions(),
eccs: preferred_eccs(),
reuse_sessions: true,
server_name_indication: hostname,
secure_renegotiate: true,
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
]
else
[
verify: :verify_none,
server_name_indication: hostname,
secure_renegotiate: true,
reuse_sessions: true,
versions: protocol_versions(),
ciphers: preferred_ciphers(),
versions: protocol_versions(),
]
end
end
defp preferred_ciphers do
preferred_ciphers =
[
# Cipher suites (TLS 1.3): TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
%{cipher: :aes_128_gcm, key_exchange: :any, mac: :aead, prf: :sha256},
%{cipher: :aes_256_gcm, key_exchange: :any, mac: :aead, prf: :sha384},
%{cipher: :chacha20_poly1305, key_exchange: :any, mac: :aead, prf: :sha256},
# Cipher suites (TLS 1.2): ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:
# ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:
# ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
%{cipher: :aes_128_gcm, key_exchange: :ecdhe_ecdsa, mac: :aead, prf: :sha256},
%{cipher: :aes_128_gcm, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha256},
%{cipher: :aes_256_gcm, key_exchange: :ecdh_ecdsa, mac: :aead, prf: :sha384},
%{cipher: :aes_256_gcm, key_exchange: :ecdh_rsa, mac: :aead, prf: :sha384},
%{cipher: :chacha20_poly1305, key_exchange: :ecdhe_ecdsa, mac: :aead, prf: :sha256},
%{cipher: :chacha20_poly1305, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha256},
%{cipher: :aes_128_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha256},
%{cipher: :aes_256_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha384}
]
:ssl.filter_cipher_suites(preferred_ciphers, [])
end
defp protocol_versions do
if otp_version() < 25 do
[:"tlsv1.2"]
else
[:"tlsv1.2", :"tlsv1.3"]
end
end
defp preferred_eccs do
# TLS curves: X25519, prime256v1, secp384r1
preferred_eccs = [:secp256r1, :secp384r1]
:ssl.eccs() -- (:ssl.eccs() -- preferred_eccs)
end
defp secure_ssl? do
case System.get_env(@cldr_unsafe_https) do
nil -> true
"FALSE" -> false
"false" -> false
"nil" -> false
"NIL" -> false
_other -> true
end
end
defp https_proxy(options) do
options[:https_proxy] ||
Application.get_env(:ex_cldr, :https_proxy) ||
System.get_env("HTTPS_PROXY") ||
System.get_env("https_proxy")
end
def otp_version do
:erlang.system_info(:otp_release) |> List.to_integer
end
end