├── .gitignore ├── lib └── resty │ └── cookie.lua ├── README.md └── t └── sanity.t /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *~ 4 | go 5 | t/servroot/ 6 | reindex 7 | *.t_ 8 | tags 9 | luacov.report.out 10 | luacov.stats.out 11 | -------------------------------------------------------------------------------- /lib/resty/cookie.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2013 Jiale Zhi (calio), Cloudflare Inc. 2 | -- See RFC6265 http://tools.ietf.org/search/rfc6265 3 | -- require "luacov" 4 | 5 | local type = type 6 | local byte = string.byte 7 | local sub = string.sub 8 | local format = string.format 9 | local log = ngx.log 10 | local ERR = ngx.ERR 11 | local ngx_header = ngx.header 12 | 13 | local EQUAL = byte("=") 14 | local SEMICOLON = byte(";") 15 | local SPACE = byte(" ") 16 | local HTAB = byte("\t") 17 | 18 | 19 | local ok, new_tab = pcall(require, "table.new") 20 | if not ok then 21 | new_tab = function (narr, nrec) return {} end 22 | end 23 | 24 | local ok, clear_tab = pcall(require, "table.clear") 25 | if not ok then 26 | clear_tab = function(tab) for k, _ in pairs(tab) do tab[k] = nil end end 27 | end 28 | 29 | local _M = new_tab(0, 2) 30 | 31 | _M._VERSION = '0.01' 32 | 33 | 34 | local mt = { __index = _M } 35 | 36 | 37 | local function get_cookie_table(text_cookie) 38 | if type(text_cookie) ~= "string" then 39 | log(ERR, format("expect text_cookie to be \"string\" but found %s", 40 | type(text_cookie))) 41 | return {} 42 | end 43 | 44 | local EXPECT_KEY = 1 45 | local EXPECT_VALUE = 2 46 | local EXPECT_SP = 3 47 | 48 | local n = 0 49 | local len = #text_cookie 50 | 51 | for i=1, len do 52 | if byte(text_cookie, i) == SEMICOLON then 53 | n = n + 1 54 | end 55 | end 56 | 57 | local cookie_table = new_tab(0, n + 1) 58 | 59 | local state = EXPECT_SP 60 | local i = 1 61 | local j = 1 62 | local key, value 63 | 64 | while j <= len do 65 | if state == EXPECT_KEY then 66 | if byte(text_cookie, j) == EQUAL then 67 | key = sub(text_cookie, i, j - 1) 68 | state = EXPECT_VALUE 69 | i = j + 1 70 | end 71 | elseif state == EXPECT_VALUE then 72 | if byte(text_cookie, j) == SEMICOLON 73 | or byte(text_cookie, j) == SPACE 74 | or byte(text_cookie, j) == HTAB 75 | then 76 | value = sub(text_cookie, i, j - 1) 77 | cookie_table[key] = value 78 | 79 | key, value = nil, nil 80 | state = EXPECT_SP 81 | i = j + 1 82 | end 83 | elseif state == EXPECT_SP then 84 | if byte(text_cookie, j) ~= SPACE 85 | and byte(text_cookie, j) ~= HTAB 86 | then 87 | state = EXPECT_KEY 88 | i = j 89 | j = j - 1 90 | end 91 | end 92 | j = j + 1 93 | end 94 | 95 | if key ~= nil and value == nil then 96 | cookie_table[key] = sub(text_cookie, i) 97 | end 98 | 99 | return cookie_table 100 | end 101 | 102 | function _M.new(self) 103 | local _cookie = ngx.var.http_cookie 104 | --if not _cookie then 105 | --return nil, "no cookie found in current request" 106 | --end 107 | return setmetatable({ _cookie = _cookie, set_cookie_table = new_tab(4, 0) }, 108 | mt) 109 | end 110 | 111 | function _M.get(self, key) 112 | if not self._cookie then 113 | return nil, "no cookie found in the current request" 114 | end 115 | if self.cookie_table == nil then 116 | self.cookie_table = get_cookie_table(self._cookie) 117 | end 118 | 119 | return self.cookie_table[key] 120 | end 121 | 122 | function _M.get_all(self) 123 | local err 124 | 125 | if not self._cookie then 126 | return nil, "no cookie found in the current request" 127 | end 128 | 129 | if self.cookie_table == nil then 130 | self.cookie_table = get_cookie_table(self._cookie) 131 | end 132 | 133 | return self.cookie_table 134 | end 135 | 136 | local function bake(cookie) 137 | if not cookie.key or not cookie.value then 138 | return nil, 'missing cookie field "key" or "value"' 139 | end 140 | 141 | if cookie["max-age"] then 142 | cookie.max_age = cookie["max-age"] 143 | end 144 | local str = cookie.key .. "=" .. cookie.value 145 | .. (cookie.expires and "; Expires=" .. cookie.expires or "") 146 | .. (cookie.max_age and "; Max-Age=" .. cookie.max_age or "") 147 | .. (cookie.domain and "; Domain=" .. cookie.domain or "") 148 | .. (cookie.path and "; Path=" .. cookie.path or "") 149 | .. (cookie.secure and "; Secure" or "") 150 | .. (cookie.httponly and "; HttpOnly" or "") 151 | .. (cookie.extension and "; " .. cookie.extension or "") 152 | return str 153 | end 154 | 155 | function _M.set(self, cookie) 156 | local cookie_str, err = bake(cookie) 157 | if not cookie_str then 158 | return nil, err 159 | end 160 | 161 | local set_cookie = ngx_header['Set-Cookie'] 162 | local set_cookie_type = type(set_cookie) 163 | local t = self.set_cookie_table 164 | clear_tab(t) 165 | 166 | if set_cookie_type == "string" then 167 | -- only one cookie has been setted 168 | if set_cookie ~= cookie_str then 169 | t[1] = set_cookie 170 | t[2] = cookie_str 171 | ngx_header['Set-Cookie'] = t 172 | end 173 | elseif set_cookie_type == "table" then 174 | -- more than one cookies has been setted 175 | local size = #set_cookie 176 | 177 | -- we can not set cookie like ngx.header['Set-Cookie'][3] = val 178 | -- so create a new table, copy all the values, and then set it back 179 | for i=1, size do 180 | t[i] = ngx_header['Set-Cookie'][i] 181 | if t[i] == cookie_str then 182 | -- new cookie is duplicated 183 | return true 184 | end 185 | end 186 | t[size + 1] = cookie_str 187 | ngx_header['Set-Cookie'] = t 188 | else 189 | -- no cookie has been setted 190 | ngx_header['Set-Cookie'] = cookie_str 191 | end 192 | return true 193 | end 194 | 195 | return _M 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-cookie - This library parses HTTP Cookie header for Nginx and returns each field in the cookie. 5 | 6 | Table of Contents 7 | ================= 8 | 9 | * [Name](#name) 10 | * [Status](#status) 11 | * [Synopsis](#synopsis) 12 | * [Methods](#methods) 13 | * [new](#new) 14 | * [get](#get) 15 | * [get_all](#get_all) 16 | * [set](#set) 17 | * [Installation](#installation) 18 | * [Authors](#authors) 19 | * [Copyright and License](#copyright-and-license) 20 | 21 | Status 22 | ====== 23 | 24 | This library is production ready. 25 | 26 | Synopsis 27 | ======== 28 | 29 | lua_package_path "/path/to/lua-resty-cookie/lib/?.lua;;"; 30 | 31 | server { 32 | location /test { 33 | local ck = require "resty.cookie" 34 | local cookie, err = ck:new() 35 | if not cookie then 36 | ngx.log(ngx.ERR, err) 37 | return 38 | end 39 | 40 | -- get single cookie 41 | local field, err = cookie:get("lang") 42 | if not field then 43 | ngx.log(ngx.ERR, err) 44 | return 45 | end 46 | ngx.say("lang", " => ", field) 47 | 48 | -- get all cookies 49 | local fields, err = cookie:get_all() 50 | if not fields then 51 | ngx.log(ngx.ERR, err) 52 | return 53 | end 54 | 55 | for k, v in pairs(fields) do 56 | ngx.say(k, " => ", v) 57 | end 58 | 59 | -- set one cookie 60 | local ok, err = cookie:set({ 61 | key = "Name", value = "Bob", path = "/", 62 | domain = "example.com", secure = true, httponly = true, 63 | expires = "Wed, 09 Jun 2021 10:18:14 GMT", max_age = 50, 64 | extension = "a4334aebaec" 65 | }) 66 | if not ok then 67 | ngx.log(ngx.ERR, err) 68 | return 69 | end 70 | 71 | -- set another cookie, both cookies will appear in HTTP response 72 | local ok, err = cookie:set({ 73 | key = "Age", value = "20", 74 | }) 75 | if not ok then 76 | ngx.log(ngx.ERR, err) 77 | return 78 | end 79 | } 80 | } 81 | 82 | Methods 83 | ======= 84 | 85 | [Back to TOC](#table-of-contents) 86 | 87 | new 88 | --- 89 | `syntax: cookie_obj = cookie()` 90 | 91 | Create a new cookie object for current request. You can get parsed cookie from client or set cookie to client later using this object. 92 | 93 | [Back to TOC](#table-of-contents) 94 | 95 | get 96 | --- 97 | `syntax: cookie_val, err = cookie_obj:get(cookie_name)` 98 | 99 | Get a single client cookie value. On error, returns `nil` and an error message. 100 | 101 | [Back to TOC](#table-of-contents) 102 | 103 | get_all 104 | ------- 105 | `syntax: fields, err = cookie_obj:get_all()` 106 | 107 | Get all client cookie key/value pairs in a lua table. On error, returns `nil` and an error message. 108 | 109 | [Back to TOC](#table-of-contents) 110 | 111 | set 112 | --- 113 | ```lua 114 | syntax: ok, err = cookie_obj:set({ 115 | key = "Name", 116 | value = "Bob", 117 | path = "/", 118 | domain = "example.com", 119 | secure = true, httponly = true, 120 | expires = "Wed, 09 Jun 2021 10:18:14 GMT", 121 | max_age = 50, 122 | extension = "a4334aebaec" 123 | }) 124 | ``` 125 | 126 | Set a cookie to client. This will add a new 'Set-Cookie' response header. `key` and `value` are required, all other fields are optional. 127 | If the same cookie (whole cookie string, e.g. "Name=Bob; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Max-Age=50; Domain=example.com; Path=/; Secure; HttpOnly;") has already been setted, new cookie will be ignored. 128 | 129 | [Back to TOC](#table-of-contents) 130 | 131 | Installation 132 | ============ 133 | 134 | You need to compile [ngx_lua](https://github.com/chaoslawful/lua-nginx-module/tags) with your Nginx. 135 | 136 | You need to configure 137 | the [lua_package_path](https://github.com/chaoslawful/lua-nginx-module#lua_package_path) directive to 138 | add the path of your `lua-resty-cookie` source tree to ngx_lua's Lua module search path, as in 139 | 140 | # nginx.conf 141 | http { 142 | lua_package_path "/path/to/lua-resty-cookie/lib/?.lua;;"; 143 | ... 144 | } 145 | 146 | and then load the library in Lua: 147 | 148 | local ck = require "resty.cookie" 149 | 150 | [Back to TOC](#table-of-contents) 151 | 152 | Authors 153 | ======= 154 | 155 | Jiale Zhi , CloudFlare Inc. 156 | 157 | Yichun Zhang (agentzh) , CloudFlare Inc. 158 | 159 | [Back to TOC](#table-of-contents) 160 | 161 | Copyright and License 162 | ===================== 163 | 164 | This module is licensed under the BSD license. 165 | 166 | Copyright (C) 2013, by Jiale Zhi , CloudFlare Inc. 167 | 168 | Copyright (C) 2013, by Yichun Zhang , CloudFlare Inc. 169 | 170 | All rights reserved. 171 | 172 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 173 | 174 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 175 | 176 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 177 | 178 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 179 | 180 | [Back to TOC](#table-of-contents) 181 | 182 | -------------------------------------------------------------------------------- /t/sanity.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | # 4 | # Run tests with: 5 | # PATH=/usr/local/openresty/nginx/sbin/:$PATH prove -r t 6 | # 7 | 8 | use Test::Nginx::Socket; 9 | use Cwd qw(cwd); 10 | 11 | repeat_each(2); 12 | 13 | plan tests => repeat_each() * (blocks() * 3 + 4); 14 | 15 | my $pwd = cwd(); 16 | 17 | our $HttpConfig = qq{ 18 | lua_package_path "$pwd/lib/?.lua;;"; 19 | lua_package_cpath "/usr/local/openresty-debug/lualib/?.so;/usr/local/openresty/lualib/?.so;;"; 20 | }; 21 | 22 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 23 | 24 | #no_long_string(); 25 | 26 | log_level('debug'); 27 | 28 | run_tests(); 29 | 30 | __DATA__ 31 | 32 | === TEST 0: TES 33 | --- http_config eval: $::HttpConfig 34 | --- config 35 | location /t { 36 | content_by_lua ' 37 | function url_encode(str) 38 | if (str) then 39 | str = string.gsub (str, "\\\\n", "\\\\r\\\\n") 40 | str = string.gsub (str, "([^%w %-%_%.%~])", 41 | function (c) return string.format ("%%%02X", string.byte(c)) end) 42 | str = string.gsub (str, " ", "+") 43 | end 44 | return str 45 | end 46 | 47 | local ck = require "resty.cookie" 48 | local cookie, err = ck:new() 49 | if not cookie then 50 | ngx.log(ngx.ERR, err) 51 | return 52 | end 53 | 54 | local fields = cookie:get_all() 55 | local new_cookie_parts = {} 56 | local i = 1 57 | for k, v in pairs(fields) do 58 | if k ~= "BADCookie" then 59 | ngx.say(k, " => ", v) 60 | new_cookie_parts[i] = k .. "=" .. v 61 | i = i + 1 62 | end 63 | end 64 | ngx.req.set_header("Cookie", table.concat(new_cookie_parts, "; ")) 65 | ngx.say(ngx.req.get_headers()["Cookie"]) 66 | '; 67 | } 68 | --- request 69 | GET /t 70 | --- more_headers 71 | Cookie: SID=31d4d96e407aad42; BADCookie=ohsnap!=== 72 | --- no_error_log 73 | [error] 74 | --- response_body 75 | SID => 31d4d96e407aad42 76 | SID=31d4d96e407aad42 77 | 78 | 79 | 80 | === TEST 1: sanity 81 | --- http_config eval: $::HttpConfig 82 | --- config 83 | location /t { 84 | content_by_lua ' 85 | local ck = require "resty.cookie" 86 | local cookie, err = ck:new() 87 | if not cookie then 88 | ngx.log(ngx.ERR, err) 89 | return 90 | end 91 | 92 | local fields = cookie:get_all() 93 | 94 | for k, v in pairs(fields) do 95 | ngx.say(k, " => ", v) 96 | end 97 | '; 98 | } 99 | --- request 100 | GET /t 101 | --- more_headers 102 | Cookie: SID=31d4d96e407aad42; lang=en-US 103 | --- no_error_log 104 | [error] 105 | --- response_body 106 | SID => 31d4d96e407aad42 107 | lang => en-US 108 | 109 | 110 | 111 | === TEST 2: sanity 2 112 | --- http_config eval: $::HttpConfig 113 | --- config 114 | location /t { 115 | content_by_lua ' 116 | local ck = require "resty.cookie" 117 | local cookie, err = ck:new() 118 | if not cookie then 119 | ngx.log(ngx.ERR, err) 120 | return 121 | end 122 | 123 | local field = cookie:get("lang") 124 | ngx.say("lang", " => ", field) 125 | '; 126 | } 127 | --- request 128 | GET /t 129 | --- more_headers 130 | Cookie: SID=31d4d96e407aad42; lang=en-US 131 | --- no_error_log 132 | [error] 133 | --- response_body 134 | lang => en-US 135 | 136 | 137 | 138 | === TEST 3: no cookie header 139 | --- http_config eval: $::HttpConfig 140 | --- config 141 | location /t { 142 | content_by_lua ' 143 | local ck = require "resty.cookie" 144 | local cookie, err = ck:new() 145 | if not cookie then 146 | ngx.log(ngx.ERR, err) 147 | ngx.say(err) 148 | return 149 | end 150 | 151 | local field, err = cookie:get("lang") 152 | if not field then 153 | ngx.log(ngx.ERR, err) 154 | ngx.say(err) 155 | return 156 | end 157 | ngx.say("lang", " => ", field) 158 | '; 159 | } 160 | --- request 161 | GET /t 162 | --- error_log 163 | no cookie found in the current request 164 | --- response_body 165 | no cookie found in the current request 166 | 167 | 168 | 169 | === TEST 4: empty value 170 | --- http_config eval: $::HttpConfig 171 | --- config 172 | location /t { 173 | content_by_lua ' 174 | local ck = require "resty.cookie" 175 | local cookie, err = ck:new() 176 | if not cookie then 177 | ngx.log(ngx.ERR, err) 178 | return 179 | end 180 | 181 | local fields = cookie:get_all() 182 | 183 | for k, v in pairs(fields) do 184 | ngx.say(k, " => ", v) 185 | end 186 | '; 187 | } 188 | --- request 189 | GET /t 190 | --- more_headers 191 | Cookie: SID= 192 | --- no_error_log 193 | [error] 194 | --- response_body 195 | SID => 196 | 197 | 198 | 199 | === TEST 5: cookie with space/tab 200 | --- http_config eval: $::HttpConfig 201 | --- config 202 | location /t { 203 | content_by_lua ' 204 | local ck = require "resty.cookie" 205 | local cookie, err = ck:new() 206 | if not cookie then 207 | ngx.log(ngx.ERR, err) 208 | return 209 | end 210 | 211 | local fields = cookie:get_all() 212 | 213 | for k, v in pairs(fields) do 214 | ngx.say(k, " => ", v) 215 | end 216 | '; 217 | } 218 | --- request 219 | GET /t 220 | --- more_headers eval: "Cookie: SID=foo\t" 221 | --- no_error_log 222 | [error] 223 | --- response_body 224 | SID => foo 225 | 226 | 227 | 228 | === TEST 6: set cookie 229 | --- http_config eval: $::HttpConfig 230 | --- config 231 | location /t { 232 | content_by_lua ' 233 | local ck = require "resty.cookie" 234 | local cookie, err = ck:new() 235 | if not cookie then 236 | ngx.log(ngx.ERR, err) 237 | return 238 | end 239 | 240 | local ok, err = cookie:set({ 241 | key = "Name", value = "Bob", path = "/", 242 | domain = "example.com", secure = true, httponly = true, 243 | expires = "Wed, 09 Jun 2021 10:18:14 GMT", max_age = 50, 244 | extension = "a4334aebaec" 245 | }) 246 | if not ok then 247 | ngx.log(ngx.ERR, err) 248 | return 249 | end 250 | ngx.say("Set cookie") 251 | '; 252 | } 253 | --- request 254 | GET /t 255 | --- no_error_log 256 | [error] 257 | --- response_headers 258 | Set-Cookie: Name=Bob; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Max-Age=50; Domain=example.com; Path=/; Secure; HttpOnly; a4334aebaec 259 | --- response_body 260 | Set cookie 261 | 262 | 263 | 264 | === TEST 7: set multiple cookie 265 | --- http_config eval: $::HttpConfig 266 | --- config 267 | location /t { 268 | content_by_lua ' 269 | local ck = require "resty.cookie" 270 | local cookie, err = ck:new() 271 | if not cookie then 272 | ngx.log(ngx.ERR, err) 273 | return 274 | end 275 | 276 | local ok, err = cookie:set({ 277 | key = "Name", value = "Bob", path = "/", 278 | }) 279 | if not ok then 280 | ngx.log(ngx.ERR, err) 281 | return 282 | end 283 | 284 | local ok, err = cookie:set({ 285 | key = "Age", value = "20", 286 | }) 287 | if not ok then 288 | ngx.log(ngx.ERR, err) 289 | return 290 | end 291 | 292 | local ok, err = cookie:set({ 293 | key = "ID", value = "0xf7898", 294 | expires = "Wed, 09 Jun 2021 10:18:14 GMT" 295 | }) 296 | if not ok then 297 | ngx.log(ngx.ERR, err) 298 | return 299 | end 300 | ngx.say("Set cookie") 301 | '; 302 | } 303 | --- request 304 | GET /t 305 | --- no_error_log 306 | [error] 307 | --- comment 308 | because "--- response_headers" does not work with multiple headers with the same 309 | key, so use "--- raw_response_headers_like" instead 310 | --- raw_response_headers_like: Set-Cookie: Name=Bob; Path=/\r\nSet-Cookie: Age=20\r\nSet-Cookie: ID=0xf7898; Expires=Wed, 09 Jun 2021 10:18:14 GMT 311 | --- response_body 312 | Set cookie 313 | 314 | 315 | 316 | === TEST 8: remove duplicated cookies in cookie:set 317 | --- http_config eval: $::HttpConfig 318 | --- config 319 | location /t { 320 | content_by_lua ' 321 | local ck = require "resty.cookie" 322 | local cookie, err = ck:new() 323 | if not cookie then 324 | ngx.log(ngx.ERR, err) 325 | return 326 | end 327 | 328 | local ok, err = cookie:set({ 329 | key = "Name", value = "Bob", path = "/", 330 | }) 331 | if not ok then 332 | ngx.log(ngx.ERR, err) 333 | return 334 | end 335 | 336 | local ok, err = cookie:set({ 337 | key = "Age", value = "20", 338 | }) 339 | if not ok then 340 | ngx.log(ngx.ERR, err) 341 | return 342 | end 343 | 344 | local ok, err = cookie:set({ 345 | key = "Name", value = "Bob", path = "/", 346 | }) 347 | if not ok then 348 | ngx.log(ngx.ERR, err) 349 | return 350 | end 351 | 352 | ngx.say("Set cookie") 353 | '; 354 | } 355 | --- request 356 | GET /t 357 | --- no_error_log 358 | [error] 359 | --- raw_response_headers_like: Set-Cookie: Name=Bob; Path=/\r\nSet-Cookie: Age=20\r\n 360 | --- raw_response_headers_unlike: Set-Cookie: Name=Bob; Path=/\r\nSet-Cookie: Age=20\r\nSet-Cookie: Name=Bob; Path=/ 361 | --- response_body 362 | Set cookie 363 | --------------------------------------------------------------------------------