Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:

jobs:
test:
runs-on: ubuntu-22.04
runs-on: ubuntu-26.04
env:
MIX_ENV: test

Expand All @@ -18,12 +18,12 @@ jobs:
matrix:
include:
- pair:
elixir: 1.14.2
otp: 25.0
elixir: 1.20
otp: 29.0
lint: lint

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- uses: erlef/setup-beam@v1
with:
Expand Down Expand Up @@ -53,10 +53,10 @@ jobs:
matrix:
include:
- pair:
elixir: 1.9.0
otp: 21.3.8
elixir: 1.15
otp: 24.3
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- uses: erlef/setup-beam@v1
with:
Expand Down
50 changes: 44 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
Tzdata
======

[![Build
Status](https://travis-ci.org/lau/tzdata.svg?branch=master)](https://travis-ci.org/lau/tzdata)
[![Build Status](https://github.com/lau/tzdata/actions/workflows/ci.yml/badge.svg)](https://github.com/lau/tzdata/actions/workflows/ci.yml)
[![Hex.pm version](https://img.shields.io/hexpm/v/tzdata.svg)](http://hex.pm/packages/tzdata)
[![Hex.pm downloads](https://img.shields.io/hexpm/dt/tzdata.svg)](https://hex.pm/packages/tzdata)

Expand All @@ -23,14 +22,24 @@ iex> Tzdata.tzdata_version

## Getting started

To use the Tzdata library with Elixir 1.8+, add it to the dependencies in your mix file:
To use the Tzdata library with Elixir 1.15+, add it to the dependencies in your mix file.
Tzdata also needs an HTTP client to download timezone data updates: add either
[Req](https://hex.pm/packages/req) or [Hackney](https://hex.pm/packages/hackney)
(see the "HTTP client" section below):

```elixir
defp deps do
[ {:tzdata, "~> 1.1"}, ]
[
{:tzdata, "~> 1.2"},
{:req, "~> 0.7"},
]
end
```

Instead of Req, you can use `{:hackney, "~> 1.17 or ~> 4.0"}`. If automatic data
updates are disabled (see the "Automatic data updates" section below), no HTTP
client is needed.

In your application you can choose to globally configure Elixir to use Tzdata.
This can be done by putting the following line in the config file of your application:

Expand Down Expand Up @@ -97,9 +106,38 @@ For use with [Calendar](https://github.com/lau/calendar) you can still
specify tzdata ~> 0.1.7 in your mix.exs file in case you experience problems
using version ~> 0.5.20

## Hackney dependency and security
## HTTP client

Tzdata uses an HTTP client to poll IANA for new timezone data releases and download them.
Both [Req](https://hex.pm/packages/req) and [Hackney](https://hex.pm/packages/hackney) are
supported out of the box as optional dependencies — add one of them to your mix file:

```elixir
{:req, "~> 0.7"}
```

or

```elixir
{:hackney, "~> 1.17 or ~> 4.0"}
```

If both are present, Req is preferred. If neither is available and automatic data updates
are enabled, an error is raised when Tzdata tries to fetch updates.

The client can also be selected explicitly with the `http_client` config option:

```elixir
config :tzdata, :http_client, Tzdata.HTTPClient.Hackney
```

The same option can be used to supply a custom client: any module implementing the
`Tzdata.HTTPClient` behaviour (its `get/3` and `head/3` callbacks).

Tzdata depends on Hackney in order to do HTTPS requests to get new updates. This is done because Erlang's built in HTTP client `httpc` does not verify SSL certificates when doing HTTPS requests. Hackney verifies the certificate of IANA when getting new tzdata releases from IANA.
An HTTP client that verifies SSL certificates is used instead of Erlang's built in
HTTP client `httpc`, because `httpc` does not verify SSL certificates when doing
HTTPS requests. Req and Hackney verify the certificate of IANA when getting new
tzdata releases from IANA.

## Documentation

Expand Down
8 changes: 3 additions & 5 deletions lib/tzdata/data_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ defmodule Tzdata.DataLoader do

defp do_latest_file_size_by_head({:error, error}), do: {:error, error}

defp do_latest_file_size_by_head({_tag, resp_code, _headers}) when resp_code != 200,
defp do_latest_file_size_by_head({:ok, {resp_code, _headers}}) when resp_code != 200,
do: {:error, :did_not_get_ok_response}

defp do_latest_file_size_by_head({_tag, _resp_code, headers}) do
defp do_latest_file_size_by_head({:ok, {_resp_code, headers}}) do
headers
|> content_length_from_headers
end
Expand Down Expand Up @@ -170,7 +170,5 @@ defmodule Tzdata.DataLoader do

defp data_dir, do: Tzdata.Util.data_dir()

defp http_client() do
Application.get_env(:tzdata, :http_client, Tzdata.HTTPClient.Hackney)
end
defp http_client, do: Tzdata.Util.http_client!()
end
32 changes: 4 additions & 28 deletions lib/tzdata/http_client/hackney.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule Tzdata.HTTPClient.Hackney do
@moduledoc false
if Code.ensure_loaded?(:hackney) do
defmodule Tzdata.HTTPClient.Hackney do
@moduledoc false

@behaviour Tzdata.HTTPClient
@behaviour Tzdata.HTTPClient

if Code.ensure_loaded?(:hackney) do
@impl true
def get(url, headers, options) do
with {:ok, status, headers, result} <- :hackney.get(url, headers, "", options),
Expand All @@ -27,29 +27,5 @@ defmodule Tzdata.HTTPClient.Hackney do
{:ok, {status, headers}}
end
end
else
@message """
missing :hackney dependency

Tzdata requires a HTTP client in order to automatically update timezone
database.

In order to use the built-in adapter based on Hackney HTTP client, add the
following to your mix.exs dependencies list:

{:hackney, "~> 4.0"}

See README for more information.
"""

@impl true
def get(_url, _headers, _options) do
raise @message
end

@impl true
def head(_url, _headers, _options) do
raise @message
end
end
end
52 changes: 52 additions & 0 deletions lib/tzdata/http_client/req.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
if Code.ensure_loaded?(Req) do
defmodule Tzdata.HTTPClient.Req do
@moduledoc false

@behaviour Tzdata.HTTPClient

@impl true
def get(url, headers, options) do
case Req.get(url, req_options(headers, options)) do
{:ok, %Req.Response{status: status, headers: resp_headers, body: body}} ->
{:ok, {status, normalize_headers(resp_headers), body}}

{:error, reason} ->
{:error, reason}
end
end

@impl true
def head(url, headers, options) do
case Req.head(url, req_options(headers, options)) do
{:ok, %Req.Response{status: status, headers: resp_headers}} ->
{:ok, {status, normalize_headers(resp_headers)}}

{:error, reason} ->
{:error, reason}
end
end

defp req_options(headers, options) do
[
headers: headers,
redirect: Keyword.get(options, :follow_redirect, false),
# The downloaded tar.gz must be returned as-is, so don't send
# accept-encoding (on by default in Req < 0.7) and disable Req's
# automatic decompression and body decoding.
compressed: false,
raw: true
]
end

# Req (v0.4+) returns headers as a map of lists.
defp normalize_headers(headers) when is_map(headers) do
for {name, values} <- headers, value <- values, do: {name, value}
end

# Req has a `legacy_headers_as_lists: true` option, for
# backwards-compatibility.
defp normalize_headers(headers) when is_list(headers) do
headers
end
end
end
15 changes: 11 additions & 4 deletions lib/tzdata/tzdata_app.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ defmodule Tzdata.App do

def start(_type, _args) do
children = [Tzdata.EtsHolder]
children = case Application.fetch_env(:tzdata, :autoupdate) do
{:ok, :enabled} -> children ++ [Tzdata.ReleaseUpdater]
{:ok, :disabled} -> children
end

children =
case Application.fetch_env!(:tzdata, :autoupdate) do
:enabled ->
# Getting the HTTP client also ensures that it exists and raises if not.
Tzdata.Util.http_client!()
children ++ [Tzdata.ReleaseUpdater]

:disabled ->
children
end

{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)

Expand Down
41 changes: 41 additions & 0 deletions lib/tzdata/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,47 @@ defmodule Tzdata.Util do
end
end

def http_client! do
case Application.get_env(:tzdata, :http_client) do
nil ->
cond do
Code.ensure_loaded?(Tzdata.HTTPClient.Req) -> Tzdata.HTTPClient.Req
Code.ensure_loaded?(Tzdata.HTTPClient.Hackney) -> Tzdata.HTTPClient.Hackney
true -> nil
end

module ->
if Code.ensure_loaded?(module) do
module
end
end
|> check_http_client!()
end

defp check_http_client!(module) do
if !(module && Code.ensure_loaded?(module)) do
raise """
missing dependency (or custom HTTP client)

Tzdata requires a HTTP client in order to automatically update timezone
database.

In order to use a built-in HTTP Client adapter, add the
following to your mix.exs dependencies list:

{:req, "~> 0.7"}

or

{:hackney, "~> 4.0"}

See README for more information, including how to use a custom HTTP Client.
"""
end

module
end

if @elixir_newer_1_12 do
# See PR #154.
# Elixir 1.17 and 1.18 deprecated using decreasing Ranges without explicit steps.
Expand Down
10 changes: 5 additions & 5 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
defmodule Tzdata.Mixfile do
use Mix.Project

@version "1.1.4"
@version "1.2.0"

def project do
[
app: :tzdata,
name: "tzdata",
version: @version,
elixir: "~> 1.9",
elixir: "~> 1.15",
package: package(),
description: description(),
deps: deps(),
Expand All @@ -27,7 +27,8 @@ defmodule Tzdata.Mixfile do

defp deps do
[
{:hackney, "~> 1.17 or ~> 4.0"},
{:req, "~> 0.5", optional: true},
{:hackney, "~> 1.17 or ~> 4.0", optional: true},
{:ex_doc, "~> 0.21", only: :dev, runtime: false}
]
end
Expand All @@ -43,8 +44,7 @@ defmodule Tzdata.Mixfile do
defp env do
[
autoupdate: :enabled,
data_dir: nil,
http_client: Tzdata.HTTPClient.Hackney
data_dir: nil
]
end

Expand Down
9 changes: 9 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@
"certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
"earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"},
"ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"},
"finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"},
"hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
"hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
"mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"req": {:hex, :req, "0.7.1", "86271f9e29dca83d382222428ced09ba03bad21e24888c50dd4527553d85f642", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "254638b15ceb9a2624d15aff13bf7903ea2e95cd4b9c1aa18da1fb06e1086b50"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}