Skip to content

Commit 8039f9d

Browse files
committed
Merge branch 'hsts-maintenance'
2 parents e3ed1a0 + d9f1e49 commit 8039f9d

File tree

4 files changed

+115
-30
lines changed

4 files changed

+115
-30
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ UNRELEASED
55
- Add workaround to allow being required in openresty (#98)
66
- Add http.tls.old_cipher_list (#112)
77
- Add http.cookie module (#117)
8+
- Improvements to http.hsts module (#119)
89

910

1011
0.2 - 2017-05-28

doc/modules/http.hsts.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ Data structures useful for HSTS (HTTP Strict Transport Security)
77
Creates and returns a new HSTS store.
88

99

10+
### `hsts_store.max_items` <!-- --> {#http.hsts.max_items}
11+
12+
The maximum number of items allowed in the store.
13+
Decreasing this value will only prevent new items from being added, it will not remove old items.
14+
15+
Defaults to infinity (any number of items is allowed).
16+
17+
1018
### `hsts_store:clone()` <!-- --> {#http.hsts:clone}
1119

1220
Creates and returns a copy of a store.
@@ -16,12 +24,24 @@ Creates and returns a copy of a store.
1624

1725
Add new directives to the store about the given `host`. `directives` should be a table of directives, which *must* include the key `"max-age"`.
1826

27+
Returns a boolean indicating if the item was accepted.
28+
29+
30+
### `hsts_store:remove(host)` <!-- --> {#http.hsts:remove}
31+
32+
Removes the entry for `host` from the store (if it exists).
33+
1934

2035
### `hsts_store:check(host)` <!-- --> {#http.hsts:check}
2136

2237
Returns a boolean indicating if the given `host` is a known HSTS host.
2338

2439

40+
### `hsts_store:clean_due()` <!-- --> {#http.hsts:clean_due}
41+
42+
Returns the number of seconds until the next item in the store expires.
43+
44+
2545
### `hsts_store:clean()` <!-- --> {#http.hsts:clean}
2646

2747
Removes expired entries from the store.

http/hsts.lua

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ Data structures useful for HSTS (HTTP Strict Transport Security)
33
HSTS is described in RFC 6797
44
]]
55

6-
local EOF = require "lpeg".P(-1)
7-
local IPv4address = require "lpeg_patterns.IPv4".IPv4address
8-
local IPv6address = require "lpeg_patterns.IPv6".IPv6address
9-
local IPaddress = (IPv4address + IPv6address) * EOF
6+
local binaryheap = require "binaryheap"
7+
local http_util = require "http.util"
108

119
local store_methods = {
1210
time = function() return os.time() end;
11+
max_items = (1e999);
1312
}
1413

1514
local store_mt = {
@@ -23,25 +22,22 @@ local store_item_mt = {
2322
__index = store_item_methods;
2423
}
2524

26-
local function host_is_ip(host)
27-
if IPaddress:match(host) then
28-
return true
29-
else
30-
return false
31-
end
32-
end
33-
3425
local function new_store()
3526
return setmetatable({
3627
domains = {};
28+
expiry_heap = binaryheap.minUnique();
29+
n_items = 0;
3730
}, store_mt)
3831
end
3932

4033
function store_methods:clone()
4134
local r = new_store()
4235
r.time = rawget(self, "time")
36+
r.n_items = rawget(self, "n_items")
37+
r.expiry_heap = binaryheap.minUnique()
4338
for host, item in pairs(self.domains) do
4439
r.domains[host] = item
40+
r.expiry_heap:insert(item.expires, item)
4541
end
4642
return r
4743
end
@@ -56,34 +52,62 @@ function store_methods:store(host, directives)
5652
else
5753
max_age = tonumber(max_age, 10)
5854
end
59-
if host_is_ip(host) then
60-
return false
61-
end
55+
56+
-- Clean now so that we can assume there are no expired items in store
57+
self:clean()
58+
6259
if max_age == 0 then
63-
-- delete from store
64-
self.domains[host] = nil
60+
return self:remove(host)
6561
else
62+
if http_util.is_ip(host) then
63+
return false
64+
end
6665
-- add to store
67-
self.domains[host] = setmetatable({
66+
local old_item = self.domains[host]
67+
if old_item then
68+
self.expiry_heap:remove(old_item)
69+
else
70+
local n_items = self.n_items
71+
if n_items >= self.max_items then
72+
return false
73+
end
74+
self.n_items = n_items + 1
75+
end
76+
local expires = now + max_age
77+
local item = setmetatable({
78+
host = host;
6879
includeSubdomains = directives.includeSubdomains;
69-
expires = now + max_age;
80+
expires = expires;
7081
}, store_item_mt)
82+
self.domains[host] = item
83+
self.expiry_heap:insert(expires, item)
84+
end
85+
return true
86+
end
87+
88+
function store_methods:remove(host)
89+
local item = self.domains[host]
90+
if item then
91+
self.expiry_heap:remove(item)
92+
self.domains[host] = nil
93+
self.n_items = self.n_items - 1
7194
end
7295
return true
7396
end
7497

7598
function store_methods:check(host)
76-
if host_is_ip(host) then
99+
if http_util.is_ip(host) then
77100
return false
78101
end
79-
local now = self.time()
102+
103+
-- Clean now so that we can assume there are no expired items in store
104+
self:clean()
105+
80106
local h = host
81107
repeat
82108
local item = self.domains[h]
83109
if item then
84-
if item.expires < now then
85-
self:clean()
86-
elseif host == h or item.includeSubdomains then
110+
if host == h or item.includeSubdomains then
87111
return true
88112
end
89113
end
@@ -93,12 +117,20 @@ function store_methods:check(host)
93117
return false
94118
end
95119

120+
function store_methods:clean_due()
121+
local next_expiring = self.expiry_heap:peek()
122+
if not next_expiring then
123+
return (1e999)
124+
end
125+
return next_expiring.expires
126+
end
127+
96128
function store_methods:clean()
97129
local now = self.time()
98-
for host, item in pairs(self.domains) do
99-
if item.expires < now then
100-
self.domains[host] = nil
101-
end
130+
while self:clean_due() < now do
131+
local item = self.expiry_heap:pop()
132+
self.domains[item.host] = nil
133+
self.n_items = self.n_items - 1
102134
end
103135
return true
104136
end

spec/hsts_spec.lua

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,26 @@ describe("hsts module", function()
99
end)
1010
it("can be cloned", function()
1111
local s = http_hsts.new_store()
12-
assert.same(s, s:clone())
12+
do
13+
local clone = s:clone()
14+
local old_heap = s.expiry_heap
15+
s.expiry_heap = nil
16+
clone.expiry_heap = nil
17+
assert.same(s, clone)
18+
s.expiry_heap = old_heap
19+
end
1320
assert.truthy(s:store("foo.example.com", {
1421
["max-age"] = "100";
1522
}))
23+
do
24+
local clone = s:clone()
25+
local old_heap = s.expiry_heap
26+
s.expiry_heap = nil
27+
clone.expiry_heap = nil
28+
assert.same(s, clone)
29+
s.expiry_heap = old_heap
30+
end
1631
local clone = s:clone()
17-
assert.same(s, clone)
1832
assert.truthy(s:check("foo.example.com"))
1933
assert.truthy(clone:check("foo.example.com"))
2034
end)
@@ -93,4 +107,22 @@ describe("hsts module", function()
93107
assert.falsy(s:check("example.com"))
94108
assert.truthy(s:check("keep.me"))
95109
end)
110+
it("enforces .max_items", function()
111+
local s = http_hsts.new_store()
112+
s.max_items = 0
113+
assert.falsy(s:store("example.com", {
114+
["max-age"] = "100";
115+
}))
116+
s.max_items = 1
117+
assert.truthy(s:store("example.com", {
118+
["max-age"] = "100";
119+
}))
120+
assert.falsy(s:store("other.com", {
121+
["max-age"] = "100";
122+
}))
123+
s:remove("example.com", "/", "foo")
124+
assert.truthy(s:store("other.com", {
125+
["max-age"] = "100";
126+
}))
127+
end)
96128
end)

0 commit comments

Comments
 (0)