Skip to content

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

Closed
wants to merge 20 commits into from

Conversation

RyanSquared
Copy link
Contributor

No description provided.

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
Copy link
Owner

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(
Copy link
Owner

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))
Copy link
Owner

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"]);
Copy link
Owner

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
Copy link
Owner

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 |?

Copy link
Contributor Author

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.

Copy link
Owner

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 = {
Copy link
Owner

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.

Copy link
Contributor Author

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}
Copy link
Owner

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()
Copy link
Owner

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)
Copy link
Owner

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

Copy link
Contributor Author

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
Copy link
Owner

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)
Copy link
Owner

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)
Copy link
Owner

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
Copy link
Owner

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
Copy link
Owner

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, "; ")
Copy link
Owner

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
Copy link
Owner

@daurnimator daurnimator Aug 10, 2017

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?

Copy link
Contributor Author

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

Copy link
Owner

@daurnimator daurnimator Aug 10, 2017

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.

Copy link
Contributor Author

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?

Copy link
Owner

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

Copy link
Contributor Author

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))
Copy link
Owner

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"])
Copy link
Owner

@daurnimator daurnimator Aug 17, 2017

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.

Copy link
Contributor Author

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")
Copy link
Owner

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No :lower() here?

Copy link
Contributor Author

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)))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ,1?

Copy link
Contributor Author

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.

Copy link
Owner

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
Copy link
Owner

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?

Copy link
Contributor Author

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)

Copy link
Owner

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)
Copy link
Owner

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
Copy link
Owner

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
Copy link
Owner

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
Copy link
Owner

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 = {}
Copy link
Owner

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)
Copy link
Owner

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
Copy link
Owner

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
Copy link
Owner

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?

Copy link
Contributor Author

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
Copy link
Owner

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
Copy link
Owner

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))
Copy link
Owner

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.

Copy link
Contributor Author

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
Copy link
Owner

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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)

Copy link
Contributor Author

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
Copy link
Owner

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...

@daurnimator
Copy link
Owner

You'll also need to install libpsl in the travis file.

@daurnimator
Copy link
Owner

:( looks like trusty and precise (ubuntu versions supported by travis) don't have libpsl.
https://packages.ubuntu.com/search?suite=default&section=all&arch=any&keywords=libpsl&searchon=names

@RyanSquared
Copy link
Contributor Author

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)
Copy link
Contributor Author

@RyanSquared RyanSquared Feb 6, 2018

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()?

Copy link
Contributor Author

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
Copy link
Contributor Author

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)
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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

@daurnimator daurnimator mentioned this pull request Jul 15, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants