Skip to content

Commit 8a48967

Browse files
committed
Added ability to set and get cookie without escaping
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
1 parent fbb88bd commit 8a48967

File tree

3 files changed

+140
-37
lines changed

3 files changed

+140
-37
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,9 @@ end
226226
* `req:query_param(name)` - returns a single GET request parameter value.
227227
If `name` is `nil`, returns a Lua table with all arguments.
228228
* `req:param(name)` - any request parameter, either GET or POST.
229-
* `req:cookie(name)` - to get a cookie in the request.
229+
* `req:cookie(name, {raw = true})` | to get a cookie in the request. if `raw`
230+
option was set then cookie will not be unescaped, otherwise cookie's value
231+
will be unescaped.
230232
* `req:stash(name[, value])` - get or set a variable "stashed"
231233
when dispatching a route.
232234
* `req:url_for(name, args, query)` - returns the route's exact URL.
@@ -238,8 +240,9 @@ end
238240
* `resp.status` - HTTP response code.
239241
* `resp.headers` - a Lua table with normalized headers.
240242
* `resp.body` - response body (string|table|wrapped\_iterator).
241-
* `resp:setcookie({ name = 'name', value = 'value', path = '/', expires = '+1y', domain = 'example.com'))` -
242-
adds `Set-Cookie` headers to `resp.headers`.
243+
* `resp:setcookie({ name = 'name', value = 'value', path = '/', expires = '+1y', domain = 'example.com'}, {raw = true})` -
244+
adds `Set-Cookie` headers to `resp.headers`, if `raw` option was set then cookie will not be escaped,
245+
otherwise cookie's value and path will be escaped
243246

244247
### Examples
245248

http/server.lua

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,58 @@ local function sprintf(fmt, ...)
2323
return string.format(fmt, ...)
2424
end
2525

26+
local function valid_cookie_value_byte(byte)
27+
-- https://tools.ietf.org/html/rfc6265#section-4.1.1
28+
-- US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon,
29+
-- and backslash.
30+
return 32 < byte and byte < 127 and byte ~= string.byte('"') and
31+
byte ~= string.byte(",") and byte ~= string.byte(";") and byte ~= string.byte("\\")
32+
end
33+
34+
local function valid_cookie_path_byte(byte)
35+
-- https://tools.ietf.org/html/rfc6265#section-4.1.1
36+
-- <any CHAR except CTLs or ";">
37+
return 32 <= byte and byte < 127 and byte ~= string.byte(";")
38+
end
39+
40+
local function escape_char(char)
41+
return string.format('%%%02X', string.byte(char))
42+
end
43+
44+
local function unescape_char(char)
45+
return string.char(tonumber(char, 16))
46+
end
47+
48+
49+
local function escape_string(str, byte_filter)
50+
local result = {}
51+
for i = 1, str:len() do
52+
local char = str:sub(i,i)
53+
if byte_filter(string.byte(char)) then
54+
result[i] = char
55+
else
56+
result[i] = escape_char(char)
57+
end
58+
end
59+
return table.concat(result)
60+
end
61+
62+
local function escape_value(cookie_value)
63+
return escape_string(cookie_value, valid_cookie_value_byte)
64+
end
65+
66+
local function escape_path(cookie_path)
67+
return escape_string(cookie_path, valid_cookie_path_byte)
68+
end
69+
2670
local function uri_escape(str)
2771
local res = {}
2872
if type(str) == 'table' then
2973
for _, v in pairs(str) do
3074
table.insert(res, uri_escape(v))
3175
end
3276
else
33-
res = string.gsub(str, '[^a-zA-Z0-9_]',
34-
function(c)
35-
return string.format('%%%02X', string.byte(c))
36-
end
37-
)
77+
res = string.gsub(str, '[^a-zA-Z0-9_]', escape_char)
3878
end
3979
return res
4080
end
@@ -50,11 +90,7 @@ local function uri_unescape(str, unescape_plus_sign)
5090
str = string.gsub(str, '+', ' ')
5191
end
5292

53-
res = string.gsub(str, '%%([0-9a-fA-F][0-9a-fA-F])',
54-
function(c)
55-
return string.char(tonumber(c, 16))
56-
end
57-
)
93+
res = string.gsub(str, '%%([0-9a-fA-F][0-9a-fA-F])', unescape_char)
5894
end
5995
return res
6096
end
@@ -265,7 +301,8 @@ local function expires_str(str)
265301
return os.date(fmt, gmtnow + diff)
266302
end
267303

268-
local function setcookie(resp, cookie)
304+
local function setcookie(resp, cookie, options)
305+
options = options or {}
269306
local name = cookie.name
270307
local value = cookie.value
271308

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

279-
local str = sprintf('%s=%s', name, uri_escape(value))
316+
if not options.raw then
317+
value = escape_value(value)
318+
end
319+
local str = sprintf('%s=%s', name, value)
280320
if cookie.path ~= nil then
281-
str = sprintf('%s;path=%s', str, cookie.path)
321+
local cookie_path = cookie.path
322+
if not options.raw then
323+
cookie_path = escape_path(cookie.path)
324+
end
325+
str = sprintf('%s;path=%s', str, cookie_path)
282326
end
283327
if cookie.domain ~= nil then
284328
str = sprintf('%s;domain=%s', str, cookie.domain)
@@ -304,14 +348,18 @@ local function setcookie(resp, cookie)
304348
return resp
305349
end
306350

