Skip to content

Commit

Permalink
πŸ›βž•βž– Swaps out calendar for timex module;
Browse files Browse the repository at this point in the history
fix error (UndefinedFunctionError) function Calendar.ContainsDate.date_struct/1 is undefined
  • Loading branch information
mollyretter committed Oct 23, 2023
1 parent dadbaa5 commit edc0018
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 33 deletions.
66 changes: 55 additions & 11 deletions lib/api_auth/date_header.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ defmodule ApiAuth.DateHeader do
@header_key :DATE
@value_key :timestamp
@allowed_skew 900
@httpdate_format_string "%a, %d %b %Y %H:%M:%S GMT"
@utc_timezone "Etc/UTC"

alias ApiAuth.HeaderValues
alias ApiAuth.HeaderCompare

alias Calendar.DateTime
alias Calendar.DateTime.Format
alias Calendar.DateTime.Parse
alias Timex.Parse.DateTime.Parser

def headers(hv) do
hv |> HeaderValues.put_new(@keys, @header_key, @value_key, timestamp())
Expand All @@ -21,25 +21,69 @@ defmodule ApiAuth.DateHeader do
hc |> HeaderCompare.compare(@keys, &timestamp_compare/2)
end

@doc """
Takes a DateTime and returns a string with the date-time in RFC 2616 format.
This format is used in the HTTP protocol. Note that the date-time will always be "shifted" to UTC.
"""
def httpdate(dt) do
Calendar.strftime(dt, @httpdate_format_string)
end

@doc """
Parses httpdates into Datetime structs
iex> parse_httpdate("Sat, 06 Sep 2014 09:09:08 GMT")
{:ok,
%DateTime{
year: 2014,
month: 9,
day: 6,
hour: 9,
minute: 9,
second: 8,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0,
microsecond: {0, 0}
}
}
"""
def parse_httpdate(dt) do
case Parser.parse(dt, @httpdate_format_string, :strftime) do
{:ok, result} -> DateTime.from_naive(result, @utc_timezone)
{:error, error_msg} -> {:error, error_msg}
end
end

defp now do
Timex.now(@utc_timezone)
end

# NOTE: Returns current datetime in RFC 2616 format.
# Uses 'GMT' instead of 'UTC' for timezone.
# e.g. "Mon, 23 Oct 2023 14:45:18 GMT"
defp timestamp do
DateTime.now_utc()
|> Format.httpdate()
now()
|> httpdate
end

defp timestamp_compare(t1, t2) do
t1 == t2 && timestamp_within_skew?(Parse.httpdate(t1))
t1 == t2 && timestamp_within_skew?(parse_httpdate(t1))
end

defp timestamp_within_skew?({:ok, time}) do
now = DateTime.now_utc()

case DateTime.diff(now, time) do
{:ok, _, _, :same_time} -> true
{:ok, seconds, _, :after} -> seconds < @allowed_skew
case Timex.diff(now(), time, :second) do
seconds when seconds < 0 -> false
seconds when seconds == 0 -> true
seconds when seconds <= @allowed_skew -> true
_ -> false
end
end

defp timestamp_within_skew?({:error, _}) do
false
end

defp timestamp_within_skew?(_timestamp) do
false
end
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ defmodule ApiAuth.Mixfile do

