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
`resp:setcookie` implicitly escaped cookie values. Added ability to set cookie without any escaping `resp:setcookie('name', 'value', {raw = true})`.
Also added escaping for cookie path, and changed escaping algorithm according to https://tools.ietf.org/html/rfc6265.

`req:cookie` implicitly unescaped cookie values. Added ability to get cookie without unescaping `req:cookie('name', {raw = true})`.
  • Loading branch information
Satbek committed Jun 26, 2020
1 parent adf1a90 commit 596b0b3
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 45 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ end
| `req:post_param(name)` | returns a single POST request a parameter value. If `name` is `nil`, returns all parameters as a Lua table. |
| `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])` | **NOTE**: currently not supported inside middleware handlers. Get or set a variable "stashed" when dispatching a route. |
| `req:url_for(name, args, query)` | returns the route's exact URL.
| `req:redirect_to` | create a **Response** object with an HTTP redirect.
Expand All @@ -221,7 +221,7 @@ 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
9 changes: 7 additions & 2 deletions http/router/request.lua
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,19 @@ local function param(self, name)
return utils.extend(post, query, false)
end

local function cookie(self, cookiename)
local function cookie(self, cookiename, options)
options = options or {}
if self:header('cookie') == nil then
return nil
end
for k, v in string.gmatch(
self:header('cookie'), "([^=,; \t]+)=([^,; \t]+)") do
if k == cookiename then
return utils.uri_unescape(v)
if not options.raw then
return utils.uri_unescape(v)
else
return v
end
end
end
return nil
Expand Down
52 changes: 49 additions & 3 deletions http/router/response.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,43 @@ local function expires_str(str)
return os.date(fmt, gmtnow + diff)
end

local function setcookie(resp, cookie)
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_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 = result .. char
else
result = result .. utils.escape_char(char)
end
end
return 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 setcookie(resp, cookie, options)
options = options or {}

local name = cookie.name
local value = cookie.value

Expand All @@ -39,10 +75,20 @@ local function setcookie(resp, cookie)
error('cookie.value is undefined')
end

local str = utils.sprintf('%s=%s', name, utils.uri_escape(value))
if not options.raw then
value = escape_value(value)
end

local str = utils.sprintf('%s=%s', name, value)

if cookie.path ~= nil then
str = utils.sprintf('%s;path=%s', str, cookie.path)
if options.raw then
str = utils.sprintf('%s;path=%s', str, cookie.path)
else
str = utils.sprintf('%s;path=%s', str, escape_path(cookie.path))
end
end

if cookie.domain ~= nil then
str = utils.sprintf('%s;domain=%s', str, cookie.domain)
end
Expand Down
22 changes: 12 additions & 10 deletions http/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ local function extend(tbl, tblu, raise)
return res
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 uri_unescape(str, unescape_plus_sign)
local res = {}
if type(str) == 'table' then
Expand All @@ -47,11 +55,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 All @@ -63,11 +67,7 @@ local function uri_escape(str)
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 @@ -80,4 +80,6 @@ return {
extend = extend,
uri_unescape = uri_unescape,
uri_escape = uri_escape,
escape_char = escape_char,
unescape_char = unescape_char,
}
79 changes: 51 additions & 28 deletions test/integration/request_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,57 @@ g.test_redirect_to = function()
t.assert_equals(r.body, "OK")
end

g.test_get_cookie = function()
g.router:route({path = '/receive_cookie'}, function(req)
local foo = req:cookie('foo')
local baz = req:cookie('baz')
return req:render({
text = ('foo=%s; baz=%s'):format(foo, baz)
})
end)

local r = http_client.get(helper.base_uri .. 'receive_cookie', {
headers = {
cookie = 'foo=f%3Bf; baz=f%5Cf',
}
})

t.assert_equals(r.status, 200, 'status')
t.assert_equals(r.body, 'foo=f;f; baz=f\\f', 'body')
end

g.test_get_cookie_raw = function()
g.router:route({path = '/receive_cookie_raw'}, function(req)
local foo = req:cookie('foo', {raw = true})
local baz = req:cookie('baz', {raw = true})
return req:render({
text = ('foo=%s; baz=%s'):format(foo, baz)
})
end)

local r = http_client.get(helper.base_uri .. 'receive_cookie_raw', {
headers = {
cookie = 'foo=f%3Bf; baz=f%5Cf',
}
})

t.assert_equals(r.status, 200, 'status')
t.assert_equals(r.body, 'foo=f%3Bf; baz=f%5Cf', 'body')
end

g.test_set_cookie = function()
g.router:route({path = '/cookie'}, function(req)
local resp = req:render({text = ''})
resp:setcookie({ name = 'test', value = 'tost',
expires = '+1y', path = '/abc' })
resp:setcookie({ name = 'xxx', value = 'yyy' })
return resp
end)
local r = http_client.get(helper.base_uri .. 'cookie')
t.assert_equals(r.status, 200, 'status')
t.assert(r.headers['set-cookie'] ~= nil, "header")
end

g.test_server_requests = function()
local r = http_client.get(helper.base_uri .. 'test')
t.assert_equals(r.status, 200, '/test code')
Expand Down Expand Up @@ -152,34 +203,6 @@ g.test_server_requests = function()
t.assert_equals(r.headers['transfer-encoding'], 'chunked', 'chunked headers')
t.assert_equals(r.body, 'chunkedencodingt\r\nest', 'chunked body')

-- get cookie
g.router:route({path = '/receive_cookie'}, function(req)
local foo = req:cookie('foo')
local baz = req:cookie('baz')
return req:render({
text = ('foo=%s; baz=%s'):format(foo, baz)
})
end)
r = http_client.get(helper.base_uri .. 'receive_cookie', {
headers = {
cookie = 'foo=bar; baz=feez',
}
})
t.assert_equals(r.status, 200, 'status')
t.assert_equals(r.body, 'foo=bar; baz=feez', 'body')

-- cookie
g.router:route({path = '/cookie'}, function(req)
local resp = req:render({text = ''})
resp:setcookie({ name = 'test', value = 'tost',
expires = '+1y', path = '/abc' })
resp:setcookie({ name = 'xxx', value = 'yyy' })
return resp
end)
r = http_client.get(helper.base_uri .. 'cookie')
t.assert_equals(r.status, 200, 'status')
t.assert(r.headers['set-cookie'] ~= nil, "header")


-- request object with GET method
g.router:route({path = '/check_req_properties'}, function(req)
Expand Down
Loading

0 comments on commit 596b0b3

Please sign in to comment.