307-
local function cookie(tx, cookie)
351+
local function cookie(tx, cookie, options)
352+
options = options or {}
308353
if tx.headers.cookie == nil then
309354
return nil
310355
end
311356
for k, v in string.gmatch(
312357
tx.headers.cookie, "([^=,; \t]+)=([^,; \t]+)") do
313358
if k == cookie then
314-
return uri_unescape(v)
359+
if not options.raw then
360+
v = uri_unescape(v)
361+
end
362+
return v
315363
end
316364
end
317365
return nil

test/integration/http_server_requests_test.lua

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -211,49 +211,101 @@ pgroup.test_chunked_encoding = function(g)
211211
t.assert_equals(r.body, 'chunkedencodingt\r\nest', 'chunked body')
212212
end
213213

214-
-- Get cookie.
215-
pgroup.test_get_cookie = function(g)
214+
-- Get raw cookie value (Günter -> Günter).
215+
pgroup.test_get_raw_cookie = function(g)
216+
local cookie = 'Günter'
216217
local httpd = g.httpd
217218
httpd:route({
218219
path = '/receive_cookie'
219220
}, function(req)
220-
local foo = req:cookie('foo')
221-
local baz = req:cookie('baz')
221+
local name = req:cookie('name', {
222+
raw = true
223+
})
222224
return req:render({
223-
text = ('foo=%s; baz=%s'):format(foo, baz)
225+
text = ('name=%s'):format(name)
224226
})
225227
end)
228+
226229
local r = http_client.get(helpers.base_uri .. '/receive_cookie', {
227230
headers = {
228-
cookie = 'foo=bar; baz=feez',
231+
cookie = 'name=' .. cookie,
229232
}
230233
})
231-
t.assert_equals(r.status, 200, 'status')
232-
t.assert_equals(r.body, 'foo=bar; baz=feez', 'body')
234+
235+
t.assert_equals(r.status, 200, 'response status')
236+
t.assert_equals(r.body, 'name=' .. cookie, 'body with raw cookie')
237+
end
238+
239+
-- Get escaped cookie (G%C3%BCnter -> Günter).
240+
pgroup.test_get_escaped_cookie = function(g)
241+
local str_escaped = 'G%C3%BCnter'
242+
local str_non_escaped = 'Günter'
243+
local httpd = g.httpd
244+
httpd:route({
245+
path = '/receive_cookie'
246+
}, function(req)
247+
local name = req:cookie('name')
248+
return req:render({
249+
text = ('name=%s'):format(name)
250+
})
251+
end)
252+
253+
local r = http_client.get(helpers.base_uri .. '/receive_cookie', {
254+
headers = {
255+
cookie = 'name=' .. str_escaped,
256+
}
257+
})
258+
259+
t.assert_equals(r.status, 200, 'response status')
260+
t.assert_equals(r.body, 'name=' .. str_non_escaped, 'body with escaped cookie')
233261
end
234262

235-
-- Cookie.
236-
pgroup.test_set_cookie = function(g)
263+
-- Set escaped cookie (Günter -> G%C3%BCnter).
264+
pgroup.test_set_escaped_cookie = function(g)
265+
local str_escaped = 'G%C3%BCnter'
266+
local str_non_escaped = 'Günter'
267+
237268
local httpd = g.httpd
238269
httpd:route({
239270
path = '/cookie'
240271
}, function(req)
241-
local resp = req:render({text = ''})
272+
local resp = req:render({
273+
text = ''
274+
})
242275
resp:setcookie({
243-
name = 'test',
244-
value = 'tost',
245-
expires = '+1y',
246-
path = '/abc'
276+
name = 'name',
277+
value = str_non_escaped
278+
})
279+
return resp
280+
end)
281+
282+
local r = http_client.get(helpers.base_uri .. '/cookie')
283+
t.assert_equals(r.status, 200, 'response status')
284+
t.assert_equals(r.headers['set-cookie'], 'name=' .. str_escaped, 'header with escaped cookie')
285+
end
286+
287+
-- Set raw cookie (Günter -> Günter).
288+
pgroup.test_set_raw_cookie = function(g)
289+
local cookie = 'Günter'
290+
local httpd = g.httpd
291+
httpd:route({
292+
path = '/cookie'
293+
}, function(req)
294+
local resp = req:render({
295+
text = ''
247296
})
248297
resp:setcookie({
249-
name = 'xxx',
250-
value = 'yyy'
298+
name = 'name',
299+
value = cookie
300+
}, {
301+
raw = true
251302
})
252303
return resp
253304
end)
305+
254306
local r = http_client.get(helpers.base_uri .. '/cookie')
255-
t.assert_equals(r.status, 200, 'status')
256-
t.assert(r.headers['set-cookie'] ~= nil, 'header')
307+
t.assert_equals(r.status, 200, 'response status')
308+
t.assert_equals(r.headers['set-cookie'], 'name=' .. cookie, 'header with raw cookie')
257309
end
258310

259311
-- Request object methods.

0 commit comments

Comments
 (0)