Skip to content

Commit

Permalink
Added ability to set 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.
  • Loading branch information
Satbek committed Jun 25, 2020
1 parent adf1a90 commit 55490fa
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 52 additions & 3 deletions http/router/response.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,46 @@ 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.base_escape(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)
if options == nil then
options = {}
end
options = utils.extend({raw = false}, options, true)

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

Expand All @@ -39,10 +78,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 base_escape(char)
return string.format('%%%02X', string.byte(char))
end

local function base_unescape(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])', base_unescape)
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_]', base_escape)
end
return res
end
Expand All @@ -80,4 +80,6 @@ return {
extend = extend,
uri_unescape = uri_unescape,
uri_escape = uri_escape,
base_escape = base_escape,
base_unescape = base_unescape,
}
176 changes: 176 additions & 0 deletions test/unit/setcookie_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
local t = require('luatest')
local g = t.group()

local response = require('http.router.response')

local function get_object()
return setmetatable({}, response.metatable)
end

g.test_values_escaping = function()
local test_table = {
whitespace = {
value = "f f",
result = 'f%20f',
},
dquote = {
value = 'f"f',
result = 'f%22f',
},
comma = {
value = "f,f",
result = "f%2Cf",
},
semicolon = {
value = "f;f",
result = "f%3Bf",
},
backslash = {
value = "f\\f",
result = "f%5Cf",
},
unicode = {
value = "fюf",
result = "f%D1%8Ef"
},
unprintable_ascii = {
value = string.char(15),
result = "%0F"
}
}

for byte = 33, 126 do
if byte ~= string.byte('"') and byte ~= string.byte(",") and byte ~= string.byte(";") and
byte ~= string.byte("\\") then
test_table[byte] = {
value = "f" .. string.char(byte) .. "f",
result = "f" .. string.char(byte) .. "f",
}
end
end

for case_name, case in pairs(test_table) do
local resp = get_object()
resp:setcookie({ name='name', value = case.value })
t.assert_equals(resp.headers['set-cookie'], {"name=" .. case.result}, case_name)
end
end

g.test_values_raw = function()
local test_table = {}
for byte = 0, 127 do
test_table[byte] = {
value = "f" .. string.char(byte) .. "f",
result = "f" .. string.char(byte) .. "f",
}
end

test_table.unicode = {
value = "fюf",
result = "fюf"
}

for case_name, case in pairs(test_table) do
local resp = get_object()
resp:setcookie({ name='name', value = case.value }, {raw = true})
t.assert_equals(resp.headers['set-cookie'], {"name=" .. case.result}, case_name)
end
end

g.test_path_escaping = function()
local test_table = {
semicolon = {
path = "f;f",
result = "f%3Bf",
},
unicode = {
path = "fюf",
result = "f%D1%8Ef"
},
unprintable_ascii = {
path = string.char(15),
result = "%0F"
}
}

for byte = 32, 126 do
if byte ~= string.byte(";") then
test_table[byte] = {
path = "f" .. string.char(byte) .. "f",
result = "f" .. string.char(byte) .. "f",
}
end
end

for case_name, case in pairs(test_table) do
local resp = get_object()
resp:setcookie({ name='name', value = 'value', path = case.path })
t.assert_equals(resp.headers['set-cookie'], {"name=value;" .. 'path=' .. case.result}, case_name)
end
end

g.test_path_raw = function()
local test_table = {}
for byte = 0, 127 do
test_table[byte] = {
path = "f" .. string.char(byte) .. "f",
result = "f" .. string.char(byte) .. "f",
}
end

test_table.unicode = {
path = "fюf",
result = "fюf"
}

for case_name, case in pairs(test_table) do
local resp = get_object()
resp:setcookie({ name='name', value = 'value', path = case.path }, {raw = true})
t.assert_equals(resp.headers['set-cookie'], {"name=value;" .. 'path=' .. case.result}, case_name)
end
end

g.test_set_header = function()
local test_table = {
name_value = {
cookie = {
name = 'name',
value = 'value'
},
result = {"name=value"},
},
name_value_path = {
cookie = {
name = 'name',
value = 'value',
path = 'path'
},
result = {"name=value;path=path"},
},
name_value_path_domain = {
cookie = {
name = 'name',
value = 'value',
path = 'path',
domain = 'domain',
},
result = {"name=value;path=path;domain=domain"},
},
name_value_path_domain_expires = {
cookie = {
name = 'name',
value = 'value',
path = 'path',
domain = 'domain',
expires = 'expires'
},
result = {"name=value;path=path;domain=domain;expires=expires"},
},
}

for case_name, case in pairs(test_table) do
local resp = get_object()
resp:setcookie(case.cookie)
t.assert_equals(resp.headers["set-cookie"], case.result, case_name)
end
end

0 comments on commit 55490fa

Please sign in to comment.