-
-
Notifications
You must be signed in to change notification settings - Fork 85
http/cookies: New module for parsing, serializing, and storing cookies #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
http/cookies.lua
Outdated
local domain = request.headers:get("host") | ||
local key, value, matched_cookie = assert(http_patts.Set_Cookie:match( | ||
text_cookie)) | ||
-- allow custom time for specs to not error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think time
should default to anything.
http/cookies.lua
Outdated
|
||
local function parse_set_cookie(text_cookie, request, time) | ||
local domain = request.headers:get("host") | ||
local key, value, matched_cookie = assert(http_patts.Set_Cookie:match( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lpeg pattern needs to be anchored
http/cookies.lua
Outdated
local function parse_set_cookie(text_cookie, request, time) | ||
local domain = request.headers:get("host") | ||
local key, value, matched_cookie = assert(http_patts.Set_Cookie:match( | ||
text_cookie)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't wrap lines
http/cookies.lua
Outdated
creation = time; | ||
last_access = time; | ||
persistent = | ||
not not (matched_cookie.expires or matched_cookie["max-age"]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't wrap lines
http/cookies.lua
Outdated
value = value; | ||
host_only = not not matched_cookie.domain | ||
} | ||
cookie.indexname = cookie.domain .."|".. cookie.path .."|".. cookie.key |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer this was a function.
And what stops path or key from containing a |
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to a function and changed the separator to \1
. However, I don't see much of a use for it and might remove it if I don't see anything that uses it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea :) you shouldn't need this at all.
http/cookies.lua
Outdated
text_cookie)) | ||
-- allow custom time for specs to not error | ||
time = time or os.time() | ||
local cookie = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One intention of http_patts.Set_Cookie
was that the output would be usable as a cookie directly.
To set defaults, use a metatable with __index.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
19:40 <~daurnimator> ryan: right. but you can modify the table returned by the pattern
19:42 <~daurnimator> though I guess you might not want to do that
19:42 <~daurnimator> as the pattern can return arbitrary keys
19:42 <~daurnimator> so maybe it's fine as it is...
19:44 <~ryan> another issue is, converting it just ended up as the same line count because most of the items have defaults
19:44 <~ryan> plus the fact that there's so many different naming conventions
19:45 <~ryan> httponly, ["max-age"], same_site
19:46 <~daurnimator> ryan: I was more thinking about saving the extra allocation
19:46 <~ryan> yeah.
19:46 <~daurnimator> but yeah; in this case not worth it
19:46 <~ryan> i think the way i did it is better though
19:47 <~ryan> most of the keys are new keys, so doing it by appending on the old table means reallocating a lot
19:47 <~ryan> the new table means that all but one field are preallocated
19:48 <~ryan> which i actually just fixed to make it so all fields are prealloced
http/cookies.lua
Outdated
end | ||
|
||
local cookiejar_methods = {} | ||
local cookiejar_mt = {__index = cookiejar_methods} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put __index on new line.
Don't forget a __name
http/cookies.lua
Outdated
local cookiejar_mt = {__index = cookiejar_methods} | ||
|
||
local function new_cookiejar(psl_object) | ||
psl_object = psl_object or psl.builtin() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set this default in cookiejar_methods.
Also use psl.latest()
if available rather than builtin
http/cookies.lua
Outdated
return setmetatable({cookies={}, psl = psl_object}, cookiejar_mt) | ||
end | ||
|
||
function cookiejar_methods:add(cookie) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should take time as argument
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, the cookie is "created" when it's parsed - which is also where other values relating to the time are set. I put in a time
value here; however, according to the RFC it should be automatically generated instead of able to be variable (see the line below).
http/cookies.lua
Outdated
table.insert(cookies, 1, cookie) | ||
end | ||
|
||
if not cookies[domain] then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Structure this code a little differently:
local by_domain = cookies[domain]
if not by_domain then
by_domain = {}
cookies[domain] = by_domain
end
local by_path = by_domain[path]
if not by_path then
by_path = {}
by_domain[path] = by_path
end
by_path[key] = cookie
http/cookies.lua
Outdated
return returned_cookies | ||
end | ||
|
||
function cookiejar_methods:get_by_indexname(name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is hugely expensive splitting it apart, you only need a :get(domain, path, key)
http/cookies.lua
Outdated
local util = require "http.util" | ||
local psl = require "psl" | ||
|
||
local function parse_set_cookie(text_cookie, request, time) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of passing in request
here, consider passing in host
and path
?
http/cookies.lua
Outdated
end | ||
|
||
-- check all paths and flatten into a list of sets | ||
for _path, set in pairs(self.cookies[domain]) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid local vars starting with _ unless it is literally _
http/cookies.lua
Outdated
else -- luacheck: ignore | ||
-- ::TODO:: make use of `expires` cookie value | ||
end | ||
local same_site do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for do
: you don't introduce any temporary vars to the scope
http/cookies.lua
Outdated
cookie[#cookie + 1] = "SameSite=" .. data.same_site:gsub("^.", | ||
string.upper) | ||
end | ||
return table.concat(cookie, "; ") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of passing "; "
to table.concat
we could prefix all the keys, e.g. cookie[#cookie + 1] = "; HttpOnly"
http/cookies.lua
Outdated
if data.http_only then | ||
cookie[#cookie + 1] = "HttpOnly" | ||
end | ||
if data.same_site then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this check be == nil
? false might be permited?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no case in which it can be false. Your patterns only give true
:
https://github.com/daurnimator/lpeg_patterns/blob/master/lpeg_patterns/http.lua#L372
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. I was imagining what I user might try and call this with.
Reading https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1 it doesn't look like a value of true
(which is what the pattern produces when no attribute-value is present) should be coerced to Strict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, so I've done a bit more looking into it - I forget exactly where I found the "boolean" version but I remember finding it at least twice. However, none of those were the RFCs and I don't really see a version of the RFC that allows for a boolean-form. Should I just drop the boolean format?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I just drop the boolean format?
Yep.
Add a comment pointing to https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1 too so next person to come along knows where to look
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment added and "unbooleanified".
http/cookies.lua
Outdated
local domain = request.headers:get("host") | ||
local key, value, matched_cookie = assert(http_patts.Set_Cookie:match( | ||
text_cookie)) | ||
local key, value, matched_cookie = assert(http_patts.Set_Cookie:match(text_cookie, 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the 1
for?
http/cookies.lua
Outdated
same_site = matched_cookie.same_site; | ||
} | ||
if matched_cookie["max-age"] then | ||
cookie.expires = time + tonumber(matched_cookie["max-age"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Watch out for tonumber
parsing formats other than decimal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RFC 6265 section 5.2.2 says it should only be in base10 DIGIT
with an optional negative, so I'm adding some code to properly handle it that way.
http/cookies.lua
Outdated
local function bake_cookie(data) | ||
assert(data.key, "cookie must contain a `key` field") | ||
assert(type(data.key) == "string", "`key` field for cookie must be string") | ||
assert(data.value, "cookie must contain a `value` field") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this line now that you have the type check below
http/cookies.lua
Outdated
local v | ||
if data.same_site:lower() == "strict" then | ||
v = "; SameSite=Strict" | ||
elseif data.same_site == "Lax" then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No :lower()
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, totally missed that.
http/cookies.lua
Outdated
end | ||
|
||
local function iterate_cookies(cookie) | ||
return pairs(assert(http_patts.Cookie:match(cookie, 1))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why ,1
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You made a comment on a similar match about anchoring, so I added it here as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a one here is not anchoring. anchoring is adding a *lpeg.P(-1)
to the pattern before use so that you can't have trailing junk.
http/cookies.lua
Outdated
|
||
local function parse_cookies(cookie) | ||
local cookies = {} | ||
for k, v in iterate_cookies(cookie) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern already returns a table; why are you copying it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not copying the table. The iterate_cookies
function returns a pairs()
iterator after asserting that the cookie does parse correctly (which I added an error message to, after reading this, in case parsing fails)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. However you're effectively copying the cookie in here with the cookies[k] = v
http/cookies.lua
Outdated
} | ||
|
||
local function new_cookiejar(psl_object) | ||
return setmetatable({cookies={}, psl_object = psl_object}, cookiejar_mt) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the constructor needs to take a psl_object
http/cookies.lua
Outdated
|
||
function cookiejar_methods:get(domain, path, key) | ||
local cookies = self.cookies | ||
if cookies[domain] then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please restructure
http/cookies.lua
Outdated
local cookie = cookies[i] | ||
cookies[i] = nil | ||
cookies[cookie.domain][cookie.path][cookie.key] = nil | ||
if not next(cookies[cookie.domain][cookie.path]) then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cache these indexing operations in a local
http/cookies.lua
Outdated
for _, cookie in pairs(sets) do | ||
if not cookie.host_only then | ||
if self.psl_object:is_cookie_domain_acceptable(domain, | ||
cookie.domain) then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't wrap
http/cookies.lua
Outdated
function cookiejar_methods:serialize_cookies_for(domain, path, secure) | ||
-- explicitly check for secure; the other two will fail if given bad args | ||
assert(type(secure) == "boolean", "expected boolean for `secure`") | ||
local cookies = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't initialise before it's used
http/cookies.lua
Outdated
-- representable expiration time. | ||
cookie.expires = 0 | ||
else | ||
cookie.expires = time + tonumber(age) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
be careful with tonumber.
http/cookies.lua
Outdated
local age = matched_cookie["max-age"] | ||
if age then | ||
local negative = age:match("^-") | ||
if negative then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably validate it as a number first?
http/cookies.lua
Outdated
|
||
-- check all paths and flatten into a list of sets | ||
local sets = {} | ||
for stored_, set in pairs(self.cookies[domain]) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why stored_
with an underscore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I might've messed up refactoring something on that.
http/cookies.lua
Outdated
end | ||
local by_path = by_domain[path] | ||
if not by_path then | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nil
vs nothing
http/cookies.lua
Outdated
end | ||
|
||
function cookiejar_methods:get(domain, path, key) | ||
local cookies = self.cookies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to localise cookies
end | ||
|
||
function cookiejar_methods:remove_expired(time) | ||
self:remove_cookies(get_expired(self, time)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could inline :remove_cookies
into here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather not, given you should still be able to remove cookies from other methods - such as if they're not persistent and a session is closed.
cookie.expires = time + tonumber(match, 10) | ||
end | ||
else -- luacheck: ignore | ||
-- ::TODO:: make use of `expires` cookie value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plans to implement this TODO?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, after I've got all the other bugs done and I think of a decent way to do it. I plan on getting rid of every 'TODO' before it's merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, didn't see "plans", saw "plan". I'm thinking just use an RFC pattern to match the values and then use os.time()
on the values, but I need to look on what common implementations support for patterns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(as a note, while it's on my mind, there's several various patterns that could match the "time", which makes it difficult to match - it should be mentioned somewhere)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@daurnimator if you plan to build off of this one, this should be the only thing missing outside of optimizing the sequence for a bheap. more documentation would be nice but i can add that later
http/cookies.lua
Outdated
local n = #cookies | ||
-- remove cookies requiring secure connections on insecure connections | ||
for index, cookie in pairs(cookies) do | ||
if cookie.secure and not secure then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do this check when adding; don't go through making holes that you later need to fixup...
You'll also need to install libpsl in the travis file. |
:( looks like trusty and precise (ubuntu versions supported by travis) don't have libpsl. |
Yeah, found that out after spending what feels like forever pulling down the Docker image. Would any other GitHub-supported CI have something we can use for it? Alternatively, can we get Travis CI working on a later Ubuntu version? |
end | ||
end | ||
|
||
local old_cookie = self:get(cookie.domain, cookie.path, cookie.key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: return index with self:get()
to optimize self:remove_cookie()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not reasonable because :get() doesn't iterate
local cookie_exp_time = cookie.expires | ||
local inserted = false | ||
for i=1, #cookies do | ||
-- insert into first spot where cookie expires after |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: In-depth review and add more comments
end | ||
else | ||
cookie.expires = math.huge | ||
table.insert(cookies, 1, cookie) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: Add explanation; the "older" a nonpermanent cookie is, the more likely it should be nuked, hence why it's put in position 1
|
||
function cookiejar_methods:remove_cookie(cookie) | ||
local cookies = self.cookies | ||
for i=1, #cookies do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: Implement faster search; since cookies are sorted using time, it's possible to optimize by "leaping" through until the time is - either less than or greater than, can't recall ATM - and then go back one "leap" and going from there.
local function serialize_cookies(cookies) | ||
local out_values = {} | ||
for _, cookie in pairs(cookies) do | ||
out_values[#out_values + 1] = cookie.key .. "=" .. cookie.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: Use a value n
to avoid getting length every time, then increment every time
No description provided.