Skip to content
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

Added ability to set cookie without escaping. #126

Merged
merged 1 commit into from
Jul 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[i] = char
else
result[i] = utils.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 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