Skip to content

Commit

Permalink
Added ability to set and get cookie without escaping
Browse files Browse the repository at this point in the history
TODO: add unit tests

Method resp:setcookie() implicitly escapes cookie values. Commit adds
ability to set cookie without any escaping with option 'raw':

resp:setcookie('name', 'value', {
    raw = true
})`

Method req:cookie() implicitly unescapes cookie values. Commit adds
ability to get cookie without unescaping:

req:cookie('name', {
    raw = true
})

Also added escaping of cookie path, and changed escaping algorithm
according to [1].

These changes were added as a part of http v2 support in commit 'Added
ability to set and get cookie without escaping'
(42e3002) and later reverted in scope
of ticket with discard v2.

1. https://tools.ietf.org/html/rfc6265

Follows up #126
Part of #134
  • Loading branch information
ligurio committed Oct 26, 2021
1 parent fbb88bd commit 8a48967
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 37 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ end
* `req:query_param(name)` - returns a single GET request parameter value.
If `name` is `nil`, returns a Lua table with all arguments.
* `req:param(name)` - any request parameter, either GET or POST.
* `req:cookie(name)` - to get a cookie in the request.
* `req:cookie(name, {raw = true})` | to get a cookie in the request. if `raw`
option was set then cookie will not be unescaped, otherwise cookie's value
will be unescaped.
* `req:stash(name[, value])` - get or set a variable "stashed"
when dispatching a route.
* `req:url_for(name, args, query)` - returns the route's exact URL.
Expand All @@ -238,8 +240,9 @@ end
* `resp.status` - HTTP response code.
* `resp.headers` - a Lua table with normalized headers.
* `resp.body` - response body (string|table|wrapped\_iterator).
* `resp:setcookie({ name = 'name', value = 'value', path = '/', expires = '+1y', domain = 'example.com'))` -
adds `Set-Cookie` headers to `resp.headers`.
* `resp:setcookie({ name = 'name', value = 'value', path = '/', expires = '+1y', domain = 'example.com'}, {raw = true})` -
adds `Set-Cookie` headers to `resp.headers`, if `raw` option was set then cookie will not be escaped,
otherwise cookie's value and path will be escaped
### Examples
Expand Down
78 changes: 63 additions & 15 deletions http/server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,58 @@ local function sprintf(fmt, ...)
return string.format(fmt, ...)
end

local function valid_cookie_value_byte(byte)
-- https://tools.ietf.org/html/rfc6265#section-4.1.1
-- US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon,
-- and backslash.
return 32 < byte and byte < 127 and byte ~= string.byte('"') and
byte ~= string.byte(",") and byte ~= string.byte(";") and byte ~= string.byte("\\")
end

local function valid_cookie_path_byte(byte)
-- https://tools.ietf.org/html/rfc6265#section-4.1.1
-- <any CHAR except CTLs or ";">
return 32 <= byte and byte < 127 and byte ~= string.byte(";")
end

local function escape_char(char)
return string.format('%%%02X', string.byte(char))
end

local function unescape_char(char)
return string.char(tonumber(char, 16))
end


local function escape_string(str, byte_filter)
local result = {}
for i = 1, str:len() do
local char = str:sub(i,i)
if byte_filter(string.byte(char)) then
result[i] = char
else
result[i] = escape_char(char)
end
end
return table.concat(result)
end

local function escape_value(cookie_value)
return escape_string(cookie_value, valid_cookie_value_byte)
end

local function escape_path(cookie_path)
return escape_string(cookie_path, valid_cookie_path_byte)
end

local function uri_escape(str)
local res = {}
if type(str) == 'table' then
for _, v in pairs(str) do
table.insert(res, uri_escape(v))
end
else
res = string.gsub(str, '[^a-zA-Z0-9_]',
function(c)
return string.format('%%%02X', string.byte(c))
end
)
res = string.gsub(str, '[^a-zA-Z0-9_]', escape_char)
end
return res
end
Expand All @@ -50,11 +90,7 @@ local function uri_unescape(str, unescape_plus_sign)
str = string.gsub(str, '+', ' ')
end

res = string.gsub(str, '%%([0-9a-fA-F][0-9a-fA-F])',
function(c)
return string.char(tonumber(c, 16))
end
)
res = string.gsub(str, '%%([0-9a-fA-F][0-9a-fA-F])', unescape_char)
end
return res
end
Expand Down Expand Up @@ -265,7 +301,8 @@ local function expires_str(str)
return os.date(fmt, gmtnow + diff)
end

local function setcookie(resp, cookie)
local function setcookie(resp, cookie, options)
options = options or {}
local name = cookie.name
local value = cookie.value

Expand All @@ -276,9 +313,16 @@ local function setcookie(resp, cookie)
error('cookie.value is undefined')
end

local str = sprintf('%s=%s', name, uri_escape(value))
if not options.raw then
value = escape_value(value)
end
local str = sprintf('%s=%s', name, value)
if cookie.path ~= nil then
str = sprintf('%s;path=%s', str, cookie.path)
local cookie_path = cookie.path
if not options.raw then
cookie_path = escape_path(cookie.path)
end
str = sprintf('%s;path=%s', str, cookie_path)
end
if cookie.domain ~= nil then
str = sprintf('%s;domain=%s', str, cookie.domain)
Expand All @@ -304,14 +348,18 @@ local function setcookie(resp, cookie)
return resp
end

local function cookie(tx, cookie)
local function cookie(tx, cookie, options)
options = options or {}
if tx.headers.cookie == nil then
return nil
end
for k, v in string.gmatch(
tx.headers.cookie, "([^=,; \t]+)=([^,; \t]+)") do
if k == cookie then
return uri_unescape(v)
if not options.raw then
v = uri_unescape(v)
end
return v
end
end
return nil
Expand Down
90 changes: 71 additions & 19 deletions test/integration/http_server_requests_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -211,49 +211,101 @@ pgroup.test_chunked_encoding = function(g)
t.assert_equals(r.body, 'chunkedencodingt\r\nest', 'chunked body')
end

-- Get cookie.
pgroup.test_get_cookie = function(g)
-- Get raw cookie value (Günter -> Günter).
pgroup.test_get_raw_cookie = function(g)
local cookie = 'Günter'
local httpd = g.httpd
httpd:route({
path = '/receive_cookie'
}, function(req)
local foo = req:cookie('foo')
local baz = req:cookie('baz')
local name = req:cookie('name', {
raw = true
})
return req:render({
text = ('foo=%s; baz=%s'):format(foo, baz)
text = ('name=%s'):format(name)
})
end)

local r = http_client.get(helpers.base_uri .. '/receive_cookie', {
headers = {
cookie = 'foo=bar; baz=feez',
cookie = 'name=' .. cookie,
}
})
t.assert_equals(r.status, 200, 'status')
t.assert_equals(r.body, 'foo=bar; baz=feez', 'body')

t.assert_equals(r.status, 200, 'response status')
t.assert_equals(r.body, 'name=' .. cookie, 'body with raw cookie')
end

-- Get escaped cookie (G%C3%BCnter -> Günter).
pgroup.test_get_escaped_cookie = function(g)
local str_escaped = 'G%C3%BCnter'
local str_non_escaped = 'Günter'
local httpd = g.httpd
httpd:route({
path = '/receive_cookie'
}, function(req)
local name = req:cookie('name')
return req:render({
text = ('name=%s'):format(name)
})
end)

local r = http_client.get(helpers.base_uri .. '/receive_cookie', {
headers = {
cookie = 'name=' .. str_escaped,
}
})

t.assert_equals(r.status, 200, 'response status')
t.assert_equals(r.body, 'name=' .. str_non_escaped, 'body with escaped cookie')
end

-- Cookie.
pgroup.test_set_cookie = function(g)
-- Set escaped cookie (Günter -> G%C3%BCnter).
pgroup.test_set_escaped_cookie = function(g)
local str_escaped = 'G%C3%BCnter'
local str_non_escaped = 'Günter'

local httpd = g.httpd
httpd:route({
path = '/cookie'
}, function(req)
local resp = req:render({text = ''})
local resp = req:render({
text = ''
})
resp:setcookie({
name = 'test',
value = 'tost',
expires = '+1y',
path = '/abc'
name = 'name',
value = str_non_escaped
})
return resp
end)

local r = http_client.get(helpers.base_uri .. '/cookie')
t.assert_equals(r.status, 200, 'response status')
t.assert_equals(r.headers['set-cookie'], 'name=' .. str_escaped, 'header with escaped cookie')
end

-- Set raw cookie (Günter -> Günter).
pgroup.test_set_raw_cookie = function(g)
local cookie = 'Günter'
local httpd = g.httpd
httpd:route({
path = '/cookie'
}, function(req)
local resp = req:render({
text = ''
})
resp:setcookie({
name = 'xxx',
value = 'yyy'
name = 'name',
value = cookie
}, {
raw = true
})
return resp
end)

local r = http_client.get(helpers.base_uri .. '/cookie')
t.assert_equals(r.status, 200, 'status')
t.assert(r.headers['set-cookie'] ~= nil, 'header')
t.assert_equals(r.status, 200, 'response status')
t.assert_equals(r.headers['set-cookie'], 'name=' .. cookie, 'header with raw cookie')
end

-- Request object methods.
Expand Down

0 comments on commit 8a48967

Please sign in to comment.