defp deps do
[
{:calendar, "~> 1.0"},
{:timex, "~> 3.7.9"},
{:plug_crypto, "~> 1.0"},
{:credo, "~> 1.6.1", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.14", only: :dev, runtime: false},
Expand Down
5 changes: 4 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
"certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"credo": {:hex, :credo, "1.6.2", "2f82b29a47c0bb7b72f023bf3a34d151624f1cbe1e6c4e52303b05a11166a701", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ae9dc112bc368e7b145c547bec2ed257ef88955851c15057c7835251a17211c6"},
"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm", "3b1dcad3067985dd8618c38399a8ee9c4e652d52a17a4aae7a6d6fc4fcc24856"},
"ex_doc": {:hex, :ex_doc, "0.16.2", "3b3e210ebcd85a7c76b4e73f85c5640c011d2a0b2f06dcdf5acdb2ae904e5084", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "b6fb4aef8125c62e6b6a7d7507eff70f376a7050c7745af11f08333ea9beebd3"},
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"gettext": {:hex, :gettext, "0.23.1", "821e619a240e6000db2fc16a574ef68b3bd7fe0167ccc264a81563cc93e67a31", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"},
"hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.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.3.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", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"},
"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.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
Expand All @@ -15,6 +17,7 @@
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
Expand Down
63 changes: 43 additions & 20 deletions test/api_auth/date_header_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ defmodule ApiAuth.DateHeaderTest do
alias ApiAuth.HeaderValues
alias ApiAuth.HeaderCompare

alias Calendar.DateTime.Format

describe "headers" do
test "it gets the value from the headers" do
headers = [HTTP_DATE: "Sat, 01 Jan 2000 00:00:00 GMT"]
Expand All @@ -29,9 +27,8 @@ defmodule ApiAuth.DateHeaderTest do
|> DateHeader.headers()
|> HeaderValues.get(:timestamp)

diff =
Calendar.DateTime.now_utc()
|> DateTime.diff(Calendar.DateTime.Parse.httpdate!(value), :second)
{:ok, parsed} = DateHeader.parse_httpdate(value)
diff = Timex.diff(Timex.now(:utc), parsed, :second)

assert diff == 0
end
Expand All @@ -46,9 +43,8 @@ defmodule ApiAuth.DateHeaderTest do
|> HeaderValues.unwrap()
|> Keyword.fetch!(:DATE)

diff =
Calendar.DateTime.now_utc()
|> DateTime.diff(Calendar.DateTime.Parse.httpdate!(value), :second)
{:ok, parsed} = DateHeader.parse_httpdate(value)
diff = Timex.diff(Timex.now(:utc), parsed, :second)

assert diff == 0
end
Expand All @@ -68,7 +64,7 @@ defmodule ApiAuth.DateHeaderTest do

describe "compare" do
test "it is true when the timestamps match" do
timestamp = Format.httpdate(Calendar.DateTime.now_utc())
timestamp = DateHeader.httpdate(Timex.now(:utc))
valid_headers = [DATE: timestamp]
request_headers = [HTTP_DATE: timestamp]

Expand All @@ -80,9 +76,10 @@ defmodule ApiAuth.DateHeaderTest do
end

test "it is false when the timestamps don't match" do
time = Calendar.DateTime.now_utc()
timestamp1 = Format.httpdate(time)
timestamp2 = Format.httpdate(Calendar.DateTime.subtract!(time, 1))
now = Timex.now(:utc)
timestamp1 = DateHeader.httpdate(now)
past = Timex.shift(now, seconds: -1)
timestamp2 = DateHeader.httpdate(past)

valid_headers = [DATE: timestamp1]
request_headers = [HTTP_DATE: timestamp2]
Expand All @@ -106,9 +103,8 @@ defmodule ApiAuth.DateHeaderTest do
end

test "it is true if the timestamps are within 15 minutes from now" do
time = Calendar.DateTime.subtract!(Calendar.DateTime.now_utc(), 800)
timestamp = Format.httpdate(time)

time = Timex.shift(Timex.now(:utc), seconds: -800)
timestamp = DateHeader.httpdate(time)
valid_headers = [DATE: timestamp]
request_headers = [HTTP_DATE: timestamp]

Expand All @@ -120,8 +116,8 @@ defmodule ApiAuth.DateHeaderTest do
end

test "it is false if the timestamps are older than 15 minutes" do
time = Calendar.DateTime.subtract!(Calendar.DateTime.now_utc(), 901)
timestamp = Format.httpdate(time)
time = Timex.shift(Timex.now(:utc), seconds: -901)
timestamp = DateHeader.httpdate(time)

valid_headers = [DATE: timestamp]
request_headers = [HTTP_DATE: timestamp]
Expand All @@ -134,9 +130,8 @@ defmodule ApiAuth.DateHeaderTest do
end

test "it is false if the timestamps are in the future" do
time = Calendar.DateTime.add!(Calendar.DateTime.now_utc(), 1)
timestamp = Format.httpdate(time)

time = Timex.shift(Timex.now(:utc), seconds: 7)
timestamp = DateHeader.httpdate(time)
valid_headers = [DATE: timestamp]
request_headers = [HTTP_DATE: timestamp]

Expand All @@ -147,4 +142,32 @@ defmodule ApiAuth.DateHeaderTest do
|> refute()
end
end

describe "httpdate" do
test "it formats the datetime object correctly" do
date = DateTime.new!(~D[2023-10-31], ~T[12:34:16], "Etc/UTC")

assert DateHeader.httpdate(date) == "Tue, 31 Oct 2023 12:34:16 GMT"
end
end

describe "parse_httpdate" do
test "it parses httpdate into datetime object" do
{:ok, result} = DateHeader.parse_httpdate("Tue, 31 Oct 2023 04:59:03 GMT")

assert result == %DateTime{
year: 2023,
month: 10,
day: 31,
hour: 4,
minute: 59,
second: 3,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0,
microsecond: {0, 0}
}
end
end
end

0 comments on commit edc0018

Please sign in to comment.