bunkerweb 1.4.0

This commit is contained in:
bunkerity
2022-06-03 17:24:14 +02:00
parent 3a078326c5
commit a9f886804a
5245 changed files with 1432051 additions and 27894 deletions

1
deps/src/lua-resty-session/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/.project

View File

@@ -0,0 +1,2 @@
std = "ngx_lua"

437
deps/src/lua-resty-session/Changes.md vendored Normal file
View File

@@ -0,0 +1,437 @@
# Changelog
All notable changes to `lua-resty-session` will be documented in this file.
## [3.10] - 2022-01-14
### Fixed
- 3.9 introduced an issue where calling session:regenerate with flush=true,
didn't really flush if the session strategy was `regenerate`.
## [3.9] - 2022-01-14
### Fixed
- Fix #138 issue of chunked cookies are not expired when session shrinks,
thanks @alexdowad.
- Fix #134 where regenerate strategy destroyed previous session when calling
`session:regenerate`, it should just `ttl` the old session.
### Added
- AES GCM mode support was added to AES cipher.
This is recommended, but for backward compatibility it was not set as default.
It will be changed in 4.0 release.
- Redis ACL authentication is now available.
- Add `session_redis_username`
- Add `session_redis_password`
- Deprecate `session_redis_auth`; use `session_redis_password`
### Changed
- Optimize Redis and Memcache storage adapters to not connect to database
when not needed.
## [3.8] - 2021-01-04
### Added
- Connection options are now passed to `redis cluster client` as well.
## [3.7] - 2020-10-27
### Fixed
- Fix #107 where `session.start` could release a lock for a short period
### Added
- Add `keep_lock` argument to `session.open`
- Add pluggable compressors, and implement `none` and `zlib` compressor
## [3.6] - 2020-06-24
### Fixed
- Fix `session:hide()` to only send a single `Cookie` header at most as
reported by @jharriman who also provided a fix with #103. Thank you!
## [3.5] - 2020-05-22
### Fixed
- Fix `session:hide()` to not clear non-session request cookies that it
seemed to do in some cases as reported by @altexy who also provided
initial fix with #100. Thank you!
## [3.4] - 2020-05-08
### Fixed
- Fix session_cookie_maxsize - error attempt to compare string with number,
fixes #98, thank you @vavra5
### Changed
- More robust and uniform configuration parsing
## [3.3] - 2020-05-06
### Fixed
- Fix `set_timeouts` is only called if all parameters are available,
should fix #96, thank you @notdodo.
### Added
- Add `$session_memcache_connect_timeout` configuration option
- Add `$session_memcache_read_timeout` configuration option
- Add `$session_memcache_send_timeout` configuration option
- Add `$session_memcache_pool_name` configuration option
- Add `$session_memcache_pool_backlog` configuration option
- Add `$session_dshm_connect_timeout` configuration option
- Add `$session_dshm_read_timeout` configuration option
- Add `$session_dshm_send_timeout` configuration option
- Add `$session_dshm_pool_name` configuration option
- Add `$session_dshm_pool_backlog` configuration option
## [3.2] - 2020-04-30
### Added
- Support for Redis clusters
- Add `$session_redis_connect_timeout` configuration option
- Add `$session_redis_read_timeout` configuration option
- Add `$session_redis_send_timeout` configuration option
- Add `$session_redis_pool_name` configuration option
- Add `$session_redis_pool_backlog` configuration option
- Add `$session_redis_cluster_name` configuration option
- Add `$session_redis_cluster_dict` configuration option
- Add `$session_redis_cluster_maxredirections` configuration option
- Add `$session_redis_cluster_nodes` configuration option
## [3.1] - 2020-03-28
### Added
- A more flexible way to specify custom implementations:
`require "resty.session".new { storage = require "my.storage" }`
## [3.0] - 2020-03-27
### Fixed
- Lock releasing is a lot more robust now
### Added
- Add idletime setting (thanks @Tieske), see `session.cookie.idletime`
- Add support for Cookie prefixes `__Host-` and `__Secure-` on Cookie
name (see: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05#section-4.1.3)
### Changed
- The whole codebase was refactored and simplified, especially implementing
new storage adapters is now a lot easier
- Redis and Memcached `spinlockwait` was changed from microseconds to milliseconds and default
is set to `150` milliseconds,
- Redis and Memcache will only release locks that current session instance holds
- DSHM `session_dshm_store` was renamed to `session_dshm_region`
- BASE64 encoding now strips the padding
## [2.26] - 2020-02-11
### Added
- Add support for `SameSite=None` (#83) (thanks @bodewig)
- Style changes (#77) (thanks @Tieske)
## [2.25] - 2019-11-06
### Added
- Add SSL support for the Redis storage option (#75) (thanks @tieske)
- DSHM storage adapter (a distributed SHM storage based on Hazelcast for Nginx)
(thanks @grrolland)
## [2.24] - 2019-07-09
### Fixed
- Avoid use unix socket and redis password with empty string
- Provide session id when closing, otherwise the lock is not deleted
### Added
- Added a configuration for session cookie max size (`session.cookie.maxsize`)
## [2.23] - 2018-12-12
### Added
- Added pluggable strategies with `default` and a new `regenerate` strategy
- Added pluggable `hmac`s
- Added `session.close`
- Added `ttl` to `storages`
- Added `session.cookie.discard`, a `ttl` how long to keep old sessions when
renewing (used by `regenerate` strategy
## [2.22] - 2018-03-17
### Fixed
- Only sets self.cookie.secure if not defined.
## [2.21] - 2018-03-16
### Screwed
- Forgot to bump version number.
## [2.20] - 2018-03-16
### Fixed
- Fixes issue where check addr and check scheme could be faked.
See also: https://github.com/bungle/lua-resty-session/issues/47
Thanks @nielsole
## [2.19] - 2017-09-19
### Fixed
- Fixes small bug where aes could generate invalid salt on invalid input
that further crashes Lua with error: bad argument #2 to 'salt' (number
expected, got no value)
## [2.18] - 2017-07-10
### Fixed
- Automatically creates exactly 64 bits salt as required by the latest
lua-resty-string.
See also: https://github.com/bungle/lua-resty-session/issues/40
Thanks @peturorri
## [2.17] - 2017-06-12
### Added
- Added session.hide() function to hide session cookies from upstream
on reverse proxy scenarios.
## [2.16] - 2017-05-31
### Changed
- Delays setting the defaults until needed, allowing users to safely
require "resty.session" in different contexts.
## [2.15] - 2017-02-13
## Added
- Added a support for chunked cookies.
See also: https://github.com/bungle/lua-resty-session/issues/35
Thanks @zandbelt
## [2.14] - 2016-12-16
### Fixed
- Lua code configuration parsing corrections (especially on boolean
options).
## Added
- Added a more natural way to pass config arguments to storage
adapters and ciphers in Lua code.
See also: https://github.com/bungle/lua-resty-session/issues/34
Thanks @hanxi
## [2.13] - 2016-11-21
### Changed
- On start we do send cookie now also if the settings have changed
and the cookie expiry time needs to be reduced.
### Fixed
- Memcache storage adapter had a missing ngx.null.
## [2.12] - 2016-11-21
### Added
- Implemented pluggable session identifier generators.
- Implemented random session idenfier generator.
### Changed
- Now checks if headers were already sent before trying to set the
cookie headers.
- SSL session identifier is not checked by default anymore.
- Lua session.identifier.length changed to session.random.length.
- Nginx $session_identifier_length changed to $session_random_length.
## [2.11] - 2016-09-30
### Changed
- Just another OPM release to correct the name.
## [2.10] - 2016-09-29
### Added
- Support for the official OpenResty package manager (opm).
### Changed
- Changed the change log format to keep-a-changelog.
## [2.9] - 2016-09-01
### Fixed
- Bugfix: Weird bug where RAND_bytes was not working on Windows platform.
Code changed to use resty.random. See Also:
https://github.com/bungle/lua-resty-session/issues/31
Thanks @gtuxyco
## [2.8] - 2016-07-05
### Fixed
- Bugfix: AES Cipher used a wrong table for cipher sizes.
See Also: https://github.com/bungle/lua-resty-session/issues/30
Thanks @pronan
## [2.7] - 2016-05-18
### Added
- Redis storage adapter now supports Redis authentication.
See Also: https://github.com/bungle/lua-resty-session/pull/28
Thanks @cheng5533062
## [2.6] - 2016-04-18
### Changed
- Just cleanups and changed _VERSION to point correct version.
## [2.5] - 2016-04-18
### Fixed
- session.save close argument was not defaulting to true.
## [2.4] - 2016-04-17
### Added
- Cookie will now have SameSite attribute set as "Lax" by default.
You can turn it off or set to "Strict" by configuration.
### Changed
- Calling save will now also set session.id if the save was called
without calling start first.
See Also: https://github.com/bungle/lua-resty-session/issues/27
Thanks @hcaihao
## [2.3] - 2015-10-16
### Fixed
- Fixes issue #19 where regenerating session would throw an error
when using cookie storage.
See Also: https://github.com/bungle/lua-resty-session/issues/19
Thanks @hulu1522
## [2.2] - 2015-09-17
### Changed
- Removed all session_cipher_* deprecated settings (it was somewhat
broken in 2.1).
- Changed session secret to be by default 32 bytes random data
See Also: https://github.com/bungle/lua-resty-session/issues/18
Thanks @iain-buclaw-sociomantic
### Added
- Added documentation about removed features and corrected about
session secret size accordingly.
## [2.1] - 2015-09-07
### Added
- Added architecture for Cipher adapter plugins.
See Also: https://github.com/bungle/lua-resty-session/issues/16
Thanks @mingfang
- Implemented AES cipher adapter (just like it was before)
- Implemented None cipher adapter (no encryption)
- Added documentation about pluggable ciphers
### Changed
- Changed JSON serializer to use cjson.safe instead
## [2.0] - 2015-08-31
### Added
- Added architecture for Storage adapter plugins.
See Also: https://github.com/bungle/lua-resty-session/issues/13
- Implemented Client Side Cookie storage adapter.
- Implemented Memcache storage adapter.
See Also: https://github.com/bungle/lua-resty-session/pull/14
Thanks @zandbelt
- Implemented Redis storage adapter.
- Implemented Shared Dictionary (shm) storage adapter.
- Added architecture for Encoder and Decoder plugins.
- Implemented Base 64 encoder / decoder.
- Implemented Base 16 (hex) encoder / decoder.
- Added architecture for Serializer plugins
- Implemented JSON serializer.
- Persistent cookies will now also contain Max-Age in addition to Expires.
- Cookie domain attribute is not set anymore if not specified.
- Added notes about using lua-resty-session with Lua code cache turned off.
See also: https://github.com/bungle/lua-resty-session/issues/15
Thanks @BizShuk
## [1.7] - 2015-08-03
### Added
- Added session.open() function that only opens a session but doesn't send
the cookie (until start is called).
See also: https://github.com/bungle/lua-resty-session/issues/12
Thanks @junhanamaki
### Fixed
- Fixed cookie expiration time format on Firefox bug:
https://github.com/bungle/lua-resty-session/pull/10
Thanks @junhanamaki
- Bugfix: Fixed an issue of overwriting a variable:
https://github.com/bungle/lua-resty-session/pull/11
Thanks @junhanamaki
## [1.6] - 2015-05-05
### Fixed
- Fixed truncated cookie value bug:
https://github.com/bungle/lua-resty-session/pull/8
Thanks @kipras
## [1.5] - 2014-11-27
### Fixed
- Cookies are not always "secure":
https://github.com/bungle/lua-resty-session/issues/5
Thanks @vladimir-smirnov-sociomantic
### Added
- Added documentation about Nginx SSL/TLS configuration settings related
to session lifetime and ssl session ids.
## [1.4] - 2014-11-26
### Fixed
- Bugfix: Fixed an issue where session configurations did get cached
on a module level. This issue is discussed in pull-request #4:
https://github.com/bungle/lua-resty-session/pull/4
Thanks @kipras.
### Added
- Added session.new function.
- Added documentation about Nginx configuration used as defaults (not read
on every request), and documented session.new.
### Changed
- session.start{ ... } (a call with config parameters) works now as expected.
- session.start now returns additional extra boolean parameter that can be
used to check if the session is s new session (false) or a previously
started one (true).
## [1.3] - 2014-11-14
### Added
- Added support for persistent sessions. See issue #2.
- Added session.check.ssi, session.cookie.persistent and the related Nginx
configuration variables.
- Added Max-Age=0 to expiration code.
## [1.2] - 2014-10-12
### Fixed
- Changed encode and decode functions to operate with correct number of
arguments. See issue #1.
## [1.1] - 2014-10-03
### Security
- There was a bug where additional user agent, scheme, and remote addr
(disabled by default) was not checked.
### Added
- Added _VERSION field.
### Changed
- Simplied a code a lot (e.g. internal setcookie and getcookie functions are
now cleaner). Removed a lot of unneccessary lines from session.start by
adding configs directly to session prototype.
## [1.0] - 2014-09-24
### Added
- LuaRocks Support via MoonRocks.

23
deps/src/lua-resty-session/LICENSE vendored Normal file
View File

@@ -0,0 +1,23 @@
Copyright (c) 2014 2022, Aapo Talvensaari
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* 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.
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.

4
deps/src/lua-resty-session/Makefile vendored Normal file
View File

@@ -0,0 +1,4 @@
.PHONY: lint
lint:
@luacheck -q ./lib

1196
deps/src/lua-resty-session/README.md vendored Normal file

File diff suppressed because it is too large Load Diff

7
deps/src/lua-resty-session/dist.ini vendored Normal file
View File

@@ -0,0 +1,7 @@
name = lua-resty-session
abstract = Session Library for OpenResty - Flexible and Secure
author = Aapo Talvensaari (@bungle)
is_original = yes
license = 2bsd
repo_link = https://github.com/bungle/lua-resty-session
requires = openresty, openresty/lua-resty-string

View File

@@ -0,0 +1,771 @@
local require = require
local random = require "resty.random"
local ngx = ngx
local var = ngx.var
local time = ngx.time
local header = ngx.header
local http_time = ngx.http_time
local set_header = ngx.req.set_header
local clear_header = ngx.req.clear_header
local concat = table.concat
local ceil = math.ceil
local max = math.max
local find = string.find
local gsub = string.gsub
local byte = string.byte
local sub = string.sub
local type = type
local pcall = pcall
local tonumber = tonumber
local setmetatable = setmetatable
local getmetatable = getmetatable
local bytes = random.bytes
local UNDERSCORE = byte("_")
local EXPIRE_FLAGS = "; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0"
local COOKIE_PARTS = {
DEFAULT = {
n = 3,
"id",
"expires", -- may also contain: `expires:usebefore`
"hash"
},
cookie = {
n = 4,
"id",
"expires", -- may also contain: `expires:usebefore`
"data",
"hash",
},
}
local function enabled(value)
if value == nil then
return nil
end
return value == true
or value == "1"
or value == "true"
or value == "on"
end
local function ifnil(value, default)
if value == nil then
return default
end
return enabled(value)
end
local function prequire(prefix, package, default)
if type(package) == "table" then
return package, package.name
end
local ok, module = pcall(require, prefix .. package)
if not ok then
return require(prefix .. default), default
end
return module, package
end
local function is_session_cookie(cookie, name, name_len)
if not cookie or cookie == "" then
return false, nil
end
cookie = gsub(cookie, "^%s+", "")
if cookie == "" then
return false, nil
end
cookie = gsub(cookie, "%s+$", "")
if cookie == "" then
return false, nil
end
local eq_pos = find(cookie, "=", 1, true)
if not eq_pos then
return false, cookie
end
local cookie_name = sub(cookie, 1, eq_pos - 1)
if cookie_name == "" then
return false, cookie
end
cookie_name = gsub(cookie_name, "%s+$", "")
if cookie_name == "" then
return false, cookie
end
if cookie_name ~= name then
if find(cookie_name, name, 1, true) ~= 1 then
return false, cookie
end
if byte(cookie_name, name_len + 1) ~= UNDERSCORE then
return false, cookie
end
if not tonumber(sub(cookie_name, name_len + 2), 10) then
return false, cookie
end
end
return true, cookie
end
local function set_cookie(session, value, expires)
if ngx.headers_sent then
return nil, "attempt to set session cookie after sending out response headers"
end
value = value or ""
local cookie = session.cookie
local output = {}
local i = 3
-- build cookie parameters, elements 1+2 will be set later
if expires then
-- we're expiring/deleting the data, so set an expiry in the past
output[i] = EXPIRE_FLAGS
elseif cookie.persistent then
-- persistent cookies have an expiry
output[i] = "; Expires=" .. http_time(session.expires) .. "; Max-Age=" .. cookie.lifetime
else
-- just to reserve index 3 for expiry as cookie might get smaller,
-- and some cookies need to be expired.
output[i] = ""
end
if cookie.domain and cookie.domain ~= "localhost" and cookie.domain ~= "" then
i = i + 1
output[i] = "; Domain=" .. cookie.domain
end
i = i + 1
output[i] = "; Path=" .. (cookie.path or "/")
if cookie.samesite == "Lax"
or cookie.samesite == "Strict"
or cookie.samesite == "None"
then
i = i + 1
output[i] = "; SameSite=" .. cookie.samesite
end
if cookie.secure then
i = i + 1
output[i] = "; Secure"
end
if cookie.httponly then
i = i + 1
output[i] = "; HttpOnly"
end
-- How many chunks do we need?
local cookie_parts
local cookie_chunks
if expires then
-- expiring cookie, so deleting data. Do not measure data, but use
-- existing chunk count to make sure we clear all of them
cookie_parts = cookie.chunks or 1
else
-- calculate required chunks from data
cookie_chunks = max(ceil(#value / cookie.maxsize), 1)
cookie_parts = max(cookie_chunks, cookie.chunks or 1)
end
local cookie_header = header["Set-Cookie"]
for j = 1, cookie_parts do
-- create numbered chunk names if required
local chunk_name = { session.name }
if j > 1 then
chunk_name[2] = "_"
chunk_name[3] = j
chunk_name[4] = "="
else
chunk_name[2] = "="
end
chunk_name = concat(chunk_name)
output[1] = chunk_name
if expires then
-- expiring cookie, so deleting data; clear it
output[2] = ""
elseif j > cookie_chunks then
-- less chunks than before, clearing excess cookies
output[2] = ""
output[3] = EXPIRE_FLAGS
else
-- grab the piece for the current chunk
local sp = j * cookie.maxsize - (cookie.maxsize - 1)
if j < cookie_chunks then
output[2] = sub(value, sp, sp + (cookie.maxsize - 1)) .. "0"
else
output[2] = sub(value, sp)
end
end
-- build header value and add it to the header table/string
-- replace existing chunk-name, or append
local cookie_content = concat(output)
local header_type = type(cookie_header)
if header_type == "table" then
local found = false
local cookie_count = #cookie_header
for cookie_index = 1, cookie_count do
if find(cookie_header[cookie_index], chunk_name, 1, true) == 1 then
cookie_header[cookie_index] = cookie_content
found = true
break
end
end
if not found then
cookie_header[cookie_count + 1] = cookie_content
end
elseif header_type == "string" and find(cookie_header, chunk_name, 1, true) ~= 1 then
cookie_header = { cookie_header, cookie_content }
else
cookie_header = cookie_content
end
end
header["Set-Cookie"] = cookie_header
return true
end
local function get_cookie(session, i)
local cookie_name = { "cookie_", session.name }
if i then
cookie_name[3] = "_"
cookie_name[4] = i
else
i = 1
end
local cookie = var[concat(cookie_name)]
if not cookie then
return nil
end
session.cookie.chunks = i
local cookie_size = #cookie
if cookie_size <= session.cookie.maxsize then
return cookie
end
return concat{ sub(cookie, 1, session.cookie.maxsize), get_cookie(session, i + 1) or "" }
end
local function set_usebefore(session)
local usebefore = session.usebefore
local idletime = session.cookie.idletime
if idletime == 0 then -- usebefore is disabled
if usebefore then
session.usebefore = nil
return true
end
return false
end
usebefore = usebefore or 0
local new_usebefore = session.now + idletime
if new_usebefore - usebefore > 60 then
session.usebefore = new_usebefore
return true
end
return false
end
local function save(session, close)
session.expires = session.now + session.cookie.lifetime
set_usebefore(session)
local cookie, err = session.strategy.save(session, close)
if not cookie then
return nil, err or "unable to save session cookie"
end
return set_cookie(session, cookie)
end
local function touch(session, close)
if set_usebefore(session) then
-- usebefore was updated, so set cookie
local cookie, err = session.strategy.touch(session, close)
if not cookie then
return nil, err or "unable to touch session cookie"
end
return set_cookie(session, cookie)
end
if close then
local ok, err = session.strategy.close(session)
if not ok then
return nil, err
end
end
return true
end
local function regenerate(session, flush)
if session.strategy.destroy then
session.strategy.destroy(session)
elseif session.strategy.close then
session.strategy.close(session)
end
if flush then
session.data = {}
end
session.id = session:identifier()
end
local secret = bytes(32, true) or bytes(32)
local defaults
local function init()
defaults = {
name = var.session_name or "session",
identifier = var.session_identifier or "random",
strategy = var.session_strategy or "default",
storage = var.session_storage or "cookie",
serializer = var.session_serializer or "json",
compressor = var.session_compressor or "none",
encoder = var.session_encoder or "base64",
cipher = var.session_cipher or "aes",
hmac = var.session_hmac or "sha1",
cookie = {
path = var.session_cookie_path or "/",
domain = var.session_cookie_domain,
samesite = var.session_cookie_samesite or "Lax",
secure = enabled(var.session_cookie_secure),
httponly = enabled(var.session_cookie_httponly or true),
persistent = enabled(var.session_cookie_persistent or false),
discard = tonumber(var.session_cookie_discard, 10) or 10,
renew = tonumber(var.session_cookie_renew, 10) or 600,
lifetime = tonumber(var.session_cookie_lifetime, 10) or 3600,
idletime = tonumber(var.session_cookie_idletime, 10) or 0,
maxsize = tonumber(var.session_cookie_maxsize, 10) or 4000,
}, check = {
ssi = enabled(var.session_check_ssi or false),
ua = enabled(var.session_check_ua or true),
scheme = enabled(var.session_check_scheme or true),
addr = enabled(var.session_check_addr or false)
}
}
defaults.secret = var.session_secret or secret
end
local session = {
_VERSION = "3.10"
}
session.__index = session
function session:get_cookie()
return get_cookie(self)
end
function session:parse_cookie(value)
local cookie
local cookie_parts = COOKIE_PARTS[self.cookie.storage] or COOKIE_PARTS.DEFAULT
local count = 1
local pos = 1
local p_pos = find(value, "|", 1, true)
while p_pos do
if count > (cookie_parts.n - 1) then
return nil, "too many session cookie parts"
end
if not cookie then
cookie = {}
end
if count == 2 then
local cookie_part = sub(value, pos, p_pos - 1)
local c_pos = find(cookie_part, ":", 2, true)
if c_pos then
cookie.expires = tonumber(sub(cookie_part, 1, c_pos - 1), 10)
if not cookie.expires then
return nil, "invalid session cookie expiry"
end
cookie.usebefore = tonumber(sub(cookie_part, c_pos + 1), 10)
if not cookie.usebefore then
return nil, "invalid session cookie usebefore"
end
else
cookie.expires = tonumber(cookie_part, 10)
if not cookie.expires then
return nil, "invalid session cookie expiry"
end
end
else
local name = cookie_parts[count]
local cookie_part = self.encoder.decode(sub(value, pos, p_pos - 1))
if not cookie_part then
return nil, "unable to decode session cookie part (" .. name .. ")"
end
cookie[name] = cookie_part
end
count = count + 1
pos = p_pos + 1
p_pos = find(value, "|", pos, true)
end
if count ~= cookie_parts.n then
return nil, "invalid number of session cookie parts"
end
local name = cookie_parts[count]
local cookie_part = self.encoder.decode(sub(value, pos))
if not cookie_part then
return nil, "unable to decode session cookie part (" .. name .. ")"
end
cookie[name] = cookie_part
if not cookie.id then
return nil, "missing session cookie id"
end
if not cookie.expires then
return nil, "missing session cookie expiry"
end
if cookie.expires <= self.now then
return nil, "session cookie has expired"
end
if cookie.usebefore and cookie.usebefore <= self.now then
return nil, "session cookie idle time has passed"
end
if not cookie.hash then
return nil, "missing session cookie signature"
end
return cookie
end
function session.new(opts)
if opts and getmetatable(opts) == session then
return opts
end
if not defaults then
init()
end
opts = type(opts) == "table" and opts or defaults
local cookie = opts.cookie or defaults.cookie
local name = opts.name or defaults.name
local sec = opts.secret or defaults.secret
local secure
local path
local domain
if find(name, "__Host-", 1, true) == 1 then
secure = true
path = "/"
else
if find(name, "__Secure-", 1, true) == 1 then
secure = true
else
secure = ifnil(cookie.secure, defaults.cookie.secure)
end
domain = cookie.domain or defaults.cookie.domain
path = cookie.path or defaults.cookie.path
end
local check = opts.check or defaults.check
local ide, iden = prequire("resty.session.identifiers.", opts.identifier or defaults.identifier, "random")
local ser, sern = prequire("resty.session.serializers.", opts.serializer or defaults.serializer, "json")
local com, comn = prequire("resty.session.compressors.", opts.compressor or defaults.compressor, "none")
local enc, encn = prequire("resty.session.encoders.", opts.encoder or defaults.encoder, "base64")
local cip, cipn = prequire("resty.session.ciphers.", opts.cipher or defaults.cipher, "aes")
local sto, ston = prequire("resty.session.storage.", opts.storage or defaults.storage, "cookie")
local str, strn = prequire("resty.session.strategies.", opts.strategy or defaults.strategy, "default")
local hma, hman = prequire("resty.session.hmac.", opts.hmac or defaults.hmac, "sha1")
local self = {
now = time(),
name = name,
secret = sec,
identifier = ide,
serializer = ser,
strategy = str,
encoder = enc,
hmac = hma,
cookie = {
storage = ston,
encoder = enc,
path = path,
domain = domain,
secure = secure,
samesite = cookie.samesite or defaults.cookie.samesite,
httponly = ifnil(cookie.httponly, defaults.cookie.httponly),
persistent = ifnil(cookie.persistent, defaults.cookie.persistent),
discard = tonumber(cookie.discard, 10) or defaults.cookie.discard,
renew = tonumber(cookie.renew, 10) or defaults.cookie.renew,
lifetime = tonumber(cookie.lifetime, 10) or defaults.cookie.lifetime,
idletime = tonumber(cookie.idletime, 10) or defaults.cookie.idletime,
maxsize = tonumber(cookie.maxsize, 10) or defaults.cookie.maxsize,
}, check = {
ssi = ifnil(check.ssi, defaults.check.ssi),
ua = ifnil(check.ua, defaults.check.ua),
scheme = ifnil(check.scheme, defaults.check.scheme),
addr = ifnil(check.addr, defaults.check.addr),
}
}
if self.cookie.idletime > 0 and self.cookie.discard > self.cookie.idletime then
-- if using idletime, then the discard period must be less or equal
self.cookie.discard = self.cookie.idletime
end
if iden and not self[iden] then self[iden] = opts[iden] end
if sern and not self[sern] then self[sern] = opts[sern] end
if comn and not self[comn] then self[comn] = opts[comn] end
if encn and not self[encn] then self[encn] = opts[encn] end
if cipn and not self[cipn] then self[cipn] = opts[cipn] end
if ston and not self[ston] then self[ston] = opts[ston] end
if strn and not self[strn] then self[strn] = opts[strn] end
if hman and not self[hman] then self[hman] = opts[hman] end
self.cipher = cip.new(self)
self.storage = sto.new(self)
self.compressor = com.new(self)
return setmetatable(self, session)
end
function session.open(opts, keep_lock)
local self = opts
if self and getmetatable(self) == session then
if self.opened then
return self, self.present
end
else
self = session.new(opts)
end
if self.cookie.secure == nil then
self.cookie.secure = var.scheme == "https" or var.https == "on"
end
self.now = time()
self.key = concat {
self.check.ssi and var.ssl_session_id or "",
self.check.ua and var.http_user_agent or "",
self.check.addr and var.remote_addr or "",
self.check.scheme and var.scheme or "",
}
self.opened = true
local err
local cookie = self:get_cookie()
if cookie then
cookie, err = self:parse_cookie(cookie)
if cookie then
local ok
ok, err = self.strategy.open(self, cookie, keep_lock)
if ok then
return self, true
end
end
end
regenerate(self, true)
return self, false, err
end
function session.start(opts)
if opts and getmetatable(opts) == session and opts.started then
return opts, opts.present
end
local self, present, reason = session.open(opts, true)
self.started = true
if not present then
local ok, err = save(self)
if not ok then
return nil, err or "unable to save session cookie"
end
return self, present, reason
end
if self.strategy.start then
local ok, err = self.strategy.start(self)
if not ok then
return nil, err or "unable to start session"
end
end
if self.expires - self.now < self.cookie.renew
or self.expires > self.now + self.cookie.lifetime
then
local ok, err = save(self)
if not ok then
return nil, err or "unable to save session cookie"
end
else
-- we're not saving, so we must touch to update idletime/usebefore
local ok, err = touch(self)
if not ok then
return nil, err or "unable to touch session cookie"
end
end
return self, true
end
function session.destroy(opts)
if opts and getmetatable(opts) == session and opts.destroyed then
return true
end
local self, err = session.start(opts)
if not self then
return nil, err
end
if self.strategy.destroy then
self.strategy.destroy(self)
elseif self.strategy.close then
self.strategy.close(self)
end
self.data = {}
self.present = nil
self.opened = nil
self.started = nil
self.closed = true
self.destroyed = true
return set_cookie(self, "", true)
end
function session:regenerate(flush, close)
close = close ~= false
if self.strategy.regenerate then
if flush then
self.data = {}
end
if not self.id then
self.id = session:identifier()
end
else
regenerate(self, flush)
end
return save(self, close)
end
function session:save(close)
close = close ~= false
if not self.id then
self.id = self:identifier()
end
return save(self, close)
end
function session:close()
self.closed = true
if self.strategy.close then
return self.strategy.close(self)
end
return true
end
function session:hide()
local cookies = var.http_cookie
if not cookies or cookies == "" then
return
end
local results = {}
local name = self.name
local name_len = #name
local found
local i = 1
local j = 0
local sc_pos = find(cookies, ";", i, true)
while sc_pos do
local isc, cookie = is_session_cookie(sub(cookies, i, sc_pos - 1), name, name_len)
if isc then
found = true
elseif cookie then
j = j + 1
results[j] = cookie
end
i = sc_pos + 1
sc_pos = find(cookies, ";", i, true)
end
local isc, cookie
if i == 1 then
isc, cookie = is_session_cookie(cookies, name, name_len)
else
isc, cookie = is_session_cookie(sub(cookies, i), name, name_len)
end
if not isc and cookie then
if not found then
return
end
j = j + 1
results[j] = cookie
end
if j == 0 then
clear_header("Cookie")
else
set_header("Cookie", concat(results, "; ", 1, j))
end
end
return session

View File

@@ -0,0 +1,113 @@
local aes = require "resty.aes"
local setmetatable = setmetatable
local tonumber = tonumber
local ceil = math.ceil
local var = ngx.var
local sub = string.sub
local rep = string.rep
local HASHES = aes.hash
local CIPHER_MODES = {
ecb = "ecb",
cbc = "cbc",
cfb1 = "cfb1",
cfb8 = "cfb8",
cfb128 = "cfb128",
ofb = "ofb",
ctr = "ctr",
gcm = "gcm",
}
local CIPHER_SIZES = {
[128] = 128,
[192] = 192,
[256] = 256,
}
local defaults = {
size = CIPHER_SIZES[tonumber(var.session_aes_size, 10)] or 256,
mode = CIPHER_MODES[var.session_aes_mode] or "cbc",
hash = HASHES[var.session_aes_hash] or HASHES.sha512,
rounds = tonumber(var.session_aes_rounds, 10) or 1,
}
local function adjust_salt(salt)
if not salt then
return nil
end
local z = #salt
if z < 8 then
return sub(rep(salt, ceil(8 / z)), 1, 8)
end
if z > 8 then
return sub(salt, 1, 8)
end
return salt
end
local function get_cipher(self, key, salt)
local mode = aes.cipher(self.size, self.mode)
if not mode then
return nil, "invalid cipher mode " .. self.mode .. "(" .. self.size .. ")"
end
return aes:new(key, adjust_salt(salt), mode, self.hash, self.rounds)
end
local cipher = {}
cipher.__index = cipher
function cipher.new(session)
local config = session.aes or defaults
return setmetatable({
size = CIPHER_SIZES[tonumber(config.size, 10)] or defaults.size,
mode = CIPHER_MODES[config.mode] or defaults.mode,
hash = HASHES[config.hash] or defaults.hash,
rounds = tonumber(config.rounds, 10) or defaults.rounds,
}, cipher)
end
function cipher:encrypt(data, key, salt, _)
local cip, err = get_cipher(self, key, salt)
if not cip then
return nil, err or "unable to aes encrypt data"
end
local encrypted_data
encrypted_data, err = cip:encrypt(data)
if not encrypted_data then
return nil, err or "aes encryption failed"
end
if self.mode == "gcm" then
return encrypted_data[1], nil, encrypted_data[2]
end
return encrypted_data
end
function cipher:decrypt(data, key, salt, _, tag)
local cip, err = get_cipher(self, key, salt)
if not cip then
return nil, err or "unable to aes decrypt data"
end
local decrypted_data
decrypted_data, err = cip:decrypt(data, tag)
if not decrypted_data then
return nil, err or "aes decryption failed"
end
if self.mode == "gcm" then
return decrypted_data, nil, tag
end
return decrypted_data
end
return cipher

View File

@@ -0,0 +1,15 @@
local cipher = {}
function cipher.new()
return cipher
end
function cipher.encrypt(_, data, _, _)
return data
end
function cipher.decrypt(_, data, _, _, _)
return data
end
return cipher

View File

@@ -0,0 +1,15 @@
local compressor = {}
function compressor.new()
return compressor
end
function compressor.compress(_, data)
return data
end
function compressor.decompress(_, data)
return data
end
return compressor

View File

@@ -0,0 +1,43 @@
local zlib = require "ffi-zlib"
local sio = require "pl.stringio"
local concat = table.concat
local function gzip(func, input)
local stream = sio.open(input)
local output = {}
local n = 0
local ok, err = func(function(size)
return stream:read(size)
end, function(data)
n = n + 1
output[n] = data
end, 8192)
if not ok then
return nil, err
end
if n == 0 then
return ""
end
return concat(output, nil, 1, n)
end
local compressor = {}
function compressor.new()
return compressor
end
function compressor.compress(_, data)
return gzip(zlib.deflateGzip, data)
end
function compressor.decompress(_, data)
return gzip(zlib.inflateGzip, data)
end
return compressor

View File

@@ -0,0 +1,29 @@
local to_hex = require "resty.string".to_hex
local tonumber = tonumber
local gsub = string.gsub
local char = string.char
local function chr(c)
return char(tonumber(c, 16) or 0)
end
local encoder = {}
function encoder.encode(value)
if not value then
return nil, "unable to base16 encode value"
end
return to_hex(value)
end
function encoder.decode(value)
if not value then
return nil, "unable to base16 decode value"
end
return (gsub(value, "..", chr))
end
return encoder

View File

@@ -0,0 +1,39 @@
local encode_base64 = ngx.encode_base64
local decode_base64 = ngx.decode_base64
local gsub = string.gsub
local ENCODE_CHARS = {
["+"] = "-",
["/"] = "_",
}
local DECODE_CHARS = {
["-"] = "+",
["_"] = "/",
}
local encoder = {}
function encoder.encode(value)
if not value then
return nil, "unable to base64 encode value"
end
local encoded = encode_base64(value, true)
if not encoded then
return nil, "unable to base64 encode value"
end
return gsub(encoded, "[+/]", ENCODE_CHARS)
end
function encoder.decode(value)
if not value then
return nil, "unable to base64 decode value"
end
return decode_base64((gsub(value, "[-_]", DECODE_CHARS)))
end
return encoder

View File

@@ -0,0 +1 @@
return require "resty.session.encoders.base16"

View File

@@ -0,0 +1 @@
return ngx.hmac_sha1

View File

@@ -0,0 +1,13 @@
local tonumber = tonumber
local random = require "resty.random".bytes
local var = ngx.var
local defaults = {
length = tonumber(var.session_random_length, 10) or 16
}
return function(session)
local config = session.random or defaults
local length = tonumber(config.length, 10) or defaults.length
return random(length, true) or random(length)
end

View File

@@ -0,0 +1,6 @@
local json = require "cjson.safe"
return {
serialize = json.encode,
deserialize = json.decode,
}

View File

@@ -0,0 +1,7 @@
local storage = {}
function storage.new()
return storage
end
return storage

View File

@@ -0,0 +1,163 @@
local dshm = require "resty.dshm"
local setmetatable = setmetatable
local tonumber = tonumber
local concat = table.concat
local var = ngx.var
local defaults = {
region = var.session_dshm_region or "sessions",
connect_timeout = tonumber(var.session_dshm_connect_timeout, 10),
read_timeout = tonumber(var.session_dshm_read_timeout, 10),
send_timeout = tonumber(var.session_dshm_send_timeout, 10),
host = var.session_dshm_host or "127.0.0.1",
port = tonumber(var.session_dshm_port, 10) or 4321,
pool = {
name = var.session_dshm_pool_name,
size = tonumber(var.session_dshm_pool_size, 10) or 100,
timeout = tonumber(var.session_dshm_pool_timeout, 10) or 1000,
backlog = tonumber(var.session_dshm_pool_backlog, 10),
},
}
local storage = {}
storage.__index = storage
function storage.new(session)
local config = session.dshm or defaults
local pool = config.pool or defaults.pool
local connect_timeout = tonumber(config.connect_timeout, 10) or defaults.connect_timeout
local store = dshm:new()
if store.set_timeouts then
local send_timeout = tonumber(config.send_timeout, 10) or defaults.send_timeout
local read_timeout = tonumber(config.read_timeout, 10) or defaults.read_timeout
if connect_timeout then
if send_timeout and read_timeout then
store:set_timeouts(connect_timeout, send_timeout, read_timeout)
else
store:set_timeout(connect_timeout)
end
end
elseif store.set_timeout and connect_timeout then
store:set_timeout(connect_timeout)
end
local self = {
store = store,
encoder = session.encoder,
region = config.region or defaults.region,
host = config.host or defaults.host,
port = tonumber(config.port, 10) or defaults.port,
pool_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout,
connect_opts = {
pool = pool.name or defaults.pool.name,
pool_size = tonumber(pool.size, 10) or defaults.pool.size,
backlog = tonumber(pool.backlog, 10) or defaults.pool.backlog,
},
}
return setmetatable(self, storage)
end
function storage:connect()
return self.store:connect(self.host, self.port, self.connect_opts)
end
function storage:set_keepalive()
return self.store:set_keepalive(self.pool_timeout)
end
function storage:key(id)
return concat({ self.region, id }, "::")
end
function storage:set(key, ttl, data)
local ok, err = self:connect()
if not ok then
return nil, err
end
data, err = self.encoder.encode(data)
if not data then
self:set_keepalive()
return nil, err
end
ok, err = self.store:set(key, data, ttl)
self:set_keepalive()
return ok, err
end
function storage:get(key)
local ok, err = self:connect()
if not ok then
return nil, err
end
local data
data, err = self.store:get(key)
if data then
data, err = self.encoder.decode(data)
end
self:set_keepalive()
return data, err
end
function storage:delete(key)
local ok, err = self:connect()
if not ok then
return nil, err
end
ok, err = self.store:delete(key)
self:set_keepalive()
return ok, err
end
function storage:touch(key, ttl)
local ok, err = self:connect()
if not ok then
return nil, err
end
ok, err = self.store:touch(key, ttl)
self:set_keepalive()
return ok, err
end
function storage:open(id)
local key = self:key(id)
return self:get(key)
end
function storage:save(id, ttl, data)
local key = self:key(id)
return self:set(key, ttl, data)
end
function storage:destroy(id)
local key = self:key(id)
return self:delete(key)
end
function storage:ttl(id, ttl)
local key = self:key(id)
return self:touch(key, ttl)
end
return storage

View File

@@ -0,0 +1,303 @@
local memcached = require "resty.memcached"
local setmetatable = setmetatable
local tonumber = tonumber
local concat = table.concat
local sleep = ngx.sleep
local null = ngx.null
local var = ngx.var
local function enabled(value)
if value == nil then
return nil
end
return value == true
or value == "1"
or value == "true"
or value == "on"
end
local function ifnil(value, default)
if value == nil then
return default
end
return enabled(value)
end
local defaults = {
prefix = var.session_memcache_prefix or "sessions",
socket = var.session_memcache_socket,
host = var.session_memcache_host or "127.0.0.1",
uselocking = enabled(var.session_memcache_uselocking or true),
connect_timeout = tonumber(var.session_memcache_connect_timeout, 10),
read_timeout = tonumber(var.session_memcache_read_timeout, 10),
send_timeout = tonumber(var.session_memcache_send_timeout, 10),
port = tonumber(var.session_memcache_port, 10) or 11211,
spinlockwait = tonumber(var.session_memcache_spinlockwait, 10) or 150,
maxlockwait = tonumber(var.session_memcache_maxlockwait, 10) or 30,
pool = {
name = var.session_memcache_pool_name,
timeout = tonumber(var.session_memcache_pool_timeout, 10),
size = tonumber(var.session_memcache_pool_size, 10),
backlog = tonumber(var.session_memcache_pool_backlog, 10),
},
}
local storage = {}
storage.__index = storage
function storage.new(session)
local config = session.memcache or defaults
local pool = config.pool or defaults.pool
local locking = ifnil(config.uselocking, defaults.uselocking)
local connect_timeout = tonumber(config.connect_timeout, 10) or defaults.connect_timeout
local memcache = memcached:new()
if memcache.set_timeouts then
local send_timeout = tonumber(config.send_timeout, 10) or defaults.send_timeout
local read_timeout = tonumber(config.read_timeout, 10) or defaults.read_timeout
if connect_timeout then
if send_timeout and read_timeout then
memcache:set_timeouts(connect_timeout, send_timeout, read_timeout)
else
memcache:set_timeout(connect_timeout)
end
end
elseif memcache.set_timeout and connect_timeout then
memcache:set_timeout(connect_timeout)
end
local self = {
memcache = memcache,
prefix = config.prefix or defaults.prefix,
uselocking = locking,
spinlockwait = tonumber(config.spinlockwait, 10) or defaults.spinlockwait,
maxlockwait = tonumber(config.maxlockwait, 10) or defaults.maxlockwait,
pool_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout,
connect_opts = {
pool = pool.name or defaults.pool.name,
pool_size = tonumber(pool.size, 10) or defaults.pool.size,
backlog = tonumber(pool.backlog, 10) or defaults.pool.backlog,
},
}
local socket = config.socket or defaults.socket
if socket and socket ~= "" then
self.socket = socket
else
self.host = config.host or defaults.host
self.port = config.port or defaults.port
end
return setmetatable(self, storage)
end
function storage:connect()
local socket = self.socket
if socket then
return self.memcache:connect(socket, self.connect_opts)
end
return self.memcache:connect(self.host, self.port, self.connect_opts)
end
function storage:set_keepalive()
return self.memcache:set_keepalive(self.pool_timeout)
end
function storage:key(id)
return concat({ self.prefix, id }, ":" )
end
function storage:lock(key)
if not self.uselocking or self.locked then
return true
end
if not self.token then
self.token = var.request_id
end
local lock_key = concat({ key, "lock" }, "." )
local lock_ttl = self.maxlockwait + 1
local attempts = (1000 / self.spinlockwait) * self.maxlockwait
local waittime = self.spinlockwait / 1000
for _ = 1, attempts do
local ok = self.memcache:add(lock_key, self.token, lock_ttl)
if ok then
self.locked = true
return true
end
sleep(waittime)
end
return false, "unable to acquire a session lock"
end
function storage:unlock(key)
if not self.uselocking or not self.locked then
return true
end
local lock_key = concat({ key, "lock" }, "." )
local token = self:get(lock_key)
if token == self.token then
self.memcache:delete(lock_key)
self.locked = nil
end
end
function storage:get(key)
local data, err = self.memcache:get(key)
if not data then
return nil, err
end
if data == null then
return nil
end
return data
end
function storage:set(key, data, ttl)
return self.memcache:set(key, data, ttl)
end
function storage:expire(key, ttl)
return self.memcache:touch(key, ttl)
end
function storage:delete(key)
return self.memcache:delete(key)
end
function storage:open(id, keep_lock)
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
ok, err = self:lock(key)
if not ok then
self:set_keepalive()
return nil, err
end
local data
data, err = self:get(key)
if err or not data or not keep_lock then
self:unlock(key)
end
self:set_keepalive()
return data, err
end
function storage:start(id)
if not self.uselocking or not self.locked then
return true
end
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
ok, err = self:lock(key)
self:set_keepalive()
return ok, err
end
function storage:save(id, ttl, data, close)
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
ok, err = self:set(key, data, ttl)
if close then
self:unlock(key)
end
self:set_keepalive()
if not ok then
return nil, err
end
return true
end
function storage:close(id)
if not self.uselocking or not self.locked then
return true
end
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
self:unlock(key)
self:set_keepalive()
return true
end
function storage:destroy(id)
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
ok, err = self:delete(key)
self:unlock(key)
self:set_keepalive()
return ok, err
end
function storage:ttl(id, ttl, close)
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
ok, err = self:expire(key, ttl)
if close then
self:unlock(key)
end
self:set_keepalive()
return ok, err
end
return storage

View File

@@ -0,0 +1 @@
return require "resty.session.storage.memcache"

View File

@@ -0,0 +1,478 @@
local setmetatable = setmetatable
local tonumber = tonumber
local type = type
local reverse = string.reverse
local gmatch = string.gmatch
local find = string.find
local byte = string.byte
local sub = string.sub
local concat = table.concat
local sleep = ngx.sleep
local null = ngx.null
local var = ngx.var
local LB = byte("[")
local RB = byte("]")
local function parse_cluster_nodes(nodes)
if not nodes or nodes == "" then
return nil
end
if type(nodes) == "table" then
return nodes
end
local addrs
local i
for node in gmatch(nodes, "%S+") do
local ip = node
local port = 6379
local pos = find(reverse(ip), ":", 2, true)
if pos then
local p = tonumber(sub(ip, -pos + 1), 10)
if p >= 1 and p <= 65535 then
local addr = sub(ip, 1, -pos - 1)
if find(addr, ":", 1, true) then
if byte(addr, -1) == RB then
ip = addr
port = p
end
else
ip = addr
port = p
end
end
end
if byte(ip, 1, 1) == LB then
ip = sub(ip, 2)
end
if byte(ip, -1) == RB then
ip = sub(ip, 1, -2)
end
if not addrs then
i = 1
addrs = {{
ip = ip,
port = port,
}}
else
i = i + 1
addrs[i] = {
ip = ip,
port = port,
}
end
end
if not i then
return
end
return addrs
end
local redis_single = require "resty.redis"
local redis_cluster
do
local pcall = pcall
local require = require
local ok
ok, redis_cluster = pcall(require, "resty.rediscluster")
if not ok then
ok, redis_cluster = pcall(require, "rediscluster")
if not ok then
redis_cluster = nil
end
end
end
local UNLOCK = [[
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
]]
local function enabled(value)
if value == nil then return nil end
return value == true or (value == "1" or value == "true" or value == "on")
end
local function ifnil(value, default)
if value == nil then
return default
end
return enabled(value)
end
local defaults = {
prefix = var.session_redis_prefix or "sessions",
socket = var.session_redis_socket,
host = var.session_redis_host or "127.0.0.1",
username = var.session_redis_username,
password = var.session_redis_password or var.session_redis_auth,
server_name = var.session_redis_server_name,
ssl = enabled(var.session_redis_ssl) or false,
ssl_verify = enabled(var.session_redis_ssl_verify) or false,
uselocking = enabled(var.session_redis_uselocking or true),
port = tonumber(var.session_redis_port, 10) or 6379,
database = tonumber(var.session_redis_database, 10) or 0,
connect_timeout = tonumber(var.session_redis_connect_timeout, 10),
read_timeout = tonumber(var.session_redis_read_timeout, 10),
send_timeout = tonumber(var.session_redis_send_timeout, 10),
spinlockwait = tonumber(var.session_redis_spinlockwait, 10) or 150,
maxlockwait = tonumber(var.session_redis_maxlockwait, 10) or 30,
pool = {
name = var.session_redis_pool_name,
timeout = tonumber(var.session_redis_pool_timeout, 10),
size = tonumber(var.session_redis_pool_size, 10),
backlog = tonumber(var.session_redis_pool_backlog, 10),
},
}
if redis_cluster then
defaults.cluster = {
name = var.session_redis_cluster_name,
dict = var.session_redis_cluster_dict,
maxredirections = tonumber(var.session_redis_cluster_maxredirections, 10),
nodes = parse_cluster_nodes(var.session_redis_cluster_nodes),
}
end
local storage = {}
storage.__index = storage
function storage.new(session)
local config = session.redis or defaults
local pool = config.pool or defaults.pool
local cluster = config.cluster or defaults.cluster
local locking = ifnil(config.uselocking, defaults.uselocking)
local self = {
prefix = config.prefix or defaults.prefix,
uselocking = locking,
spinlockwait = tonumber(config.spinlockwait, 10) or defaults.spinlockwait,
maxlockwait = tonumber(config.maxlockwait, 10) or defaults.maxlockwait,
}
local username = config.username or defaults.username
if username == "" then
username = nil
end
local password = config.password or config.auth or defaults.password
if password == "" then
password = nil
end
local connect_timeout = tonumber(config.connect_timeout, 10) or defaults.connect_timeout
local cluster_nodes
if redis_cluster then
cluster_nodes = parse_cluster_nodes(cluster.nodes or defaults.cluster.nodes)
end
local connect_opts = {
pool = pool.name or defaults.pool.name,
pool_size = tonumber(pool.size, 10) or defaults.pool.size,
backlog = tonumber(pool.backlog, 10) or defaults.pool.backlog,
server_name = config.server_name or defaults.server_name,
ssl = ifnil(config.ssl, defaults.ssl),
ssl_verify = ifnil(config.ssl_verify, defaults.ssl_verify),
}
if cluster_nodes then
self.redis = redis_cluster:new({
name = cluster.name or defaults.cluster.name,
dict_name = cluster.dict or defaults.cluster.dict,
username = var.session_redis_username,
password = var.session_redis_password or defaults.password,
connection_timout = connect_timeout, -- typo in library
connection_timeout = connect_timeout,
keepalive_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout,
keepalive_cons = tonumber(pool.size, 10) or defaults.pool.size,
max_redirection = tonumber(cluster.maxredirections, 10) or defaults.cluster.maxredirections,
serv_list = cluster_nodes,
connect_opts = connect_opts,
})
self.cluster = true
else
local redis = redis_single:new()
if redis.set_timeouts then
local send_timeout = tonumber(config.send_timeout, 10) or defaults.send_timeout
local read_timeout = tonumber(config.read_timeout, 10) or defaults.read_timeout
if connect_timeout then
if send_timeout and read_timeout then
redis:set_timeouts(connect_timeout, send_timeout, read_timeout)
else
redis:set_timeout(connect_timeout)
end
end
elseif redis.set_timeout and connect_timeout then
redis:set_timeout(connect_timeout)
end
self.redis = redis
self.username = username
self.password = password
self.database = tonumber(config.database, 10) or defaults.database
self.pool_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout
self.connect_opts = connect_opts
local socket = config.socket or defaults.socket
if socket and socket ~= "" then
self.socket = socket
else
self.host = config.host or defaults.host
self.port = config.port or defaults.port
end
end
return setmetatable(self, storage)
end
function storage:connect()
if self.cluster then
return true -- cluster handles this on its own
end
local ok, err
if self.socket then
ok, err = self.redis:connect(self.socket, self.connect_opts)
else
ok, err = self.redis:connect(self.host, self.port, self.connect_opts)
end
if not ok then
return nil, err
end
if self.password and self.redis:get_reused_times() == 0 then
-- usernames are supported only on Redis 6+, so use new AUTH form only when absolutely necessary
if self.username then
ok, err = self.redis:auth(self.username, self.password)
else
ok, err = self.redis:auth(self.password)
end
if not ok then
self.redis:close()
return nil, err
end
end
if self.database ~= 0 then
ok, err = self.redis:select(self.database)
if not ok then
self.redis:close()
end
end
return ok, err
end
function storage:set_keepalive()
if self.cluster then
return true -- cluster handles this on its own
end
return self.redis:set_keepalive(self.pool_timeout)
end
function storage:key(id)
return concat({ self.prefix, id }, ":" )
end
function storage:lock(key)
if not self.uselocking or self.locked then
return true
end
if not self.token then
self.token = var.request_id
end
local lock_key = concat({ key, "lock" }, "." )
local lock_ttl = self.maxlockwait + 1
local attempts = (1000 / self.spinlockwait) * self.maxlockwait
local waittime = self.spinlockwait / 1000
for _ = 1, attempts do
local ok = self.redis:set(lock_key, self.token, "EX", lock_ttl, "NX")
if ok ~= null then
self.locked = true
return true
end
sleep(waittime)
end
return false, "unable to acquire a session lock"
end
function storage:unlock(key)
if not self.uselocking or not self.locked then
return
end
local lock_key = concat({ key, "lock" }, "." )
self.redis:eval(UNLOCK, 1, lock_key, self.token)
self.locked = nil
end
function storage:get(key)
local data, err = self.redis:get(key)
if not data then
return nil, err
end
if data == null then
return nil
end
return data
end
function storage:set(key, data, lifetime)
return self.redis:setex(key, lifetime, data)
end
function storage:expire(key, lifetime)
return self.redis:expire(key, lifetime)
end
function storage:delete(key)
return self.redis:del(key)
end
function storage:open(id, keep_lock)
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
ok, err = self:lock(key)
if not ok then
self:set_keepalive()
return nil, err
end
local data
data, err = self:get(key)
if err or not data or not keep_lock then
self:unlock(key)
end
self:set_keepalive()
return data, err
end
function storage:start(id)
if not self.uselocking or not self.locked then
return true
end
local ok, err = self:connect()
if not ok then
return nil, err
end
ok, err = self:lock(self:key(id))
self:set_keepalive()
return ok, err
end
function storage:save(id, ttl, data, close)
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
ok, err = self:set(key, data, ttl)
if close then
self:unlock(key)
end
self:set_keepalive()
if not ok then
return nil, err
end
return true
end
function storage:close(id)
if not self.uselocking or not self.locked then
return true
end
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
self:unlock(key)
self:set_keepalive()
return true
end
function storage:destroy(id)
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
ok, err = self:delete(key)
self:unlock(key)
self:set_keepalive()
return ok, err
end
function storage:ttl(id, ttl, close)
local ok, err = self:connect()
if not ok then
return nil, err
end
local key = self:key(id)
ok, err = self:expire(key, ttl)
if close then
self:unlock(key)
end
self:set_keepalive()
return ok, err
end
return storage

View File

@@ -0,0 +1,125 @@
local lock = require "resty.lock"
local setmetatable = setmetatable
local tonumber = tonumber
local concat = table.concat
local var = ngx.var
local shared = ngx.shared
local function enabled(value)
if value == nil then return nil end
return value == true or (value == "1" or value == "true" or value == "on")
end
local function ifnil(value, default)
if value == nil then
return default
end
return enabled(value)
end
local defaults = {
store = var.session_shm_store or "sessions",
uselocking = enabled(var.session_shm_uselocking or true),
lock = {
exptime = tonumber(var.session_shm_lock_exptime, 10) or 30,
timeout = tonumber(var.session_shm_lock_timeout, 10) or 5,
step = tonumber(var.session_shm_lock_step, 10) or 0.001,
ratio = tonumber(var.session_shm_lock_ratio, 10) or 2,
max_step = tonumber(var.session_shm_lock_max_step, 10) or 0.5,
}
}
local storage = {}
storage.__index = storage
function storage.new(session)
local config = session.shm or defaults
local store = config.store or defaults.store
local locking = ifnil(config.uselocking, defaults.uselocking)
local self = {
store = shared[store],
uselocking = locking,
}
if locking then
local lock_opts = config.lock or defaults.lock
local opts = {
exptime = tonumber(lock_opts.exptime, 10) or defaults.exptime,
timeout = tonumber(lock_opts.timeout, 10) or defaults.timeout,
step = tonumber(lock_opts.step, 10) or defaults.step,
ratio = tonumber(lock_opts.ratio, 10) or defaults.ratio,
max_step = tonumber(lock_opts.max_step, 10) or defaults.max_step,
}
self.lock = lock:new(store, opts)
end
return setmetatable(self, storage)
end
function storage:open(id, keep_lock)
if self.uselocking then
local ok, err = self.lock:lock(concat{ id, ".lock" })
if not ok then
return nil, err
end
end
local data, err = self.store:get(id)
if self.uselocking and (err or not data or not keep_lock) then
self.lock:unlock()
end
return data, err
end
function storage:start(id)
if self.uselocking then
return self.lock:lock(concat{ id, ".lock" })
end
return true
end
function storage:save(id, ttl, data, close)
local ok, err = self.store:set(id, data, ttl)
if close and self.uselocking then
self.lock:unlock()
end
return ok, err
end
function storage:close()
if self.uselocking then
self.lock:unlock()
end
return true
end
function storage:destroy(id)
self.store:delete(id)
if self.uselocking then
self.lock:unlock()
end
return true
end
function storage:ttl(id, lifetime, close)
local ok, err = self.store:expire(id, lifetime)
if close and self.uselocking then
self.lock:unlock()
end
return ok, err
end
return storage

View File

@@ -0,0 +1,232 @@
local type = type
local concat = table.concat
local strategy = {}
function strategy.load(session, cookie, key, keep_lock)
local storage = session.storage
local id = cookie.id
local id_encoded = session.encoder.encode(id)
local data, err, tag
if storage.open then
data, err = storage:open(id_encoded, keep_lock)
if not data then
return nil, err or "cookie data was not found"
end
else
data = cookie.data
end
local expires = cookie.expires
local usebefore = cookie.usebefore
local hash = cookie.hash
if not key then
key = concat{ id, expires, usebefore }
end
local hkey = session.hmac(session.secret, key)
data, err, tag = session.cipher:decrypt(data, hkey, id, session.key, hash)
if not data then
if storage.close then
storage:close(id_encoded)
end
return nil, err or "unable to decrypt data"
end
if tag then
if tag ~= hash then
if storage.close then
storage:close(id_encoded)
end
return nil, "cookie has invalid tag"
end
else
local input = concat{ key, data, session.key }
if session.hmac(hkey, input) ~= hash then
if storage.close then
storage:close(id_encoded)
end
return nil, "cookie has invalid signature"
end
end
data, err = session.compressor:decompress(data)
if not data then
if storage.close then
storage:close(id_encoded)
end
return nil, err or "unable to decompress data"
end
data, err = session.serializer.deserialize(data)
if not data then
if storage.close then
storage:close(id_encoded)
end
return nil, err or "unable to deserialize data"
end
session.id = id
session.expires = expires
session.usebefore = usebefore
session.data = type(data) == "table" and data or {}
session.present = true
return true
end
function strategy.open(session, cookie, keep_lock)
return strategy.load(session, cookie, nil, keep_lock)
end
function strategy.start(session)
local storage = session.storage
if not storage.start then
return true
end
local id_encoded = session.encoder.encode(session.id)
local ok, err = storage:start(id_encoded)
if not ok then
return nil, err or "unable to start session"
end
return true
end
function strategy.modify(session, action, close, key)
local id = session.id
local id_encoded = session.encoder.encode(id)
local storage = session.storage
local expires = session.expires
local usebefore = session.usebefore
local ttl = expires - session.now
if ttl <= 0 then
if storage.close then
storage:close(id_encoded)
end
return nil, "session is already expired"
end
if not key then
key = concat{ id, expires, usebefore }
end
local data, err = session.serializer.serialize(session.data)
if not data then
if close and storage.close then
storage:close(id_encoded)
end
return nil, err or "unable to serialize data"
end
data, err = session.compressor:compress(data)
if not data then
if close and storage.close then
storage:close(id_encoded)
end
return nil, err or "unable to compress data"
end
local hkey = session.hmac(session.secret, key)
local encrypted_data, tag
encrypted_data, err, tag = session.cipher:encrypt(data, hkey, id, session.key)
if not encrypted_data then
if close and storage.close then
storage:close(id_encoded)
end
return nil, err
end
local hash
if tag then
hash = tag
else
-- it would be better to calculate signature from encrypted_data,
-- but this is kept for backward compatibility
hash = session.hmac(hkey, concat{ key, data, session.key })
end
if action == "save" and storage.save then
local ok
ok, err = storage:save(id_encoded, ttl, encrypted_data, close)
if not ok then
return nil, err
end
elseif close and storage.close then
local ok
ok, err = storage:close(id_encoded)
if not ok then
return nil, err
end
end
if usebefore then
expires = expires .. ":" .. usebefore
end
hash = session.encoder.encode(hash)
local cookie
if storage.save then
cookie = concat({ id_encoded, expires, hash }, "|")
else
local encoded_data = session.encoder.encode(encrypted_data)
cookie = concat({ id_encoded, expires, encoded_data, hash }, "|")
end
return cookie
end
function strategy.touch(session, close)
return strategy.modify(session, "touch", close)
end
function strategy.save(session, close)
return strategy.modify(session, "save", close)
end
function strategy.destroy(session)
local id = session.id
if id then
local storage = session.storage
if storage.destroy then
return storage:destroy(session.encoder.encode(id))
elseif storage.close then
return storage:close(session.encoder.encode(id))
end
end
return true
end
function strategy.close(session)
local id = session.id
if id then
local storage = session.storage
if storage.close then
return storage:close(session.encoder.encode(id))
end
end
return true
end
return strategy

View File

@@ -0,0 +1,43 @@
local default = require "resty.session.strategies.default"
local concat = table.concat
local strategy = {
regenerate = true,
start = default.start,
destroy = default.destroy,
close = default.close,
}
local function key(source)
if source.usebefore then
return concat{ source.id, source.usebefore }
end
return source.id
end
function strategy.open(session, cookie, keep_lock)
return default.load(session, cookie, key(cookie), keep_lock)
end
function strategy.touch(session, close)
return default.modify(session, "touch", close, key(session))
end
function strategy.save(session, close)
if session.present then
local storage = session.storage
if storage.ttl then
storage:ttl(session.encoder.encode(session.id), session.cookie.discard, true)
elseif storage.close then
storage:close(session.encoder.encode(session.id))
end
session.id = session:identifier()
end
return default.modify(session, "save", close, key(session))
end
return strategy

View File

@@ -0,0 +1,39 @@
package = "lua-resty-session"
version = "dev-1"
source = {
url = "git://github.com/bungle/lua-resty-session.git"
}
description = {
summary = "Session Library for OpenResty Flexible and Secure",
detailed = "lua-resty-session is a secure, and flexible session library for OpenResty.",
homepage = "https://github.com/bungle/lua-resty-session",
maintainer = "Aapo Talvensaari <aapo.talvensaari@gmail.com>",
license = "BSD"
}
dependencies = {
"lua >= 5.1"
}
build = {
type = "builtin",
modules = {
["resty.session"] = "lib/resty/session.lua",
["resty.session.identifiers.random"] = "lib/resty/session/identifiers/random.lua",
["resty.session.storage.shm"] = "lib/resty/session/storage/shm.lua",
["resty.session.storage.dshm"] = "lib/resty/session/storage/dshm.lua",
["resty.session.storage.redis"] = "lib/resty/session/storage/redis.lua",
["resty.session.storage.cookie"] = "lib/resty/session/storage/cookie.lua",
["resty.session.storage.memcache"] = "lib/resty/session/storage/memcache.lua",
["resty.session.storage.memcached"] = "lib/resty/session/storage/memcached.lua",
["resty.session.strategies.default"] = "lib/resty/session/strategies/default.lua",
["resty.session.strategies.regenerate"] = "lib/resty/session/strategies/regenerate.lua",
["resty.session.hmac.sha1"] = "lib/resty/session/hmac/sha1.lua",
["resty.session.ciphers.aes"] = "lib/resty/session/ciphers/aes.lua",
["resty.session.ciphers.none"] = "lib/resty/session/ciphers/none.lua",
["resty.session.compressors.none"] = "lib/resty/session/compressors/none.lua",
["resty.session.compressors.zlib"] = "lib/resty/session/compressors/zlib.lua",
["resty.session.encoders.hex"] = "lib/resty/session/encoders/hex.lua",
["resty.session.encoders.base16"] = "lib/resty/session/encoders/base16.lua",
["resty.session.encoders.base64"] = "lib/resty/session/encoders/base64.lua",
["resty.session.serializers.json"] = "lib/resty/session/serializers/json.lua"
}
}