From a3cd342f3e1fffd7b16b83a24e03bb9ed501b319 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Th=C3=A9ophile=20Diot?=
Date: Fri, 30 Jun 2023 15:38:54 -0400
Subject: [PATCH] Squashed 'src/deps/src/lua-resty-session/' content from
commit 8b5f8752f
git-subtree-dir: src/deps/src/lua-resty-session
git-subtree-split: 8b5f8752f3046396c414c5b97850e784c07e1641
---
.busted | 8 +
.github/workflows/integration_tests.yaml | 19 +
.github/workflows/unit_tests.yaml | 42 +
.gitignore | 1 +
.luacheckrc | 4 +
.luacov | 31 +
Changes.md | 467 +++
LICENSE | 23 +
Makefile | 16 +
README.md | 1477 +++++++++
config.ld | 10 +
dev/Dockerfile | 20 +
dist.ini | 7 +
docs/classes/resty.session.html | 906 ++++++
docs/classes/session.html | 545 ++++
docs/index.html | 127 +
docs/ldoc.css | 303 ++
docs/modules/resty.session.dshm.html | 397 +++
docs/modules/resty.session.file-thread.html | 195 ++
docs/modules/resty.session.file.html | 367 +++
docs/modules/resty.session.file.thread.html | 331 ++
docs/modules/resty.session.file.utils.html | 350 ++
docs/modules/resty.session.html | 1307 ++++++++
docs/modules/resty.session.memcached.html | 400 +++
docs/modules/resty.session.mysql.html | 480 +++
docs/modules/resty.session.postgres.html | 476 +++
docs/modules/resty.session.redis-cluster.html | 413 +++
.../modules/resty.session.redis-sentinel.html | 410 +++
docs/modules/resty.session.redis.cluster.html | 463 +++
docs/modules/resty.session.redis.common.html | 321 ++
docs/modules/resty.session.redis.html | 409 +++
.../modules/resty.session.redis.sentinel.html | 460 +++
docs/modules/resty.session.shm.html | 364 +++
docs/modules/resty.session.utils.html | 1370 ++++++++
lib/resty/session.lua | 2839 +++++++++++++++++
lib/resty/session/dshm.lua | 348 ++
lib/resty/session/file.lua | 177 +
lib/resty/session/file/thread.lua | 209 ++
lib/resty/session/file/utils.lua | 290 ++
lib/resty/session/memcached.lua | 395 +++
lib/resty/session/mysql.lua | 379 +++
lib/resty/session/postgres.lua | 354 ++
lib/resty/session/redis.lua | 279 ++
lib/resty/session/redis/cluster.lua | 271 ++
lib/resty/session/redis/common.lua | 172 +
lib/resty/session/redis/sentinel.lua | 260 ++
lib/resty/session/shm.lua | 315 ++
lib/resty/session/utils.lua | 1121 +++++++
lua-resty-session-4.0.4-1.rockspec | 37 +
spec/01-utils_spec.lua | 139 +
spec/02-file-utils_spec.lua | 108 +
spec/03-session_spec.lua | 405 +++
spec/04-storage-1_spec.lua | 223 ++
spec/05-storage-2_spec.lua | 252 ++
t/01-cookies.t | 149 +
55 files changed, 21241 insertions(+)
create mode 100644 .busted
create mode 100644 .github/workflows/integration_tests.yaml
create mode 100644 .github/workflows/unit_tests.yaml
create mode 100644 .gitignore
create mode 100644 .luacheckrc
create mode 100644 .luacov
create mode 100644 Changes.md
create mode 100644 LICENSE
create mode 100644 Makefile
create mode 100644 README.md
create mode 100644 config.ld
create mode 100644 dev/Dockerfile
create mode 100644 dist.ini
create mode 100644 docs/classes/resty.session.html
create mode 100644 docs/classes/session.html
create mode 100644 docs/index.html
create mode 100644 docs/ldoc.css
create mode 100644 docs/modules/resty.session.dshm.html
create mode 100644 docs/modules/resty.session.file-thread.html
create mode 100644 docs/modules/resty.session.file.html
create mode 100644 docs/modules/resty.session.file.thread.html
create mode 100644 docs/modules/resty.session.file.utils.html
create mode 100644 docs/modules/resty.session.html
create mode 100644 docs/modules/resty.session.memcached.html
create mode 100644 docs/modules/resty.session.mysql.html
create mode 100644 docs/modules/resty.session.postgres.html
create mode 100644 docs/modules/resty.session.redis-cluster.html
create mode 100644 docs/modules/resty.session.redis-sentinel.html
create mode 100644 docs/modules/resty.session.redis.cluster.html
create mode 100644 docs/modules/resty.session.redis.common.html
create mode 100644 docs/modules/resty.session.redis.html
create mode 100644 docs/modules/resty.session.redis.sentinel.html
create mode 100644 docs/modules/resty.session.shm.html
create mode 100644 docs/modules/resty.session.utils.html
create mode 100644 lib/resty/session.lua
create mode 100644 lib/resty/session/dshm.lua
create mode 100644 lib/resty/session/file.lua
create mode 100644 lib/resty/session/file/thread.lua
create mode 100644 lib/resty/session/file/utils.lua
create mode 100644 lib/resty/session/memcached.lua
create mode 100644 lib/resty/session/mysql.lua
create mode 100644 lib/resty/session/postgres.lua
create mode 100644 lib/resty/session/redis.lua
create mode 100644 lib/resty/session/redis/cluster.lua
create mode 100644 lib/resty/session/redis/common.lua
create mode 100644 lib/resty/session/redis/sentinel.lua
create mode 100644 lib/resty/session/shm.lua
create mode 100644 lib/resty/session/utils.lua
create mode 100644 lua-resty-session-4.0.4-1.rockspec
create mode 100644 spec/01-utils_spec.lua
create mode 100644 spec/02-file-utils_spec.lua
create mode 100644 spec/03-session_spec.lua
create mode 100644 spec/04-storage-1_spec.lua
create mode 100644 spec/05-storage-2_spec.lua
create mode 100644 t/01-cookies.t
diff --git a/.busted b/.busted
new file mode 100644
index 000000000..f8b320062
--- /dev/null
+++ b/.busted
@@ -0,0 +1,8 @@
+return {
+ default = {
+ lua = "resty --shdict 'sessions 1m' --main-conf 'thread_pool default threads=32 max_queue=65536;'",
+ verbose = true,
+ coverage = true,
+ output = "gtest",
+ }
+}
diff --git a/.github/workflows/integration_tests.yaml b/.github/workflows/integration_tests.yaml
new file mode 100644
index 000000000..b27f0c74c
--- /dev/null
+++ b/.github/workflows/integration_tests.yaml
@@ -0,0 +1,19 @@
+name: integration_tests
+on: [pull_request]
+
+jobs:
+ test:
+ strategy:
+ fail-fast: false
+ matrix:
+ luaVersion: ["luajit-openresty"]
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Setup environment
+ run: docker build dev/ -t resty-session
+
+ - name: Run tests
+ run: docker run -v $PWD:/test -w /test resty-session bash -c "luarocks make && make prove"
diff --git a/.github/workflows/unit_tests.yaml b/.github/workflows/unit_tests.yaml
new file mode 100644
index 000000000..edd6aa9c1
--- /dev/null
+++ b/.github/workflows/unit_tests.yaml
@@ -0,0 +1,42 @@
+name: unit_tests
+on: [pull_request]
+
+jobs:
+ test:
+ strategy:
+ fail-fast: false
+ matrix:
+ luaVersion: ["luajit-openresty"]
+
+ services:
+ redis:
+ image: bitnami/redis
+ env:
+ REDIS_PASSWORD: password
+ ports:
+ - 6379:6379
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ memcached:
+ image: memcached
+ ports:
+ - 11211:11211
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Setup environment
+ run: docker build dev/ -t resty-session
+
+ - name: Run tests
+ run: docker run --network=host -v $PWD:/test -w /test resty-session bash -c "luarocks make && make unit"
+
+ - name: Generate report
+ run: docker run --network=host -v $PWD:/test -w /test resty-session bash -c "luarocks make && luacov"
+
+ - name: Print report summary
+ run: docker run --network=host -v $PWD:/test -w /test resty-session sed -n '/Summary/,$p' luacov.report.out
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..d8fe4fa70
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/.project
diff --git a/.luacheckrc b/.luacheckrc
new file mode 100644
index 000000000..abad11fa5
--- /dev/null
+++ b/.luacheckrc
@@ -0,0 +1,4 @@
+std = "ngx_lua"
+redefined = false
+max_line_length = false
+files["lib/resty/session/file.lua"] = { ignore = {"143"}}
\ No newline at end of file
diff --git a/.luacov b/.luacov
new file mode 100644
index 000000000..84c7529ed
--- /dev/null
+++ b/.luacov
@@ -0,0 +1,31 @@
+return {
+
+ -- filename to store stats collected
+ ["statsfile"] = "luacov.stats.out",
+
+ -- filename to store report
+ ["reportfile"] = "luacov.report.out",
+
+ -- Run reporter on completion? (won't work for ticks)
+ runreport = true,
+
+ -- Patterns for files to include when reporting
+ -- all will be included if nothing is listed
+ -- (exclude overrules include, do not include
+ -- the .lua extension, path separator is always '/')
+ ["include"] = { 'resty/session' },
+
+ -- Patterns for files to exclude when reporting
+ -- all will be included if nothing is listed
+ -- (exclude overrules include, do not include
+ -- the .lua extension, path separator is always '/')
+ ["exclude"] = {
+ "luacov$",
+ "luacov/reporter$",
+ "luacov/defaults$",
+ "luacov/runner$",
+ "luacov/stats$",
+ "luacov/tick$",
+ },
+
+}
\ No newline at end of file
diff --git a/Changes.md b/Changes.md
new file mode 100644
index 000000000..6b7366d0b
--- /dev/null
+++ b/Changes.md
@@ -0,0 +1,467 @@
+# Changelog
+
+All notable changes to `lua-resty-session` will be documented in this file.
+
+## [4.0.4] - 2023-06-05
+### Changed
+- chore(utils): remove dependency for lua_pack, fix #158
+
+
+## [4.0.3] - 2023-02-21
+### Fixed
+- fix(*): redis authorization
+
+
+## [4.0.2] - 2023-02-15
+### Fixed
+- fix(*): hkdf is not approved by FIPS, use PBKDF2 instead on FIPS-mode
+
+
+## [4.0.1] - 2023-02-05
+### Fixed
+- fix(session): clear_request cookie to check remember_meta correctly before using it
+
+### Added
+- feat(opm): add more dependencies in requires
+- feat(opm): add right version number requirements
+- docs(readme): add remark on dependencies on installation section
+
+
+## [4.0.0] - 2023-02-01
+- Full rewrite of the library, and is not backwards compatible. Refer new
+ documentation on this new library.
+
+
+## [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.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..d6be106e9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2014 – 2023 Aapo Talvensaari, 2022 – 2023 Samuele Illuminati
+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.
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..0321e1b02
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+.PHONY: lint test docs
+
+lint:
+ @luacheck -q ./lib
+
+unit:
+ busted --exclude-tags=noci --coverage
+
+unit-all:
+ busted --coverage
+
+prove:
+ prove
+
+docs:
+ ldoc .
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..6f6b76d0b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1477 @@
+# lua-resty-session
+
+**lua-resty-session** is a secure, and flexible session library for OpenResty.
+
+## TL;DR;
+
+- Sessions are immutable (each save generates a new session), and lockless.
+- Session data is AES-256-GCM encrypted with a key derived using HKDF-SHA256.
+- Session has a fixed size header that is protected with HMAC-SHA256 MAC with
+ a key derived using HKDF-SHA256.
+- Session data can be stored in a stateless cookie or in various backend storages.
+- A single session cookie can maintain multiple sessions across different audiences.
+
+*Note:* Version 4.0.0 was a rewrite of this library with a lot of lessons learned
+during the years. If you still use older version, please refer
+[old documentation](https://github.com/bungle/lua-resty-session/tree/v3.10).
+
+
+## Status
+
+This library is considered production ready.
+
+
+## Synopsis
+
+```nginx
+worker_processes 1;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ init_by_lua_block {
+ require "resty.session".init({
+ remember = true,
+ audience = "demo",
+ secret = "RaJKp8UQW1",
+ storage = "cookie",
+ })
+ }
+
+ server {
+ listen 8080;
+ server_name localhost;
+ default_type text/html;
+
+ location / {
+ content_by_lua_block {
+ ngx.say([[
+
+
+ Start the test
+
+
+ ]])
+ }
+ }
+
+ location /start {
+ content_by_lua_block {
+ local session = require "resty.session".new()
+ session:set_subject("OpenResty Fan")
+ session:set("quote", "The quick brown fox jumps over the lazy dog")
+ local ok, err = session:save()
+
+ ngx.say(string.format([[
+
+
+ Session started (%s)
+ Check if it really was
+
+
+ ]], err or "no error"))
+ }
+ }
+
+ location /started {
+ content_by_lua_block {
+ local session, err = require "resty.session".start()
+
+ ngx.say(string.format([[
+
+
+ Session was started by %s (%s)
+
%s
+ Modify the session
+
+
+ ]],
+ session:get_subject() or "Anonymous",
+ err or "no error",
+ session:get("quote") or "no quote"
+ ))
+ }
+ }
+
+ location /modify {
+ content_by_lua_block {
+ local session, err = require "resty.session".start()
+ session:set_subject("Lua Fan")
+ session:set("quote", "Lorem ipsum dolor sit amet")
+ local _, err_save = session:save()
+
+ ngx.say(string.format([[
+
+
+ Session was modified (%s)
+ Check if it is modified
+
+
+ ]], err or err_save or "no error"))
+ }
+ }
+
+ location /modified {
+ content_by_lua_block {
+ local session, err = require "resty.session".start()
+
+ ngx.say(string.format([[
+
+
+ Session was started by %s (%s)
+
%s
+ Destroy the session
+
+
+ ]],
+ session:get_subject() or "Anonymous",
+ err or "no error",
+ session:get("quote") or "no quote"
+ ))
+ }
+ }
+
+ location /destroy {
+ content_by_lua_block {
+ local ok, err = require "resty.session".destroy()
+
+ ngx.say(string.format([[
+
+
+ Session was destroyed (%s)
+ Check that it really was?
+
+
+ ]], err or "no error"))
+ }
+ }
+
+ location /destroyed {
+ content_by_lua_block {
+ local session, err = require "resty.session".open()
+
+ ngx.say(string.format([[
+
+
+ Session was really destroyed, you are known as %s (%s)
+ Start again
+
+
+ ]],
+ session:get_subject() or "Anonymous",
+ err or "no error"
+ ))
+ }
+ }
+ }
+}
+```
+
+
+# Table of Contents
+
+* [Installation](#installation)
+ * [Using OpenResty Package Manager (opm)](#using-openresty-package-manager-opm)
+ * [Using LuaRocks](#using-luarocks)
+* [Configuration](#configuration)
+ * [Session Configuration](#session-configuration)
+ * [Cookie Storage Configuration](#cookie-storage-configuration)
+ * [DSHM Storage Configuration](#dshm-storage-configuration)
+ * [File Storage Configuration](#file-storage-configuration)
+ * [Memcached Storage Configuration](#memcached-storage-configuration)
+ * [MySQL / MariaDB Storage Configuration](#mysql--mariadb-storage-configuration)
+ * [Postgres Configuration](#postgres-configuration)
+ * [Redis Configuration](#redis-configuration)
+ * [Single Redis Configuration](#single-redis-configuration)
+ * [Redis Sentinels Configuration](#redis-sentinels-configuration)
+ * [Redis Cluster Configuration](#redis-cluster-configuration)
+ * [SHM Configuration](#shm-configuration)
+* [API](#api)
+ * [Initialization](#initialization)
+ * [session.init](#sessioninit)
+ * [Constructors](#constructors)
+ * [session.new](#sessionnew)
+ * [Helpers](#helpers)
+ * [session.open](#sessionopen)
+ * [session.start](#sessionstart)
+ * [session.logout](#sessionlogout)
+ * [session.destroy](#sessiondestroy)
+ * [Instance Methods](#instance-methods)
+ * [session:open](#sessionopen-1)
+ * [session:save](#sessionsave)
+ * [session:touch](#sessiontouch)
+ * [session:refresh](#sessionrefresh)
+ * [session:logout](#sessionlogout-1)
+ * [session:destroy](#sessiondestroy-1)
+ * [session:close](#sessionclose)
+ * [session:set_data](#sessionset_data)
+ * [session:get_data](#sessionget_data)
+ * [session:set](#sessionset)
+ * [session:get](#sessionget)
+ * [session:set_audience](#sessionset_audience)
+ * [session:get_audience](#sessionget_audience)
+ * [session:set_subject](#sessionset_subject)
+ * [session:get_subject](#sessionget_subject)
+ * [session:get_property](#sessionget_property)
+ * [session:set_remember](#sessionset_remember)
+ * [session:get_remember](#sessionget_remember)
+ * [session:clear_request_cookie](#sessionclear_request_cookie)
+ * [session:set_headers](#sessionset_headers)
+ * [session:set_request_headers](#sessionset_request_headers)
+ * [session:set_response_headers](#sessionset_response_headers)
+ * [session.info:set](#sessioninfoset)
+ * [session.info:get](#sessioninfoget)
+ * [session.info:save](#sessioninfosave)
+* [Cookie Format](#cookie-format)
+* [Data Encryption](#data-encryption)
+* [Cookie Header Authentication](#cookie-header-authentication)
+* [Custom Storage Interface](#custom-storage-interface)
+* [License](#license)
+
+
+# Installation
+
+## Using OpenResty Package Manager (opm)
+
+```bash
+❯ opm get bungle/lua-resty-session
+```
+
+OPM repository for `lua-resty-session` is located at https://opm.openresty.org/package/bungle/lua-resty-session/.
+
+Also check the dependencies for each storage (there may be additional dependencies).
+
+## Using LuaRocks
+
+```bash
+❯ luarocks install lua-resty-session
+```
+
+LuaRocks repository for `lua-resty-session` is located at https://luarocks.org/modules/bungle/lua-resty-session.
+
+Also check the dependencies for each storage (there may be additional dependencies).
+
+
+# Configuration
+
+The configuration can be divided to generic session configuration and the server
+side storage configuration.
+
+Here is an example:
+
+```lua
+init_by_lua_block {
+ require "resty.session".init({
+ remember = true,
+ store_metadata = true,
+ secret = "RaJKp8UQW1",
+ secret_fallbacks = {
+ "X88FuG1AkY",
+ "fxWNymIpbb",
+ },
+ storage = "postgres",
+ postgres = {
+ username = "my-service",
+ password = "kVgIXCE5Hg",
+ database = "sessions",
+ },
+ })
+}
+```
+
+
+## Session Configuration
+
+Session configuration can be passed to [initialization](#initialization), [constructor](#constructors),
+and [helper](#helpers) functions.
+
+Here are the possible session configuration options:
+
+| Option | Default | Description |
+|-----------------------------|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `secret` | `nil` | Secret used for the key derivation. The secret is hashed with SHA-256 before using it. E.g. `"RaJKp8UQW1"`. |
+| `secret_fallbacks` | `nil` | Array of secrets that can be used as alternative secrets (when doing key rotation), E.g. `{ "6RfrAYYzYq", "MkbTkkyF9C" }`. |
+| `ikm` | (random) | Initial keying material (or ikm) can be specified directly (without using a secret) with exactly 32 bytes of data. E.g. `"5ixIW4QVMk0dPtoIhn41Eh1I9enP2060"` |
+| `ikm_fallbacks` | `nil` | Array of initial keying materials that can be used as alternative keys (when doing key rotation), E.g. `{ "QvPtlPKxOKdP5MCu1oI3lOEXIVuDckp7" }`. |
+| `cookie_prefix` | `nil` | Cookie prefix, use `nil`, `"__Host-"` or `"__Secure-"`. |
+| `cookie_name` | `"session"` | Session cookie name, e.g. `"session"`. |
+| `cookie_path` | `"/"` | Cookie path, e.g. `"/"`. |
+| `cookie_http_only` | `true` | Mark cookie HTTP only, use `true` or `false`. |
+| `cookie_secure` | `nil` | Mark cookie secure, use `nil`, `true` or `false`. |
+| `cookie_priority` | `nil` | Cookie priority, use `nil`, `"Low"`, `"Medium"`, or `"High"`. |
+| `cookie_same_site` | `"Lax"` | Cookie same-site policy, use `nil`, `"Lax"`, `"Strict"`, `"None"`, or `"Default"` |
+| `cookie_same_party` | `nil` | Mark cookie with same party flag, use `nil`, `true`, or `false`. |
+| `cookie_partitioned` | `nil` | Mark cookie with partitioned flag, use `nil`, `true`, or `false`. |
+| `remember` | `false` | Enable or disable persistent sessions, use `nil`, `true`, or `false`. |
+| `remember_safety` | `"Medium"` | Remember cookie key derivation complexity, use `nil`, `"None"` (fast), `"Low"`, `"Medium"`, `"High"` or `"Very High"` (slow). |
+| `remember_cookie_name` | `"remember"` | Persistent session cookie name, e.g. `"remember"`. |
+| `audience` | `"default"` | Session audience, e.g. `"my-application"`. |
+| `subject` | `nil` | Session subject, e.g. `"john.doe@example.com"`. |
+| `enforce_same_subject` | `false` | When set to `true`, audiences need to share the same subject. The library removes non-subject matching audience data on save. |
+| `stale_ttl` | `10` | When session is saved a new session is created, stale ttl specifies how long the old one can still be used, e.g. `10` (in seconds). |
+| `idling_timeout` | `900` | Idling timeout specifies how long the session can be inactive until it is considered invalid, e.g. `900` (15 minutes) (in seconds), `0` disables the checks and touching. |
+| `rolling_timeout` | `3600` | Rolling timeout specifies how long the session can be used until it needs to be renewed, e.g. `3600` (an hour) (in seconds), `0` disables the checks and rolling. |
+| `absolute_timeout` | `86400` | Absolute timeout limits how long the session can be renewed, until re-authentication is required, e.g. `86400` (a day) (in seconds), `0` disables the checks. |
+| `remember_rolling_timeout` | `604800` | Remember timeout specifies how long the persistent session is considered valid, e.g. `604800` (a week) (in seconds), `0` disables the checks and rolling. |
+| `remember_absolute_timeout` | `2592000` | Remember absolute timeout limits how long the persistent session can be renewed, until re-authentication is required, e.g. `2592000` (30 days) (in seconds), `0` disables the checks. |
+| `hash_storage_key` | `false` | Whether to hash or not the storage key. With storage key hashed it is impossible to decrypt data on server side without having a cookie too, use `nil`, `true` or `false`. |
+| `hash_subject` | `false` | Whether to hash or not the subject when `store_metadata` is enabled, e.g. for PII reasons. |
+| `store_metadata` | `false` | Whether to also store metadata of sessions, such as collecting data of sessions for a specific audience belonging to a specific subject. |
+| `touch_threshold` | `60` | Touch threshold controls how frequently or infrequently the `session:refresh` touches the cookie, e.g. `60` (a minute) (in seconds) |
+| `compression_threshold` | `1024` | Compression threshold controls when the data is deflated, e.g. `1024` (a kilobyte) (in bytes), `0` disables compression. |
+| `request_headers` | `nil` | Set of headers to send to upstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` request headers when `set_headers` is called. |
+| `response_headers` | `nil` | Set of headers to send to downstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` response headers when `set_headers` is called. |
+| `storage` | `nil` | Storage is responsible of storing session data, use `nil` or `"cookie"` (data is stored in cookie), `"dshm"`, `"file"`, `"memcached"`, `"mysql"`, `"postgres"`, `"redis"`, or `"shm"`, or give a name of custom module (`"custom-storage"`), or a `table` that implements session storage interface. |
+| `dshm` | `nil` | Configuration for dshm storage, e.g. `{ prefix = "sessions" }` (see below) |
+| `file` | `nil` | Configuration for file storage, e.g. `{ path = "/tmp", suffix = "session" }` (see below) |
+| `memcached` | `nil` | Configuration for memcached storage, e.g. `{ prefix = "sessions" }` (see below) |
+| `mysql` | `nil` | Configuration for MySQL / MariaDB storage, e.g. `{ database = "sessions" }` (see below) |
+| `postgres` | `nil` | Configuration for Postgres storage, e.g. `{ database = "sessions" }` (see below) |
+| `redis` | `nil` | Configuration for Redis / Redis Sentinel / Redis Cluster storages, e.g. `{ prefix = "sessions" }` (see below) |
+| `shm` | `nil` | Configuration for shared memory storage, e.g. `{ zone = "sessions" }` |
+| `["custom-storage"]` | `nil` | custom storage (loaded with `require "custom-storage"`) configuration. |
+
+
+## Cookie Storage Configuration
+
+When storing data to cookie, there is no additional configuration required,
+just set the `storage` to `nil` or `"cookie"`.
+
+
+## DSHM Storage Configuration
+
+With DHSM storage you can use the following settings (set the `storage` to `"dshm"`):
+
+| Option | Default | Description |
+|---------------------|:-------------:|----------------------------------------------------------------------------------------------|
+| `prefix` | `nil` | The Prefix for the keys stored in DSHM. |
+| `suffix` | `nil` | The suffix for the keys stored in DSHM. |
+| `host` | `"127.0.0.1"` | The host to connect. |
+| `port` | `4321` | The port to connect. |
+| `connect_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `connect` method. |
+| `send_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `send` method. |
+| `read_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `receive` method. |
+| `keepalive_timeout` | `nil` | Controls the default maximal idle time of the connections in the connection pool. |
+| `pool` | `nil` | A custom name for the connection pool being used. |
+| `pool_size` | `nil` | The size of the connection pool. |
+| `backlog` | `nil` | A queue size to use when the connection pool is full (configured with pool_size). |
+| `ssl` | `nil` | Enable SSL. |
+| `ssl_verify` | `nil` | Verify server certificate. |
+| `server_name` | `nil` | The server name for the new TLS extension Server Name Indication (SNI). |
+
+Please refer to [ngx-distributed-shm](https://github.com/grrolland/ngx-distributed-shm) to get necessary
+dependencies installed.
+
+
+## File Storage Configuration
+
+With file storage you can use the following settings (set the `storage` to `"file"`):
+
+| Option | Default | Description |
+|---------------------|:---------------:|-------------------------------------------------------------------------------------|
+| `prefix` | `nil` | File prefix for session file. |
+| `suffix` | `nil` | File suffix (or extension without `.`) for session file. |
+| `pool` | `nil` | Name of the thread pool under which file writing happens (available on Linux only). |
+| `path` | (tmp directory) | Path (or directory) under which session files are created. |
+
+
+The implementation requires `LuaFileSystem` which you can install with LuaRocks:
+```sh
+❯ luarocks install LuaFileSystem
+```
+
+
+## Memcached Storage Configuration
+
+With file Memcached you can use the following settings (set the `storage` to `"memcached"`):
+
+| Option | Default | Description |
+|---------------------|:-----------:|----------------------------------------------------------------------------------------------|
+| `prefix` | `nil` | Prefix for the keys stored in memcached. |
+| `suffix` | `nil` | Suffix for the keys stored in memcached. |
+| `host` | `127.0.0.1` | The host to connect. |
+| `port` | `11211` | The port to connect. |
+| `socket` | `nil` | The socket file to connect to. |
+| `connect_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `connect` method. |
+| `send_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `send` method. |
+| `read_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `receive` method. |
+| `keepalive_timeout` | `nil` | Controls the default maximal idle time of the connections in the connection pool. |
+| `pool` | `nil` | A custom name for the connection pool being used. |
+| `pool_size` | `nil` | The size of the connection pool. |
+| `backlog` | `nil` | A queue size to use when the connection pool is full (configured with pool_size). |
+| `ssl` | `false` | Enable SSL |
+| `ssl_verify` | `nil` | Verify server certificate |
+| `server_name` | `nil` | The server name for the new TLS extension Server Name Indication (SNI). |
+
+
+## MySQL / MariaDB Storage Configuration
+
+With file MySQL / MariaDB you can use the following settings (set the `storage` to `"mysql"`):
+
+| Option | Default | Description |
+|---------------------|:-----------------:|----------------------------------------------------------------------------------------------|
+| `host` | `"127.0.0.1"` | The host to connect. |
+| `port` | `3306` | The port to connect. |
+| `socket` | `nil` | The socket file to connect to. |
+| `username` | `nil` | The database username to authenticate. |
+| `password` | `nil` | Password for authentication, may be required depending on server configuration. |
+| `charset` | `"ascii"` | The character set used on the MySQL connection. |
+| `database` | `nil` | The database name to connect. |
+| `table_name` | `"sessions"` | Name of database table to which to store session data. |
+| `table_name_meta` | `"sessions_meta"` | Name of database meta data table to which to store session meta data. |
+| `max_packet_size` | `1048576` | The upper limit for the reply packets sent from the MySQL server (in bytes). |
+| `connect_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `connect` method. |
+| `send_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `send` method. |
+| `read_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `receive` method. |
+| `keepalive_timeout` | `nil` | Controls the default maximal idle time of the connections in the connection pool. |
+| `pool` | `nil` | A custom name for the connection pool being used. |
+| `pool_size` | `nil` | The size of the connection pool. |
+| `backlog` | `nil` | A queue size to use when the connection pool is full (configured with pool_size). |
+| `ssl` | `false` | Enable SSL. |
+| `ssl_verify` | `nil` | Verify server certificate. |
+
+You also need to create following tables in your database:
+
+```sql
+--
+-- Database table that stores session data.
+--
+CREATE TABLE IF NOT EXISTS sessions (
+ sid CHAR(43) PRIMARY KEY,
+ name VARCHAR(255),
+ data MEDIUMTEXT,
+ exp DATETIME,
+ INDEX (exp)
+) CHARACTER SET ascii;
+
+--
+-- Sessions metadata table.
+--
+-- This is only needed if you want to store session metadata.
+--
+CREATE TABLE IF NOT EXISTS sessions_meta (
+ aud VARCHAR(255),
+ sub VARCHAR(255),
+ sid CHAR(43),
+ PRIMARY KEY (aud, sub, sid),
+ CONSTRAINT FOREIGN KEY (sid) REFERENCES sessions(sid) ON DELETE CASCADE ON UPDATE CASCADE
+) CHARACTER SET ascii;
+```
+
+
+## Postgres Configuration
+
+With file Postgres you can use the following settings (set the `storage` to `"postgres"`):
+
+| Option | Default | Description |
+|---------------------|:-----------------:|-----------------------------------------------------------------------------------------------------------|
+| `host` | `"127.0.0.1"` | The host to connect. |
+| `port` | `5432` | The port to connect. |
+| `application` | `5432` | Set the name of the connection as displayed in pg_stat_activity (defaults to `"pgmoon"`). |
+| `username` | `"postgres"` | The database username to authenticate. |
+| `password` | `nil` | Password for authentication, may be required depending on server configuration. |
+| `database` | `nil` | The database name to connect. |
+| `table_name` | `"sessions"` | Name of database table to which to store session data (can be `database schema` prefixed). |
+| `table_name_meta` | `"sessions_meta"` | Name of database meta data table to which to store session meta data (can be `database schema` prefixed). |
+| `connect_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `connect` method. |
+| `send_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `send` method. |
+| `read_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `receive` method. |
+| `keepalive_timeout` | `nil` | Controls the default maximal idle time of the connections in the connection pool. |
+| `pool` | `nil` | A custom name for the connection pool being used. |
+| `pool_size` | `nil` | The size of the connection pool. |
+| `backlog` | `nil` | A queue size to use when the connection pool is full (configured with pool_size). |
+| `ssl` | `false` | Enable SSL. |
+| `ssl_verify` | `nil` | Verify server certificate. |
+| `ssl_required` | `nil` | Abort the connection if the server does not support SSL connections. |
+
+You also need to create following tables in your database:
+
+```sql
+--
+-- Database table that stores session data.
+--
+CREATE TABLE IF NOT EXISTS sessions (
+ sid TEXT PRIMARY KEY,
+ name TEXT,
+ data TEXT,
+ exp TIMESTAMP WITH TIME ZONE
+);
+CREATE INDEX ON sessions (exp);
+
+--
+-- Sessions metadata table.
+--
+-- This is only needed if you want to store session metadata.
+--
+CREATE TABLE IF NOT EXISTS sessions_meta (
+ aud TEXT,
+ sub TEXT,
+ sid TEXT REFERENCES sessions (sid) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY (aud, sub, sid)
+);
+```
+
+The implementation requires `pgmoon` which you can install with LuaRocks:
+```sh
+❯ luarocks install pgmoon
+```
+
+
+## Redis Configuration
+
+The session library supports single Redis, Redis Sentinel, and Redis Cluster
+connections. Common configuration settings among them all:
+
+| Option | Default | Description |
+|---------------------|:-------:|----------------------------------------------------------------------------------------------|
+| `prefix` | `nil` | Prefix for the keys stored in Redis. |
+| `suffix` | `nil` | Suffix for the keys stored in Redis. |
+| `username` | `nil` | The database username to authenticate. |
+| `password` | `nil` | Password for authentication. |
+| `connect_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `connect` method. |
+| `send_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `send` method. |
+| `read_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `receive` method. |
+| `keepalive_timeout` | `nil` | Controls the default maximal idle time of the connections in the connection pool. |
+| `pool` | `nil` | A custom name for the connection pool being used. |
+| `pool_size` | `nil` | The size of the connection pool. |
+| `backlog` | `nil` | A queue size to use when the connection pool is full (configured with pool_size). |
+| `ssl` | `false` | Enable SSL |
+| `ssl_verify` | `nil` | Verify server certificate |
+| `server_name` | `nil` | The server name for the new TLS extension Server Name Indication (SNI). |
+
+The `single redis` implementation is selected when you don't pass either `sentinels` or `nodes`,
+which would lead to selecting `sentinel` or `cluster` implementation.
+
+### Single Redis Configuration
+
+Single Redis has following additional configuration options (set the `storage` to `"redis"`):
+
+| Option | Default | Description |
+|-------------|:---------------:|--------------------------------|
+| `host` | `"127.0.0.1"` | The host to connect. |
+| `port` | `6379` | The port to connect. |
+| `socket` | `nil` | The socket file to connect to. |
+| `database` | `nil` | The database to connect. |
+
+
+### Redis Sentinels Configuration
+
+Redis Sentinel has following additional configuration options (set the `storage` to `"redis"`
+and configure the `sentinels`):
+
+| Option | Default | Description |
+|---------------------|:--------:|--------------------------------|
+| `master` | `nil` | Name of master. |
+| `role` | `nil` | `"master"` or `"slave"`. |
+| `socket` | `nil` | The socket file to connect to. |
+| `sentinels` | `nil` | Redis Sentinels. |
+| `sentinel_username` | `nil` | Optional sentinel username. |
+| `sentinel_password` | `nil` | Optional sentinel password. |
+| `database` | `nil` | The database to connect. |
+
+The `sentinels` is an array of Sentinel records:
+
+| Option | Default | Description |
+|--------|:-------:|----------------------|
+| `host` | `nil` | The host to connect. |
+| `port` | `nil` | The port to connect. |
+
+The `sentinel` implementation is selected when you pass `sentinels` as part of `redis`
+configuration (and do not pass `nodes`, which would select `cluster` implementation).
+
+The implementation requires `lua-resty-redis-connector` which you can install with LuaRocks:
+```sh
+❯ luarocks install lua-resty-redis-connector
+```
+
+
+### Redis Cluster Configuration
+
+Redis Cluster has following additional configuration options (set the `storage` to `"redis"`
+and configure the `nodes`):
+
+| Option | Default | Description |
+|---------------------------|:-------:|--------------------------------------------------------|
+| `name` | `nil` | Redis cluster name. |
+| `nodes` | `nil` | Redis cluster nodes. |
+| `lock_zone` | `nil` | Shared dictionary name for locks. |
+| `lock_prefix` | `nil` | Shared dictionary name prefix for lock. |
+| `max_redirections` | `nil` | Maximum retry attempts for redirection. |
+| `max_connection_attempts` | `nil` | Maximum retry attempts for connection. |
+| `max_connection_timeout` | `nil` | Maximum connection timeout in total among the retries. |
+
+The `nodes` is an array of Cluster node records:
+
+| Option | Default | Description |
+|--------|:-------------:|----------------------------|
+| `ip` | `"127.0.0.1"` | The IP address to connect. |
+| `port` | `6379` | The port to connect. |
+
+The `cluster` implementation is selected when you pass `nodes` as part of `redis`
+configuration.
+
+For `cluster` to work properly, you need to configure `lock_zone`, so also add this
+to your Nginx configuration:
+
+```nginx
+lua_shared_dict redis_cluster_locks 100k;
+```
+
+And set the `lock_zone` to `"redis_cluster_locks"`
+
+The implementation requires `resty-redis-cluster` or `kong-redis-cluster` which you can install with LuaRocks:
+```sh
+❯ luarocks install resty-redis-cluster
+# or
+❯ luarocks install kong-redis-cluster
+```
+
+
+## SHM Configuration
+
+With SHM storage you can use the following settings (set the `storage` to `"shm"`):
+
+| Option | Default | Description |
+|----------|:------------:|------------------------------------|
+| `prefix` | `nil` | Prefix for the keys stored in SHM. |
+| `suffix` | `nil` | Suffix for the keys stored in SHM. |
+| `zone` | `"sessions"` | A name of shared memory zone. |
+
+You will also need to create a shared dictionary `zone` in Nginx:
+
+```nginx
+lua_shared_dict sessions 10m;
+```
+
+*Note:* you may need to adjust the size of shared memory zone according to your needs.
+
+
+# API
+
+LDoc generated API docs can also be viewed at [bungle.github.io/lua-resty-session](https://bungle.github.io/lua-resty-session/).
+
+
+## Initialization
+
+### session.init
+
+**syntax:** *session.init(configuration)*
+
+Initialize the session library.
+
+This function can be called on `init` or `init_worker` phases on OpenResty
+to set global default configuration to all session instances created by this
+library.
+
+```lua
+require "resty.session".init({
+ audience = "my-application",
+ storage = "redis",
+ redis = {
+ username = "session",
+ password = "storage",
+ },
+})
+```
+
+See [configuration](#configuration) for possible configuration settings.
+
+
+## Constructors
+
+### session.new
+
+**syntax:** *session = session.new(configuration)*
+
+Creates a new session instance.
+
+```lua
+local session = require "resty.session".new()
+-- OR
+local session = require "resty.session".new({
+ audience = "my-application",
+})
+```
+
+See [configuration](#configuration) for possible configuration settings.
+
+
+## Helpers
+
+### session.open
+
+**syntax:** *session, err, exists = session.open(configuration)*
+
+This can be used to open a session, and it will either return an existing
+session or a new session. The `exists` (a boolean) return parameters tells whether
+it was existing or new session that was returned. The `err` (a string) contains
+a message of why opening might have failed (the function will still return
+`session` too).
+
+```lua
+local session = require "resty.session".open()
+-- OR
+local session, err, exists = require "resty.session".open({
+ audience = "my-application",
+})
+```
+
+See [configuration](#configuration) for possible configuration settings.
+
+
+### session.start
+
+**syntax:** *session, err, exists, refreshed = session.start(configuration)*
+
+This can be used to start a session, and it will either return an existing
+session or a new session. In case there is an existing session, the
+session will be refreshed as well (as needed). The `exists` (a boolean)
+return parameters tells whether it was existing or new session that was
+returned. The `refreshed` (a boolean) tells whether the call to `refresh`
+was succesful. The `err` (a string) contains a message of why opening or
+refreshing might have failed (the function will still return `session` too).
+
+```lua
+local session = require "resty.session".start()
+-- OR
+local session, err, exists, refreshed = require "resty.session".start({
+ audience = "my-application",
+})
+```
+
+See [configuration](#configuration) for possible configuration settings.
+
+
+### session.logout
+
+**syntax:** *ok, err, exists, logged_out = session.logout(configuration)*
+
+It logouts from a specific audience.
+
+A single session cookie may be shared between multiple audiences
+(or applications), thus there is a need to be able to logout from
+just a single audience while keeping the session for the other
+audiences. The `exists` (a boolean) return parameters tells whether
+session existed. The `logged_out` (a boolean) return parameter signals
+if the session existed and was also logged out. The `err` (a string)
+contains a reason why session didn't exists or why the logout failed.
+The `ok` (truthy) will be `true` when session existed and was
+successfully logged out.
+
+When there is only a single audience, then this can be considered
+equal to `session.destroy`.
+
+When the last audience is logged out, the cookie will be destroyed
+as well and invalidated on a client.
+
+```lua
+require "resty.session".logout()
+-- OR
+local ok, err, exists, logged_out = require "resty.session".logout({
+ audience = "my-application",
+})
+```
+
+
+See [configuration](#configuration) for possible configuration settings.
+
+
+### session.destroy
+
+**syntax:** *ok, err, exists, destroyed = session.destroy(configuration)*
+
+It destroys the whole session and clears the cookies.
+
+A single session cookie may be shared between multiple audiences
+(or applications), thus there is a need to be able to logout from
+just a single audience while keeping the session for the other
+audiences. The `exists` (a boolean) return parameters tells whether
+session existed. The `destroyed` (a boolean) return parameter signals
+if the session existed and was also destroyed out. The `err` (a string)
+contains a reason why session didn't exists or why the logout failed.
+The `ok` (truthy) will be `true` when session existed and was
+successfully logged out.
+
+```lua
+require "resty.session".destroy()
+-- OR
+local ok, err, exists, destroyed = require "resty.session".destroy({
+ cookie_name = "auth",
+})
+```
+
+See [configuration](#configuration) for possible configuration settings.
+
+
+## Instance Methods
+
+### session:open
+
+**syntax:** *ok, err = session:open()*
+
+This can be used to open a session. It returns `true` when
+session was opened and validated. Otherwise, it returns `nil` and
+an error message.
+
+```lua
+local session = require "resty.session".new()
+local ok, err = session:open()
+if ok then
+ -- session exists
+
+else
+ -- session did not exists or was invalid
+end
+```
+
+
+### session:save
+
+**syntax:** *ok, err = session:save()*
+
+Saves the session data and issues a new session cookie with a new session id.
+When `remember` is enabled, it will also issue a new persistent cookie and
+possibly save the data in backend store. It returns `true` when session was saved.
+Otherwise, it returns `nil` and an error message.
+
+```lua
+local session = require "resty.session".new()
+session:set_subject("john")
+local ok, err = session:save()
+if not ok then
+ -- error when saving session
+end
+```
+
+
+### session:touch
+
+**syntax:** *ok, err = session:touch()*
+
+Updates idling offset of the session by sending an updated session cookie.
+It only sends the client cookie and never calls any backend session store
+APIs. Normally the `session:refresh` is used to call this indirectly. In
+error case it returns `nil` and an error message, otherwise `true`.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ ok, err = session:touch()
+end
+```
+
+
+### session:refresh
+
+**syntax:** *ok, err = session:refresh()*
+
+Either saves the session (creating a new session id) or touches the session
+depending on whether the rolling timeout is getting closer, which means
+by default when 3/4 of rolling timeout is spent, that is 45 minutes with default
+rolling timeout of an hour. The touch has a threshold, by default one minute,
+so it may be skipped in some cases (you can call `session:touch()` to force it).
+In error case it returns `nil` and an error message, otherwise `true`.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ local ok, err = session:refresh()
+end
+```
+
+The above code looks a bit like `session.start()` helper.
+
+
+### session:logout
+
+**syntax:** *ok, err = session:logout()*
+
+Logout either destroys the session or just clears the data for the current audience,
+and saves it (logging out from the current audience). In error case it returns `nil`
+and an error message, otherwise `true`.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ local ok, err = session:logout()
+end
+```
+
+
+### session:destroy
+
+**syntax:** *ok, err = session:destroy()*
+
+Destroy the session and clear the cookies. In error case it returns `nil`
+and an error message, otherwise `true`.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ local ok, err = session:destroy()
+end
+```
+
+
+### session:close
+
+**syntax:** *session:close()*
+
+Just closes the session instance so that it cannot be used anymore.
+
+```lua
+local session = require "resty.session".new()
+session:set_subject("john")
+local ok, err = session:save()
+if not ok then
+ -- error when saving session
+end
+session:close()
+```
+
+
+### session:set_data
+
+**syntax:** *session:set_data(data)*
+
+Set session data. The `data` needs to be a `table`.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if not exists then
+ session:set_data({
+ cart = {},
+ })
+ session:save()
+end
+```
+
+
+### session:get_data
+
+**syntax:** *data = session:get_data()*
+
+Get session data.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ local data = session:get_data()
+ ngx.req.set_header("Authorization", "Bearer " .. data.access_token)
+end
+```
+
+
+### session:set
+
+**syntax:** *session:set(key, value)*
+
+Set a value in session.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if not exists then
+ session:set("access-token", "eyJ...")
+ session:save()
+end
+```
+
+
+### session:get
+
+**syntax:** *value = session:get(key)*
+
+Get a value from session.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ local access_token = session:get("access-token")
+ ngx.req.set_header("Authorization", "Bearer " .. access_token)
+end
+```
+
+### session:set_audience
+
+**syntax:** *session:set_audience(audience)*
+
+Set session audience.
+
+```lua
+local session = require "resty.session".new()
+session.set_audience("my-service")
+```
+
+
+### session:get_audience
+
+**syntax:** *audience = session:get_audience()*
+
+Set session subject.
+
+
+### session:set_subject
+
+**syntax:** *session:set_subject(subject)*
+
+Set session audience.
+
+```lua
+local session = require "resty.session".new()
+session.set_subject("john@doe.com")
+```
+
+
+### session:get_subject
+
+**syntax:** *subject = session:get_subject()*
+
+Get session subject.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ local subject = session.get_subject()
+end
+```
+
+
+### session:get_property
+
+**syntax:** *value = session:get_property(name)*
+
+Get session property. Possible property names:
+
+- `"id"`: 43 bytes session id (same as nonce, but base64 url-encoded)
+- `"nonce"`: 32 bytes nonce (same as session id but in raw bytes)
+- `"audience"`: Current session audience
+- `"subject"`: Current session subject
+- `"timeout"`: Closest timeout (in seconds) (what's left of it)
+- `"idling-timeout`"`: Session idling timeout (in seconds) (what's left of it)
+- `"rolling-timeout`"`: Session rolling timeout (in seconds) (what's left of it)
+- `"absolute-timeout`"`: Session absolute timeout (in seconds) (what's left of it)
+
+*Note:* the returned value may be `nil`.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ local timeout = session.get_property("timeout")
+end
+```
+
+
+### session:set_remember
+
+**syntax:** *session:set_remember(value)*
+
+Set persistent sessions on/off.
+
+In many login forms user is given an option for "remember me".
+You can call this function based on what user selected.
+
+```lua
+local session = require "resty.session".new()
+if ngx.var.args.remember then
+ session:set_remember(true)
+end
+session:set_subject(ngx.var.args.username)
+session:save()
+```
+
+
+### session:get_remember
+
+**syntax:** *remember = session:get_remember()*
+
+Get state of persistent sessions.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ local remember = session.get_remember()
+end
+```
+
+
+### session:clear_request_cookie
+
+**syntax:** *session:clear_request_cookie()*
+
+Modifies the request headers by removing the session related
+cookies. This is useful when you use the session library on
+a proxy server and don't want the session cookies to be forwarded
+to the upstream service.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ session:clear_request_cookie()
+end
+```
+
+
+### session:set_headers
+
+**syntax:** *session:set_headers(arg1, arg2, ...)*
+
+Sets request and response headers based on configuration.
+
+```lua
+local session, err, exists = require "resty.session".open({
+ request_headers = { "audience", "subject", "id" },
+ response_headers = { "timeout", "idling-timeout", "rolling-timeout", "absolute-timeout" },
+})
+if exists then
+ session:set_headers()
+end
+```
+
+When called without arguments it will set request headers configured with `request_headers`
+and response headers configured with `response_headers`.
+
+See [configuration](#configuration) for possible header names.
+
+
+### session:set_request_headers
+
+**syntax:** *session:set_request_headers(arg1, arg2, ...)*
+
+Set request headers.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ session:set_request_headers("audience", "subject", "id")
+end
+```
+
+When called without arguments it will set request headers configured with `request_headers`.
+
+See [configuration](#configuration) for possible header names.
+
+
+### session:set_response_headers
+
+**syntax:** *session:set_response_headers(arg1, arg2, ...)*
+
+Set request headers.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ session:set_response_headers("timeout", "idling-timeout", "rolling-timeout", "absolute-timeout")
+end
+```
+
+When called without arguments it will set request headers configured with `response_headers`.
+
+See [configuration](#configuration) for possible header names.
+
+
+### session.info:set
+
+**syntax:** *session.info:set(key, value)*
+
+Set a value in session information store. Session information store
+may be used in scenarios when you want to store data on server side
+storage, but do not want to create a new session and send a new
+session cookie. The information store data is not considered when
+checking authentication tag or message authentication code. Thus if
+you want to use this for data that needs to be encrypted, you need
+to encrypt value before passing it to thus function.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ session.info:set("last-access", ngx.now())
+ session.info:save()
+end
+```
+
+With cookie storage this still works, but it is then almost the same as
+`session:set`.
+
+
+### session.info:get
+
+**syntax:** *value = session.info:get(key)*
+
+Get a value from session information store.
+
+```lua
+local session, err, exists = require "resty.session".open()
+if exists then
+ local last_access = session.info:get("last-access")
+end
+```
+
+
+### session.info:save
+
+**syntax:** *value = session.info:save()*
+
+Save information. Only updates backend storage. Does not send a new cookie (except with cookie storage).
+
+```lua
+local session = require "resty.session".new()
+session.info:set("last-access", ngx.now())
+local ok, err = session.info:save()
+```
+
+
+# Cookie Format
+
+```
+[ HEADER -------------------------------------------------------------------------------------]
+[ Type || Flags || SID || Created at || Rolling Offset || Size || Tag || Idling Offset || Mac ]
+[ 1B || 2B || 32B || 5B || 4B || 3B || 16B || 3B || 16B ]
+```
+
+and
+
+```
+[ PAYLOAD --]
+[ Data *B ]
+```
+
+Both the `HEADER` and `PAYLOAD` are base64 url-encoded before putting in a `Set-Cookie` header.
+When using a server side storage, the `PAYLOAD` is not put in the cookie. With cookie storage
+the base64 url-encoded header is concatenated with base64 url-encoded payload.
+
+The `HEADER` is fixed size 82 bytes binary or 110 bytes in base64 url-encoded form.
+
+Header fields explained:
+
+- Type: number `1` binary packed in a single little endian byte (currently the only supported `type`).
+- Flags: binary packed flags (short) in a two byte little endian form.
+- SID: `32` bytes of crypto random data (Session ID).
+- Created at: binary packed secs from epoch in a little endian form, truncated to 5 bytes.
+- Rolling Offset: binary packed secs from creation time in a little endian form (integer).
+- Size: binary packed data size (short) in a two byte little endian form.
+- Tag: `16` bytes of authentication tag from AES-256-GCM encryption of the data.
+- Idling Offset: binary packed secs from creation time + rolling offset in a little endian form, truncated to 3 bytes.
+- Mac: `16` bytes message authentication code of the header.
+
+
+# Data Encryption
+
+1. Initial keying material (IKM):
+ 1. derive IKM from `secret` by hashing `secret` with SHA-256, or
+ 2. use 32 byte IKM when passed to library with `ikm`
+2. Generate 32 bytes of crypto random session id (`sid`)
+3. Derive 32 byte encryption key and 12 byte initialization vector with HKDF using SHA-256 (on FIPS-mode it uses PBKDF2 with SHA-256 instead)
+ 1. Use HKDF extract to derive a new key from `ikm` to get `key` (this step can be done just once per `ikm`):
+ - output length: `32`
+ - digest: `"sha256"`
+ - key: ``
+ - mode: `extract only`
+ - info: `""`
+ - salt: `""`
+ 2. Use HKDF expand to derive `44` bytes of `output`:
+ - output length: `44`
+ - digest: `"sha256"`
+ - key: ``
+ - mode: `expand only`
+ - info: `"encryption:"`
+ - salt: `""`
+ 3. The first 32 bytes of `output` are the encryption key (`aes-key`), and the last 12 bytes are the initialization vector (`iv`)
+4. Encrypt `plaintext` (JSON encoded and optionally deflated) using AES-256-GCM to get `ciphertext` and `tag`
+ 1. cipher: `"aes-256-gcm"`
+ 2. key: ``
+ 3. iv: ``
+ 4. plaintext: ``
+ 5. aad: use the first 47 bytes of `header` as `aad`, that includes:
+ 1. Type
+ 2. Flags
+ 3. Session ID
+ 4. Creation Time
+ 5. Rolling Offset
+ 6. Data Size
+
+There is a variation for `remember` cookies on step 3, where we may use `PBKDF2`
+instead of `HKDF`, depending on `remember_safety` setting (we also use it in FIPS-mode).
+The `PBKDF2` settings:
+
+- output length: `44`
+- digest: `"sha256"`
+- password: ``
+- salt: `"encryption:"`
+- iterations: `<1000|10000|100000|1000000>`
+
+Iteration counts are based on `remember_safety` setting (`"Low"`, `"Medium"`, `"High"`, `"Very High"`),
+if `remember_safety` is set to `"None"`, we will use the HDKF as above.
+
+
+# Cookie Header Authentication
+
+1. Derive 32 byte authentication key (`mac_key`) with HKDF using SHA-256 (on FIPS-mode it uses PBKDF2 with SHA-256 instead):
+ 1. Use HKDF extract to derive a new key from `ikm` to get `key` (this step can be done just once per `ikm` and reused with encryption key generation):
+ - output length: `32`
+ - digest: `"sha256"`
+ - key: ``
+ - mode: `extract only`
+ - info: `""`
+ - salt: `""`
+ 2. Use HKDF expand to derive `32` bytes of `mac-key`:
+ - output length: `32`
+ - digest: `"sha256"`
+ - key: ``
+ - mode: `expand only`
+ - info: `"authentication:"`
+ - salt: `""`
+2. Calculate message authentication code using HMAC-SHA256:
+ - digest: `"sha256"`
+ - key: ``
+ - message: use the first 66 bytes of `header`, that includes:
+ 1. Type
+ 2. Flags
+ 3. Session ID
+ 4. Creation Time
+ 5. Rolling Offset
+ 6. Data Size
+ 7. Tag
+ 8. Idling Offset
+
+
+# Custom Storage Interface
+
+If you want to implement custom storage, you need to implement following interface:
+
+```lua
+---
+-- backend for session library
+--
+-- @module
+
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam boolean remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+ -- NYI
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(name, key)
+ -- NYI
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(name, key, current_time, metadata)
+ -- NYI
+end
+
+
+local storage = {}
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- storage backend configuration
+-- @field TBD
+-- @table configuration
+
+
+---
+-- Create a storage.
+--
+-- This creates a new shared memory storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration storage @{configuration}
+-- @treturn table storage instance
+function storage.new(configuration)
+ -- NYI
+ -- return setmetatable({}, metatable)
+end
+
+
+return storage
+```
+
+Please check the existing implementations for the defails. And please
+make a pull-request so that we can integrate it directly to library
+for other users as well.
+
+
+# License
+
+`lua-resty-session` uses two clause BSD license.
+
+```
+Copyright (c) 2014 – 2023 Aapo Talvensaari, 2022 – 2023 Samuele Illuminati
+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
+```
diff --git a/config.ld b/config.ld
new file mode 100644
index 000000000..7082fc286
--- /dev/null
+++ b/config.ld
@@ -0,0 +1,10 @@
+project = "resty.session"
+description = "Session Library for OpenResty"
+full_description = "`lua-resty-session` is a secure, and flexible session library for OpenResty"
+title = "Session Library for OpenResty Documentation"
+dir = "docs"
+use_markdown_titles = true
+package = "session"
+format = "discount"
+sort_modules=true
+file = "./lib/resty"
diff --git a/dev/Dockerfile b/dev/Dockerfile
new file mode 100644
index 000000000..a031b3551
--- /dev/null
+++ b/dev/Dockerfile
@@ -0,0 +1,20 @@
+FROM openresty/openresty:1.21.4.1-focal
+
+ENV DEBIAN_FRONTEND noninteractive
+ENV TEST_NGINX_BINARY openresty
+
+USER root
+RUN apt-get update && apt-get install -y gcc git cpanminus
+
+RUN git clone https://github.com/Olivine-Labs/busted
+RUN cd busted && luarocks make
+
+RUN luarocks install pgmoon
+RUN luarocks install lua-resty-rsa
+RUN luarocks install lua-resty-redis-connector
+RUN luarocks install lua-resty-redis-cluster
+RUN luarocks install inspect
+RUN luarocks install lua_pack
+RUN luarocks install LuaCov
+
+RUN cpanm --notest Test::Nginx
diff --git a/dist.ini b/dist.ini
new file mode 100644
index 000000000..e1cc3f953
--- /dev/null
+++ b/dist.ini
@@ -0,0 +1,7 @@
+name = lua-resty-session
+abstract = Session Library for OpenResty - Flexible and Secure
+author = Aapo Talvensaari (@bungle), Samuele Illuminati (@samugi)
+is_original = yes
+license = 2bsd
+repo_link = https://github.com/bungle/lua-resty-session
+requires = luajit, nginx, ngx_http_lua, fffonion/lua-resty-openssl >= 0.8.0, hamishforbes/lua-ffi-zlib >= 0.5
diff --git a/docs/classes/resty.session.html b/docs/classes/resty.session.html
new file mode 100644
index 000000000..7178c7d21
--- /dev/null
+++ b/docs/classes/resty.session.html
@@ -0,0 +1,906 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Classes
+
+
Modules
+
+
+
+
+
+
+
Class resty.session
+
Session library provides HTTP session management capabilities for OpenResty based
+ applications, libraries and proxies.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ session.info:set (key, value)
+
+
+ Set a value in session information store.
+
+
+ Parameters:
+
+
+
+
+
+
+
+
+
+ session.info:get (key)
+
+
+ Get a value from session information store.
+
+
+ Parameters:
+
+
+ Returns:
+
+
+ value
+
+
+
+
+
+
+
+
+ session.info:save ()
+
+
+ Save information.
+
+ Only updates backend storage. Does not send a new cookie.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ resty.session:set (key, value)
+
+
+ Set a value in session.
+
+
+ Parameters:
+
+
+
+
+
+
+
+
+
+ resty.session:get (key)
+
+
+ Get a value from session.
+
+
+ Parameters:
+
+
+ Returns:
+
+
+ value
+
+
+
+
+
+
+
+
+ resty.session:open ()
+
+
+ Open a session.
+
+ This can be used to open a session. It will either return an existing
+ session or a new session.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ resty.session:save ()
+
+
+ Save the session.
+
+ Saves the session data and issues a new session cookie with a new session id.
+ When remember is enabled, it will also issue a new persistent cookie and
+ possibly save the data in backend store.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ resty.session:touch ()
+
+
+ Touch the session.
+
+ Updates idling offset of the session by sending an updated session cookie.
+ It only sends the client cookie and never calls any backend session store
+ APIs. Normally the session:refresh is used to call this indirectly.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ resty.session:refresh ()
+
+
+ Refresh the session.
+
+ Either saves the session (creating a new session id) or touches the session
+ depending on whether the rolling timeout is getting closer. The touch has
+ a threshold, by default one minute, so it may be skipped in some cases.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ resty.session:logout ()
+
+
+ Logout the session.
+
+ Logout either destroys the session or just clears the data for the current audience,
+ and saves it (logging out from the current audience).
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ resty.session:destroy ()
+
+
+ Destroy the session.
+
+ Destroy the session and clear the cookies.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ resty.session:close ()
+
+
+ Close the session.
+
+ Just closes the session instance so that it cannot be used anymore.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:hide ()
+
+
+ Hide the session.
+
+ Modifies the request headers by removing the session related
+ cookies. This is useful when you use the session library on
+ a proxy server and don’t want the session cookies to be forwarded
+ to the upstream service.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+
+
+
+
+
+
+
+
+
+
+
+ resty.session.configuration
+
+
+ Session configuration.
+
+
+ Fields:
+
+ secret
+ Secret used for the key derivation. The secret is hashed with SHA-256 before using it. E.g. "RaJKp8UQW1".
+
+ secret_fallbacks
+ Array of secrets that can be used as alternative secrets (when doing key rotation), E.g. { "6RfrAYYzYq", "MkbTkkyF9C" }.
+
+ ikm
+ Initial key material (or ikm) can be specified directly (without using a secret) with exactly 32 bytes of data, e.g. "5ixIW4QVMk0dPtoIhn41Eh1I9enP2060"
+
+ ikm_fallbacks
+ Array of initial key materials that can be used as alternative keys (when doing key rotation), E.g. { "QvPtlPKxOKdP5MCu1oI3lOEXIVuDckp7" }.
+
+ cookie_prefix
+ Cookie prefix, use nil, "__Host-" or "__Secure-" (defaults to nil)
+
+ cookie_name
+ Session cookie name, e.g. "session" (defaults to "session")
+
+ cookie_path
+ Cookie path, e.g. "/" (defaults to "/")
+
+ cookie_domain
+ Cookie domain, e.g. "example.com" (defaults to nil)
+
+ cookie_http_only
+ Mark cookie HTTP only, use true or false (defaults to true)
+
+ cookie_secure
+ Mark cookie secure, use nil, true or false (defaults to nil)
+
+ cookie_priority
+ Cookie priority, use nil, "Low", "Medium", or "High" (defaults to nil)
+
+ cookie_same_site
+ Cookie same-site policy, use nil, "Lax", "Strict", or "None" (defaults to "Lax")
+
+ cookie_same_party
+ Mark cookie with same party flag, use nil, true, or false (default: nil)
+
+ cookie_partitioned
+ Mark cookie with partitioned flag, use nil, true, or false (default: nil)
+
+ remember
+ Enable or disable persistent sessions, use nil, true, or false (defaults to false)
+
+ remember_cookie_name
+ Persistent session cookie name, e.g. "remember" (defaults to "remember")
+
+ audience
+ Session audience, e.g. "my-application" (defaults to "default")
+
+ subject
+ Session subject, e.g. "john.doe@example.com" (defaults to nil)
+
+ stale_ttl
+ When session is saved a new session is created, stale ttl specifies how long the old one can still be used, e.g. 10 (defaults to 10) (in seconds)
+
+ idling_timeout
+ Idling timeout specifies how long the session can be inactive until it is considered invalid, e.g. 900 (defaults to 900, or 15 minutes) (in seconds)
+
+ rolling_timeout
+ Rolling timeout specifies how long the session can be used until it needs to be renewed, e.g. 3600 (defaults to 3600, or an hour) (in seconds)
+
+ absolute_timeout
+ Absolute timeout limits how long the session can be renewed, until re-authentication is required, e.g. 86400 (defaults to 86400, or a day) (in seconds)
+
+ remember_timeout
+ Remember timeout specifies how long the persistent session is considered valid, e.g. 604800 (defaults to 604800, or a week) (in seconds)
+
+ touch_threshold
+ Touch threshold controls how frequently or infrequently the session:refresh touches the cookie, e.g. 60 (defaults to 60, or a minute) (in seconds)
+
+ compression_threshold
+ Compression threshold controls when the data is deflated, e.g. 1024 (defaults to 1024, or a kilobyte) (in bytes)
+
+ storage
+ Storage is responsible of storing session data, use nil (data is stored in cookie), dshm , file , memcached , mysql , postgres , redis , redis-cluster , redis-sentinel , or shm , or give a name of custom module ("custom.session.storage"), or a table that implements session storage interface (defaults to nil)
+
+ dshm
+ Configuration for dshm storage, e.g. { prefix = "sessions" }
+
+ file
+ Configuration for file storage, e.g. { path = "/tmp", suffix = "session" }
+
+ memcached
+ Configuration for memcached storage, e.g. { prefix = "sessions" }
+
+ mysql
+ Configuration for MySQL / MariaDB storage, e.g. { database = "sessions" }
+
+ postgres
+ Configuration for Postgres storage, e.g. { database = "sessions" }
+
+ redis
+ Configuration for Redis / Redis Sentinel / Redis Cluster storages, e.g. { prefix = "sessions" }
+
+ shm
+ Configuration for shared memory storage, e.g. { zone = "sessions" }
+
+
+
+
+
+
+
+
+
+
+ module.init ([configuration])
+
+
+ Initialize the session library.
+
+ This function can be called on init or init_worker phases on OpenResty
+ to set global default configuration to all session instances created by this
+ library.
+
+
+
Parameters:
+
+
+
+
+
+ Usage:
+ require "resty.session" .init({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a new session.
+
+ This creates a new session instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ session instance
+
+
+
+
+ Usage:
+ local session = require "resty.session" .new()
+local session = require "resty.session" .new({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ module.open ([configuration])
+
+
+ Open a session.
+
+ This can be used to open a session, and it will either return an existing
+ session or a new session.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ session instance
+
+ string
+ information why session could not be opened
+
+ boolean
+ true, if session existed, otherwise false
+
+
+
+
+ Usage:
+ local session = require "resty.session" .open()
+local session, err, exists = require "resty.session" .open({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ module.start ([configuration])
+
+
+ Start a session and refresh it as needed.
+
+ This can be used to start a session, and it will either return an existing
+ session or a new session. In case there is an existing session, the
+ session will be refreshed as well (as needed).
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ session instance
+
+ string
+ information why session could not be logged out
+
+ boolean
+ true, if session existed, otherwise false
+
+ boolean
+ true, if session was refreshed, otherwise false
+
+
+
+
+ Usage:
+ local session = require "resty.session" .start()
+local session, err, exists, refreshed = require "resty.session" .start()
+ audience = "my-application" ,
+})
+
+
+
+
+
+ module.logout ([configuration])
+
+
+ Logout a session.
+
+ It logouts from a specific audience.
+
+ A single session cookie may be shared between multiple audiences
+ (or applications), thus there is a need to be able to logout from
+ just a single audience while keeping the session for the other
+ audiences.
+
+ When there is only a single audience, then this can be considered
+ equal to session.destroy .
+
+ When the last audience is logged out, the cookie will be destroyed
+ as well and invalidated on a client.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ boolean
+ true session exists for an audience and was logged out successfully, otherwise false
+
+ string
+ information why the session could not be logged out
+
+ boolean
+ true if session existed, otherwise false
+
+ boolean
+ true if session was logged out, otherwise false
+
+
+
+
+ Usage:
+ require "resty.session" .logout()
+local ok, err, exists, logged_out = require "resty.session" .logout({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ module.destroy ([configuration])
+
+
+ Destroy a session.
+
+ It destroys the whole session and clears the cookies.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ boolean
+ true session exists and was destroyed successfully, otherwise nil
+
+ string
+ information why session could not be destroyed
+
+ boolean
+ true if session existed, otherwise false
+
+ boolean
+ true if session was destroyed, otherwise false
+
+
+
+
+ Usage:
+ require "resty.session" .destroy()
+local ok, err, exists = require "resty.session" .destroy({
+ cookie_name = "auth" ,
+})
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2022-12-16 17:07:08
+
+
+
+
diff --git a/docs/classes/session.html b/docs/classes/session.html
new file mode 100644
index 000000000..1d66f2a1c
--- /dev/null
+++ b/docs/classes/session.html
@@ -0,0 +1,545 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Classes
+
+
Modules
+
+
+
+
+
+
+
Class session
+
Session library provides HTTP session management capabilities for OpenResty based
+ applications, libraries and proxies.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Methods
+
+
+
+ session_instance:open ()
+
+
+ Opens session
+
+ This can be used to open a session. It will either return an existing
+ session or a new session.
+
+
+
+
Returns:
+
+
+ true or nil
+ session instance
+
+ string
+ information why session could not be opened
+
+
+
+
+
+
+
+
+
+
+
+
+ session.configuration
+
+
+ Session configuration options
+
+
+ Fields:
+
+ secret
+ Secret used for the key derivation. The secret is hashed with SHA-256 before using it. E.g. "RaJKp8UQW1".
+
+ secret_fallbacks
+ Array of secrets that can be used as alternative secrets (when doing key rotation), E.g. { "6RfrAYYzYq", "MkbTkkyF9C" }.
+
+ ikm
+ Initial key material (or ikm) can be specified directly (without using a secret) with exactly 32 bytes of data, e.g. "5ixIW4QVMk0dPtoIhn41Eh1I9enP2060"
+
+ ikm_fallbacks
+ Array of initial key materials that can be used as alternative keys (when doing key rotation), E.g. { "QvPtlPKxOKdP5MCu1oI3lOEXIVuDckp7" }.
+
+ cookie_prefix
+ Cookie prefix, use nil, "__Host-" or "__Secure-" (defaults to nil)
+
+ cookie_name
+ Session cookie name, e.g. "session" (defaults to "session")
+
+ cookie_path
+ Cookie path, e.g. "/" (defaults to "/")
+
+ cookie_domain
+ Cookie domain, e.g. "example.com" (defaults to nil)
+
+ cookie_http_only
+ Mark cookie HTTP only, use true or false (defaults to true)
+
+ cookie_secure
+ Mark cookie secure, use nil, true or false (defaults to nil)
+
+ cookie_priority
+ Cookie priority, use nil, "Low", "Medium", or "High" (defaults to nil)
+
+ cookie_same_site
+ Cookie same-site policy, use nil, "Lax", "Strict", or "None" (defaults to "Lax")
+
+ cookie_same_party
+ Mark cookie with same party flag, use nil, true, or false (default: nil)
+
+ cookie_partitioned
+ Mark cookie with partitioned flag, use nil, true, or false (default: nil)
+
+ remember
+ Enable or disable persistent sessions, use nil, true, or false (defaults to false)
+
+ remember_cookie_name
+ Persistent session cookie name, e.g. "remember" (defaults to "remember")
+
+ audience
+ Session audience, e.g. "my-application" (defaults to "default")
+
+ subject
+ Session subject, e.g. "john.doe@example.com" (defaults to nil)
+
+ stale_ttl
+ When session is saved a new session is created, stale ttl specifies how long the old one can still be used, e.g. 10 (defaults to 10) (in seconds)
+
+ idling_timeout
+ Idling timeout specifies how long the session can be inactive until it is considered invalid, e.g. 900 (defaults to 900, or 15 minutes) (in seconds)
+
+ rolling_timeout
+ Rolling timeout specifies how long the session can be used until it needs to be renewed, e.g. 3600 (defaults to 3600, or an hour) (in seconds)
+
+ absolute_timeout
+ Absolute timeout limits how long the session can be renewed, until re-authentication is required, e.g. 86400 (defaults to 86400, or a day) (in seconds)
+
+ remember_timeout
+ Remember timeout specifies how long the persistent session is considered valid, e.g. 604800 (defaults to 604800, or a week) (in seconds)
+
+ storage
+ Storage is responsible of storing session data, use nil (data is stored in cookie), dshm, file, memcached, mysql, postgres, redis, redis-cluster, redis-sentinel, or shm, or give a name of custom module ("custom.session.storage"), or a table that implements session storage interface (defaults to nil)
+
+ dshm
+ Configuration for dshm storage, e.g. { prefix = "sessions" }
+
+ file
+ Configuration for file storage, e.g. { path = "/tmp", suffix = "session" }
+
+ memcached
+ Configuration for memcached storage, e.g. { prefix = "sessions" }
+
+ mysql
+ Configuration for MySQL / MariaDB storage, e.g. { database = "sessions" }
+
+ postgres
+ Configuration for Postgres storage, e.g. { database = "sessions" }
+
+ redis
+ Configuration for Redis / Redis Sentinel / Redis Cluster storages, e.g. { prefix = "sessions" }
+
+ shm
+ Configuration for shared memory storage, e.g. { zone = "sessions" }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ session:init ([configuration])
+
+
+ Initializes session library This function can be called on init or init_worker phases on OpenResty
+ to set global default configuration to all session instances created by this
+ library.
+
+
+ Parameters:
+
+
+
+
+
+ Usage:
+ require "resty.session" .init({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ session:new ([configuration])
+
+
+ Creates new session This creates a new session instance.
+
+
+ Parameters:
+
+
+ Returns:
+
+
+ table
+ session instance
+
+
+
+
+ Usage:
+ local session = require "resty.session" .new()
+local session = require "resty.session" .new({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ session:open ([configuration])
+
+
+ Opens session
+
+ This can be used to open a session. It will either return an existing
+ session or a new session.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ session instance
+
+ string
+ information why session could not be opened
+
+ boolean
+ true, if session existed, otherwise false
+
+
+
+
+ Usage:
+ local session = require "resty.session" .open()
+local session, err, exists = require "resty.session" .open({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ session:start ([configuration])
+
+
+ Starts the session and refreshes it as needed
+
+ This can be used to start a session. It will either return an existing
+ session or a new session. In case there is an existing session, the
+ session will be refreshed as well (as needed).
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ session instance
+
+ string
+ information why session could not be logged out
+
+ boolean
+ true, if session existed, otherwise false
+
+ boolean
+ true, if session was refreshed, otherwise false
+
+
+
+
+ Usage:
+ local session = require "resty.session" .start()
+local session, err, exists, refreshed = require "resty.session" .start()
+ audience = "my-application" ,
+})
+
+
+
+
+
+ session:logout ([configuration])
+
+
+ Logouts session
+
+ It logouts from a specific audience.
+
+ A single session cookie may be shared between multiple audiences
+ (or applications), thus there is a need to be able to logout from
+ just a single audience while keeping the session for the other
+ audiences.
+
+ When there is only a single audience, then this can be considered
+ equal to session.destroy .
+
+ When the last audience is logged out, the cookie will be destroyed
+ as well and invalidated on a client.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ boolean
+ true session exists for an audience and was logged out successfully, otherwise false
+
+ string
+ information why the session could not be logged out
+
+ boolean
+ true if session existed, otherwise false
+
+ boolean
+ true if session was logged out, otherwise false
+
+
+
+
+ Usage:
+ require "resty.session" .logout()
+local ok, err, exists, logged_out = require "resty.session" .logout({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ session:destroy ([configuration])
+
+
+ Destroys session It destroys the whole session and clears the cookies.
+
+
+ Parameters:
+
+
+ Returns:
+
+
+ boolean
+ true session exists and was destroyed successfully, otherwise nil
+
+ string
+ information why session could not be destroyed
+
+ boolean
+ true if session existed, otherwise false
+
+ boolean
+ true if session was destroyed, otherwise false
+
+
+
+
+ Usage:
+ require "resty.session" .destroy()
+local ok, err, exists = require "resty.session" .destroy({
+ cookie_name = "auth" ,
+})
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2022-12-16 00:35:34
+
+
+
+
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 000000000..1d4320c86
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,127 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
+
Modules
+
+
+
+
+
+
+
+
Session Library for OpenResty
+
lua-resty-session is a secure, and flexible session library for OpenResty
+
+
Modules
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/ldoc.css b/docs/ldoc.css
new file mode 100644
index 000000000..52c4ad2bd
--- /dev/null
+++ b/docs/ldoc.css
@@ -0,0 +1,303 @@
+/* BEGIN RESET
+
+Copyright (c) 2010, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.com/yui/license.html
+version: 2.8.2r1
+*/
+html {
+ color: #000;
+ background: #FFF;
+}
+body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td {
+ margin: 0;
+ padding: 0;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+fieldset,img {
+ border: 0;
+}
+address,caption,cite,code,dfn,em,strong,th,var,optgroup {
+ font-style: inherit;
+ font-weight: inherit;
+}
+del,ins {
+ text-decoration: none;
+}
+li {
+ margin-left: 20px;
+}
+caption,th {
+ text-align: left;
+}
+h1,h2,h3,h4,h5,h6 {
+ font-size: 100%;
+ font-weight: bold;
+}
+q:before,q:after {
+ content: '';
+}
+abbr,acronym {
+ border: 0;
+ font-variant: normal;
+}
+sup {
+ vertical-align: baseline;
+}
+sub {
+ vertical-align: baseline;
+}
+legend {
+ color: #000;
+}
+input,button,textarea,select,optgroup,option {
+ font-family: inherit;
+ font-size: inherit;
+ font-style: inherit;
+ font-weight: inherit;
+}
+input,button,textarea,select {*font-size:100%;
+}
+/* END RESET */
+
+body {
+ margin-left: 1em;
+ margin-right: 1em;
+ font-family: arial, helvetica, geneva, sans-serif;
+ background-color: #ffffff; margin: 0px;
+}
+
+code, tt { font-family: monospace; font-size: 1.1em; }
+span.parameter { font-family:monospace; }
+span.parameter:after { content:":"; }
+span.types:before { content:"("; }
+span.types:after { content:")"; }
+.type { font-weight: bold; font-style:italic }
+
+body, p, td, th { font-size: .95em; line-height: 1.2em;}
+
+p, ul { margin: 10px 0 0 0px;}
+
+strong { font-weight: bold;}
+
+em { font-style: italic;}
+
+h1 {
+ font-size: 1.5em;
+ margin: 20px 0 20px 0;
+}
+h2, h3, h4 { margin: 15px 0 10px 0; }
+h2 { font-size: 1.25em; }
+h3 { font-size: 1.15em; }
+h4 { font-size: 1.06em; }
+
+a:link { font-weight: bold; color: #004080; text-decoration: none; }
+a:visited { font-weight: bold; color: #006699; text-decoration: none; }
+a:link:hover { text-decoration: underline; }
+
+hr {
+ color:#cccccc;
+ background: #00007f;
+ height: 1px;
+}
+
+blockquote { margin-left: 3em; }
+
+ul { list-style-type: disc; }
+
+p.name {
+ font-family: "Andale Mono", monospace;
+ padding-top: 1em;
+}
+
+pre {
+ background-color: rgb(245, 245, 245);
+ border: 1px solid #C0C0C0; /* silver */
+ padding: 10px;
+ margin: 10px 0 10px 0;
+ overflow: auto;
+ font-family: "Andale Mono", monospace;
+}
+
+pre.example {
+ font-size: .85em;
+}
+
+table.index { border: 1px #00007f; }
+table.index td { text-align: left; vertical-align: top; }
+
+#container {
+ margin-left: 1em;
+ margin-right: 1em;
+ background-color: #f0f0f0;
+}
+
+#product {
+ text-align: center;
+ border-bottom: 1px solid #cccccc;
+ background-color: #ffffff;
+}
+
+#product big {
+ font-size: 2em;
+}
+
+#main {
+ background-color: #f0f0f0;
+ border-left: 2px solid #cccccc;
+}
+
+#navigation {
+ float: left;
+ width: 14em;
+ vertical-align: top;
+ background-color: #f0f0f0;
+ overflow: visible;
+}
+
+#navigation h2 {
+ background-color:#e7e7e7;
+ font-size:1.1em;
+ color:#000000;
+ text-align: left;
+ padding:0.2em;
+ border-top:1px solid #dddddd;
+ border-bottom:1px solid #dddddd;
+}
+
+#navigation ul
+{
+ font-size:1em;
+ list-style-type: none;
+ margin: 1px 1px 10px 1px;
+}
+
+#navigation li {
+ text-indent: -1em;
+ display: block;
+ margin: 3px 0px 0px 22px;
+}
+
+#navigation li li a {
+ margin: 0px 3px 0px -1em;
+}
+
+#content {
+ margin-left: 14em;
+ padding: 1em;
+ width: 700px;
+ border-left: 2px solid #cccccc;
+ border-right: 2px solid #cccccc;
+ background-color: #ffffff;
+}
+
+#about {
+ clear: both;
+ padding: 5px;
+ border-top: 2px solid #cccccc;
+ background-color: #ffffff;
+}
+
+@media print {
+ body {
+ font: 12pt "Times New Roman", "TimeNR", Times, serif;
+ }
+ a { font-weight: bold; color: #004080; text-decoration: underline; }
+
+ #main {
+ background-color: #ffffff;
+ border-left: 0px;
+ }
+
+ #container {
+ margin-left: 2%;
+ margin-right: 2%;
+ background-color: #ffffff;
+ }
+
+ #content {
+ padding: 1em;
+ background-color: #ffffff;
+ }
+
+ #navigation {
+ display: none;
+ }
+ pre.example {
+ font-family: "Andale Mono", monospace;
+ font-size: 10pt;
+ page-break-inside: avoid;
+ }
+}
+
+table.module_list {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #cccccc;
+ border-collapse: collapse;
+}
+table.module_list td {
+ border-width: 1px;
+ padding: 3px;
+ border-style: solid;
+ border-color: #cccccc;
+}
+table.module_list td.name { background-color: #f0f0f0; min-width: 200px; }
+table.module_list td.summary { width: 100%; }
+
+
+table.function_list {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #cccccc;
+ border-collapse: collapse;
+}
+table.function_list td {
+ border-width: 1px;
+ padding: 3px;
+ border-style: solid;
+ border-color: #cccccc;
+}
+table.function_list td.name { background-color: #f0f0f0; min-width: 200px; }
+table.function_list td.summary { width: 100%; }
+
+ul.nowrap {
+ overflow:auto;
+ white-space:nowrap;
+}
+
+dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;}
+dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;}
+dl.table h3, dl.function h3 {font-size: .95em;}
+
+/* stop sublists from having initial vertical space */
+ul ul { margin-top: 0px; }
+ol ul { margin-top: 0px; }
+ol ol { margin-top: 0px; }
+ul ol { margin-top: 0px; }
+
+/* make the target distinct; helps when we're navigating to a function */
+a:target + * {
+ background-color: #FF9;
+}
+
+
+/* styles for prettification of source */
+pre .comment { color: #558817; }
+pre .constant { color: #a8660d; }
+pre .escape { color: #844631; }
+pre .keyword { color: #aa5050; font-weight: bold; }
+pre .library { color: #0e7c6b; }
+pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; }
+pre .string { color: #8080ff; }
+pre .number { color: #f8660d; }
+pre .operator { color: #2239a8; font-weight: bold; }
+pre .preprocessor, pre .prepro { color: #a33243; }
+pre .global { color: #800080; }
+pre .user-keyword { color: #800080; }
+pre .prompt { color: #558817; }
+pre .url { color: #272fc2; text-decoration: underline; }
+
diff --git a/docs/modules/resty.session.dshm.html b/docs/modules/resty.session.dshm.html
new file mode 100644
index 000000000..7ed27d7fe
--- /dev/null
+++ b/docs/modules/resty.session.dshm.html
@@ -0,0 +1,397 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.dshm
+
Distributed Shared Memory (DSHM) backend for session library
+
+
+
+
+
+
+
+ configuration
+ Distributed shared memory storage backend configuration
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Distributed shared memory storage backend configuration
+
+
+ Fields:
+
+ prefix
+ The prefix for the keys stored in DSHM.
+
+ suffix
+ The suffix for the keys stored in DSHM.
+
+ host
+ The host to connect (defaults to "127.0.0.1").
+
+ port
+ The port to connect (defaults to 4321).
+
+ connect_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s connect method.
+
+ send_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s send method.
+
+ read_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s receive method.
+
+ keepalive_timeout
+ Controls the default maximal idle time of the connections in the connection pool.
+
+ pool
+ A custom name for the connection pool being used.
+
+ pool_size
+ The size of the connection pool.
+
+ backlog
+ A queue size to use when the connection pool is full (configured with @pool_size).
+
+ ssl
+ Enable SSL (defaults to false).
+
+ ssl_verify
+ Verify server certificate (defaults to nil).
+
+ server_name
+ The server name for the new TLS extension Server Name Indication (SNI).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a distributed shared memory storage.
+
+ This creates a new distributed shared memory storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ DSHM storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ boolean
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.file-thread.html b/docs/modules/resty.session.file-thread.html
new file mode 100644
index 000000000..6e83417e8
--- /dev/null
+++ b/docs/modules/resty.session.file-thread.html
@@ -0,0 +1,195 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.file-thread
+
File storage backend worker thread module
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ set (path, content)
+
+
+ Store data in file.
+
+
+ Parameters:
+
+ path
+ string
+ file path
+
+
+
+ content
+ string
+ file content
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ get (path)
+
+
+ Read data from a file.
+
+
+ Parameters:
+
+ path
+ string
+ file to read
+
+
+
+ Returns:
+
+
+ string or nil
+ content
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ delete (path)
+
+
+ Delete a file.
+
+
+ Parameters:
+
+ path
+ string
+ file to read
+
+
+
+ Returns:
+
+
+ string or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2022-12-23 14:06:58
+
+
+
+
diff --git a/docs/modules/resty.session.file.html b/docs/modules/resty.session.file.html
new file mode 100644
index 000000000..15c26d0d4
--- /dev/null
+++ b/docs/modules/resty.session.file.html
@@ -0,0 +1,367 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.file
+
File storage backend for session library.
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ File storage backend configuration
+
+
+ Fields:
+
+ prefix
+ File prefix for session file.
+
+ suffix
+ File suffix (or extension without .) for session file.
+
+ pool
+ Name of the thread pool under which file writing happens (available on Linux only).
+
+ path
+ Path (or directory) under which session files are created.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a file storage.
+
+ This creates a new file storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ file storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ boolean
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.file.thread.html b/docs/modules/resty.session.file.thread.html
new file mode 100644
index 000000000..65585f2ed
--- /dev/null
+++ b/docs/modules/resty.session.file.thread.html
@@ -0,0 +1,331 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.file.thread
+
File storage backend worker thread module
+
+
+
+
+
+
+
+ module.set (path, prefix, suffix, name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ module.GET (path, prefix, suffix, name, key)
+ Retrieve session data.
+
+
+ module.delete (path, prefix, suffix, name, key, current_time)
+ Delete session data.
+
+
+ module.read_metadata (path, prefix, suffix, name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+ module.set (path, prefix, suffix, name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ path
+ string
+ the path where sessions are stored
+
+ prefix
+ string
+ the prefix for session files
+
+ suffix
+ string
+ the suffix for session files
+
+ name
+ string
+ the cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ table
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ module.GET (path, prefix, suffix, name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ path
+ string
+ the path where sessions are stored
+
+ prefix
+ string
+ the prefix for session files
+
+ suffix
+ string
+ the suffix for session files
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ module.delete (path, prefix, suffix, name, key, current_time)
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ path
+ string
+ the path where sessions are stored
+
+ prefix
+ string
+ the prefix for session files
+
+ suffix
+ string
+ the suffix for session files
+
+ name
+ string
+ the cookie name
+
+ key
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ module.read_metadata (path, prefix, suffix, name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ path
+ string
+ the path where sessions are stored
+
+ prefix
+ string
+ the prefix for session files
+
+ suffix
+ string
+ the suffix for session files
+
+ name
+ string
+ the cookie name
+
+ audience
+ string
+ session audience
+
+ subject
+ string
+ session subject
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.file.utils.html b/docs/modules/resty.session.file.utils.html
new file mode 100644
index 000000000..1f87560f3
--- /dev/null
+++ b/docs/modules/resty.session.file.utils.html
@@ -0,0 +1,350 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.file.utils
+
File storage utilities
+
+
+
+
+
+
+
+ file_create (path, content)
+ Store data in file.
+
+
+ file_append (path, data)
+ Append data in file.
+
+
+ file_read (path)
+ Read data from a file.
+
+
+ get_modification (path)
+ Get the value modification time of a file.
+
+
+ meta_get_key (audience, subject)
+ Given an audience and a subject, generate a metadata key.
+
+
+ validate_file_name (prefix, suffix, name, filename)
+ Validate a file name.
+
+
+ cleanup (path, prefix, suffix, name, current_time)
+ Clean up expired session and metadata files.
+
+
+
+
+
+
+
+
+
+
+
+
+ file_create (path, content)
+
+
+ Store data in file.
+
+
+ Parameters:
+
+ path
+ string
+ file path
+
+ content
+ string
+ file content
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ file_append (path, data)
+
+
+ Append data in file.
+
+
+ Parameters:
+
+ path
+ string
+ file path
+
+ data
+ string
+ file data
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ file_read (path)
+
+
+ Read data from a file.
+
+
+ Parameters:
+
+ path
+ string
+ file to read
+
+
+
+ Returns:
+
+
+ string or nil
+ content
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ get_modification (path)
+
+
+ Get the value modification time of a file.
+
+
+ Parameters:
+
+ path
+ string
+ the path to the file
+
+
+
+
+
+
+
+
+
+
+ meta_get_key (audience, subject)
+
+
+ Given an audience and a subject, generate a metadata key.
+
+
+ Parameters:
+
+ audience
+ string
+ session audience
+
+ subject
+ string
+ session subject
+
+
+
+ Returns:
+
+
+ string
+ metadata key
+
+
+
+
+
+
+
+
+ validate_file_name (prefix, suffix, name, filename)
+
+
+ Validate a file name.
+ Run a few checks to try to determine if the file is managed by this library
+
+
+ Parameters:
+
+ prefix
+ string
+ the prefix for session files
+
+ suffix
+ string
+ the suffix for session files
+
+ name
+ string
+ cookie name
+
+ filename
+ string
+ the name of the file
+
+
+
+ Returns:
+
+
+ true or false
+ whether the file is managed by the library or not
+
+
+
+
+
+
+
+
+ cleanup (path, prefix, suffix, name, current_time)
+
+
+ Clean up expired session and metadata files.
+
+
+ Parameters:
+
+ path
+ string
+ the path where sessions are stored
+
+ prefix
+ string
+ the prefix for session files
+
+ suffix
+ string
+ the suffix for session files
+
+ name
+ string
+ cookie name
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ true or false
+ whether clean up completed
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.html b/docs/modules/resty.session.html
new file mode 100644
index 000000000..62c95db4d
--- /dev/null
+++ b/docs/modules/resty.session.html
@@ -0,0 +1,1307 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session
+
Session library.
+
Session library provides HTTP session management capabilities for OpenResty based
+ applications, libraries and proxies.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ instance.info:set (key, value)
+
+
+ Set a value in session information store.
+
+
+ Parameters:
+
+ key
+ string
+ key
+
+ value
+ value
+
+
+
+
+
+
+
+
+
+
+ instance.info:get (key)
+
+
+ Get a value from session information store.
+
+
+ Parameters:
+
+
+ Returns:
+
+
+ value
+
+
+
+
+
+
+
+
+ instance.info:save ()
+
+
+ Save information.
+
+ Only updates backend storage. Does not send a new cookie.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:set_data (data)
+
+
+ Set session data.
+
+
+ Parameters:
+
+ data
+ table
+ data
+
+
+
+
+
+
+ Usage:
+ local session, err, exists = require "resty.session" .open()
+if not exists then
+ session:set_data({
+ cart = {},
+ })
+ session:save()
+end
+
+
+
+
+
+ instance:get_data ()
+
+
+ Get session data.
+
+
+
+ Returns:
+
+
+ table
+ value
+
+
+
+
+ Usage:
+ local session, err, exists = require "resty.session" .open()
+if exists then
+ local data = session:get_data()
+ ngx.req.set_header("Authorization" , "Bearer " .. data.access_token)
+end
+
+
+
+
+
+ instance:set (key, value)
+
+
+ Set a value in session.
+
+
+ Parameters:
+
+ key
+ string
+ key
+
+ value
+ value
+
+ local session, err, exists = require “resty.session”.open()
+ if not exists then
+ session:set(“access-token”, “eyJ…”)
+ session:save()
+ end
+
+
+
+
+
+
+
+
+
+
+ instance:get (key)
+
+
+ Get a value from session.
+
+
+ Parameters:
+
+
+ Returns:
+
+
+ value
+
+
+
+
+ Usage:
+ local session, err, exists = require "resty.session" .open()
+if exists then
+ local access_token = session:get("access-token" )
+ ngx.req.set_header("Authorization" , "Bearer " .. access_token)
+end
+
+
+
+
+
+ instance:set_audience (audience)
+
+
+ Set session audience.
+
+
+ Parameters:
+
+ audience
+ string
+ audience
+
+
+
+
+
+
+ Usage:
+ local session = require "resty.session" .new()
+session.set_audience("my-service" )
+
+
+
+
+
+ instance:get_audience ()
+
+
+ Get session audience.
+
+
+
+ Returns:
+
+
+ string
+ audience
+
+
+
+
+
+
+
+
+ instance:set_subject (subject)
+
+
+ Set session subject.
+
+
+ Parameters:
+
+ subject
+ string
+ subject
+
+
+
+
+
+
+ Usage:
+ local session = require "resty.session" .new()
+session.set_subject("john@doe.com" )
+
+
+
+
+
+ instance:get_subject ()
+
+
+ Get session subject.
+
+
+
+ Returns:
+
+
+ string
+ subject
+
+
+
+
+ Usage:
+ local session, err, exists = require "resty.session" .open()
+if exists then
+ local subject = session.get_subject()
+end
+
+
+
+
+
+ instance:get_property ()
+
+
+ Get session property.
+
+ Possible property names:
+ "id": 43 bytes session id (same as nonce, but base64 url-encoded)
+ "nonce": 32 bytes nonce (same as session id but in raw bytes)
+ "audience": Current session audience
+ "subject": Current session subject
+ "timeout": Closest timeout (in seconds) (what’s left of it)
+ "idling-timeout“: Session idling timeout (in seconds) (what's left of it)
+ "rolling-timeout": Session rolling timeout (in seconds) (what’s left of it)
+ "absolute-timeout”: Session absolute timeout (in seconds) (what's left of it)
+
+
+
+
Returns:
+
+
+ string or number
+ metadata
+
+
+
+
+ Usage:
+ local session, err, exists = require "resty.session" .open()
+if exists then
+ local timeout = session.get_property("timeout" )
+end
+
+
+
+
+
+ instance:set_remember (value)
+
+
+ Set persistent sessions on/off.
+
+ In many login forms user is given an option for “remember me”.
+ You can call this function based on what user selected.
+
+
+
Parameters:
+
+ value
+ boolean
+ true to enable persistent session, false to disable them
+
+
+
+
+
+
+
+
+
+
+ instance:get_remember ()
+
+
+ Get state of persistent sessions.
+
+
+
+ Returns:
+
+
+ boolean
+ true when persistent sessions are enabled, otherwise false
+
+
+
+
+
+
+
+
+ instance:open ()
+
+
+ Open a session.
+
+ This can be used to open a session.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:save ()
+
+
+ Save the session.
+
+ Saves the session data and issues a new session cookie with a new session id.
+ When remember is enabled, it will also issue a new persistent cookie and
+ possibly save the data in backend store.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:touch ()
+
+
+ Touch the session.
+
+ Updates idling offset of the session by sending an updated session cookie.
+ It only sends the client cookie and never calls any backend session store
+ APIs. Normally the session:refresh is used to call this indirectly.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:refresh ()
+
+
+ Refresh the session.
+
+ Either saves the session (creating a new session id) or touches the session
+ depending on whether the rolling timeout is getting closer, which means
+ by default when ¾ of rolling timeout is spent – 45 minutes with default
+ rolling timeout of an hour. The touch has a threshold, by default one minute,
+ so it may be skipped in some cases (you can call session:touch() to force it).
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:logout ()
+
+
+ Logout the session.
+
+ Logout either destroys the session or just clears the data for the current audience,
+ and saves it (logging out from the current audience).
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:destroy ()
+
+
+ Destroy the session.
+
+ Destroy the session and clear the cookies.
+
+
+
+
Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:close ()
+
+
+ Close the session.
+
+ Just closes the session instance so that it cannot be used anymore.
+
+
+
+
+
+
+
+
+
+
+ instance:clear_request_cookie ()
+
+
+ Clear the request session cookie.
+
+ Modifies the request headers by removing the session related
+ cookies. This is useful when you use the session library on
+ a proxy server and don’t want the session cookies to be forwarded
+ to the upstream service.
+
+
+
+
+
+
+
+
+
+
+ instance:set_headers ([...])
+
+
+ Sets request and response headers.
+
+
+ Parameters:
+
+ ...
+ string
+
+
+ (optional )
+
+
+
+
+
+
+
+
+
+
+ instance:set_request_headers ([...])
+
+
+ Set request headers.
+
+
+ Parameters:
+
+ ...
+ string
+
+
+ (optional )
+
+
+
+
+
+
+
+
+
+
+ instance:set_response_headers ([...])
+
+
+ Set response headers.
+
+
+ Parameters:
+
+ ...
+ string
+
+
+ (optional )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Session configuration.
+
+
+ Fields:
+
+ secret
+ Secret used for the key derivation. The secret is hashed with SHA-256 before using it. E.g. "RaJKp8UQW1".
+
+ secret_fallbacks
+ Array of secrets that can be used as alternative secrets (when doing key rotation), E.g. { "6RfrAYYzYq", "MkbTkkyF9C" }.
+
+ ikm
+ Initial key material (or ikm) can be specified directly (without using a secret) with exactly 32 bytes of data. E.g. "5ixIW4QVMk0dPtoIhn41Eh1I9enP2060"
+
+ ikm_fallbacks
+ Array of initial key materials that can be used as alternative keys (when doing key rotation), E.g. { "QvPtlPKxOKdP5MCu1oI3lOEXIVuDckp7" }.
+
+ cookie_prefix
+ Cookie prefix, use nil, "__Host-" or "__Secure-" (defaults to nil)
+
+ cookie_name
+ Session cookie name, e.g. "session" (defaults to "session")
+
+ cookie_path
+ Cookie path, e.g. "/" (defaults to "/")
+
+ cookie_domain
+ Cookie domain, e.g. "example.com" (defaults to nil)
+
+ cookie_http_only
+ Mark cookie HTTP only, use true or false (defaults to true)
+
+ cookie_secure
+ Mark cookie secure, use nil, true or false (defaults to nil)
+
+ cookie_priority
+ Cookie priority, use nil, "Low", "Medium", or "High" (defaults to nil)
+
+ cookie_same_site
+ Cookie same-site policy, use nil, "Lax", "Strict", "None", or "Default" (defaults to "Lax")
+
+ cookie_same_party
+ Mark cookie with same party flag, use nil, true, or false (default: nil)
+
+ cookie_partitioned
+ Mark cookie with partitioned flag, use nil, true, or false (default: nil)
+
+ remember
+ Enable or disable persistent sessions, use nil, true, or false (defaults to false)
+
+ remember_safety
+ Remember cookie key derivation complexity, use nil, "None" (fast), "Low", "Medium", "High" or "Very High" (slow) (defaults to "Medium")
+
+ remember_cookie_name
+ Persistent session cookie name, e.g. "remember" (defaults to "remember")
+
+ audience
+ Session audience, e.g. "my-application" (defaults to "default")
+
+ subject
+ Session subject, e.g. "john.doe@example.com" (defaults to nil)
+
+ enforce_same_subject
+ When set to true, audiences need to share the same subject. The library removes non-subject matching audience data on save.
+
+ stale_ttl
+ When session is saved a new session is created, stale ttl specifies how long the old one can still be used, e.g. 10 (defaults to 10) (in seconds)
+
+ idling_timeout
+ Idling timeout specifies how long the session can be inactive until it is considered invalid, e.g. 900 (defaults to 900, or 15 minutes) (in seconds)
+
+ rolling_timeout
+ Rolling timeout specifies how long the session can be used until it needs to be renewed, e.g. 3600 (defaults to 3600, or an hour) (in seconds)
+
+ absolute_timeout
+ Absolute timeout limits how long the session can be renewed, until re-authentication is required, e.g. 86400 (defaults to 86400, or a day) (in seconds)
+
+ remember_rolling_timeout
+ Remember timeout specifies how long the persistent session is considered valid, e.g. 604800 (defaults to 604800, or a week) (in seconds)
+
+ remember_absolute_timeout
+ Remember absolute timeout limits how long the persistent session can be renewed, until re-authentication is required, e.g. 2592000 (defaults to 2592000, or 30 days) (in seconds)
+
+ hash_storage_key
+ Whether to hash or not the storage key. With storage key hashed it is impossible to decrypt data on server side without having a cookie too (defaults to false).
+
+ hash_subject
+ Whether to hash or not the subject when store_metadata is enabled, e.g. for PII reasons (defaults to false).
+
+ store_metadata
+ Whether to also store metadata of sessions, such as collecting data of sessions for a specific audience belonging to a specific subject (defaults to false).
+
+ touch_threshold
+ Touch threshold controls how frequently or infrequently the session:refresh touches the cookie, e.g. 60 (defaults to 60, or a minute) (in seconds)
+
+ compression_threshold
+ Compression threshold controls when the data is deflated, e.g. 1024 (defaults to 1024, or a kilobyte) (in bytes)
+
+ request_headers
+ Set of headers to send to upstream, use id, audience, subject, timeout, idling-timeout, rolling-timeout, absolute-timeout. E.g. { "id", "timeout" } will set Session-Id and Session-Timeout request headers when set_headers is called.
+
+ response_headers
+ Set of headers to send to downstream, use id, audience, subject, timeout, idling-timeout, rolling-timeout, absolute-timeout. E.g. { "id", "timeout" } will set Session-Id and Session-Timeout response headers when set_headers is called.
+
+ storage
+ Storage is responsible of storing session data, use nil or "cookie" (data is stored in cookie), "dshm", "file", "memcached", "mysql", "postgres", "redis", or "shm", or give a name of custom module ("custom-storage"), or a table that implements session storage interface (defaults to nil)
+
+ dshm
+ Configuration for dshm storage, e.g. { prefix = "sessions" }
+
+ file
+ Configuration for file storage, e.g. { path = "/tmp", suffix = "session" }
+
+ memcached
+ Configuration for memcached storage, e.g. { prefix = "sessions" }
+
+ mysql
+ Configuration for MySQL / MariaDB storage, e.g. { database = "sessions" }
+
+ postgres
+ Configuration for Postgres storage, e.g. { database = "sessions" }
+
+ redis
+ Configuration for Redis / Redis Sentinel / Redis Cluster storages, e.g. { prefix = "sessions" }
+
+ shm
+ Configuration for shared memory storage, e.g. { zone = "sessions" }
+
+ custom
+ -storage"] Custom storage (loaded with require "custom-storage") configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.init ([configuration])
+
+
+ Initialize the session library.
+
+ This function can be called on init or init_worker phases on OpenResty
+ to set global default configuration to all session instances created by this
+ library.
+
+
+
Parameters:
+
+
+
+
+
+ Usage:
+ require "resty.session" .init({
+ audience = "my-application" ,
+ storage = "redis" ,
+ redis = {
+ username = "session" ,
+ password = "storage" ,
+ },
+})
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a new session.
+
+ This creates a new session instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ session instance
+
+
+
+
+ Usage:
+ local session = require "resty.session" .new()
+local session = require "resty.session" .new({
+ audience = "my-application" ,
+})
+
+
+
+
+
+
+
+
+
+ module.open ([configuration])
+
+
+ Open a session.
+
+ This can be used to open a session, and it will either return an existing
+ session or a new session.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ session instance
+
+ string
+ error message
+
+ boolean
+ true, if session existed, otherwise false
+
+
+
+
+ Usage:
+ local session = require "resty.session" .open()
+local session, err, exists = require "resty.session" .open({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ module.start ([configuration])
+
+
+ Start a session and refresh it as needed.
+
+ This can be used to start a session, and it will either return an existing
+ session or a new session. In case there is an existing session, the
+ session will be refreshed as well (as needed).
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ session instance
+
+ string
+ error message
+
+ boolean
+ true, if session existed, otherwise false
+
+ boolean
+ true, if session was refreshed, otherwise false
+
+
+
+
+ Usage:
+ local session = require "resty.session" .start()
+local session, err, exists, refreshed = require "resty.session" .start({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ module.logout ([configuration])
+
+
+ Logout a session.
+
+ It logouts from a specific audience.
+
+ A single session cookie may be shared between multiple audiences
+ (or applications), thus there is a need to be able to logout from
+ just a single audience while keeping the session for the other
+ audiences.
+
+ When there is only a single audience, then this can be considered
+ equal to session.destroy .
+
+ When the last audience is logged out, the cookie will be destroyed
+ as well and invalidated on a client.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ boolean
+ true session exists for an audience and was logged out successfully, otherwise false
+
+ string
+ error message
+
+ boolean
+ true if session existed, otherwise false
+
+ boolean
+ true if session was logged out, otherwise false
+
+
+
+
+ Usage:
+ require "resty.session" .logout()
+local ok, err, exists, logged_out = require "resty.session" .logout({
+ audience = "my-application" ,
+})
+
+
+
+
+
+ module.destroy ([configuration])
+
+
+ Destroy a session.
+
+ It destroys the whole session and clears the cookies.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ boolean
+ true session exists and was destroyed successfully, otherwise nil
+
+ string
+ error message
+
+ boolean
+ true if session existed, otherwise false
+
+ boolean
+ true if session was destroyed, otherwise false
+
+
+
+
+ Usage:
+ require "resty.session" .destroy()
+local ok, err, exists, destroyed = require "resty.session" .destroy({
+ cookie_name = "auth" ,
+})
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.memcached.html b/docs/modules/resty.session.memcached.html
new file mode 100644
index 000000000..6fd39b8fb
--- /dev/null
+++ b/docs/modules/resty.session.memcached.html
@@ -0,0 +1,400 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.memcached
+
Memcached backend for session library
+
+
+
+
+
+
+
+ configuration
+ Distributed shared memory storage backend configuration
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Distributed shared memory storage backend configuration
+
+
+ Fields:
+
+ prefix
+ Prefix for the keys stored in memcached.
+
+ suffix
+ Suffix for the keys stored in memcached.
+
+ host
+ The host to connect (defaults to "127.0.0.1").
+
+ port
+ The port to connect (defaults to 11211).
+
+ socket
+ The socket file to connect to (defaults to nil).
+
+ connect_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s connect method.
+
+ send_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s send method.
+
+ read_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s receive method.
+
+ keepalive_timeout
+ Controls the default maximal idle time of the connections in the connection pool.
+
+ pool
+ A custom name for the connection pool being used.
+
+ pool_size
+ The size of the connection pool.
+
+ backlog
+ A queue size to use when the connection pool is full (configured with @pool_size).
+
+ ssl
+ Enable SSL (defaults to false).
+
+ ssl_verify
+ Verify server certificate (defaults to nil).
+
+ server_name
+ The server name for the new TLS extension Server Name Indication (SNI).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a memcached storage.
+
+ This creates a new memcached storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ memcached storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ boolean
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.mysql.html b/docs/modules/resty.session.mysql.html
new file mode 100644
index 000000000..1c5a88540
--- /dev/null
+++ b/docs/modules/resty.session.mysql.html
@@ -0,0 +1,480 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.mysql
+
MySQL / MariaDB backend for session library
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Postgres storage backend configuration
+
+
+ Fields:
+
+ host
+ The host to connect (defaults to "127.0.0.1").
+
+ port
+ The port to connect (defaults to 3306).
+
+ socket
+ The socket file to connect to (defaults to nil).
+
+ username
+ The database username to authenticate (defaults to nil).
+
+ password
+ Password for authentication, may be required depending on server configuration.
+
+ charset
+ The character set used on the MySQL connection (defaults to "ascii").
+
+ database
+ The database name to connect.
+
+ table_name
+ Name of database table to which to store session data (defaults to "sessions").
+
+ table_name_meta
+ Name of database meta data table to which to store session meta data (defaults to "sessions_meta").
+
+ max_packet_size
+ The upper limit for the reply packets sent from the MySQL server (defaults to 1 MB).
+
+ connect_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s connect method.
+
+ send_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s send method.
+
+ read_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s receive method.
+
+ keepalive_timeout
+ Controls the default maximal idle time of the connections in the connection pool.
+
+ pool
+ A custom name for the connection pool being used.
+
+ pool_size
+ The size of the connection pool.
+
+ backlog
+ A queue size to use when the connection pool is full (configured with @pool_size).
+
+ ssl
+ Enable SSL (defaults to false).
+
+ ssl_verify
+ Verify server certificate (defaults to nil).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a MySQL / MariaDB storage.
+
+ This creates a new MySQL / MariaDB storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ mysql/mariadb storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ boolean
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+ sessions
+
+
+ Sessions table.
+
+ Database table that stores session data.
+
+
+
+
+
+
+
Usage:
+ CREATE TABLE IF NOT EXISTS sessions (
+ sid CHAR(43 ) PRIMARY KEY,
+ name VARCHAR(255 ),
+ data MEDIUMTEXT,
+ exp DATETIME,
+ INDEX (exp)
+) CHARACTER SET ascii;
+
+
+
+
+
+ metadata
+
+
+ Sessions metadata table.
+
+ This is only needed if you want to store session metadata.
+
+
+
+
+
+
+
Usage:
+ CREATE TABLE IF NOT EXISTS sessions_meta (
+ aud VARCHAR(255 ),
+ sub VARCHAR(255 ),
+ sid CHAR(43 ),
+ PRIMARY KEY (aud, sub, sid),
+ CONSTRAINT FOREIGN KEY (sid) REFERENCES sessions(sid) ON DELETE CASCADE ON UPDATE CASCADE
+) CHARACTER SET ascii;
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.postgres.html b/docs/modules/resty.session.postgres.html
new file mode 100644
index 000000000..89e4935e8
--- /dev/null
+++ b/docs/modules/resty.session.postgres.html
@@ -0,0 +1,476 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.postgres
+
Postgres backend for session library.
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Postgres storage backend configuration
+
+
+ Fields:
+
+ host
+ The host to connect (defaults to "127.0.0.1").
+
+ port
+ The port to connect (defaults to 5432).
+
+ application
+ Set the name of the connection as displayed in pg_stat_activity (defaults to "pgmoon").
+
+ username
+ The database username to authenticate (defaults to "postgres").
+
+ password
+ Password for authentication, may be required depending on server configuration.
+
+ database
+ The database name to connect.
+
+ table_name
+ Name of database table to which to store session data (can be database schema prefixed) (defaults to "sessions").
+
+ table_name_meta
+ Name of database meta data table to which to store session meta data (can be database schema prefixed) (defaults to "sessions_meta").
+
+ connect_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s connect method.
+
+ send_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s send method.
+
+ read_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s receive method.
+
+ keepalive_timeout
+ Controls the default maximal idle time of the connections in the connection pool.
+
+ pool
+ A custom name for the connection pool being used.
+
+ pool_size
+ The size of the connection pool.
+
+ backlog
+ A queue size to use when the connection pool is full (configured with @pool_size).
+
+ ssl
+ Enable SSL (defaults to false).
+
+ ssl_verify
+ Verify server certificate (defaults to nil).
+
+ ssl_required
+ Abort the connection if the server does not support SSL connections (defaults to nil).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a Postgres storage.
+
+ This creates a new Postgres storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ postgres storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ boolean
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+ sessions
+
+
+ Sessions table.
+
+ Database table that stores session data.
+
+
+
+
+
+
+
Usage:
+ CREATE TABLE IF NOT EXISTS sessions (
+ sid TEXT PRIMARY KEY,
+ name TEXT,
+ data TEXT,
+ exp TIMESTAMP WITH TIME ZONE
+);
+CREATE INDEX ON sessions (exp);
+
+
+
+
+
+ metadata
+
+
+ Sessions metadata table.
+
+ This is only needed if you want to store session metadata.
+
+
+
+
+
+
+
Usage:
+ CREATE TABLE IF NOT EXISTS sessions_meta (
+ aud TEXT,
+ sub TEXT,
+ sid TEXT REFERENCES sessions (sid) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY (aud, sub, sid)
+);
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.redis-cluster.html b/docs/modules/resty.session.redis-cluster.html
new file mode 100644
index 000000000..04b67b020
--- /dev/null
+++ b/docs/modules/resty.session.redis-cluster.html
@@ -0,0 +1,413 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.redis-cluster
+
Redis Cluster backend for session library
+
+
+
+
+
+
+
+ configuration
+ Redis Cluster storage backend configuration
+
+
+ nodes
+ Cluster Nodes
+
+
+ node
+ Cluster Node
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Redis Cluster storage backend configuration
+
+
+ Fields:
+
+ prefix
+ prefix for the keys stored in redis
+
+ suffix
+ suffix for the keys stored in redis
+
+ name
+ redis cluster name
+
+ nodes
+ redis cluster nodes
+
+ lock_zone
+ shared dictionary name for locks
+
+ lock_prefix
+ shared dictionary name prefix for lock
+
+ max_redirections
+ maximum retry attempts for redirection
+
+ max_connection_attempts
+ maximum retry attempts for connection
+
+ max_connection_timeout
+ maximum connection timeout in total among the retries
+
+ username
+ the database username to authenticate
+
+ password
+ password for authentication
+
+ connect_timeout
+ controls the default timeout value used in TCP/unix-domain socket object’s connect method
+
+ send_timeout
+ controls the default timeout value used in TCP/unix-domain socket object’s send method
+
+ read_timeout
+ controls the default timeout value used in TCP/unix-domain socket object’s receive method
+
+ keepalive_timeout
+ controls the default maximal idle time of the connections in the connection pool
+
+ pool
+ a custom name for the connection pool being used.
+
+ pool_size
+ the size of the connection pool,
+
+ backlog
+ a queue size to use when the connection pool is full (configured with @pool_size)
+
+ ssl
+ enable ssl (defaults to false)
+
+ ssl_verify
+ verify server certificate (defaults to nil)
+
+ server_name
+ the server name for the new TLS extension Server Name Indication (SNI)
+
+
+
+
+
+
+
+
+
+
+ nodes
+
+
+ Cluster Nodes An array of cluster nodes.
+
+
+
+
+
+
+
+
+
+
+ node
+
+
+ Cluster Node
+
+
+ Fields:
+
+ ip
+ the ip address to connect (defaults to "127.0.0.1")
+
+ port
+ the port to connect (defaults to 6379)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a Redis Cluster storage.
+
+ This creates a new Redis Cluster storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ redis cluster storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+
+ remember
+ table
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2022-12-23 14:06:58
+
+
+
+
diff --git a/docs/modules/resty.session.redis-sentinel.html b/docs/modules/resty.session.redis-sentinel.html
new file mode 100644
index 000000000..6571d7a88
--- /dev/null
+++ b/docs/modules/resty.session.redis-sentinel.html
@@ -0,0 +1,410 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.redis-sentinel
+
Redis Sentinel backend for session library
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Redis Sentinel storage backend configuration
+
+
+ Fields:
+
+ prefix
+ prefix for the keys stored in redis
+
+ suffix
+ suffix for the keys stored in redis
+
+ master
+ name of master
+
+ role
+ "master" or "slave"
+
+ sentinels
+ redis sentinels
+
+ sentinel_username
+ optional sentinel username
+
+ sentinel_password
+ optional sentinel password
+
+ username
+ the database username to authenticate
+
+ password
+ password for authentication
+
+ database
+ the database to connect
+
+ connect_timeout
+ controls the default timeout value used in TCP/unix-domain socket object’s connect method
+
+ send_timeout
+ controls the default timeout value used in TCP/unix-domain socket object’s send method
+
+ read_timeout
+ controls the default timeout value used in TCP/unix-domain socket object’s receive method
+
+ keepalive_timeout
+ controls the default maximal idle time of the connections in the connection pool
+
+ pool
+ a custom name for the connection pool being used.
+
+ pool_size
+ the size of the connection pool,
+
+ backlog
+ a queue size to use when the connection pool is full (configured with @pool_size)
+
+ ssl
+ enable ssl (defaults to false)
+
+ ssl_verify
+ verify server certificate (defaults to nil)
+
+ server_name
+ the server name for the new TLS extension Server Name Indication (SNI)
+
+
+
+
+
+
+
+
+
+
+ sentinels
+
+
+ Sentinels An array of sentinels.
+
+
+
+
+
+
+
+
+
+
+ sentinel
+
+
+ Sentinel
+
+
+ Fields:
+
+ host
+ the host to connect
+
+ port
+ the port to connect
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a Redis Sentinel storage.
+
+ This creates a new Redis Sentinel storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ redis sentinel storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+
+ remember
+ table
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2022-12-23 14:06:58
+
+
+
+
diff --git a/docs/modules/resty.session.redis.cluster.html b/docs/modules/resty.session.redis.cluster.html
new file mode 100644
index 000000000..9b569b0cc
--- /dev/null
+++ b/docs/modules/resty.session.redis.cluster.html
@@ -0,0 +1,463 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.redis.cluster
+
Redis Cluster backend for session library
+
+
+
+
+
+
+
+ configuration
+ Redis Cluster storage backend configuration
+
+
+ nodes
+ Cluster Nodes
+
+
+ node
+ Cluster Node
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Redis Cluster storage backend configuration
+
+
+ Fields:
+
+ prefix
+ Prefix for the keys stored in redis.
+
+ suffix
+ Suffix for the keys stored in redis.
+
+ name
+ Redis cluster name.
+
+ nodes
+ Redis cluster nodes.
+
+ lock_zone
+ Shared dictionary name for locks.
+
+ lock_prefix
+ Shared dictionary name prefix for lock.
+
+ max_redirections
+ Maximum retry attempts for redirection.
+
+ max_connection_attempts
+ Maximum retry attempts for connection.
+
+ max_connection_timeout
+ Maximum connection timeout in total among the retries.
+
+ username
+ The database username to authenticate.
+
+ password
+ Password for authentication.
+
+ connect_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s connect method.
+
+ send_timeout
+ controls The default timeout value used in TCP/unix-domain socket object’s send method.
+
+ read_timeout
+ controls The default timeout value used in TCP/unix-domain socket object’s receive method.
+
+ keepalive_timeout
+ Controls the default maximal idle time of the connections in the connection pool.
+
+ pool
+ A custom name for the connection pool being used.
+
+ pool_size
+ The size of the connection pool.
+
+ backlog
+ A queue size to use when the connection pool is full (configured with @pool_size).
+
+ ssl
+ Enable SSL (defaults to false).
+
+ ssl_verify
+ Verify server certificate (defaults to nil).
+
+ server_name
+ The server name for the new TLS extension Server Name Indication (SNI).
+
+
+
+
+
+
+
+
+
+
+ nodes
+
+
+ Cluster Nodes An array of cluster nodes.
+
+
+
+
+
+
+
+
+
+
+ node
+
+
+ Cluster Node
+
+
+ Fields:
+
+ ip
+ The IP address to connect (defaults to "127.0.0.1").
+
+ port
+ The port to connect (defaults to 6379).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a Redis Cluster storage.
+
+ This creates a new Redis Cluster storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ redis cluster storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ boolean
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.redis.common.html b/docs/modules/resty.session.redis.common.html
new file mode 100644
index 000000000..c633c0362
--- /dev/null
+++ b/docs/modules/resty.session.redis.common.html
@@ -0,0 +1,321 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.redis.common
+
Common Redis functions shared between Redis,
+ Redis Cluster and Redis Sentinel implementations.
+
+
+
+
+
+
+
+ module.SET (storage, red, name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ module.GET (storage, red, name, key)
+ Retrieve session data.
+
+
+ module.UNLINK (storage, red, name, key, current_time[, metadata])
+ Delete session data.
+
+
+ module.READ_METADATA (storage, red, name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+ module.SET (storage, red, name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ storage
+ table
+ the storage
+
+ red
+ table
+ the redis instance
+
+ name
+ string
+ the cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ table
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ module.GET (storage, red, name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ storage
+ table
+ the storage
+
+ red
+ table
+ the redis instance
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ module.UNLINK (storage, red, name, key, current_time[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ storage
+ table
+ the storage
+
+ red
+ table
+ the redis instance
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ module.READ_METADATA (storage, red, name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ storage
+ table
+ the storage
+
+ red
+ table
+ the redis instance
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.redis.html b/docs/modules/resty.session.redis.html
new file mode 100644
index 000000000..788f9873c
--- /dev/null
+++ b/docs/modules/resty.session.redis.html
@@ -0,0 +1,409 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.redis
+
Redis backend for session library
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Redis storage backend configuration
+
+
+ Fields:
+
+ prefix
+ Prefix for the keys stored in Redis.
+
+ suffix
+ Suffix for the keys stored in Redis.
+
+ host
+ The host to connect (defaults to "127.0.0.1").
+
+ port
+ The port to connect (defaults to 6379).
+
+ socket
+ The socket file to connect to (defaults to nil).
+
+ username
+ The database username to authenticate.
+
+ password
+ Password for authentication.
+
+ database
+ The database to connect.
+
+ connect_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s connect method.
+
+ send_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s send method.
+
+ read_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s receive method.
+
+ keepalive_timeout
+ Controls the default maximal idle time of the connections in the connection pool.
+
+ pool
+ A custom name for the connection pool being used.
+
+ pool_size
+ The size of the connection pool.
+
+ backlog
+ A queue size to use when the connection pool is full (configured with @pool_size).
+
+ ssl
+ Enable SSL (defaults to false).
+
+ ssl_verify
+ Verify server certificate (defaults to nil).
+
+ server_name
+ The server name for the new TLS extension Server Name Indication (SNI).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a Redis storage.
+
+ This creates a new Redis storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ redis storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ boolean
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.redis.sentinel.html b/docs/modules/resty.session.redis.sentinel.html
new file mode 100644
index 000000000..04db82164
--- /dev/null
+++ b/docs/modules/resty.session.redis.sentinel.html
@@ -0,0 +1,460 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.redis.sentinel
+
Redis Sentinel backend for session library
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Redis Sentinel storage backend configuration
+
+
+ Fields:
+
+ prefix
+ Prefix for the keys stored in redis.
+
+ suffix
+ Suffix for the keys stored in redis.
+
+ master
+ Name of master.
+
+ role
+ "master" or "slave".
+
+ sentinels
+ Redis Sentinels.
+
+ sentinel_username
+ Optional sentinel username.
+
+ sentinel_password
+ Optional sentinel password.
+
+ username
+ The database username to authenticate.
+
+ password
+ Password for authentication.
+
+ database
+ The database to connect.
+
+ connect_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s connect method.
+
+ send_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s send method.
+
+ read_timeout
+ Controls the default timeout value used in TCP/unix-domain socket object’s receive method.
+
+ keepalive_timeout
+ Controls the default maximal idle time of the connections in the connection pool.
+
+ pool
+ A custom name for the connection pool being used.
+
+ pool_size
+ The size of the connection pool.
+
+ backlog
+ A queue size to use when the connection pool is full (configured with @pool_size).
+
+ ssl
+ Enable SSK (defaults to false).
+
+ ssl_verify
+ Verify server certificate (defaults to nil).
+
+ server_name
+ The server name for the new TLS extension Server Name Indication (SNI).
+
+
+
+
+
+
+
+
+
+
+ sentinels
+
+
+ Sentinels An array of sentinels.
+
+
+
+
+
+
+
+
+
+
+ sentinel
+
+
+ Sentinel
+
+
+ Fields:
+
+ host
+ The host to connect.
+
+ port
+ The port to connect.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a Redis Sentinel storage.
+
+ This creates a new Redis Sentinel storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ redis sentinel storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ table
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.shm.html b/docs/modules/resty.session.shm.html
new file mode 100644
index 000000000..91acef2db
--- /dev/null
+++ b/docs/modules/resty.session.shm.html
@@ -0,0 +1,364 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.shm
+
Shared Memory (SHM) backend for session library
+
+
+
+
+
+
+
+ configuration
+ Shared memory storage backend configuration
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+ Store session data.
+
+
+ instance:get (name, key)
+ Retrieve session data.
+
+
+ instance:delete (name, key[, metadata])
+ Delete session data.
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+ Read session metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration
+
+
+ Shared memory storage backend configuration
+
+
+ Fields:
+
+ prefix
+ Prefix for the keys stored in SHM.
+
+ suffix
+ Suffix for the keys stored in SHM.
+
+ zone
+ A name of shared memory zone (defaults to sessions).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ module.new ([configuration])
+
+
+ Create a SHM storage.
+
+ This creates a new shared memory storage instance.
+
+
+
Parameters:
+
+
+ Returns:
+
+
+ table
+ shm storage instance
+
+
+
+
+
+
+
+
+
+
+
+
+ instance:set (name, key, value, ttl, current_time[, old_key], stale_ttl[, metadata], remember)
+
+
+ Store session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ value
+ string
+ session value
+
+ ttl
+ number
+ session ttl
+
+ current_time
+ number
+ current time
+
+ old_key
+ string
+ old session id
+ (optional )
+
+ stale_ttl
+ string
+ stale ttl
+
+ metadata
+ table
+ table of metadata
+ (optional )
+
+ remember
+ boolean
+ whether storing persistent session or not
+
+
+
+ Returns:
+
+
+ true or nil
+ ok
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:get (name, key)
+
+
+ Retrieve session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+
+
+ Returns:
+
+
+ string or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:delete (name, key[, metadata])
+
+
+ Delete session data.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ key
+ string
+ session key
+
+ metadata
+ table
+ session meta data
+ (optional )
+
+
+
+ Returns:
+
+
+ boolean or nil
+ session data
+
+ string
+ error message
+
+
+
+
+
+
+
+
+ instance:read_metadata (name, audience, subject, current_time)
+
+
+ Read session metadata.
+
+
+ Parameters:
+
+ name
+ string
+ cookie name
+
+ audience
+ string
+ session key
+
+ subject
+ string
+ session key
+
+ current_time
+ number
+ current time
+
+
+
+ Returns:
+
+
+ table or nil
+ session metadata
+
+ string
+ error message
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/docs/modules/resty.session.utils.html b/docs/modules/resty.session.utils.html
new file mode 100644
index 000000000..4e5befb72
--- /dev/null
+++ b/docs/modules/resty.session.utils.html
@@ -0,0 +1,1370 @@
+
+
+
+
+ Session Library for OpenResty Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
resty.session
+
+
+
+
Contents
+
+
+
+
Modules
+
+
+
+
+
+
+
Module resty.session.utils
+
Common utilities for session library and storage backends
+
+
+
+
+
+
+
+ is_fips_mode ()
+ Returns whether OpenSSL is in FIPS-mode.
+
+
+ bpack (size, value)
+ Binary pack unsigned integer.
+
+
+ bunpack (size, value)
+ Binary unpack unsigned integer.
+
+
+ trim (value)
+ Trim whitespace from the start and from the end of string.
+
+
+ encode_json (value)
+ JSON encode value.
+
+
+ decode_json (value)
+ JSON decode value.
+
+
+ encode_base64url (value)
+ Base64 URL encode value.
+
+
+ decode_base64url (value)
+ Base64 URL decode value
+
+
+ base64_size (size)
+ Base64 size from original size (without padding).
+
+
+ deflate (data)
+ Compress the data with deflate algorithm.
+
+
+ inflate (data)
+ Decompress the data compressed with deflate algorithm.
+
+
+ rand_bytes (length)
+ Generate crypto random bytes.
+
+
+ sha256 (value)
+ Calculates SHA-256 hash of the value.
+
+
+ derive_hkdf_sha256 (ikm, nonce, usage, size)
+ Derive a new key using HKDF with SHA-256.
+
+
+ derive_pbkdf2_hmac_sha256 (pass, salt, usage, size, iterations)
+ Derive a new key using PBKDF2 with SHA-256.
+
+
+ derive_aes_gcm_256_key_and_iv (ikm, nonce[, safety])
+ Derive a new AES-256 GCM-mode key and initialization vector.
+
+
+ derive_hmac_sha256_key (ikm, nonce)
+ Derive HMAC SHA-256 key for message authentication using HDKF with SHA-256,
+ except on FIPS-mode it uses PBKDF2 with SHA-256 (single iteration).
+
+
+ encrypt_aes_256_gcm (key, iv, plaintext, aad)
+ Encrypt plain text using AES-256 in GCM-mode.
+
+
+ decrypt_aes_256_gcm (key, iv, plaintext, aad, tag)
+ Decrypt ciphertext using AES-256 in GCM-mode.
+
+
+ hmac_sha256 (key, value)
+ Calculate message authentication code with HMAC with SHA-256.
+
+
+ load_storage (storage[, configuration])
+ Loads session storage and creates a new instance using session configuration.
+
+
+ errmsg ([err], msg, ...)
+ Helper to format error messages.
+
+
+ get_name (storage, name, key, subject)
+ Helper to create a delimited key.
+
+
+ set_flag (flags, flag)
+ Helper to turn on a flag.
+
+
+ unset_flag (flags, flag)
+ Helper to turn off a flag.
+
+
+ has_flag (flags, flag)
+ Helper to check if flag is enabled.
+
+
+ meta_get_value (key, exp)
+ Helper to get the value used to store metadata for a certain aud and sub
+ Empty exp means the session id has been invalidated
+
+
+ meta_get_next (val, index)
+ Function to extract the next key and exp from a serialized
+ metadata list, starting from index
+
+
+ meta_get_latest (sessions)
+ Function to filter out the latest valid key:exp from a
+ serialized list, used to store session metadata
+
+
+
+
+
+
+
+
+
+
+
+
+ is_fips_mode ()
+
+
+ Returns whether OpenSSL is in FIPS-mode.
+
+
+
+ Returns:
+
+
+ boolean
+ true if OpenSSL is in FIPS-mode, otherwise false
+
+
+
+
+ Usage:
+ local is_fips = require "resty.session.utils" .is_fips_mode()
+
+
+
+
+
+ bpack (size, value)
+
+
+ Binary pack unsigned integer.
+
+ Returns binary packed version of an integer in little endian unsigned format.
+
+ Size can be:
+
+
+1, pack input as a little endian unsigned char (<C)
+2, pack input as a little endian unsigned short (<S)
+3, pack input as a little endian unsigned integer (truncated) (<I)
+4, pack input as a little endian unsigned integer (<I)
+5, pack input as a little endian unsigned long (truncated) (<L)
+6, pack input as a little endian unsigned long (truncated) (<L)
+7, pack input as a little endian unsigned long (truncated) (<L)
+8, pack input as a little endian unsigned long (<L)
+
+
+
+
+
+ Parameters:
+
+ size
+ number
+ size of binary packed output
+
+ value
+ number
+ value to binary pack
+
+
+
+ Returns:
+
+
+ string
+ binary packed value
+
+
+
+
+ Usage:
+ local packed_128 = require "resty.session.utils" .bpack(1 , 128 )
+local packed_now = require "resty.session.utils" .bpack(8 , ngx.time())
+
+
+
+
+
+ bunpack (size, value)
+
+
+ Binary unpack unsigned integer.
+
+ Returns number from a little endian unsigned binary packed format.
+
+ Size can be:
+
+
+1, unpack input from little endian unsigned char (<C)
+2, unpack input from little endian unsigned short (<S)
+3, unpack input from little endian unsigned integer (truncated) (<I)
+4, unpack input from little endian unsigned integer (<I)
+5, unpack input from little endian unsigned integer (truncated) (<L)
+6, unpack input from little endian unsigned integer (truncated) (<L)
+7, unpack input from little endian unsigned integer (truncated) (<L)
+8, unpack input from little endian unsigned long (<L)
+
+
+
+
+
+ Parameters:
+
+ size
+ number
+ size of binary packed output
+
+ value
+ number
+ value to binary pack
+
+
+
+ Returns:
+
+
+ number
+ binary unpacked value
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local value = 128
+local packed_value = utils.bpack(1 , value)
+local unpacked_value = utils.bunpack(1 , packed_value)
+print (value == unpacked_value)
+
+
+
+
+
+ trim (value)
+
+
+ Trim whitespace from the start and from the end of string.
+
+ Characters that are trimmed:
+
+
+space " "
+tab "\t"
+carriage return "\r"
+line feed "\n"
+vertical tab "\v"
+form feed "\f"
+
+
+
+
+
+ Parameters:
+
+ value
+ string
+ string to trim
+
+
+
+ Returns:
+
+
+ string
+ a whitespace trimmed string
+
+
+
+
+ Usage:
+ local trimmed = require "resty.session.utils" .trim(" hello world " )
+
+
+
+
+
+ encode_json (value)
+
+
+ JSON encode value.
+
+
+ Parameters:
+
+ value
+ any
+ value to json encode
+
+
+
+ Returns:
+
+
+ string
+ json encoded value
+
+
+
+
+ Usage:
+ local json = require "resty.session.utils" .encode_json({ hello = "world" })
+
+
+
+
+
+ decode_json (value)
+
+
+ JSON decode value.
+
+
+ Parameters:
+
+ value
+ string
+ string to json decode
+
+
+
+ Returns:
+
+
+ any
+ json decoded value
+
+
+
+
+ Usage:
+ local tbl = require "resty.session.utils" .decode_json('{ "hello": "world" }' )
+
+
+
+
+
+ encode_base64url (value)
+
+
+ Base64 URL encode value.
+
+
+ Parameters:
+
+ value
+ string
+ string to base64 url encode
+
+
+
+ Returns:
+
+
+ string
+ base64 url encoded value
+
+
+
+
+ Usage:
+ local encoded = require "resty.session.utils" .encode_base64url("test" )
+
+
+
+
+
+ decode_base64url (value)
+
+
+ Base64 URL decode value
+
+
+ Parameters:
+
+ value
+ string
+ string to base64 url decode
+
+
+
+ Returns:
+
+
+ string
+ base64 url decoded value
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local encoded = utils.encode_base64url("test" )
+local decoded = utils.decode_base64url(encoded)
+
+
+
+
+
+ base64_size (size)
+
+
+ Base64 size from original size (without padding).
+
+
+ Parameters:
+
+ size
+ number
+ original size
+
+
+
+ Returns:
+
+
+ number
+ base64 url encoded size without padding
+
+
+
+
+ Usage:
+ local test = "test"
+local b64len = require "resty.session.utils" .base64_size(#test)
+
+
+
+
+
+ deflate (data)
+
+
+ Compress the data with deflate algorithm.
+
+
+ Parameters:
+
+ data
+ string
+ data to deflate
+
+
+
+ Returns:
+
+
+ string
+ deflated data
+
+
+
+
+ Usage:
+ local test = "test"
+local deflated = require "resty.session.utils" .deflate(("a" ):rep(100 ))
+
+
+
+
+
+ inflate (data)
+
+
+ Decompress the data compressed with deflate algorithm.
+
+
+ Parameters:
+
+ data
+ string
+ data to inflate
+
+
+
+ Returns:
+
+
+ string
+ inflated data
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local deflated = utils.deflate(("a" ):rep(100 ))
+local inflated = utils.inflate(deflated)
+
+
+
+
+
+ rand_bytes (length)
+
+
+ Generate crypto random bytes.
+
+
+ Parameters:
+
+ length
+ number
+ how many bytes of random data to generate
+
+
+
+ Returns:
+
+
+ string or nil
+ random bytes
+
+ string or nil
+ error message
+
+
+
+
+ Usage:
+ local bytes = require "resty.session.utils" .rand_bytes(32 )
+
+
+
+
+
+ sha256 (value)
+
+
+ Calculates SHA-256 hash of the value.
+
+
+ Parameters:
+
+ value
+ string
+ value from which to calculate hash
+
+
+
+ Returns:
+
+
+ string or nil
+ sha-256 hash (32 bytes)
+
+ string or nil
+ error message
+
+
+
+
+ Usage:
+ local hash, err = require "resty.session.utils" .sha256("hello world" )
+
+
+
+
+
+ derive_hkdf_sha256 (ikm, nonce, usage, size)
+
+
+ Derive a new key using HKDF with SHA-256.
+
+
+ Parameters:
+
+ ikm
+ string
+ initial key material
+
+ nonce
+ string
+ nonce
+
+ usage
+ string
+ e.g. "encryption" or "authentication"
+
+ size
+ number
+ how many bytes to return
+
+
+
+ Returns:
+
+
+ string or nil
+ key material
+
+ string or nil
+ error message
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local ikm = utils.rand_bytes(32 )
+local nonce = utils.rand_bytes(32 )
+local key, err = utils.derive_hkdf_sha256(ikm, nonce, "encryption" , 32 )
+
+
+
+
+
+ derive_pbkdf2_hmac_sha256 (pass, salt, usage, size, iterations)
+
+
+ Derive a new key using PBKDF2 with SHA-256.
+
+
+ Parameters:
+
+ pass
+ string
+ password
+
+ salt
+ string
+ salt
+
+ usage
+ string
+ e.g. "encryption" or "authentication"
+
+ size
+ number
+ how many bytes to return
+
+ iterations
+ number
+ how many iterations to run, e.g. 10000
+
+
+
+ Returns:
+
+
+ string or nil
+ key material
+
+ string or nil
+ error message
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local pass = "my-super-secret-password"
+local salt = utils.rand_bytes(32 )
+local key, err = utils.derive_pbkdf2_hmac_sha256(pass, salt, "encryption" , 32 , 10000 )
+
+
+
+
+
+ derive_aes_gcm_256_key_and_iv (ikm, nonce[, safety])
+
+
+ Derive a new AES-256 GCM-mode key and initialization vector.
+
+ Safety can be:
+ nil or "None": key and iv will be derived using HKDF with SHA-256, except on FIPS-mode uses PBKDF2 with SHA-256 (single iteration)
+ Low: key and iv will be derived using PBKDF2 with SHA-256 (1.000 iterations)
+ Medium: key and iv will be derived using PBKDF2 with SHA-256 (10.000 iterations)
+ High: key and iv will be derived using PBKDF2 with SHA-256 (100.000 iterations)
+ * Very High: key and iv will be derived using PBKDF2 with SHA-256 (1.000.000 iterations)
+
+
+
Parameters:
+
+ ikm
+ string
+ initial key material
+
+ nonce
+ string
+ nonce
+
+ safety
+ string
+ safety of key generation
+ (optional )
+
+
+
+ Returns:
+
+
+ string or nil
+ key
+
+ string or nil
+ error message
+
+ string or nil
+ initialization vector
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local ikm = utils.rand_bytes(32 )
+local nonce = utils.rand_bytes(32 )
+local key, err, iv = utils.derive_aes_gcm_256_key_and_iv(ikm, nonce, "Medium" )
+
+
+
+
+
+ derive_hmac_sha256_key (ikm, nonce)
+
+
+ Derive HMAC SHA-256 key for message authentication using HDKF with SHA-256,
+ except on FIPS-mode it uses PBKDF2 with SHA-256 (single iteration).
+
+
+ Parameters:
+
+ ikm
+ string
+ initial key material
+
+ nonce
+ string
+ nonce
+
+
+
+ Returns:
+
+
+ string or nil
+ key
+
+ string or nil
+ error message
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local ikm = utils.rand_bytes(32 )
+local nonce = utils.rand_bytes(32 )
+local key, err = utils.derive_hmac_sha256_key(ikm, nonce)
+
+
+
+
+
+ encrypt_aes_256_gcm (key, iv, plaintext, aad)
+
+
+ Encrypt plain text using AES-256 in GCM-mode.
+
+
+ Parameters:
+
+ key
+ string
+ encryption key
+
+ iv
+ string
+ initialization vector
+
+ plaintext
+ string
+ plain text
+
+ aad
+ string
+ additional authenticated data
+
+
+
+ Returns:
+
+
+ string or nil
+ ciphertext
+
+ string or nil
+ error message
+
+ string or nil
+ authentication tag
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local ikm = utils.rand_bytes(32 )
+local nonce = utils.rand_bytes(32 )
+local key, err, iv = utils.derive_aes_gcm_256_key_and_iv(ikm, nonce)
+local enc, err, tag = utils.encrypt_aes_256_gcm(key, iv, "hello" , "john@doe.com" )
+
+
+
+
+
+ decrypt_aes_256_gcm (key, iv, plaintext, aad, tag)
+
+
+ Decrypt ciphertext using AES-256 in GCM-mode.
+
+
+ Parameters:
+
+ key
+ string
+ encryption key
+
+ iv
+ string
+ initialization vector
+
+ plaintext
+ string
+ plain text
+
+ aad
+ string
+ additional authenticated data
+
+ tag
+ string
+ authentication tag
+
+
+
+ Returns:
+
+
+ string or nil
+ ciphertext
+
+ string or nil
+ error message
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local ikm = utils.rand_bytes(32 )
+local nonce = utils.rand_bytes(32 )
+local key, err, iv = utils.derive_aes_gcm_256_key_and_iv(ikm, nonce)
+local enc, err, tag = utils.encrypt_aes_256_gcm(key, iv, "hello" , "john@doe.com" )
+local out, err = utils.decrypt_aes_256_gcm(key, iv, ciphertext, "john@doe.com" , tag)
+
+
+
+
+
+ hmac_sha256 (key, value)
+
+
+ Calculate message authentication code with HMAC with SHA-256.
+
+
+ Parameters:
+
+
+ Returns:
+
+
+ string or nil
+ message authentication code (32 bytes)
+
+ string or nil
+ error message
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local ikm = utils.rand_bytes(32 )
+local nonce = utils.rand_bytes(32 )
+local key, err = utils.derive_hmac_sha256_key(ikm, nonce)
+local mac, err = utils.hmac_sha256(key, "hello" )
+
+
+
+
+
+ load_storage (storage[, configuration])
+
+
+ Loads session storage and creates a new instance using session configuration.
+
+
+ Parameters:
+
+ storage
+ string
+ name of the storage to load
+
+ configuration
+ table
+ session configuration
+ (optional )
+
+
+
+ Returns:
+
+
+ table or nil
+ instance of session storage
+
+ string or nil
+ error message
+
+
+
+
+ Usage:
+ local postgres = require "resty.session.utils" .load_storage("postgres" , {
+ postgres = {
+ host = "127.0.0.1" ,
+ }
+})
+
+
+
+
+
+ errmsg ([err], msg, ...)
+
+
+ Helper to format error messages.
+
+
+ Parameters:
+
+ err
+ string or nil
+ a possible error coming from underlying library
+ (optional )
+
+ msg
+ string or nil
+ error message
+
+ ...
+ any
+ arguments for formatting the msg
+
+
+
+ Returns:
+
+
+ string
+ formatted error message
+
+
+
+
+ Usage:
+ local utils = require "resty.session.utils"
+local test = "aaaa"
+local data, err = utils.deflate(test)
+if not data then
+ print (utils.errmsg(err, "unable to deflate data '%s'" , test)
+end
+
+
+
+
+
+ get_name (storage, name, key, subject)
+
+
+ Helper to create a delimited key.
+
+
+ Parameters:
+
+ storage
+ table
+ a storage implementation
+
+ name
+ string
+ name
+
+ key
+ string
+ key
+
+ subject
+ string
+ subject
+
+
+
+ Returns:
+
+
+ string
+ formatted and delimited name
+
+
+
+
+
+
+
+
+ set_flag (flags, flag)
+
+
+ Helper to turn on a flag.
+
+
+ Parameters:
+
+ flags
+ number
+ flags on which the flag is applied
+
+ flag
+ number
+ flag that is applied
+
+
+
+ Returns:
+
+
+ number
+ flags with the flag applied
+
+
+
+
+ Usage:
+ local flags = 0x0000
+local FLAG_DOG = 0x001
+flags = utils.set_flag(flags, FLAG_DOG)
+
+
+
+
+
+ unset_flag (flags, flag)
+
+
+ Helper to turn off a flag.
+
+
+ Parameters:
+
+ flags
+ number
+ flags on which the flag is removed
+
+ flag
+ number
+ flag that is removed
+
+
+
+ Returns:
+
+
+ number
+ flags with the flag removed
+
+
+
+
+ Usage:
+ local options = 0x0000
+local FLAG_DOG = 0x001
+flags = utils.set_flag(options, FLAG_DOG)
+flags = utils.unset_flag(options, FLAG_DOG)
+
+
+
+
+
+ has_flag (flags, flag)
+
+
+ Helper to check if flag is enabled.
+
+
+ Parameters:
+
+ flags
+ number
+ flags on which the flag is checked
+
+ flag
+ number
+ flag that is checked
+
+
+
+ Returns:
+
+
+ boolean
+ true if flag has is present, otherwise false
+
+
+
+
+ Usage:
+ local flags = 0x0000
+local FLAG_DOG = 0x001
+local FLAG_BONE = 0x010
+flags = utils.set_flag(flags, FLAG_DOG)
+flags = utils.set_flag(flags, FLAG_BONE)
+print (utils.has_flag(flags, FLAG_BONE)
+
+
+
+
+
+ meta_get_value (key, exp)
+
+
+ Helper to get the value used to store metadata for a certain aud and sub
+ Empty exp means the session id has been invalidated
+
+
+ Parameters:
+
+ key
+ string
+ storage key
+
+ exp
+ string
+ expiration
+
+
+
+ Returns:
+
+
+ string
+ the value to store in the metadata collection
+
+
+
+
+
+
+
+
+ meta_get_next (val, index)
+
+
+ Function to extract the next key and exp from a serialized
+ metadata list, starting from index
+
+
+ Parameters:
+
+ val
+ string
+ list of key:exp;
+
+ index
+ number
+ start index
+
+
+
+ Returns:
+
+
+ key
+ string session id
+
+ err
+ string error
+
+ exp
+ number expiration
+
+ index
+ number|nil index of the cursor
+
+
+
+
+
+
+
+
+ meta_get_latest (sessions)
+
+
+ Function to filter out the latest valid key:exp from a
+ serialized list, used to store session metadata
+
+
+ Parameters:
+
+ sessions
+ string
+ list of key:exp;
+
+
+
+ Returns:
+
+
+ table
+ valid sessions and their exp
+
+
+
+
+
+
+
+
+
+
+
+
+
generated by LDoc 1.4.6
+
Last updated 2023-06-05 17:05:22
+
+
+
+
diff --git a/lib/resty/session.lua b/lib/resty/session.lua
new file mode 100644
index 000000000..c9449e91b
--- /dev/null
+++ b/lib/resty/session.lua
@@ -0,0 +1,2839 @@
+---
+-- Session library.
+--
+-- Session library provides HTTP session management capabilities for OpenResty based
+-- applications, libraries and proxies.
+--
+-- @module resty.session
+
+
+local require = require
+
+
+local table_new = require "table.new"
+local isempty = require "table.isempty"
+local buffer = require "string.buffer"
+local utils = require "resty.session.utils"
+
+
+local clear_request_header = ngx.req.clear_header
+local set_request_header = ngx.req.set_header
+local setmetatable = setmetatable
+local http_time = ngx.http_time
+local tonumber = tonumber
+local select = select
+local assert = assert
+local remove = table.remove
+local header = ngx.header
+local error = error
+local floor = math.ceil
+local time = ngx.time
+local byte = string.byte
+local type = type
+local sub = string.sub
+local fmt = string.format
+local var = ngx.var
+local log = ngx.log
+local max = math.max
+local min = math.min
+
+
+local derive_aes_gcm_256_key_and_iv = utils.derive_aes_gcm_256_key_and_iv
+local derive_hmac_sha256_key = utils.derive_hmac_sha256_key
+local encrypt_aes_256_gcm = utils.encrypt_aes_256_gcm
+local decrypt_aes_256_gcm = utils.decrypt_aes_256_gcm
+local encode_base64url = utils.encode_base64url
+local decode_base64url = utils.decode_base64url
+local load_storage = utils.load_storage
+local encode_json = utils.encode_json
+local decode_json = utils.decode_json
+local base64_size = utils.base64_size
+local hmac_sha256 = utils.hmac_sha256
+local rand_bytes = utils.rand_bytes
+local unset_flag = utils.unset_flag
+local set_flag = utils.set_flag
+local has_flag = utils.has_flag
+local inflate = utils.inflate
+local deflate = utils.deflate
+local bunpack = utils.bunpack
+local errmsg = utils.errmsg
+local sha256 = utils.sha256
+local bpack = utils.bpack
+local trim = utils.trim
+
+
+local NOTICE = ngx.NOTICE
+local WARN = ngx.WARN
+
+
+local KEY_SIZE = 32
+
+
+--[ HEADER ----------------------------------------------------------------------------------------------------] || [ PAYLOAD --]
+--[ Type || Flags || Session ID || Creation Time || Rolling Offset || Data Size || Tag || Idling Offset || Mac ] || [ Data ]
+--[ 1B || 2B || 32B || 5B || 4B || 3B || 16B || 3B || 16B ] || [ *B ]
+
+
+local COOKIE_TYPE_SIZE = 1 -- 1
+local FLAGS_SIZE = 2 -- 3
+local SID_SIZE = 32 -- 35
+local CREATION_TIME_SIZE = 5 -- 40
+local ROLLING_OFFSET_SIZE = 4 -- 44
+local DATA_SIZE = 3 -- 47
+local TAG_SIZE = 16 -- 63
+local IDLING_OFFSET_SIZE = 3 -- 66
+local MAC_SIZE = 16 -- 82
+
+
+local HEADER_TAG_SIZE = COOKIE_TYPE_SIZE + FLAGS_SIZE + SID_SIZE + CREATION_TIME_SIZE + ROLLING_OFFSET_SIZE + DATA_SIZE
+local HEADER_TOUCH_SIZE = HEADER_TAG_SIZE + TAG_SIZE
+local HEADER_MAC_SIZE = HEADER_TOUCH_SIZE + IDLING_OFFSET_SIZE
+local HEADER_SIZE = HEADER_MAC_SIZE + MAC_SIZE
+local HEADER_ENCODED_SIZE = base64_size(HEADER_SIZE)
+
+
+local COOKIE_TYPE = bpack(COOKIE_TYPE_SIZE, 1)
+
+
+local MAX_COOKIE_SIZE = 4096
+local MAX_COOKIES = 9
+local MAX_COOKIES_SIZE = MAX_COOKIES * MAX_COOKIE_SIZE -- 36864 bytes
+local MAX_CREATION_TIME = 2 ^ (CREATION_TIME_SIZE * 8) - 1 -- ~34789 years
+local MAX_ROLLING_OFFSET = 2 ^ (ROLLING_OFFSET_SIZE * 8) - 1 -- ~136 years
+local MAX_IDLING_OFFSET = 2 ^ (IDLING_OFFSET_SIZE * 8) - 1 -- ~194 days
+local MAX_DATA_SIZE = 2 ^ (DATA_SIZE * 8) - 1 -- 16777215 bytes
+local MAX_TTL = 34560000 -- 400 days
+-- see: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-11#section-4.1.2.1
+
+
+local FLAGS_NONE = 0x0000
+local FLAG_STORAGE = 0x0001
+local FLAG_FORGET = 0x0002
+local FLAG_DEFLATE = 0x0010
+
+
+local DEFAULT_AUDIENCE = "default"
+local DEFAULT_SUBJECT
+local DEFAULT_ENFORCE_SAME_SUBJECT = false
+local DEFAULT_META = {}
+local DEFAULT_IKM
+local DEFAULT_IKM_FALLBACKS
+local DEFAULT_HASH_STORAGE_KEY = false
+local DEFAULT_HASH_SUBJECT = false
+local DEFAULT_STORE_METADATA = false
+local DEFAULT_TOUCH_THRESHOLD = 60 -- 1 minute
+local DEFAULT_COMPRESSION_THRESHOLD = 1024 -- 1 kB
+local DEFAULT_REQUEST_HEADERS
+local DEFAULT_RESPONSE_HEADERS
+
+
+local DEFAULT_COOKIE_NAME = "session"
+local DEFAULT_COOKIE_PATH = "/"
+local DEFAULT_COOKIE_SAME_SITE = "Lax"
+local DEFAULT_COOKIE_SAME_PARTY
+local DEFAULT_COOKIE_PRIORITY
+local DEFAULT_COOKIE_PARTITIONED
+local DEFAULT_COOKIE_HTTP_ONLY = true
+local DEFAULT_COOKIE_PREFIX
+local DEFAULT_COOKIE_DOMAIN
+local DEFAULT_COOKIE_SECURE
+
+
+local DEFAULT_REMEMBER_COOKIE_NAME = "remember"
+local DEFAULT_REMEMBER_SAFETY = "Medium"
+local DEFAULT_REMEMBER_META = false
+local DEFAULT_REMEMBER = false
+
+
+local DEFAULT_STALE_TTL = 10 -- 10 seconds
+local DEFAULT_IDLING_TIMEOUT = 900 -- 15 minutes
+local DEFAULT_ROLLING_TIMEOUT = 3600 -- 1 hour
+local DEFAULT_ABSOLUTE_TIMEOUT = 86400 -- 1 day
+local DEFAULT_REMEMBER_ROLLING_TIMEOUT = 604800 -- 1 week
+local DEFAULT_REMEMBER_ABSOLUTE_TIMEOUT = 2592000 -- 30 days
+
+
+local DEFAULT_STORAGE
+
+
+local STATE_NEW = "new"
+local STATE_OPEN = "open"
+local STATE_CLOSED = "closed"
+
+
+local AT_BYTE = byte("@")
+local EQUALS_BYTE = byte("=")
+local SEMICOLON_BYTE = byte(";")
+
+
+local COOKIE_EXPIRE_FLAGS = "; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0"
+
+
+local HEADER_BUFFER = buffer.new(HEADER_SIZE)
+local FLAGS_BUFFER = buffer.new(128)
+local DATA_BUFFER = buffer.new(MAX_COOKIES_SIZE)
+local HIDE_BUFFER = buffer.new(256)
+
+
+local DATA = table_new(2, 0)
+
+
+local HEADERS = {
+ id = "Session-Id",
+ audience = "Session-Audience",
+ subject = "Session-Subject",
+ timeout = "Session-Timeout",
+ idling_timeout = "Session-Idling-Timeout",
+ ["idling-timeout"] = "Session-Idling-Timeout",
+ rolling_timeout = "Session-Rolling-Timeout",
+ ["rolling-timeout"] = "Session-Rolling-Timeout",
+ absolute_timeout = "Session-Absolute-Timeout",
+ ["absolute-timeout"] = "Session-Absolute-Timeout",
+}
+
+
+local function set_response_header(name, value)
+ header[name] = value
+end
+
+
+local function sha256_storage_key(sid)
+ local key, err = sha256(sid)
+ if not key then
+ return nil, errmsg(err, "unable to sha256 hash session id")
+ end
+
+ if SID_SIZE ~= 32 then
+ key = sub(key, 1, SID_SIZE)
+ end
+
+ key, err = encode_base64url(key)
+ if not key then
+ return nil, errmsg(err, "unable to base64url encode session id")
+ end
+
+ return key
+end
+
+
+local function sha256_subject(subject)
+ local hashed_subject, err = sha256(subject)
+ if not hashed_subject then
+ return nil, errmsg(err, "unable to sha256 hash subject")
+ end
+
+ hashed_subject, err = encode_base64url(sub(hashed_subject, 1, 16))
+ if not hashed_subject then
+ return nil, errmsg(err, "unable to base64url encode subject")
+ end
+
+ return hashed_subject
+end
+
+
+local function calculate_mac(ikm, nonce, msg)
+ local mac_key, err = derive_hmac_sha256_key(ikm, nonce)
+ if not mac_key then
+ return nil, errmsg(err, "unable to derive session message authentication key")
+ end
+
+ local mac, err = hmac_sha256(mac_key, msg)
+ if not mac then
+ return nil, errmsg(err, "unable to calculate session message authentication code")
+ end
+
+ if MAC_SIZE ~= 32 then
+ return sub(mac, 1, MAC_SIZE)
+ end
+
+ return mac
+end
+
+
+local function calculate_cookie_chunks(cookie_name_size, data_size)
+ local space_needed = cookie_name_size + 1 + HEADER_ENCODED_SIZE + data_size
+ if space_needed > MAX_COOKIES_SIZE then
+ return nil, "cookie size limit exceeded"
+ end
+
+ if space_needed <= MAX_COOKIE_SIZE then
+ return 1
+ end
+
+ for i = 2, MAX_COOKIES do
+ space_needed = space_needed + cookie_name_size + 2
+ if space_needed > MAX_COOKIES_SIZE then
+ return nil, "cookie size limit exceeded"
+ elseif space_needed <= (MAX_COOKIE_SIZE * i) then
+ return i
+ end
+ end
+
+ return nil, "cookie size limit exceeded"
+end
+
+
+local function merge_cookies(cookies, cookie_name_size, cookie_name, cookie_data)
+ if not cookies then
+ return cookie_data
+ end
+
+ if type(cookies) == "string" then
+ if byte(cookies, cookie_name_size + 1) == EQUALS_BYTE and
+ sub(cookies, 1, cookie_name_size) == cookie_name
+ then
+ return cookie_data
+ end
+
+ return { cookies, cookie_data }
+ end
+
+ if type(cookies) ~= "table" then
+ return nil, "unable to merge session cookies with response cookies"
+ end
+
+ local count = #cookies
+ for i = 1, count do
+ if byte(cookies[i], cookie_name_size + 1) == EQUALS_BYTE and
+ sub(cookies[i], 1, cookie_name_size) == cookie_name
+ then
+ cookies[i] = cookie_data
+ return cookies
+ end
+
+ if i == count then
+ cookies[i+1] = cookie_data
+ return cookies
+ end
+ end
+end
+
+
+local function get_store_metadata(self)
+ if not self.store_metadata then
+ return
+ end
+
+ local data = self.data
+ local count = #data
+ if count == 1 then
+ local audience = data[1][2]
+ local subject = data[1][3]
+ if audience and subject then
+ audience = encode_base64url(audience)
+ subject = self.hash_subject(subject)
+ return {
+ audiences = { audience },
+ subjects = { subject },
+ }
+ end
+
+ return
+ end
+
+ local audiences
+ local subjects
+ local index = 0
+ for i = 1, count do
+ local audience = data[i][2]
+ local subject = data[i][3]
+ if audience and subject then
+ audience = encode_base64url(audience)
+ self.hash_subject(subject)
+ if not audiences then
+ audiences = table_new(count, 0)
+ subjects = table_new(count, 0)
+ end
+
+ index = index + 1
+ audiences[index] = audience
+ subjects[index] = subject
+ end
+ end
+
+ if not audiences then
+ return
+ end
+
+ return {
+ audiences = audiences,
+ subjects = subjects,
+ }
+end
+
+
+local function get_property(self, name)
+ if name == "id" then
+ local sid = self.meta.sid
+ if not sid then
+ return
+ end
+
+ return encode_base64url(sid)
+
+ elseif name == "nonce" then
+ return self.meta.sid
+
+ elseif name == "audience" then
+ return self.data[self.data_index][2]
+
+ elseif name == "subject" then
+ return self.data[self.data_index][3]
+
+ elseif name == "timeout" then
+ local timeout
+ local meta = self.meta
+
+ if self.idling_timeout > 0 then
+ timeout = self.idling_timeout - (meta.timestamp - meta.creation_time - meta.rolling_offset - meta.idling_offset)
+ end
+
+ if self.rolling_timeout > 0 then
+ local t = self.rolling_timeout - (meta.timestamp - meta.creation_time - meta.rolling_offset)
+ timeout = timeout and min(t, timeout) or t
+ end
+
+ if self.absolute_timeout > 0 then
+ local t = self.absolute_timeout - (meta.timestamp - meta.creation_time)
+ timeout = timeout and min(t, timeout) or t
+ end
+
+ return timeout
+
+ elseif name == "idling-timeout" or name == "idling_timeout" then
+ local idling_timeout = self.idling_timeout
+ if idling_timeout == 0 then
+ return
+ end
+
+ local meta = self.meta
+ return idling_timeout - (meta.timestamp - meta.creation_time - meta.rolling_offset - meta.idling_offset)
+
+ elseif name == "rolling-timeout" or name == "rolling_timeout" then
+ local rolling_timeout = self.rolling_timeout
+ if rolling_timeout == 0 then
+ return
+ end
+
+ local meta = self.meta
+ return rolling_timeout - (meta.timestamp - meta.creation_time - meta.rolling_offset)
+
+ elseif name == "absolute-timeout" or name == "absolute_timeout" then
+ local absolute_timeout = self.absolute_timeout
+ if absolute_timeout == 0 then
+ return
+ end
+
+ local meta = self.meta
+ return absolute_timeout - (meta.timestamp - meta.creation_time)
+
+ else
+ return self.meta[name]
+ end
+end
+
+
+local function open(self, remember, meta_only)
+ local storage = self.storage
+ local current_time = time()
+ local cookie_name
+ if remember then
+ cookie_name = self.remember_cookie_name
+ else
+ cookie_name = self.cookie_name
+ end
+
+ local cookie = var["cookie_" .. cookie_name]
+ if not cookie then
+ return nil, "missing session cookie"
+ end
+
+ local header_decoded do
+ header_decoded = sub(cookie, 1, HEADER_ENCODED_SIZE)
+ if #header_decoded ~= HEADER_ENCODED_SIZE then
+ return nil, "invalid session header"
+ end
+ local err
+ header_decoded, err = decode_base64url(header_decoded)
+ if not header_decoded then
+ return nil, errmsg(err, "unable to base64url decode session header")
+ end
+ end
+
+ HEADER_BUFFER:set(header_decoded)
+
+ local cookie_type do
+ cookie_type = HEADER_BUFFER:get(COOKIE_TYPE_SIZE)
+ if #cookie_type ~= COOKIE_TYPE_SIZE then
+ return nil, "invalid session cookie type"
+ end
+ if cookie_type ~= COOKIE_TYPE then
+ return nil, "invalid session cookie type"
+ end
+ end
+
+ local flags do
+ flags = HEADER_BUFFER:get(FLAGS_SIZE)
+ if #flags ~= FLAGS_SIZE then
+ return nil, "invalid session flags"
+ end
+
+ flags = bunpack(FLAGS_SIZE, flags)
+
+ if storage then
+ if not has_flag(flags, FLAG_STORAGE) then
+ return nil, "invalid session flags"
+ end
+ elseif has_flag(flags, FLAG_STORAGE) then
+ return nil, "invalid session flags"
+ end
+ end
+
+ local sid do
+ sid = HEADER_BUFFER:get(SID_SIZE)
+ if #sid ~= SID_SIZE then
+ return nil, "invalid session id"
+ end
+ end
+
+ local creation_time do
+ creation_time = HEADER_BUFFER:get(CREATION_TIME_SIZE)
+ if #creation_time ~= CREATION_TIME_SIZE then
+ return nil, "invalid session creation time"
+ end
+
+ creation_time = bunpack(CREATION_TIME_SIZE, creation_time)
+ if not creation_time or creation_time < 0 or creation_time > MAX_CREATION_TIME then
+ return nil, "invalid session creation time"
+ end
+
+ local absolute_elapsed = current_time - creation_time
+ if absolute_elapsed > MAX_ROLLING_OFFSET then
+ return nil, "session lifetime exceeded"
+ end
+
+ if remember then
+ local remember_absolute_timeout = self.remember_absolute_timeout
+ if remember_absolute_timeout ~= 0 then
+ if absolute_elapsed > remember_absolute_timeout then
+ return nil, "session remember absolute timeout exceeded"
+ end
+ end
+
+ else
+ local absolute_timeout = self.absolute_timeout
+ if absolute_timeout ~= 0 then
+ if absolute_elapsed > absolute_timeout then
+ return nil, "session absolute timeout exceeded"
+ end
+ end
+ end
+ end
+
+ local rolling_offset do
+ rolling_offset = HEADER_BUFFER:get(ROLLING_OFFSET_SIZE)
+ if #rolling_offset ~= ROLLING_OFFSET_SIZE then
+ return nil, "invalid session rolling offset"
+ end
+
+ rolling_offset = bunpack(ROLLING_OFFSET_SIZE, rolling_offset)
+ if not rolling_offset or rolling_offset < 0 or rolling_offset > MAX_ROLLING_OFFSET then
+ return nil, "invalid session rolling offset"
+ end
+
+ local rolling_elapsed = current_time - creation_time - rolling_offset
+
+ if remember then
+ local remember_rolling_timeout = self.remember_rolling_timeout
+ if remember_rolling_timeout ~= 0 then
+ if rolling_elapsed > remember_rolling_timeout then
+ return nil, "session remember rolling timeout exceeded"
+ end
+ end
+
+ else
+ local rolling_timeout = self.rolling_timeout
+ if rolling_timeout ~= 0 then
+ if rolling_elapsed > rolling_timeout then
+ return nil, "session rolling timeout exceeded"
+ end
+ end
+ end
+ end
+
+ local data_size do
+ data_size = HEADER_BUFFER:get(DATA_SIZE)
+ if #data_size ~= DATA_SIZE then
+ return nil, "invalid session data size"
+ end
+
+ data_size = bunpack(DATA_SIZE, data_size)
+ if not data_size or data_size < 0 or data_size > MAX_DATA_SIZE then
+ return nil, "invalid session data size"
+ end
+ end
+
+ local tag do
+ tag = HEADER_BUFFER:get(TAG_SIZE)
+ if #tag ~= TAG_SIZE then
+ return nil, "invalid session tag"
+ end
+ end
+
+ local idling_offset do
+ idling_offset = HEADER_BUFFER:get(IDLING_OFFSET_SIZE)
+ if #idling_offset ~= IDLING_OFFSET_SIZE then
+ return nil, "invalid session idling offset"
+ end
+
+ idling_offset = bunpack(IDLING_OFFSET_SIZE, idling_offset)
+ if not idling_offset or idling_offset < 0 or idling_offset > MAX_IDLING_OFFSET then
+ return nil, "invalid session idling offset"
+ end
+
+ if remember then
+ if idling_offset ~= 0 then
+ return nil, "invalid session idling offset"
+ end
+
+ else
+ local idling_timeout = self.idling_timeout
+ if idling_timeout ~= 0 then
+ local idling_elapsed = current_time - creation_time - rolling_offset - idling_offset
+ if idling_elapsed > idling_timeout then
+ return nil, "session idling timeout exceeded"
+ end
+ end
+ end
+ end
+
+ local ikm do
+ ikm = self.ikm
+ local mac = HEADER_BUFFER:get(MAC_SIZE)
+ if #mac ~= MAC_SIZE then
+ return nil, "invalid session message authentication code"
+ end
+
+ local msg = sub(header_decoded, 1, HEADER_MAC_SIZE)
+ local expected_mac, err = calculate_mac(ikm, sid, msg)
+ if mac ~= expected_mac then
+ local fallback_keys = self.ikm_fallbacks
+ if fallback_keys then
+ local count = #fallback_keys
+ if count > 0 then
+ for i = 1, count do
+ ikm = fallback_keys[i]
+ expected_mac, err = calculate_mac(ikm, sid, msg)
+ if mac == expected_mac then
+ break
+ end
+
+ if i == count then
+ return nil, errmsg(err, "invalid session message authentication code")
+ end
+ end
+
+ else
+ return nil, errmsg(err, "invalid session message authentication code")
+ end
+
+ else
+ return nil, errmsg(err, "invalid session message authentication code")
+ end
+ end
+ end
+
+ local data_index = self.data_index
+ local audience = self.data[data_index][2]
+ local initial_chunk, ciphertext, ciphertext_encoded, info_data do
+ if storage then
+ local key, err = self.hash_storage_key(sid)
+ if not key then
+ return nil, err
+ end
+
+ local data, err = storage:get(cookie_name, key, current_time)
+ if not data then
+ return nil, errmsg(err, "unable to load session")
+ end
+
+ data, err = decode_json(data)
+ if not data then
+ return nil, errmsg(err, "unable to json decode session")
+ end
+
+ ciphertext = data[1]
+ ciphertext_encoded = ciphertext
+ info_data = data[2]
+ if info_data then
+ info_data, err = decode_base64url(info_data)
+ if not info_data then
+ return nil, errmsg(err, "unable to base64url decode session info")
+ end
+
+ info_data, err = decode_json(info_data)
+ if not info_data then
+ return nil, errmsg(err, "unable to json decode session info")
+ end
+
+ if not info_data[audience] then
+ info_data[audience] = self.info.data and self.info.data[audience] or nil
+ end
+ end
+
+ else
+ local cookie_chunks, err = calculate_cookie_chunks(#cookie_name, data_size)
+ if not cookie_chunks then
+ return nil, err
+ end
+
+ if cookie_chunks == 1 then
+ initial_chunk = sub(cookie, -data_size)
+ ciphertext = initial_chunk
+
+ else
+ initial_chunk = sub(cookie, HEADER_ENCODED_SIZE + 1)
+ DATA_BUFFER:reset():put(initial_chunk)
+ for i = 2, cookie_chunks do
+ local chunk = var["cookie_" .. cookie_name .. i]
+ if not chunk then
+ return nil, errmsg(err, "missing session cookie chunk")
+ end
+
+ DATA_BUFFER:put(chunk)
+ end
+
+ ciphertext = DATA_BUFFER:get()
+ end
+ end
+
+ if #ciphertext ~= data_size then
+ return nil, "invalid session payload"
+ end
+
+ local err
+ ciphertext, err = decode_base64url(ciphertext)
+ if not ciphertext then
+ return nil, errmsg(err, "unable to base64url decode session data")
+ end
+ end
+
+ if remember then
+ self.remember_meta = {
+ timestamp = current_time,
+ flags = flags,
+ sid = sid,
+ creation_time = creation_time,
+ rolling_offset = rolling_offset,
+ data_size = data_size,
+ idling_offset = idling_offset,
+ ikm = ikm,
+ header = header_decoded,
+ initial_chunk = initial_chunk,
+ ciphertext = ciphertext_encoded,
+ }
+
+ else
+ self.meta = {
+ timestamp = current_time,
+ flags = flags,
+ sid = sid,
+ creation_time = creation_time,
+ rolling_offset = rolling_offset,
+ data_size = data_size,
+ idling_offset = idling_offset,
+ ikm = ikm,
+ header = header_decoded,
+ initial_chunk = initial_chunk,
+ ciphertext = ciphertext_encoded,
+ }
+ end
+
+ if meta_only then
+ return true
+ end
+
+ local aes_key, err, iv
+ if remember then
+ aes_key, err, iv = derive_aes_gcm_256_key_and_iv(ikm, sid, self.remember_safety)
+ else
+ aes_key, err, iv = derive_aes_gcm_256_key_and_iv(ikm, sid)
+ end
+
+ if not aes_key then
+ return nil, errmsg(err, "unable to derive session decryption key")
+ end
+
+ local aad = sub(header_decoded, 1, HEADER_TAG_SIZE)
+ local plaintext, err = decrypt_aes_256_gcm(aes_key, iv, ciphertext, aad, tag)
+ if not plaintext then
+ return nil, errmsg(err, "unable to decrypt session data")
+ end
+
+ local data do
+ if has_flag(flags, FLAG_DEFLATE) then
+ plaintext, err = inflate(plaintext)
+ if not plaintext then
+ return nil, errmsg(err, "unable to inflate session data")
+ end
+ end
+
+ data, err = decode_json(plaintext)
+ if not data then
+ return nil, errmsg(err, "unable to json decode session data")
+ end
+ end
+
+ if storage then
+ self.info.data = info_data
+ end
+
+ local audience_index
+ local count = #data
+ for i = 1, count do
+ if data[i][2] == audience then
+ audience_index = i
+ break
+ end
+ end
+
+ if not audience_index then
+ data[count + 1] = self.data[data_index]
+ self.state = STATE_NEW
+ self.data = data
+ self.data_index = count + 1
+ return nil, "missing session audience", true
+ end
+
+ self.state = STATE_OPEN
+ self.data = data
+ self.data_index = audience_index
+
+ return true
+end
+
+
+local function save(self, state, remember)
+ local cookie_name
+ local meta
+ if remember then
+ cookie_name = self.remember_cookie_name
+ meta = self.remember_meta or {}
+ else
+ cookie_name = self.cookie_name
+ meta = self.meta
+ end
+
+ local cookie_name_size = #cookie_name
+ local storage = self.storage
+ local flags = self.flags
+
+ if storage then
+ flags = set_flag(flags, FLAG_STORAGE)
+ else
+ flags = unset_flag(flags, FLAG_STORAGE)
+ end
+
+ local sid, err = rand_bytes(SID_SIZE)
+ if not sid then
+ return nil, errmsg(err, "unable to generate session id")
+ end
+
+ local current_time = time()
+ local rolling_offset
+
+ local creation_time = meta.creation_time
+ if creation_time then
+ rolling_offset = current_time - creation_time
+ if rolling_offset > MAX_ROLLING_OFFSET then
+ return nil, "session maximum rolling offset exceeded"
+ end
+
+ else
+ creation_time = current_time
+ rolling_offset = 0
+ end
+
+ if creation_time > MAX_CREATION_TIME then
+ -- this should only happen at around year 36759 (most likely a clock problem)
+ return nil, "session maximum creation time exceeded"
+ end
+
+ do
+ local meta_flags = meta.flags
+ if meta_flags and has_flag(meta_flags, FLAG_FORGET) then
+ flags = set_flag(flags, FLAG_FORGET)
+ end
+ end
+
+ local data, data_size, cookie_chunks do
+ data = self.data
+ if self.enforce_same_subject then
+ local count = #data
+ if count > 1 then
+ local subject = data[self.data_index][3]
+ for i = count, 1, -1 do
+ if data[i][3] ~= subject then
+ remove(data, i)
+ end
+ end
+ end
+ end
+
+ data, err = encode_json(data)
+ if not data then
+ return nil, errmsg(err, "unable to json encode session data")
+ end
+
+ data_size = #data
+
+ local compression_threshold = self.compression_threshold
+ if compression_threshold ~= 0 and data_size > compression_threshold then
+ local deflated_data, err = deflate(data)
+ if not deflated_data then
+ log(NOTICE, "[session] unable to deflate session data (", err , ")")
+
+ else
+ if deflated_data then
+ local deflated_size = #deflated_data
+ if deflated_size < data_size then
+ flags = set_flag(flags, FLAG_DEFLATE)
+ data = deflated_data
+ data_size = deflated_size
+ end
+ end
+ end
+ end
+
+ data_size = base64_size(data_size)
+
+ if storage then
+ if data_size > MAX_DATA_SIZE then
+ return nil, "session maximum data size exceeded"
+ end
+
+ cookie_chunks = 1
+ else
+ cookie_chunks, err = calculate_cookie_chunks(cookie_name_size, data_size)
+ if not cookie_chunks then
+ return nil, err
+ end
+ end
+ end
+
+ local idling_offset = 0
+
+ local packed_flags = bpack(FLAGS_SIZE, flags)
+ local packed_data_size = bpack(DATA_SIZE, data_size)
+ local packed_creation_time = bpack(CREATION_TIME_SIZE, creation_time)
+ local packed_rolling_offset = bpack(ROLLING_OFFSET_SIZE, rolling_offset)
+ local packed_idling_offset = bpack(IDLING_OFFSET_SIZE, idling_offset)
+
+ HEADER_BUFFER:reset()
+ HEADER_BUFFER:put(COOKIE_TYPE, packed_flags, sid, packed_creation_time, packed_rolling_offset, packed_data_size)
+
+ local ikm = self.ikm
+ local aes_key, iv
+ if remember then
+ aes_key, err, iv = derive_aes_gcm_256_key_and_iv(ikm, sid, self.remember_safety)
+ else
+ aes_key, err, iv = derive_aes_gcm_256_key_and_iv(ikm, sid)
+ end
+
+ if not aes_key then
+ return nil, errmsg(err, "unable to derive session encryption key")
+ end
+
+ local ciphertext, err, tag = encrypt_aes_256_gcm(aes_key, iv, data, HEADER_BUFFER:tostring())
+ if not ciphertext then
+ return nil, errmsg(err, "unable to encrypt session data")
+ end
+
+ HEADER_BUFFER:put(tag, packed_idling_offset)
+
+ local mac, err = calculate_mac(ikm, sid, HEADER_BUFFER:tostring())
+ if not mac then
+ return nil, err
+ end
+
+ local header_decoded = HEADER_BUFFER:put(mac):get()
+ local header_encoded, err = encode_base64url(header_decoded)
+ if not header_encoded then
+ return nil, errmsg(err, "unable to base64url encode session header")
+ end
+
+ local payload, err = encode_base64url(ciphertext)
+ if not payload then
+ return nil, errmsg(err, "unable to base64url encode session data")
+ end
+
+ local cookies = header["Set-Cookie"]
+ local cookie_flags = self.cookie_flags
+
+ local initial_chunk
+ local ciphertext_encoded
+
+ local remember_flags
+ if remember then
+ local max_age = self.remember_rolling_timeout
+ if max_age == 0 or max_age > MAX_TTL then
+ max_age = MAX_TTL
+ end
+
+ local expires = http_time(creation_time + max_age)
+ remember_flags = fmt("; Expires=%s; Max-Age=%d", expires, max_age)
+ end
+
+ if cookie_chunks == 1 then
+ local cookie_data
+ if storage then
+ ciphertext_encoded = payload
+ if remember then
+ cookie_data = fmt("%s=%s%s%s", cookie_name, header_encoded, cookie_flags, remember_flags)
+ else
+ cookie_data = fmt("%s=%s%s", cookie_name, header_encoded, cookie_flags)
+ end
+
+ else
+ initial_chunk = payload
+ if remember then
+ cookie_data = fmt("%s=%s%s%s%s", cookie_name, header_encoded, payload, cookie_flags, remember_flags)
+ else
+ cookie_data = fmt("%s=%s%s%s", cookie_name, header_encoded, payload, cookie_flags)
+ end
+ end
+
+ cookies, err = merge_cookies(cookies, cookie_name_size, cookie_name, cookie_data)
+ if not cookies then
+ return nil, err
+ end
+
+ else
+ DATA_BUFFER:set(payload)
+
+ initial_chunk = DATA_BUFFER:get(MAX_COOKIE_SIZE - HEADER_ENCODED_SIZE - cookie_name_size - 1)
+
+ local cookie_data
+ if remember then
+ cookie_data = fmt("%s=%s%s%s%s", cookie_name, header_encoded, initial_chunk, cookie_flags, remember_flags)
+ else
+ cookie_data = fmt("%s=%s%s%s", cookie_name, header_encoded, initial_chunk, cookie_flags)
+ end
+
+ cookies, err = merge_cookies(cookies, cookie_name_size, cookie_name, cookie_data)
+ if not cookies then
+ return nil, err
+ end
+
+ for i = 2, cookie_chunks do
+ local name = fmt("%s%d", cookie_name, i)
+ cookie_data = DATA_BUFFER:get(MAX_COOKIE_SIZE - cookie_name_size - 2)
+ if remember then
+ cookie_data = fmt("%s=%s%s%s", name, cookie_data, cookie_flags, remember_flags)
+ else
+ cookie_data = fmt("%s=%s%s", name, cookie_data, cookie_flags)
+ end
+ cookies, err = merge_cookies(cookies, cookie_name_size + 1, name, cookie_data)
+ if not cookies then
+ return nil, err
+ end
+ end
+ end
+
+ if storage then
+ local key, err = self.hash_storage_key(sid)
+ if not key then
+ return nil, err
+ end
+
+ DATA[1] = payload
+
+ local info_data = self.info.data
+ if info_data then
+ info_data, err = encode_json(info_data)
+ if not info_data then
+ return nil, errmsg(err, "unable to json encode session info")
+ end
+
+ info_data, err = encode_base64url(info_data)
+ if not info_data then
+ return nil, errmsg(err, "unable to base64url encode session info")
+ end
+
+ DATA[2] = info_data
+
+ else
+ DATA[2] = nil
+ end
+
+ data, err = encode_json(DATA)
+ if not data then
+ return nil, errmsg(err, "unable to json encode session data")
+ end
+
+ local old_sid = meta.sid
+ local old_key
+ if old_sid then
+ old_key, err = self.hash_storage_key(old_sid)
+ if not old_key then
+ log(WARN, "[session] ", err)
+ end
+ end
+
+ local ttl = remember and self.remember_rolling_timeout or self.rolling_timeout
+ if ttl == 0 or ttl > MAX_TTL then
+ ttl = MAX_TTL
+ end
+
+ local store_metadata = get_store_metadata(self)
+
+ local ok, err = storage:set(cookie_name, key, data, ttl, current_time, old_key, self.stale_ttl, store_metadata, remember)
+ if not ok then
+ return nil, errmsg(err, "unable to store session data")
+ end
+
+ else
+ local old_data_size = meta.data_size
+ if old_data_size then
+ local old_cookie_chunks = calculate_cookie_chunks(cookie_name_size, old_data_size)
+ if old_cookie_chunks and old_cookie_chunks > cookie_chunks then
+ for i = cookie_chunks + 1, old_cookie_chunks do
+ local name = fmt("%s%d", cookie_name, i)
+ local cookie_data = fmt("%s=%s%s", name, cookie_flags, COOKIE_EXPIRE_FLAGS)
+ cookies, err = merge_cookies(cookies, cookie_name_size + 1, name, cookie_data)
+ if not cookies then
+ return nil, err
+ end
+ end
+ end
+ end
+ end
+
+ header["Set-Cookie"] = cookies
+
+ if remember then
+ self.remember_meta = {
+ timestamp = current_time,
+ flags = flags,
+ sid = sid,
+ creation_time = creation_time,
+ rolling_offset = rolling_offset,
+ data_size = data_size,
+ idling_offset = idling_offset,
+ ikm = ikm,
+ header = header_decoded,
+ initial_chunk = initial_chunk,
+ ciphertext = ciphertext_encoded,
+ }
+
+ else
+ self.state = state or STATE_OPEN
+ self.meta = {
+ timestamp = current_time,
+ flags = flags,
+ sid = sid,
+ creation_time = creation_time,
+ rolling_offset = rolling_offset,
+ data_size = data_size,
+ idling_offset = idling_offset,
+ ikm = ikm,
+ header = header_decoded,
+ initial_chunk = initial_chunk,
+ ciphertext = ciphertext_encoded,
+ }
+ end
+
+ return true
+end
+
+
+local function save_info(self, data, remember)
+ local cookie_name
+ local meta
+ if remember then
+ cookie_name = self.remember_cookie_name
+ meta = self.remember_meta or {}
+ else
+ cookie_name = self.cookie_name
+ meta = self.meta
+ end
+
+ local key, err = self.hash_storage_key(meta.sid)
+ if not key then
+ return nil, err
+ end
+
+ DATA[1] = meta.ciphertext
+ DATA[2] = data
+
+ data, err = encode_json(DATA)
+ if not data then
+ return nil, errmsg(err, "unable to json encode session data")
+ end
+
+ local current_time = time()
+
+ local ttl = self.rolling_timeout
+ if ttl == 0 or ttl > MAX_TTL then
+ ttl = MAX_TTL
+ end
+ ttl = max(ttl - (current_time - meta.creation_time - meta.rolling_offset), 1)
+ local ok, err = self.storage:set(cookie_name, key, data, ttl, current_time)
+ if not ok then
+ return nil, errmsg(err, "unable to store session info")
+ end
+end
+
+
+local function destroy(self, remember)
+ local cookie_name
+ local meta
+ if remember then
+ cookie_name = self.remember_cookie_name
+ meta = self.remember_meta or {}
+ else
+ cookie_name = self.cookie_name
+ meta = self.meta
+ end
+
+ local cookie_name_size = #cookie_name
+ local storage = self.storage
+
+ local cookie_chunks = 1
+ local data_size = meta.data_size
+ if not storage and data_size then
+ local err
+ cookie_chunks, err = calculate_cookie_chunks(cookie_name_size, data_size)
+ if not cookie_chunks then
+ return nil, err
+ end
+ end
+
+ local cookie_flags = self.cookie_flags
+ local cookie_data = fmt("%s=%s%s", cookie_name, cookie_flags, COOKIE_EXPIRE_FLAGS)
+ local cookies, err = merge_cookies(header["Set-Cookie"], cookie_name_size, cookie_name, cookie_data)
+ if not cookies then
+ return nil, err
+ end
+
+ if cookie_chunks > 1 then
+ for i = 2, cookie_chunks do
+ local name = fmt("%s%d", cookie_name, i)
+ cookie_data = fmt("%s=%s%s", name, cookie_flags, COOKIE_EXPIRE_FLAGS)
+ cookies, err = merge_cookies(cookies, cookie_name_size + 1, name, cookie_data)
+ if not cookies then
+ return nil, err
+ end
+ end
+ end
+
+ if storage then
+ local sid = meta.sid
+ if sid then
+ local key, err = self.hash_storage_key(sid)
+ if not key then
+ return nil, err
+ end
+
+ local ok, err = storage:delete(cookie_name, key, meta.timestamp, get_store_metadata(self))
+ if not ok then
+ return nil, errmsg(err, "unable to destroy session")
+ end
+ end
+ end
+
+ header["Set-Cookie"] = cookies
+
+ self.state = STATE_CLOSED
+
+ return true
+end
+
+
+local function clear_request_cookie(self, remember)
+ local cookies = var.http_cookie
+ if not cookies or cookies == "" then
+ return
+ end
+
+ local cookie_name
+ if remember then
+ cookie_name = self.remember_cookie_name
+ else
+ cookie_name = self.cookie_name
+ end
+
+ local cookie_name_size = #cookie_name
+
+ local cookie_chunks
+ if self.storage then
+ cookie_chunks = 1
+ else
+ local data_size = remember and
+ self.remember_meta and
+ self.remember_meta.data_size or
+ self.meta.data_size
+ cookie_chunks = calculate_cookie_chunks(cookie_name_size, data_size) or 1
+ end
+
+ HIDE_BUFFER:reset()
+
+ local size = #cookies
+ local name
+ local skip = false
+ local start = 1
+ for i = 1, size do
+ local b = byte(cookies, i)
+ if name then
+ if b == SEMICOLON_BYTE or i == size then
+ if not skip then
+ local value
+ if b == SEMICOLON_BYTE then
+ value = trim(sub(cookies, start, i - 1))
+ else
+ value = trim(sub(cookies, start))
+ end
+
+ if value ~= "" then
+ HIDE_BUFFER:put(value)
+ end
+
+ if i ~= size then
+ HIDE_BUFFER:put("; ")
+ end
+ end
+
+ if i == size then
+ break
+ end
+
+ name = nil
+ start = i + 1
+ skip = false
+ end
+
+ else
+ if b == EQUALS_BYTE or b == SEMICOLON_BYTE then
+ name = sub(cookies, start, i - 1)
+ elseif i == size then
+ name = sub(cookies, start, i)
+ end
+
+ if name then
+ name = trim(name)
+ if b == SEMICOLON_BYTE or i == size then
+ if name ~= "" then
+ HIDE_BUFFER:put(name)
+ if i ~= size then
+ HIDE_BUFFER:put(";")
+ end
+
+ elseif i == size then
+ break
+ end
+
+ name = nil
+
+ else
+ if name == cookie_name then
+ skip = true
+
+ elseif cookie_chunks > 1 then
+ local chunk_number = tonumber(sub(name, -1), 10)
+ if chunk_number and chunk_number > 1 and chunk_number <= cookie_chunks
+ and sub(name, 1, -2) == cookie_name
+ then
+ skip = true
+ end
+ end
+
+ if not skip then
+ if name ~= "" then
+ HIDE_BUFFER:put(name)
+ end
+
+ if b == EQUALS_BYTE then
+ HIDE_BUFFER:put("=")
+ end
+ end
+ end
+
+ start = i + 1
+ end
+ end
+ end
+
+ if #HIDE_BUFFER == 0 then
+ clear_request_header("Cookie")
+ else
+ set_request_header("Cookie", HIDE_BUFFER:get())
+ end
+
+ return true
+end
+
+
+
+local function get_remember(self)
+ local flags = self.meta.flags
+ if flags and has_flag(flags, FLAG_FORGET) then
+ return false
+ end
+
+ if has_flag(self.flags, FLAG_FORGET) then
+ return false
+ end
+
+ return self.remember
+end
+
+
+local function set_property_header(self, property_header, set_header)
+ local name = HEADERS[property_header]
+ if not name then
+ return
+ end
+
+ local value = get_property(self, property_header)
+ if not value then
+ return
+ end
+
+ set_header(name, value)
+end
+
+
+local function set_property_headers(self, headers, set_header)
+ if not headers then
+ return
+ end
+
+ local count = #headers
+ if count == 0 then
+ return
+ end
+
+ for i = 1, count do
+ set_property_header(self, headers[i], set_header)
+ end
+end
+
+
+local function set_property_headers_vararg(self, set_header, count, ...)
+ if count == 1 then
+ local header_or_headers = select(1, ...)
+ if type(header_or_headers) == "table" then
+ return set_property_headers(self, header_or_headers, set_header)
+ end
+
+ return set_property_header(self, header_or_headers, set_header)
+ end
+
+ for i = 1, count do
+ local property_header = select(i, ...)
+ set_property_header(self, property_header, set_header)
+ end
+end
+
+
+---
+-- Session
+-- @section instance
+
+
+local fake_info_mt = {}
+
+
+function fake_info_mt:set(key, value)
+ local session = self.session
+ assert(session.state ~= STATE_CLOSED, "unable to set session info on closed session")
+ session.data[session.data_index][1]["@" .. key] = value
+end
+
+
+function fake_info_mt:get(key)
+ local session = self.session
+ assert(session.state ~= STATE_CLOSED, "unable to get session info on closed session")
+ return session.data[session.data_index][1]["@" .. key]
+end
+
+
+function fake_info_mt:save()
+ local session = self.session
+ assert(session.state == STATE_OPEN, "unable to save session info on nonexistent or closed session")
+ return session:save()
+end
+
+
+fake_info_mt.__index = fake_info_mt
+
+
+local fake_info = {}
+
+
+function fake_info.new(session)
+ return setmetatable({
+ session = session,
+ data = false,
+ }, fake_info_mt)
+end
+
+
+local info_mt = {}
+
+
+info_mt.__index = info_mt
+
+
+---
+-- Set a value in session information store.
+--
+-- @function instance.info:set
+-- @tparam string key key
+-- @param value value
+function info_mt:set(key, value)
+ local session = self.session
+ assert(session.state ~= STATE_CLOSED, "unable to set session info on closed session")
+ local audience = session.data[session.data_index][2]
+ local data = self.data
+ if data then
+ if data[audience] then
+ data[audience][key] = value
+
+ else
+ data[audience] = {
+ [key] = value,
+ }
+ end
+
+ else
+ self.data = {
+ [audience] = {
+ [key] = value,
+ },
+ }
+ end
+end
+
+
+---
+-- Get a value from session information store.
+--
+-- @function instance.info:get
+-- @tparam string key key
+-- @return value
+function info_mt:get(key)
+ local session = self.session
+ assert(session.state ~= STATE_CLOSED, "unable to get session info on closed session")
+ local data = self.data
+ if not data then
+ return
+ end
+
+ local audience = session.data[session.data_index][2]
+ data = self.data[audience]
+ if not data then
+ return
+ end
+
+ return data[key]
+end
+
+
+---
+-- Save information.
+--
+-- Only updates backend storage. Does not send a new cookie.
+--
+-- @function instance.info:save
+-- @treturn true|nil ok
+-- @treturn string error message
+function info_mt:save()
+ local session = self.session
+ assert(session.state == STATE_OPEN, "unable to save session info on nonexistent or closed session")
+ local data = self.data
+ if not data then
+ return true
+ end
+
+ local err
+ data, err = encode_json(data)
+ if not data then
+ return nil, errmsg(err, "unable to json encode session info")
+ end
+
+ data, err = encode_base64url(data)
+ if not data then
+ return nil, errmsg(err, "unable to base64url encode session info")
+ end
+
+ local ok, err = save_info(session, data)
+ if not ok then
+ return nil, err
+ end
+
+ if get_remember(session) then
+ if not session.remember_meta then
+ local remembered = open(self, true, true)
+ if not remembered then
+ return save(session, nil, true)
+ end
+ end
+
+ return save_info(session, data, true)
+ end
+
+ return true
+end
+
+
+local info = {}
+
+
+function info.new(session)
+ return setmetatable({
+ session = session,
+ data = false,
+ }, info_mt)
+end
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Set session data.
+--
+-- @function instance:set_data
+-- @tparam table data data
+--
+-- @usage
+-- local session, err, exists = require "resty.session".open()
+-- if not exists then
+-- session:set_data({
+-- cart = {},
+-- })
+-- session:save()
+-- end
+function metatable:set_data(data)
+ assert(self.state ~= STATE_CLOSED, "unable to set session data on closed session")
+ assert(type(data) == "table", "invalid session data")
+ self.data[self.data_index][1] = data
+end
+
+
+---
+-- Get session data.
+--
+-- @function instance:get_data
+-- @treturn table value
+--
+-- @usage
+-- local session, err, exists = require "resty.session".open()
+-- if exists then
+-- local data = session:get_data()
+-- ngx.req.set_header("Authorization", "Bearer " .. data.access_token)
+-- end
+function metatable:get_data()
+ assert(self.state ~= STATE_CLOSED, "unable to set session data on closed session")
+ return self.data[self.data_index][1]
+end
+
+
+---
+-- Set a value in session.
+--
+-- @function instance:set
+-- @tparam string key key
+-- @param value value
+--
+-- local session, err, exists = require "resty.session".open()
+-- if not exists then
+-- session:set("access-token", "eyJ...")
+-- session:save()
+-- end
+function metatable:set(key, value)
+ assert(self.state ~= STATE_CLOSED, "unable to set session data value on closed session")
+ if self.storage or byte(key, 1) ~= AT_BYTE then
+ self.data[self.data_index][1][key] = value
+ else
+ self.data[self.data_index][1]["$" .. key] = value
+ end
+end
+
+
+---
+-- Get a value from session.
+--
+-- @function instance:get
+-- @tparam string key key
+-- @return value
+--
+-- @usage
+-- local session, err, exists = require "resty.session".open()
+-- if exists then
+-- local access_token = session:get("access-token")
+-- ngx.req.set_header("Authorization", "Bearer " .. access_token)
+-- end
+function metatable:get(key)
+ assert(self.state ~= STATE_CLOSED, "unable to get session data value on closed session")
+ if self.storage or byte(key, 1) ~= AT_BYTE then
+ return self.data[self.data_index][1][key]
+ else
+ return self.data[self.data_index][1]["$" .. key]
+ end
+end
+
+
+---
+-- Set session audience.
+--
+-- @function instance:set_audience
+-- @tparam string audience audience
+--
+-- @usage
+-- local session = require "resty.session".new()
+-- session.set_audience("my-service")
+function metatable:set_audience(audience)
+ assert(self.state ~= STATE_CLOSED, "unable to set audience on closed session")
+
+ local data = self.data
+ local data_index = self.data_index
+ local current_audience = data[data_index][2]
+ if audience == current_audience then
+ return
+ end
+
+ local info_data = self.info.data
+ if info_data then
+ info_data[audience] = info_data[current_audience]
+ info_data[current_audience] = nil
+ end
+
+ local count = #data
+ if count == 1 then
+ data[1][2] = audience
+ return
+ end
+
+ local previous_index
+ for i = 1, count do
+ if data[i][2] == audience then
+ previous_index = i
+ break
+ end
+ end
+
+ data[data_index][2] = audience
+
+ if not previous_index or previous_index == data_index then
+ return
+ end
+
+ remove(data, previous_index)
+
+ if previous_index < data_index then
+ self.data_index = data_index - 1
+ end
+end
+
+
+---
+-- Get session audience.
+--
+-- @function instance:get_audience
+-- @treturn string audience
+function metatable:get_audience()
+ assert(self.state ~= STATE_CLOSED, "unable to get audience on closed session")
+ return self.data[self.data_index][2]
+end
+
+
+---
+-- Set session subject.
+--
+-- @function instance:set_subject
+-- @tparam string subject subject
+--
+-- @usage
+-- local session = require "resty.session".new()
+-- session.set_subject("john@doe.com")
+function metatable:set_subject(subject)
+ assert(self.state ~= STATE_CLOSED, "unable to set subject on closed session")
+ self.data[self.data_index][3] = subject
+end
+
+
+---
+-- Get session subject.
+--
+-- @function instance:get_subject
+-- @treturn string subject
+--
+-- @usage
+-- local session, err, exists = require "resty.session".open()
+-- if exists then
+-- local subject = session.get_subject()
+-- end
+function metatable:get_subject()
+ assert(self.state ~= STATE_CLOSED, "unable to get subject on closed session")
+ return self.data[self.data_index][3]
+end
+
+
+---
+-- Get session property.
+--
+-- Possible property names:
+-- * `"id"`: 43 bytes session id (same as nonce, but base64 url-encoded)
+-- * `"nonce"`: 32 bytes nonce (same as session id but in raw bytes)
+-- * `"audience"`: Current session audience
+-- * `"subject"`: Current session subject
+-- * `"timeout"`: Closest timeout (in seconds) (what's left of it)
+-- * `"idling-timeout`"`: Session idling timeout (in seconds) (what's left of it)
+-- * `"rolling-timeout`"`: Session rolling timeout (in seconds) (what's left of it)
+-- * `"absolute-timeout`"`: Session absolute timeout (in seconds) (what's left of it)
+--
+-- @function instance:get_property
+-- @treturn string|number metadata
+--
+-- @usage
+-- local session, err, exists = require "resty.session".open()
+-- if exists then
+-- local timeout = session.get_property("timeout")
+-- end
+function metatable:get_property(name)
+ assert(self.state ~= STATE_CLOSED, "unable to get session property on closed session")
+ return get_property(self, name)
+end
+
+
+---
+-- Set persistent sessions on/off.
+--
+-- In many login forms user is given an option for "remember me".
+-- You can call this function based on what user selected.
+--
+-- @function instance:set_remember
+-- @tparam boolean value `true` to enable persistent session, `false` to disable them
+function metatable:set_remember(value)
+ assert(self.state ~= STATE_CLOSED, "unable to set remember on closed session")
+ assert(type(value) == "boolean", "invalid remember value")
+ if value == false then
+ set_flag(self.flags, FLAG_FORGET)
+ else
+ unset_flag(self.flags, FLAG_FORGET)
+ end
+
+ self.remember = value
+end
+
+
+---
+-- Get state of persistent sessions.
+--
+-- @function instance:get_remember
+-- @treturn boolean `true` when persistent sessions are enabled, otherwise `false`
+function metatable:get_remember()
+ assert(self.state ~= STATE_CLOSED, "unable to get remember on closed session")
+ return get_remember(self)
+end
+
+
+---
+-- Open a session.
+--
+-- This can be used to open a session.
+--
+-- @function instance:open
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:open()
+ local exists, err, audience_error = open(self)
+ if exists then
+ return true
+ end
+
+ if audience_error then
+ return nil, err
+ end
+
+ if not self.remember then
+ return nil, err
+ end
+
+ local remembered = open(self, true)
+ if not remembered then
+ return nil, err
+ end
+
+ local ok, err = save(self, nil, true)
+ if not ok then
+ return nil, err
+ end
+
+ self.state = STATE_NEW
+ self.meta = DEFAULT_META
+
+ local ok, err = save(self, STATE_OPEN)
+ if not ok then
+ return nil, err
+ end
+
+ return true
+end
+
+
+---
+-- Save the session.
+--
+-- Saves the session data and issues a new session cookie with a new session id.
+-- When `remember` is enabled, it will also issue a new persistent cookie and
+-- possibly save the data in backend store.
+--
+-- @function instance:save
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:save()
+ assert(self.state ~= STATE_CLOSED, "unable to save closed session")
+
+ local ok, err = save(self)
+ if not ok then
+ return nil, err
+ end
+
+ if get_remember(self) then
+ if not self.remember_meta then
+ open(self, true, true)
+ end
+
+ local ok, err = save(self, nil, true)
+ if not ok then
+ log(WARN, "[session] ", err)
+ end
+ end
+
+ return true
+end
+
+
+---
+-- Touch the session.
+--
+-- Updates idling offset of the session by sending an updated session cookie.
+-- It only sends the client cookie and never calls any backend session store
+-- APIs. Normally the `session:refresh` is used to call this indirectly.
+--
+-- @function instance:touch
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:touch()
+ assert(self.state == STATE_OPEN, "unable to touch nonexistent or closed session")
+
+ local meta = self.meta
+ local idling_offset = min(time() - meta.creation_time - meta.rolling_offset, MAX_IDLING_OFFSET)
+
+ HEADER_BUFFER:reset():put(sub(meta.header, 1, HEADER_TOUCH_SIZE),
+ bpack(IDLING_OFFSET_SIZE, idling_offset))
+
+ local mac, err = calculate_mac(meta.ikm, meta.sid, HEADER_BUFFER:tostring())
+ if not mac then
+ return nil, err
+ end
+
+ local payload_header = HEADER_BUFFER:put(mac):get()
+
+ meta.idling_offset = idling_offset
+ meta.header = payload_header
+
+ payload_header, err = encode_base64url(payload_header)
+ if not payload_header then
+ return nil, errmsg(err, "unable to base64url encode session header")
+ end
+
+ local cookie_flags = self.cookie_flags
+ local cookie_name = self.cookie_name
+ local cookie_data
+ if self.storage then
+ cookie_data = fmt("%s=%s%s", cookie_name, payload_header, cookie_flags)
+ else
+ cookie_data = fmt("%s=%s%s%s", cookie_name, payload_header, meta.initial_chunk, cookie_flags)
+ end
+
+ header["Set-Cookie"] = merge_cookies(header["Set-Cookie"], #cookie_name, cookie_name, cookie_data)
+
+ return true
+end
+
+
+---
+-- Refresh the session.
+--
+-- Either saves the session (creating a new session id) or touches the session
+-- depending on whether the rolling timeout is getting closer, which means
+-- by default when 3/4 of rolling timeout is spent - 45 minutes with default
+-- rolling timeout of an hour. The touch has a threshold, by default one minute,
+-- so it may be skipped in some cases (you can call `session:touch()` to force it).
+--
+-- @function instance:refresh
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:refresh()
+ assert(self.state == STATE_OPEN, "unable to refresh nonexistent or closed session")
+
+ local rolling_timeout = self.rolling_timeout
+ local idling_timeout = self.idling_timeout
+ if rolling_timeout == 0 and idling_timeout == 0 then
+ return true
+ end
+
+ local meta = self.meta
+ local rolling_elapsed = meta.timestamp - meta.creation_time - meta.rolling_offset
+
+ if rolling_timeout > 0 then
+ if rolling_elapsed > floor(rolling_timeout * 0.75) then
+ -- TODO: in case session was modified before calling this function, the possible remember me cookie needs to be saved too?
+ return save(self)
+ end
+ end
+
+ if idling_timeout > 0 then
+ local idling_elapsed = rolling_elapsed - meta.idling_offset
+ if idling_elapsed > self.touch_threshold then
+ return self:touch()
+ end
+ end
+
+ return true
+end
+
+
+---
+-- Logout the session.
+--
+-- Logout either destroys the session or just clears the data for the current audience,
+-- and saves it (logging out from the current audience).
+--
+-- @function instance:logout
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:logout()
+ assert(self.state == STATE_OPEN, "unable to logout nonexistent or closed session")
+
+ local data = self.data
+ if #data == 1 then
+ return self:destroy()
+ end
+
+ local data_index = self.data_index
+ local info_store = self.info
+ local info_data = info_store and info_store.data
+ if info_data then
+ local audience = data[data_index][2]
+ info_data[audience] = nil
+ if isempty(info_data) then
+ info_store.data = false
+ end
+ end
+
+ remove(data, data_index)
+
+ local ok, err = save(self, STATE_CLOSED)
+ if not ok then
+ return nil, err
+ end
+
+ if get_remember(self) then
+ if not self.remember_meta then
+ open(self, true, true)
+ end
+ local ok, err = save(self, nil, true)
+ if not ok then
+ log(WARN, "[session] ", err)
+ end
+ end
+
+ return true
+end
+
+
+---
+-- Destroy the session.
+--
+-- Destroy the session and clear the cookies.
+--
+-- @function instance:destroy
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:destroy()
+ assert(self.state == STATE_OPEN, "unable to destroy nonexistent or closed session")
+
+ local ok, err = destroy(self)
+ if not ok then
+ return nil, err
+ end
+
+ if get_remember(self) then
+ if not self.remember_meta then
+ local remembered = open(self, true, true)
+ if not remembered then
+ return true
+ end
+ end
+
+ ok, err = destroy(self, true)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ return true
+end
+
+
+---
+-- Close the session.
+--
+-- Just closes the session instance so that it cannot be used anymore.
+--
+-- @function instance:close
+function metatable:close()
+ self.state = STATE_CLOSED
+end
+
+
+---
+-- Clear the request session cookie.
+--
+-- Modifies the request headers by removing the session related
+-- cookies. This is useful when you use the session library on
+-- a proxy server and don't want the session cookies to be forwarded
+-- to the upstream service.
+--
+-- @function instance:clear_request_cookie
+function metatable:clear_request_cookie()
+ assert(self.state == STATE_OPEN, "unable to hide nonexistent or closed session")
+
+ local ok = clear_request_cookie(self)
+ if not ok then
+ log(NOTICE, "[session] unable to clear session request cookie")
+ end
+
+ if get_remember(self) then
+ ok = clear_request_cookie(self, true)
+ if not ok then
+ log(NOTICE, "[session] unable to clear persistent session request cookie")
+ end
+ end
+end
+
+
+---
+-- Sets request and response headers.
+--
+-- @function instance:set_headers
+-- @tparam[opt] string ...
+function metatable:set_headers(...)
+ assert(self.state == STATE_OPEN, "unable to set request/response headers of nonexistent or closed session")
+
+ local count = select("#", ...)
+ if count == 0 then
+ set_property_headers(self, self.request_headers, set_request_header)
+ set_property_headers(self, self.response_headers, set_response_header)
+ return
+ end
+
+ set_property_headers_vararg(self, set_request_header, count, ...)
+ set_property_headers_vararg(self, set_response_header, count, ...)
+end
+
+
+---
+-- Set request headers.
+--
+-- @function instance:set_request_headers
+-- @tparam[opt] string ...
+function metatable:set_request_headers(...)
+ assert(self.state == STATE_OPEN, "unable to set request headers of nonexistent or closed session")
+
+ local count = select("#", ...)
+ if count == 0 then
+ set_property_headers(self, self.request_headers, set_request_header)
+ return
+ end
+
+ set_property_headers_vararg(self, set_request_header, count, ...)
+end
+
+
+---
+-- Set response headers.
+--
+-- @function instance:set_response_headers
+-- @tparam[opt] string ...
+function metatable:set_response_headers(...)
+ assert(self.state == STATE_OPEN, "unable to set response headers of nonexistent or closed session")
+
+ local count = select("#", ...)
+ if count == 0 then
+ set_property_headers(self, self.response_headers, set_response_header)
+ return
+ end
+
+ set_property_headers_vararg(self, set_response_header, count, ...)
+end
+
+
+
+local session = {
+ _VERSION = "4.0.4",
+ metatable = metatable,
+}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- Session configuration.
+-- @field secret Secret used for the key derivation. The secret is hashed with SHA-256 before using it. E.g. `"RaJKp8UQW1"`.
+-- @field secret_fallbacks Array of secrets that can be used as alternative secrets (when doing key rotation), E.g. `{ "6RfrAYYzYq", "MkbTkkyF9C" }`.
+-- @field ikm Initial key material (or ikm) can be specified directly (without using a secret) with exactly 32 bytes of data. E.g. `"5ixIW4QVMk0dPtoIhn41Eh1I9enP2060"`
+-- @field ikm_fallbacks Array of initial key materials that can be used as alternative keys (when doing key rotation), E.g. `{ "QvPtlPKxOKdP5MCu1oI3lOEXIVuDckp7" }`.
+-- @field cookie_prefix Cookie prefix, use `nil`, `"__Host-"` or `"__Secure-"` (defaults to `nil`)
+-- @field cookie_name Session cookie name, e.g. `"session"` (defaults to `"session"`)
+-- @field cookie_path Cookie path, e.g. `"/"` (defaults to `"/"`)
+-- @field cookie_domain Cookie domain, e.g. `"example.com"` (defaults to `nil`)
+-- @field cookie_http_only Mark cookie HTTP only, use `true` or `false` (defaults to `true`)
+-- @field cookie_secure Mark cookie secure, use `nil`, `true` or `false` (defaults to `nil`)
+-- @field cookie_priority Cookie priority, use `nil`, `"Low"`, `"Medium"`, or `"High"` (defaults to `nil`)
+-- @field cookie_same_site Cookie same-site policy, use `nil`, `"Lax"`, `"Strict"`, `"None"`, or `"Default"` (defaults to `"Lax"`)
+-- @field cookie_same_party Mark cookie with same party flag, use `nil`, `true`, or `false` (default: `nil`)
+-- @field cookie_partitioned Mark cookie with partitioned flag, use `nil`, `true`, or `false` (default: `nil`)
+-- @field remember Enable or disable persistent sessions, use `nil`, `true`, or `false` (defaults to `false`)
+-- @field remember_safety Remember cookie key derivation complexity, use `nil`, `"None"` (fast), `"Low"`, `"Medium"`, `"High"` or `"Very High"` (slow) (defaults to `"Medium"`)
+-- @field remember_cookie_name Persistent session cookie name, e.g. `"remember"` (defaults to `"remember"`)
+-- @field audience Session audience, e.g. `"my-application"` (defaults to `"default"`)
+-- @field subject Session subject, e.g. `"john.doe@example.com"` (defaults to `nil`)
+-- @field enforce_same_subject When set to `true`, audiences need to share the same subject. The library removes non-subject matching audience data on save.
+-- @field stale_ttl When session is saved a new session is created, stale ttl specifies how long the old one can still be used, e.g. `10` (defaults to `10`) (in seconds)
+-- @field idling_timeout Idling timeout specifies how long the session can be inactive until it is considered invalid, e.g. `900` (defaults to `900`, or 15 minutes) (in seconds)
+-- @field rolling_timeout Rolling timeout specifies how long the session can be used until it needs to be renewed, e.g. `3600` (defaults to `3600`, or an hour) (in seconds)
+-- @field absolute_timeout Absolute timeout limits how long the session can be renewed, until re-authentication is required, e.g. `86400` (defaults to `86400`, or a day) (in seconds)
+-- @field remember_rolling_timeout Remember timeout specifies how long the persistent session is considered valid, e.g. `604800` (defaults to `604800`, or a week) (in seconds)
+-- @field remember_absolute_timeout Remember absolute timeout limits how long the persistent session can be renewed, until re-authentication is required, e.g. `2592000` (defaults to `2592000`, or 30 days) (in seconds)
+-- @field hash_storage_key Whether to hash or not the storage key. With storage key hashed it is impossible to decrypt data on server side without having a cookie too (defaults to `false`).
+-- @field hash_subject Whether to hash or not the subject when `store_metadata` is enabled, e.g. for PII reasons (defaults to `false`).
+-- @field store_metadata Whether to also store metadata of sessions, such as collecting data of sessions for a specific audience belonging to a specific subject (defaults to `false`).
+-- @field touch_threshold Touch threshold controls how frequently or infrequently the `session:refresh` touches the cookie, e.g. `60` (defaults to `60`, or a minute) (in seconds)
+-- @field compression_threshold Compression threshold controls when the data is deflated, e.g. `1024` (defaults to `1024`, or a kilobyte) (in bytes)
+-- @field request_headers Set of headers to send to upstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` request headers when `set_headers` is called.
+-- @field response_headers Set of headers to send to downstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` response headers when `set_headers` is called.
+-- @field storage Storage is responsible of storing session data, use `nil` or `"cookie"` (data is stored in cookie), `"dshm"`, `"file"`, `"memcached"`, `"mysql"`, `"postgres"`, `"redis"`, or `"shm"`, or give a name of custom module (`"custom-storage"`), or a `table` that implements session storage interface (defaults to `nil`)
+-- @field dshm Configuration for dshm storage, e.g. `{ prefix = "sessions" }`
+-- @field file Configuration for file storage, e.g. `{ path = "/tmp", suffix = "session" }`
+-- @field memcached Configuration for memcached storage, e.g. `{ prefix = "sessions" }`
+-- @field mysql Configuration for MySQL / MariaDB storage, e.g. `{ database = "sessions" }`
+-- @field postgres Configuration for Postgres storage, e.g. `{ database = "sessions" }`
+-- @field redis Configuration for Redis / Redis Sentinel / Redis Cluster storages, e.g. `{ prefix = "sessions" }`
+-- @field shm Configuration for shared memory storage, e.g. `{ zone = "sessions" }`
+-- @field ["custom-storage"] Custom storage (loaded with `require "custom-storage"`) configuration
+-- @table configuration
+
+
+---
+-- Initialization
+-- @section initialization
+
+
+---
+-- Initialize the session library.
+--
+-- This function can be called on `init` or `init_worker` phases on OpenResty
+-- to set global default configuration to all session instances created by this
+-- library.
+--
+-- @function module.init
+-- @tparam[opt] table configuration session @{configuration} overrides
+--
+-- @usage
+-- require "resty.session".init({
+-- audience = "my-application",
+-- storage = "redis",
+-- redis = {
+-- username = "session",
+-- password = "storage",
+-- },
+-- })
+function session.init(configuration)
+ if configuration then
+ local ikm = configuration.ikm
+ if ikm then
+ assert(#ikm == KEY_SIZE, "invalid ikm size")
+ DEFAULT_IKM = ikm
+
+ else
+ local secret = configuration.secret
+ if secret then
+ DEFAULT_IKM = assert(sha256(secret))
+ end
+ end
+
+ local ikm_fallbacks = configuration.ikm_fallbacks
+ if ikm_fallbacks then
+ local count = #ikm_fallbacks
+ for i = 1, count do
+ assert(#ikm_fallbacks[i] == KEY_SIZE, "invalid ikm size in ikm_fallbacks")
+ end
+
+ DEFAULT_IKM_FALLBACKS = ikm_fallbacks
+
+ else
+ local secret_fallbacks = configuration.secret_fallbacks
+ if secret_fallbacks then
+ local count = #secret_fallbacks
+ if count > 0 then
+ DEFAULT_IKM_FALLBACKS = table_new(count, 0)
+ for i = 1, count do
+ DEFAULT_IKM_FALLBACKS[i] = assert(sha256(secret_fallbacks[i]))
+ end
+
+ else
+ DEFAULT_IKM_FALLBACKS = nil
+ end
+ end
+ end
+
+ local request_headers = configuration.request_headers
+ if request_headers then
+ local count = #request_headers
+ for i = 1, count do
+ assert(HEADERS[request_headers[i]], "invalid request header")
+ end
+
+ DEFAULT_REQUEST_HEADERS = request_headers
+ end
+
+ local response_headers = configuration.response_headers
+ if response_headers then
+ local count = #response_headers
+ for i = 1, count do
+ assert(HEADERS[response_headers[i]], "invalid response header")
+ end
+
+ DEFAULT_RESPONSE_HEADERS = response_headers
+ end
+
+ DEFAULT_COOKIE_NAME = configuration.cookie_name or DEFAULT_COOKIE_NAME
+ DEFAULT_COOKIE_PATH = configuration.cookie_path or DEFAULT_COOKIE_PATH
+ DEFAULT_COOKIE_DOMAIN = configuration.cookie_domain or DEFAULT_COOKIE_DOMAIN
+ DEFAULT_COOKIE_SAME_SITE = configuration.cookie_same_site or DEFAULT_COOKIE_SAME_SITE
+ DEFAULT_COOKIE_PRIORITY = configuration.cookie_priority or DEFAULT_COOKIE_PRIORITY
+ DEFAULT_COOKIE_PREFIX = configuration.cookie_prefix or DEFAULT_COOKIE_PREFIX
+ DEFAULT_REMEMBER_SAFETY = configuration.remember_safety or DEFAULT_REMEMBER_SAFETY
+ DEFAULT_REMEMBER_COOKIE_NAME = configuration.remember_cookie_name or DEFAULT_REMEMBER_COOKIE_NAME
+ DEFAULT_AUDIENCE = configuration.audience or DEFAULT_AUDIENCE
+ DEFAULT_SUBJECT = configuration.subject or DEFAULT_SUBJECT
+ DEFAULT_STALE_TTL = configuration.stale_ttl or DEFAULT_STALE_TTL
+ DEFAULT_IDLING_TIMEOUT = configuration.idling_timeout or DEFAULT_IDLING_TIMEOUT
+ DEFAULT_ROLLING_TIMEOUT = configuration.rolling_timeout or DEFAULT_ROLLING_TIMEOUT
+ DEFAULT_ABSOLUTE_TIMEOUT = configuration.absolute_timeout or DEFAULT_ABSOLUTE_TIMEOUT
+ DEFAULT_REMEMBER_ROLLING_TIMEOUT = configuration.remember_rolling_timeout or DEFAULT_REMEMBER_ROLLING_TIMEOUT
+ DEFAULT_REMEMBER_ABSOLUTE_TIMEOUT = configuration.remember_absolute_timeout or DEFAULT_REMEMBER_ABSOLUTE_TIMEOUT
+ DEFAULT_TOUCH_THRESHOLD = configuration.touch_threshold or DEFAULT_TOUCH_THRESHOLD
+ DEFAULT_COMPRESSION_THRESHOLD = configuration.compression_threshold or DEFAULT_COMPRESSION_THRESHOLD
+ DEFAULT_STORAGE = configuration.storage or DEFAULT_STORAGE
+
+ local cookie_http_only = configuration.cookie_http_only
+ if cookie_http_only ~= nil then
+ DEFAULT_COOKIE_HTTP_ONLY = cookie_http_only
+ end
+
+ local cookie_same_party = configuration.cookie_same_party
+ if cookie_same_party ~= nil then
+ DEFAULT_COOKIE_SAME_PARTY = cookie_same_party
+ end
+
+ local cookie_partitioned = configuration.cookie_partitioned
+ if cookie_partitioned ~= nil then
+ DEFAULT_COOKIE_PARTITIONED = cookie_partitioned
+ end
+
+ local cookie_secure = configuration.cookie_secure
+ if cookie_secure ~= nil then
+ DEFAULT_COOKIE_SECURE = cookie_secure
+ end
+
+ local remember = configuration.remember
+ if remember ~= nil then
+ DEFAULT_REMEMBER = remember
+ end
+
+ local hash_storage_key = configuration.hash_storage_key
+ if hash_storage_key ~= nil then
+ DEFAULT_HASH_STORAGE_KEY = hash_storage_key
+ end
+
+ local hash_subject = configuration.hash_subject
+ if hash_subject ~= nil then
+ DEFAULT_HASH_SUBJECT = hash_subject
+ end
+
+ local store_metadate = configuration.store_metadata
+ if store_metadate ~= nil then
+ DEFAULT_STORE_METADATA = store_metadate
+ end
+
+ local enforce_same_subject = configuration.enforce_same_subject
+ if enforce_same_subject ~= nil then
+ DEFAULT_ENFORCE_SAME_SUBJECT = enforce_same_subject
+ end
+ end
+
+ if not DEFAULT_IKM then
+ DEFAULT_IKM = assert(sha256(assert(rand_bytes(KEY_SIZE))))
+ end
+
+ if type(DEFAULT_STORAGE) == "string" then
+ DEFAULT_STORAGE = load_storage(DEFAULT_STORAGE, configuration)
+ end
+end
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a new session.
+--
+-- This creates a new session instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration session @{configuration} overrides
+-- @treturn table session instance
+--
+-- @usage
+-- local session = require "resty.session".new()
+-- -- OR
+-- local session = require "resty.session".new({
+-- audience = "my-application",
+-- })
+function session.new(configuration)
+ local cookie_name = configuration and configuration.cookie_name or DEFAULT_COOKIE_NAME
+ local cookie_path = configuration and configuration.cookie_path or DEFAULT_COOKIE_PATH
+ local cookie_domain = configuration and configuration.cookie_domain or DEFAULT_COOKIE_DOMAIN
+ local cookie_same_site = configuration and configuration.cookie_same_site or DEFAULT_COOKIE_SAME_SITE
+ local cookie_priority = configuration and configuration.cookie_priority or DEFAULT_COOKIE_PRIORITY
+ local cookie_prefix = configuration and configuration.cookie_prefix or DEFAULT_COOKIE_PREFIX
+ local remember_safety = configuration and configuration.remember_safety or DEFAULT_REMEMBER_SAFETY
+ local remember_cookie_name = configuration and configuration.remember_cookie_name or DEFAULT_REMEMBER_COOKIE_NAME
+ local audience = configuration and configuration.audience or DEFAULT_AUDIENCE
+ local subject = configuration and configuration.subject or DEFAULT_SUBJECT
+ local stale_ttl = configuration and configuration.stale_ttl or DEFAULT_STALE_TTL
+ local idling_timeout = configuration and configuration.idling_timeout or DEFAULT_IDLING_TIMEOUT
+ local rolling_timeout = configuration and configuration.rolling_timeout or DEFAULT_ROLLING_TIMEOUT
+ local absolute_timeout = configuration and configuration.absolute_timeout or DEFAULT_ABSOLUTE_TIMEOUT
+ local remember_rolling_timeout = configuration and configuration.remember_rolling_timeout or DEFAULT_REMEMBER_ROLLING_TIMEOUT
+ local remember_absolute_timeout = configuration and configuration.remember_absolute_timeout or DEFAULT_REMEMBER_ABSOLUTE_TIMEOUT
+ local touch_threshold = configuration and configuration.touch_threshold or DEFAULT_TOUCH_THRESHOLD
+ local compression_threshold = configuration and configuration.compression_threshold or DEFAULT_COMPRESSION_THRESHOLD
+ local storage = configuration and configuration.storage or DEFAULT_STORAGE
+ local ikm = configuration and configuration.ikm
+ local ikm_fallbacks = configuration and configuration.ikm_fallbacks
+ local request_headers = configuration and configuration.request_headers
+ local response_headers = configuration and configuration.response_headers
+
+ local cookie_http_only = configuration and configuration.cookie_http_only
+ if cookie_http_only == nil then
+ cookie_http_only = DEFAULT_COOKIE_HTTP_ONLY
+ end
+
+ local cookie_secure = configuration and configuration.cookie_secure
+ if cookie_secure == nil then
+ cookie_secure = DEFAULT_COOKIE_SECURE
+ end
+
+ local cookie_same_party = configuration and configuration.cookie_same_party
+ if cookie_same_party == nil then
+ cookie_same_party = DEFAULT_COOKIE_SAME_PARTY
+ end
+
+ local cookie_partitioned = configuration and configuration.cookie_partitioned
+ if cookie_partitioned == nil then
+ cookie_partitioned = DEFAULT_COOKIE_PARTITIONED
+ end
+
+ local remember = configuration and configuration.remember
+ if remember == nil then
+ remember = DEFAULT_REMEMBER
+ end
+
+ local hash_storage_key = configuration and configuration.hash_storage_key
+ if hash_storage_key == nil then
+ hash_storage_key = DEFAULT_HASH_STORAGE_KEY
+ end
+
+ local hash_subject = configuration and configuration.hash_subject
+ if hash_subject == nil then
+ hash_subject = DEFAULT_HASH_SUBJECT
+ end
+
+ local store_metadata = configuration and configuration.store_metadata
+ if store_metadata == nil then
+ store_metadata = DEFAULT_STORE_METADATA
+ end
+
+ local enforce_same_subject = configuration and configuration.enforce_same_subject
+ if enforce_same_subject == nil then
+ enforce_same_subject = DEFAULT_ENFORCE_SAME_SUBJECT
+ end
+
+ if cookie_prefix == "__Host-" then
+ cookie_name = cookie_prefix .. cookie_name
+ remember_cookie_name = cookie_prefix .. remember_cookie_name
+ cookie_path = DEFAULT_COOKIE_PATH
+ cookie_domain = nil
+ cookie_secure = true
+
+ elseif cookie_prefix == "__Secure-" then
+ cookie_name = cookie_prefix .. cookie_name
+ remember_cookie_name = cookie_prefix .. remember_cookie_name
+ cookie_secure = true
+
+ elseif cookie_same_site == "None" then
+ cookie_secure = true
+ end
+
+ if cookie_same_party then
+ assert(cookie_same_site ~= "Strict", "SameParty session cookies cannot use SameSite=Strict")
+ cookie_secure = true
+ end
+
+ FLAGS_BUFFER:reset()
+
+ if cookie_domain and cookie_domain ~= "localhost" and cookie_domain ~= "" then
+ FLAGS_BUFFER:put("; Domain=", cookie_domain)
+ end
+
+ FLAGS_BUFFER:put("; Path=", cookie_path, "; SameSite=", cookie_same_site)
+
+ if cookie_priority then
+ FLAGS_BUFFER:put("; Priority=", cookie_priority)
+ end
+
+ if cookie_same_party then
+ FLAGS_BUFFER:put("; SameParty")
+ end
+
+ if cookie_partitioned then
+ FLAGS_BUFFER:put("; Partitioned")
+ end
+
+ if cookie_secure then
+ FLAGS_BUFFER:put("; Secure")
+ end
+
+ if cookie_http_only then
+ FLAGS_BUFFER:put("; HttpOnly")
+ end
+
+ local cookie_flags = FLAGS_BUFFER:get()
+
+ if ikm then
+ assert(#ikm == KEY_SIZE, "invalid ikm size")
+
+ else
+ local secret = configuration and configuration.secret
+ if secret then
+ ikm = assert(sha256(secret))
+
+ else
+ if not DEFAULT_IKM then
+ DEFAULT_IKM = assert(sha256(assert(rand_bytes(KEY_SIZE))))
+ end
+
+ ikm = DEFAULT_IKM
+ end
+ end
+
+ if ikm_fallbacks then
+ local count = #ikm_fallbacks
+ for i = 1, count do
+ assert(#ikm_fallbacks[i] == KEY_SIZE, "invalid ikm size in ikm_fallbacks")
+ end
+
+ else
+ local secret_fallbacks = configuration and configuration.secret_fallbacks
+ if secret_fallbacks then
+ local count = #secret_fallbacks
+ if count > 0 then
+ ikm_fallbacks = table_new(count, 0)
+ for i = 1, count do
+ ikm_fallbacks[i] = assert(sha256(secret_fallbacks[i]))
+ end
+ end
+
+ else
+ ikm_fallbacks = DEFAULT_IKM_FALLBACKS
+ end
+ end
+
+ if request_headers then
+ local count = #request_headers
+ for i = 1, count do
+ assert(HEADERS[request_headers[i]], "invalid request header")
+ end
+
+ else
+ request_headers = DEFAULT_REQUEST_HEADERS
+ end
+
+ if response_headers then
+ local count = #response_headers
+ for i = 1, count do
+ assert(HEADERS[response_headers[i]], "invalid response header")
+ end
+
+ else
+ response_headers = DEFAULT_RESPONSE_HEADERS
+ end
+
+ local t = type(storage)
+ if t == "string" then
+ storage = load_storage(storage, configuration)
+
+ elseif t ~= "table" then
+ assert(storage == nil, "invalid session storage")
+ end
+
+ local self = setmetatable({
+ stale_ttl = stale_ttl,
+ idling_timeout = idling_timeout,
+ rolling_timeout = rolling_timeout,
+ absolute_timeout = absolute_timeout,
+ remember_rolling_timeout = remember_rolling_timeout,
+ remember_absolute_timeout = remember_absolute_timeout,
+ touch_threshold = touch_threshold,
+ compression_threshold = compression_threshold,
+ hash_storage_key = hash_storage_key and sha256_storage_key or encode_base64url,
+ hash_subject = hash_subject and sha256_subject or encode_base64url,
+ store_metadata = store_metadata,
+ enforce_same_subject = enforce_same_subject,
+ cookie_name = cookie_name,
+ cookie_flags = cookie_flags,
+ remember_cookie_name = remember_cookie_name,
+ remember_safety = remember_safety,
+ remember = remember,
+ flags = FLAGS_NONE,
+ storage = storage,
+ ikm = ikm,
+ ikm_fallbacks = ikm_fallbacks,
+ request_headers = request_headers,
+ response_headers = response_headers,
+ state = STATE_NEW,
+ meta = DEFAULT_META,
+ remember_meta = DEFAULT_REMEMBER_META,
+ info = info,
+ data_index = 1,
+ data = {
+ {
+ {},
+ audience,
+ subject,
+ },
+ },
+ }, metatable)
+
+ if storage then
+ self.info = info.new(self)
+ else
+ self.info = fake_info.new(self)
+ end
+
+ return self
+end
+
+
+---
+-- Helpers
+-- @section helpers
+
+
+---
+-- Open a session.
+--
+-- This can be used to open a session, and it will either return an existing
+-- session or a new session.
+--
+-- @function module.open
+-- @tparam[opt] table configuration session @{configuration} overrides
+-- @treturn table session instance
+-- @treturn string error message
+-- @treturn boolean `true`, if session existed, otherwise `false`
+--
+-- @usage
+-- local session = require "resty.session".open()
+-- -- OR
+-- local session, err, exists = require "resty.session".open({
+-- audience = "my-application",
+-- })
+function session.open(configuration)
+ local self = session.new(configuration)
+ local exists, err = self:open()
+ if not exists then
+ return self, err, false
+ end
+
+ return self, err, true
+end
+
+
+---
+-- Start a session and refresh it as needed.
+--
+-- This can be used to start a session, and it will either return an existing
+-- session or a new session. In case there is an existing session, the
+-- session will be refreshed as well (as needed).
+--
+-- @function module.start
+-- @tparam[opt] table configuration session @{configuration} overrides
+-- @treturn table session instance
+-- @treturn string error message
+-- @treturn boolean `true`, if session existed, otherwise `false`
+-- @treturn boolean `true`, if session was refreshed, otherwise `false`
+--
+-- @usage
+-- local session = require "resty.session".start()
+-- -- OR
+-- local session, err, exists, refreshed = require "resty.session".start({
+-- audience = "my-application",
+-- })
+function session.start(configuration)
+ local self, err, exists = session.open(configuration)
+ if not exists then
+ return self, err, false, false
+ end
+
+ local refreshed, err = self:refresh()
+ if not refreshed then
+ return self, err, true, false
+ end
+
+ return self, nil, true, true
+end
+
+
+---
+-- Logout a session.
+--
+-- It logouts from a specific audience.
+--
+-- A single session cookie may be shared between multiple audiences
+-- (or applications), thus there is a need to be able to logout from
+-- just a single audience while keeping the session for the other
+-- audiences.
+--
+-- When there is only a single audience, then this can be considered
+-- equal to `session.destroy`.
+--
+-- When the last audience is logged out, the cookie will be destroyed
+-- as well and invalidated on a client.
+--
+-- @function module.logout
+-- @tparam[opt] table configuration session @{configuration} overrides
+-- @treturn boolean `true` session exists for an audience and was logged out successfully, otherwise `false`
+-- @treturn string error message
+-- @treturn boolean `true` if session existed, otherwise `false`
+-- @treturn boolean `true` if session was logged out, otherwise `false`
+--
+-- @usage
+-- require "resty.session".logout()
+-- -- OR
+-- local ok, err, exists, logged_out = require "resty.session".logout({
+-- audience = "my-application",
+-- })
+function session.logout(configuration)
+ local self, err, exists = session.open(configuration)
+ if not exists then
+ return nil, err, false, false
+ end
+
+ local ok, err = self:logout()
+ if not ok then
+ return nil, err, true, false
+ end
+
+ return true, nil, true, true
+end
+
+---
+-- Destroy a session.
+--
+-- It destroys the whole session and clears the cookies.
+--
+-- @function module.destroy
+-- @tparam[opt] table configuration session @{configuration} overrides
+-- @treturn boolean `true` session exists and was destroyed successfully, otherwise `nil`
+-- @treturn string error message
+-- @treturn boolean `true` if session existed, otherwise `false`
+-- @treturn boolean `true` if session was destroyed, otherwise `false`
+--
+-- @usage
+-- require "resty.session".destroy()
+-- -- OR
+-- local ok, err, exists, destroyed = require "resty.session".destroy({
+-- cookie_name = "auth",
+-- })
+function session.destroy(configuration)
+ local self, err, exists = session.open(configuration)
+ if not exists then
+ return nil, err, false, false
+ end
+
+ local ok, err = self:destroy()
+ if not ok then
+ return nil, err, true, false
+ end
+
+ return true, nil, true, true
+end
+
+
+function session.__set_ngx_log(ngx_log)
+ log = ngx_log
+end
+
+
+function session.__set_ngx_var(ngx_var)
+ var = ngx_var
+end
+
+
+function session.__set_ngx_header(ngx_header)
+ header = ngx_header
+end
+
+
+function session.__set_ngx_req_clear_header(clear_header)
+ clear_request_header = clear_header
+end
+
+
+function session.__set_ngx_req_set_header(set_header)
+ set_request_header = set_header
+end
+
+
+return session
diff --git a/lib/resty/session/dshm.lua b/lib/resty/session/dshm.lua
new file mode 100644
index 000000000..325564d0a
--- /dev/null
+++ b/lib/resty/session/dshm.lua
@@ -0,0 +1,348 @@
+---
+-- Distributed Shared Memory (DSHM) backend for session library
+--
+-- @module resty.session.dshm
+
+
+local buffer = require "string.buffer"
+local utils = require "resty.session.utils"
+local dshm = require "resty.dshm"
+
+
+local meta_get_latest = utils.meta_get_latest
+local meta_get_value = utils.meta_get_value
+local get_name = utils.get_name
+
+
+local setmetatable = setmetatable
+local error = error
+local pairs = pairs
+local null = ngx.null
+local max = math.max
+
+
+local DEFAULT_HOST = "127.0.0.1"
+local DEFAULT_PORT = 4321
+
+
+local SESSIONS_BUFFER = buffer.new(128)
+
+
+-- not safe for concurrent access
+local function update_meta(dshmc, meta_key, key, exp, current_time)
+ local metadata = dshmc:get(meta_key)
+ local sessions = metadata and meta_get_latest(metadata, current_time) or {}
+
+ SESSIONS_BUFFER:reset()
+
+ sessions[key] = exp > 0 and exp or nil
+
+ local max_expiry = current_time
+ for s, e in pairs(sessions) do
+ SESSIONS_BUFFER:put(meta_get_value(s, e))
+ max_expiry = max(max_expiry, e)
+ end
+
+ local ser = SESSIONS_BUFFER:get()
+
+ if #ser > 0 then
+ return dshmc:set(meta_key, ser, max_expiry - current_time)
+ end
+
+ return dshmc:delete(meta_key)
+end
+
+
+local function READ_METADATA(self, dshmc, name, audience, subject, current_time)
+ local meta_key = get_name(self, name, audience, subject)
+ local res = dshmc:get(meta_key)
+ if not res then
+ return nil, "not found"
+ end
+
+ return meta_get_latest(res, current_time)
+end
+
+
+local function SET(self, dshmc, name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+ local inferred_key = get_name(self, name, key)
+
+ if not metadata and not old_key then
+ return dshmc:set(inferred_key, value, ttl)
+ end
+
+ local ok, err = dshmc:set(inferred_key, value, ttl)
+ if err then
+ return nil, err
+ end
+
+ local old_name = old_key and get_name(self, name, old_key)
+ if old_name then
+ if remember then
+ dshmc:delete(old_name)
+ else
+ dshmc:touch(old_name, stale_ttl)
+ end
+ end
+
+ if metadata then
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = get_name(self, name, audiences[i], subjects[i])
+ update_meta(dshmc, meta_key, key, current_time + ttl, current_time)
+
+ if old_key then
+ update_meta(dshmc, meta_key, old_key, 0, current_time)
+ end
+ end
+ end
+ return ok
+end
+
+
+local function GET(self, dshmc, name, key)
+ local res, err = dshmc:get(get_name(self, name, key))
+ if err then
+ return nil, err
+ end
+ return res
+end
+
+
+local function DELETE(self, dshmc, name, key, current_time, metadata)
+ local key_name = get_name(self, name, key)
+ local ok, err = dshmc:delete(key_name)
+
+ if not metadata then
+ return ok, err
+ end
+
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = get_name(self, name, audiences[i], subjects[i])
+ update_meta(dshmc, meta_key, key, 0, current_time)
+ end
+
+ return ok, err
+end
+
+
+local function exec(self, func, ...)
+ local dshmc = dshm:new()
+ local connect_timeout = self.connect_timeout
+ local send_timeout = self.send_timeout
+ local read_timeout = self.read_timeout
+ if connect_timeout or send_timeout or read_timeout then
+ dshmc.sock:settimeouts(connect_timeout, send_timeout, read_timeout)
+ end
+
+ local ok, err = dshmc:connect(self.host, self.port, self.options)
+ if not ok then
+ return nil, err
+ end
+
+ if self.ssl and dshmc:get_reused_times() == 0 then
+ ok, err = dshmc.sock:sslhandshake(false, self.server_name, self.ssl_verify)
+ if not ok then
+ dshmc:close()
+ return nil, err
+ end
+ end
+
+ ok, err = func(self, dshmc, ...)
+ if err then
+ dshmc:close()
+ return nil, err
+ end
+
+ if not dshmc:set_keepalive(self.keepalive_timeout) then
+ dshmc:close()
+ end
+
+ if ok == null then
+ ok = nil
+ end
+
+ return ok, err
+end
+
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam boolean remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(...)
+ return exec(self, SET, ...)
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(...)
+ return exec(self, GET, ...)
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(...)
+ return exec(self, DELETE, ...)
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function instance:read_metadata
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+function metatable:read_metadata(...)
+ return exec(self, READ_METADATA, ...)
+end
+
+
+local storage = {}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- Distributed shared memory storage backend configuration
+-- @field prefix The prefix for the keys stored in DSHM.
+-- @field suffix The suffix for the keys stored in DSHM.
+-- @field host The host to connect (defaults to `"127.0.0.1"`).
+-- @field port The port to connect (defaults to `4321`).
+-- @field connect_timeout Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.
+-- @field send_timeout Controls the default timeout value used in TCP/unix-domain socket object's `send` method.
+-- @field read_timeout Controls the default timeout value used in TCP/unix-domain socket object's `receive` method.
+-- @field keepalive_timeout Controls the default maximal idle time of the connections in the connection pool.
+-- @field pool A custom name for the connection pool being used.
+-- @field pool_size The size of the connection pool.
+-- @field backlog A queue size to use when the connection pool is full (configured with @pool_size).
+-- @field ssl Enable SSL (defaults to `false`).
+-- @field ssl_verify Verify server certificate (defaults to `nil`).
+-- @field server_name The server name for the new TLS extension Server Name Indication (SNI).
+-- @table configuration
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a distributed shared memory storage.
+--
+-- This creates a new distributed shared memory storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration DSHM storage @{configuration}
+-- @treturn table DSHM storage instance
+function storage.new(configuration)
+ local prefix = configuration and configuration.prefix
+ local suffix = configuration and configuration.suffix
+
+ local host = configuration and configuration.host or DEFAULT_HOST
+ local port = configuration and configuration.port or DEFAULT_PORT
+
+ local connect_timeout = configuration and configuration.connect_timeout
+ local send_timeout = configuration and configuration.send_timeout
+ local read_timeout = configuration and configuration.read_timeout
+ local keepalive_timeout = configuration and configuration.keepalive_timeout
+
+ local pool = configuration and configuration.pool
+ local pool_size = configuration and configuration.pool_size
+ local backlog = configuration and configuration.backlog
+ local ssl = configuration and configuration.ssl
+ local ssl_verify = configuration and configuration.ssl_verify
+ local server_name = configuration and configuration.server_name
+
+ if pool or pool_size or backlog then
+ setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ host = host,
+ port = port,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ server_name = server_name,
+ options = {
+ pool = pool,
+ pool_size = pool_size,
+ backlog = backlog,
+ }
+ }, metatable)
+ end
+
+ return setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ host = host,
+ port = port,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ server_name = server_name,
+ }, metatable)
+end
+
+
+return storage
diff --git a/lib/resty/session/file.lua b/lib/resty/session/file.lua
new file mode 100644
index 000000000..414447d41
--- /dev/null
+++ b/lib/resty/session/file.lua
@@ -0,0 +1,177 @@
+---
+-- File storage backend for session library.
+--
+-- @module resty.session.file
+
+
+local file_utils = require "resty.session.file.utils"
+
+
+local run_worker_thread = file_utils.run_worker_thread
+
+
+local setmetatable = setmetatable
+local error = error
+local byte = string.byte
+
+
+local SLASH_BYTE = byte("/")
+
+
+local THREAD_MODULE = "resty.session.file.thread"
+
+
+local DEFAULT_POOL = "default"
+local DEFAULT_PATH do
+ local path = os.tmpname()
+ local pos
+ for i = #path, 1, -1 do
+ if byte(path, i) == SLASH_BYTE then
+ pos = i
+ break
+ end
+ end
+
+ DEFAULT_PATH = path:sub(1, pos)
+end
+
+
+local function run_thread(self, func, ...)
+ local ok, res, err = run_worker_thread(self.pool, THREAD_MODULE, func, self.path, self.prefix, self.suffix, ...)
+ if not ok then
+ return nil, res
+ end
+
+ return res, err
+end
+
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam boolean remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(...)
+ return run_thread(self, "set", ...)
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(...)
+ return run_thread(self, "get", ...)
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(...)
+ return run_thread(self, "delete", ...)
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function instance:read_metadata
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+function metatable:read_metadata(...)
+ return run_thread(self, "read_metadata", ...)
+end
+
+
+local storage = {}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- File storage backend configuration
+-- @field prefix File prefix for session file.
+-- @field suffix File suffix (or extension without `.`) for session file.
+-- @field pool Name of the thread pool under which file writing happens (available on Linux only).
+-- @field path Path (or directory) under which session files are created.
+-- @table configuration
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a file storage.
+--
+-- This creates a new file storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration file storage @{configuration}
+-- @treturn table file storage instance
+function storage.new(configuration)
+ local prefix = configuration and configuration.prefix
+ local suffix = configuration and configuration.suffix
+
+ local pool = configuration and configuration.pool or DEFAULT_POOL
+ local path = configuration and configuration.path or DEFAULT_PATH
+
+ if byte(path, -1) ~= SLASH_BYTE then
+ path = path .. "/"
+ end
+
+ return setmetatable({
+ prefix = prefix ~= "" and prefix or nil,
+ suffix = suffix ~= "" and suffix or nil,
+ pool = pool,
+ path = path,
+ }, metatable)
+end
+
+
+return storage
diff --git a/lib/resty/session/file/thread.lua b/lib/resty/session/file/thread.lua
new file mode 100644
index 000000000..127a6f999
--- /dev/null
+++ b/lib/resty/session/file/thread.lua
@@ -0,0 +1,209 @@
+---
+-- File storage backend worker thread module
+--
+-- @module resty.session.file.thread
+
+
+local file_utils = require "resty.session.file.utils"
+local utils = require "resty.session.utils"
+
+
+local get_modification = file_utils.get_modification
+local meta_get_key = file_utils.meta_get_key
+local file_create = file_utils.file_create
+local file_append = file_utils.file_append
+local file_delete = file_utils.file_delete
+local file_touch = file_utils.file_touch
+local file_read = file_utils.file_read
+local get_path = file_utils.get_path
+local cleanup = file_utils.cleanup
+
+
+local meta_get_latest = utils.meta_get_latest
+local meta_get_value = utils.meta_get_value
+
+
+local max = math.max
+
+
+local function update_meta(path, prefix, suffix, name, meta_key, key, exp)
+ local meta_value = meta_get_value(key, exp)
+ if not meta_value then
+ return
+ end
+
+ local file_path = get_path(path, prefix, suffix, name, meta_key)
+ file_append(file_path, meta_value)
+ local current_expiry = get_modification(file_path) or 0
+
+ local new_expiry = max(current_expiry, exp)
+ file_touch(file_path, new_expiry)
+end
+
+
+---
+-- Store session data.
+--
+-- @function module.set
+-- @tparam string path the path where sessions are stored
+-- @tparam string prefix the prefix for session files
+-- @tparam string suffix the suffix for session files
+-- @tparam string name the cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam table remember whether storing persistent session or not
+-- @treturn table|nil session metadata
+-- @treturn string error message
+local function set(path, prefix, suffix, name, key, value, ttl,
+ current_time, old_key, stale_ttl, metadata, remember)
+ local file_path = get_path(path, prefix, suffix, name, key)
+ if not metadata and not old_key then
+ local ok, err = file_create(file_path, value)
+
+ if ok and current_time and ttl then
+ file_touch(file_path, current_time + ttl)
+ end
+
+ cleanup(path, prefix, suffix, name, current_time)
+
+ return ok, err
+ end
+
+ local old_ttl, old_file_path
+ if old_key then
+ old_file_path = get_path(path, prefix, suffix, name, old_key)
+ if not remember then
+ local exp = get_modification(old_file_path)
+ if exp then
+ old_ttl = exp - current_time
+ end
+ end
+ end
+
+ local ok, err = file_create(file_path, value)
+ if ok and current_time and ttl then
+ file_touch(file_path, current_time + ttl)
+ end
+
+ if old_file_path then
+ if remember then
+ file_delete(old_file_path)
+
+ elseif not old_ttl or old_ttl > stale_ttl then
+ file_touch(old_file_path, current_time + stale_ttl)
+ end
+ end
+
+ if metadata then
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = meta_get_key(audiences[i], subjects[i])
+ update_meta(path, prefix, suffix, name, meta_key, key, current_time + ttl)
+
+ if old_key then
+ update_meta(path, prefix, suffix, name, meta_key, old_key, 0)
+ end
+ end
+ end
+
+ cleanup(path, prefix, suffix, name, current_time)
+
+ return ok, err
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function module.GET
+-- @tparam string path the path where sessions are stored
+-- @tparam string prefix the prefix for session files
+-- @tparam string suffix the suffix for session files
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+local function get(path, prefix, suffix, name, key, current_time)
+ local file_path = get_path(path, prefix, suffix, name, key)
+
+ -- TODO: do we want to check expiry here?
+ -- The cookie header already has the info and has a MAC too.
+ local exp = get_modification(file_path)
+ if exp and exp < current_time then
+ return nil, "expired"
+ end
+
+ return file_read(file_path)
+end
+
+
+---
+-- Delete session data.
+--
+-- @function module.delete
+-- @tparam string path the path where sessions are stored
+-- @tparam string prefix the prefix for session files
+-- @tparam string suffix the suffix for session files
+-- @tparam string name the cookie name
+-- @tparam string key session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+local function delete(path, prefix, suffix, name, key, current_time, metadata)
+ local file_path = get_path(path, prefix, suffix, name, key)
+ file_delete(file_path)
+
+ if metadata then
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = meta_get_key(audiences[i], subjects[i])
+ update_meta(path, prefix, suffix, name, meta_key, key, 0)
+ end
+ end
+
+ cleanup(path, prefix, suffix, name, current_time)
+
+ return true
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function module.read_metadata
+-- @tparam string path the path where sessions are stored
+-- @tparam string prefix the prefix for session files
+-- @tparam string suffix the suffix for session files
+-- @tparam string name the cookie name
+-- @tparam string audience session audience
+-- @tparam string subject session subject
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+local function read_metadata(path, prefix, suffix, name, audience, subject, current_time)
+ local meta_key = meta_get_key(audience, subject)
+ local file_path = get_path(path, prefix, suffix, name, meta_key)
+ local metadata, err = file_read(file_path)
+ if not metadata then
+ return nil, err
+ end
+
+ return meta_get_latest(metadata, current_time)
+end
+
+
+return {
+ set = set,
+ get = get,
+ delete = delete,
+ read_metadata = read_metadata,
+}
diff --git a/lib/resty/session/file/utils.lua b/lib/resty/session/file/utils.lua
new file mode 100644
index 000000000..af4a12c03
--- /dev/null
+++ b/lib/resty/session/file/utils.lua
@@ -0,0 +1,290 @@
+---
+-- File storage utilities
+--
+-- @module resty.session.file.utils
+
+
+local lfs = require "lfs"
+
+
+local attributes = lfs.attributes
+local touch = lfs.touch
+local dir = lfs.dir
+
+
+
+local file_delete = os.remove
+local random = math.random
+local pcall = pcall
+local open = io.open
+local fmt = string.format
+
+
+local CLEANUP_PROBABILITY = 0.0005 -- 1 / 2000
+
+
+local run_worker_thread do
+ run_worker_thread = ngx.run_worker_thread -- luacheck: ignore
+ if not run_worker_thread then
+ local require = require
+ run_worker_thread = function(_, module, func, ...)
+ local m = require(module)
+ return pcall(m[func], ...)
+ end
+ end
+end
+
+
+local function file_touch(path, mtime)
+ return touch(path, nil, mtime)
+end
+
+
+---
+-- Store data in file.
+--
+-- @function file_create
+-- @tparam string path file path
+-- @tparam string content file content
+-- @treturn true|nil ok
+-- @treturn string error message
+local function file_create(path, content)
+ local file, err = open(path, "wb")
+ if not file then
+ return nil, err
+ end
+
+ local ok, err = file:write(content)
+
+ file:close()
+
+ if not ok then
+ file_delete(path)
+ return nil, err
+ end
+
+ return true
+end
+
+
+---
+-- Append data in file.
+--
+-- @function file_append
+-- @tparam string path file path
+-- @tparam string data file data
+-- @treturn true|nil ok
+-- @treturn string error message
+local function file_append(path, data)
+ local file, err = open(path, "a")
+ if not file then
+ return nil, err
+ end
+
+ local ok, err = file:write(data)
+
+ file:close()
+
+ if not ok then
+ file_delete(path)
+ return nil, err
+ end
+
+ return true
+end
+
+
+---
+-- Read data from a file.
+--
+-- @function file_read
+-- @tparam string path file to read
+-- @treturn string|nil content
+-- @treturn string error message
+local function file_read(path)
+ local file, err = open(path, "rb")
+ if not file then
+ return nil, err
+ end
+
+ local content, err = file:read("*a")
+
+ file:close()
+
+ if not content then
+ return nil, err
+ end
+
+ return content
+end
+
+
+---
+-- Generate the path for a file to be stored at.
+--
+-- @tparam string path the path where sessions are stored
+-- @tparam string prefix the prefix for session files
+-- @tparam string suffix the suffix for session files
+-- @tparam string name the cookie name
+-- @tparam string key session key
+-- @treturn string path
+local function get_path(path, prefix, suffix, name, key)
+ if prefix and suffix then
+ return fmt("%s%s_%s_%s.%s", path, prefix, name, key, suffix)
+ elseif prefix then
+ return fmt("%s%s_%s_%s", path, prefix, name, key)
+ elseif suffix then
+ return fmt("%s%s_%s.%s", path, name, key, suffix)
+ else
+ return fmt("%s%s_%s", path, name, key)
+ end
+end
+
+
+---
+-- Get the value modification time of a file.
+--
+-- @function utils.get_modification
+-- @tparam string path the path to the file
+local function get_modification(path)
+ local attr = attributes(path)
+ if not attr or attr.mode ~= "file" then
+ return
+ end
+
+ return attr.modification or nil
+end
+
+
+---
+-- Given an audience and a subject, generate a metadata key.
+--
+-- @function utils.meta_get_key
+-- @tparam string audience session audience
+-- @tparam string subject session subject
+-- @treturn string metadata key
+local function meta_get_key(audience, subject)
+ return fmt("%s:%s", audience, subject)
+end
+
+
+---
+-- Validate a file name.
+-- Run a few checks to try to determine if the file is managed by this library
+--
+-- @function utils.validate_file_name
+-- @tparam string prefix the prefix for session files
+-- @tparam string suffix the suffix for session files
+-- @tparam string name cookie name
+-- @tparam string filename the name of the file
+-- @treturn true|false whether the file is managed by the library or not
+local validate_file_name do
+ local byte = string.byte
+ local sub = string.sub
+ local find = string.find
+
+ local UNDERSCORE = byte("_")
+ local DOT = byte(".")
+
+ validate_file_name = function(prefix, suffix, name, filename)
+ if filename == "." or filename == ".." then
+ return false
+ end
+
+ local plen = 0
+ if prefix then
+ plen = #prefix
+ if byte(filename, plen + 1) ~= UNDERSCORE or
+ (plen > 0 and sub(filename, 1, plen) ~= prefix) then
+ return false
+ end
+ end
+
+ local slen = 0
+ if suffix then
+ slen = #suffix
+
+ if byte(filename, -1 - slen) ~= DOT or
+ (slen > 0 and sub(filename, -slen) ~= suffix)
+ then
+ return false
+ end
+ end
+
+ local nlen = #name
+ local name_start = plen == 0 and 1 or plen + 2
+ local name_end = name_start + nlen - 1
+
+ if byte(filename, name_end + 1) ~= UNDERSCORE or
+ sub(filename, name_start, name_end) ~= name
+ then
+ return false
+ end
+
+ local rest
+ if slen == 0 then
+ rest = sub(filename, name_end + 2)
+ else
+ rest = sub(filename, name_end + 2, -2 - slen)
+ end
+
+ local rlen = #rest
+ if rlen < 3 then
+ return false
+ end
+
+ if rlen ~= 43 then
+ local colon_pos = find(rest, ":", 2, true)
+ if not colon_pos or colon_pos == 43 then
+ return false
+ end
+ end
+
+ return true
+ end
+end
+
+
+---
+-- Clean up expired session and metadata files.
+--
+-- @function utils.cleanup
+-- @tparam string path the path where sessions are stored
+-- @tparam string prefix the prefix for session files
+-- @tparam string suffix the suffix for session files
+-- @tparam string name cookie name
+-- @tparam number current_time current time
+-- @treturn true|false whether clean up completed
+local function cleanup(path, prefix, suffix, name, current_time)
+ if random() > CLEANUP_PROBABILITY then
+ return false
+ end
+
+ local deleted = 0
+
+ for file in dir(path) do
+ if validate_file_name(prefix, suffix, name, file) then
+ local exp = get_modification(path .. file)
+ if exp and exp < current_time then
+ file_delete(path .. file)
+ deleted = deleted + 1
+ end
+ end
+ end
+ return true
+end
+
+
+return {
+ validate_file_name = validate_file_name,
+ run_worker_thread = run_worker_thread,
+ get_modification = get_modification,
+ meta_get_key = meta_get_key,
+ file_create = file_create,
+ file_append = file_append,
+ file_delete = file_delete,
+ file_touch = file_touch,
+ file_read = file_read,
+ get_path = get_path,
+ cleanup = cleanup,
+}
diff --git a/lib/resty/session/memcached.lua b/lib/resty/session/memcached.lua
new file mode 100644
index 000000000..ab82a3296
--- /dev/null
+++ b/lib/resty/session/memcached.lua
@@ -0,0 +1,395 @@
+---
+-- Memcached backend for session library
+--
+-- @module resty.session.memcached
+
+
+local memcached = require "resty.memcached"
+local buffer = require "string.buffer"
+local utils = require "resty.session.utils"
+
+
+local meta_get_latest = utils.meta_get_latest
+local meta_get_value = utils.meta_get_value
+local get_name = utils.get_name
+local errmsg = utils.errmsg
+
+
+local setmetatable = setmetatable
+local random = math.random
+local error = error
+local pairs = pairs
+local null = ngx.null
+local log = ngx.log
+local max = math.max
+
+
+local WARN = ngx.WARN
+
+
+local CLEANUP_PROBABILITY = 0.1 -- 1 / 10
+local SESSIONS_BUFFER = buffer.new(128)
+
+
+local function cleanup(memc, meta_key, current_time)
+ local res, _, cas_unique, err = memc:gets(meta_key)
+ if not res then
+ return nil, err
+ end
+
+ local sessions = meta_get_latest(res, current_time)
+
+ SESSIONS_BUFFER:reset()
+
+ local max_expiry = current_time
+ for key, exp in pairs(sessions) do
+ SESSIONS_BUFFER:put(meta_get_value(key, exp))
+ max_expiry = max(max_expiry, exp)
+ end
+
+ local exp = max_expiry - current_time
+ if exp > 0 then
+ return memc:cas(meta_key, SESSIONS_BUFFER:get(), cas_unique, exp)
+ end
+
+ return memc:delete(meta_key)
+end
+
+
+local function read_metadata(self, memc, name, audience, subject, current_time)
+ local meta_key = get_name(self, name, audience, subject)
+ local res, _, err = memc:get(meta_key)
+ if not res then
+ return nil, err
+ end
+
+ return meta_get_latest(res, current_time)
+end
+
+
+-- TODO possible improvement: when available in the lib, use pipelines
+local function SET(self, memc, name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+ local inferred_key = get_name(self, name, key)
+
+ if not metadata and not old_key then
+ return memc:set(inferred_key, value, ttl)
+ end
+
+ local ok, err = memc:set(inferred_key, value, ttl)
+ if not ok then
+ return nil, err
+ end
+
+ local old_name = old_key and get_name(self, name, old_key)
+ if old_name then
+ if remember then
+ memc:delete(old_name)
+ else
+ memc:touch(old_name, stale_ttl)
+ end
+ end
+
+ if not metadata then
+ return true
+ end
+
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = get_name(self, name, audiences[i], subjects[i])
+ local meta_value = meta_get_value(key, current_time + ttl)
+
+ local added, err = memc:add(meta_key, meta_value)
+ if not added then
+ local appended, err2 = memc:append(meta_key, meta_value)
+ if not appended then
+ log(WARN, "[session] ", errmsg(err2 or err, "failed to store metadata"))
+ end
+ end
+
+ if old_key then
+ meta_value = meta_get_value(old_key, 0)
+ local ok, err = memc:append(meta_key, meta_value)
+ if not ok then
+ log(WARN, "[session] ", errmsg(err, "failed to update metadata"))
+ end
+ end
+
+ -- no need to clean up every time we write
+ -- it is just beneficial when a key is used a lot
+ if random() < CLEANUP_PROBABILITY then
+ cleanup(memc, meta_key, current_time)
+ end
+ end
+
+ return true
+end
+
+
+local function GET(self, memc, name, key)
+ local res, _, err = memc:get(get_name(self, name, key))
+ if err then
+ return nil, err
+ end
+ return res
+end
+
+
+local function DELETE(self, memc, name, key, current_time, metadata)
+ local key_name = get_name(self, name, key)
+ local ok, err = memc:delete(key_name)
+ if not metadata then
+ return ok, err
+ end
+
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = get_name(self, name, audiences[i], subjects[i])
+ local meta_value = meta_get_value(key, 0)
+ local ok, err = memc:append(meta_key, meta_value)
+ if not ok and err ~= "NOT_STORED" then
+ log(WARN, "[session] ", errmsg(err, "failed to update metadata"))
+ end
+
+ cleanup(memc, meta_key, current_time)
+ end
+
+ return ok, err
+end
+
+
+local DEFAULT_HOST = "127.0.0.1"
+local DEFAULT_PORT = 11211
+
+
+local function exec(self, func, ...)
+ local memc = memcached:new()
+
+ local connect_timeout = self.connect_timeout
+ local send_timeout = self.send_timeout
+ local read_timeout = self.read_timeout
+ if connect_timeout or send_timeout or read_timeout then
+ memc:set_timeouts(connect_timeout, send_timeout, read_timeout)
+ end
+
+ local ok, err do
+ local socket = self.socket
+ if socket then
+ ok, err = memc:connect(socket, self.options)
+ else
+ ok, err = memc:connect(self.host, self.port, self.options)
+ end
+ end
+ if not ok then
+ return nil, err
+ end
+
+ if self.ssl and memc:get_reused_times() == 0 then
+ ok, err = memc:sslhandshake(false, self.server_name, self.ssl_verify)
+ if not ok then
+ memc:close()
+ return nil, err
+ end
+ end
+
+ ok, err = func(self, memc, ...)
+
+ if err then
+ memc:close()
+ return nil, err
+ end
+
+ if not memc:set_keepalive(self.keepalive_timeout) then
+ memc:close()
+ end
+
+ if ok == null then
+ ok = nil
+ end
+
+ return ok, err
+end
+
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam boolean remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(...)
+ return exec(self, SET, ...)
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(...)
+ return exec(self, GET, ...)
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(...)
+ return exec(self, DELETE, ...)
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function instance:read_metadata
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+function metatable:read_metadata(...)
+ return exec(self, read_metadata, ...)
+end
+
+
+local storage = {}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- Distributed shared memory storage backend configuration
+-- @field prefix Prefix for the keys stored in memcached.
+-- @field suffix Suffix for the keys stored in memcached.
+-- @field host The host to connect (defaults to `"127.0.0.1"`).
+-- @field port The port to connect (defaults to `11211`).
+-- @field socket The socket file to connect to (defaults to `nil`).
+-- @field connect_timeout Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.
+-- @field send_timeout Controls the default timeout value used in TCP/unix-domain socket object's `send` method.
+-- @field read_timeout Controls the default timeout value used in TCP/unix-domain socket object's `receive` method.
+-- @field keepalive_timeout Controls the default maximal idle time of the connections in the connection pool.
+-- @field pool A custom name for the connection pool being used.
+-- @field pool_size The size of the connection pool.
+-- @field backlog A queue size to use when the connection pool is full (configured with @pool_size).
+-- @field ssl Enable SSL (defaults to `false`).
+-- @field ssl_verify Verify server certificate (defaults to `nil`).
+-- @field server_name The server name for the new TLS extension Server Name Indication (SNI).
+-- @table configuration
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a memcached storage.
+--
+-- This creates a new memcached storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration memcached storage @{configuration}
+-- @treturn table memcached storage instance
+function storage.new(configuration)
+ local prefix = configuration and configuration.prefix
+ local suffix = configuration and configuration.suffix
+
+ local host = configuration and configuration.host or DEFAULT_HOST
+ local port = configuration and configuration.port or DEFAULT_PORT
+ local socket = configuration and configuration.socket
+
+ local connect_timeout = configuration and configuration.connect_timeout
+ local send_timeout = configuration and configuration.send_timeout
+ local read_timeout = configuration and configuration.read_timeout
+ local keepalive_timeout = configuration and configuration.keepalive_timeout
+
+ local pool = configuration and configuration.pool
+ local pool_size = configuration and configuration.pool_size
+ local backlog = configuration and configuration.backlog
+ local ssl = configuration and configuration.ssl
+ local ssl_verify = configuration and configuration.ssl_verify
+ local server_name = configuration and configuration.server_name
+
+ if pool or pool_size or backlog then
+ setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ host = host,
+ port = port,
+ socket = socket,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ server_name = server_name,
+ options = {
+ pool = pool,
+ pool_size = pool_size,
+ backlog = backlog,
+ }
+ }, metatable)
+ end
+
+ return setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ host = host,
+ port = port,
+ socket = socket,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ server_name = server_name,
+ }, metatable)
+end
+
+
+return storage
diff --git a/lib/resty/session/mysql.lua b/lib/resty/session/mysql.lua
new file mode 100644
index 000000000..33a202905
--- /dev/null
+++ b/lib/resty/session/mysql.lua
@@ -0,0 +1,379 @@
+---
+-- MySQL / MariaDB backend for session library
+--
+-- @module resty.session.mysql
+
+
+---
+-- Database
+-- @section database
+
+
+---
+-- Sessions table.
+--
+-- Database table that stores session data.
+--
+-- @usage
+-- CREATE TABLE IF NOT EXISTS sessions (
+-- sid CHAR(43) PRIMARY KEY,
+-- name VARCHAR(255),
+-- data MEDIUMTEXT,
+-- exp DATETIME,
+-- INDEX (exp)
+-- ) CHARACTER SET ascii;
+-- @table sessions
+
+
+---
+-- Sessions metadata table.
+--
+-- This is only needed if you want to store session metadata.
+--
+-- @usage
+-- CREATE TABLE IF NOT EXISTS sessions_meta (
+-- aud VARCHAR(255),
+-- sub VARCHAR(255),
+-- sid CHAR(43),
+-- PRIMARY KEY (aud, sub, sid),
+-- CONSTRAINT FOREIGN KEY (sid) REFERENCES sessions(sid) ON DELETE CASCADE ON UPDATE CASCADE
+-- ) CHARACTER SET ascii;
+-- @table metadata
+
+
+local buffer = require "string.buffer"
+local mysql = require "resty.mysql"
+
+
+local setmetatable = setmetatable
+local random = math.random
+local ipairs = ipairs
+local error = error
+local fmt = string.format
+
+
+local DEFAULT_HOST = "127.0.0.1"
+local DEFAULT_PORT = 3306
+local DEFAULT_TABLE = "sessions"
+local DEFAULT_CHARSET = "ascii"
+
+
+local SET = "INSERT INTO %s (sid, name, data, exp) VALUES ('%s', '%s', '%s', FROM_UNIXTIME(%d)) AS new ON DUPLICATE KEY UPDATE data = new.data"
+local SET_META_PREFIX = "INSERT INTO %s (aud, sub, sid) VALUES "
+local SET_META_VALUES = "('%s', '%s', '%s')"
+local SET_META_SUFFIX = " ON DUPLICATE KEY UPDATE sid = sid"
+local GET_META = "SELECT sid, exp FROM %s JOIN %s USING (sid) WHERE aud = '%s' AND sub = '%s' AND exp >= FROM_UNIXTIME(%d)"
+local GET = "SELECT data FROM %s WHERE sid = '%s' AND exp >= FROM_UNIXTIME(%d)"
+local EXPIRE = "UPDATE %s SET exp = FROM_UNIXTIME(%d) WHERE sid = '%s' AND exp > FROM_UNIXTIME(%d)"
+local DELETE = "DELETE FROM %s WHERE sid = '%s'"
+local CLEANUP = "DELETE FROM %s WHERE exp < FROM_UNIXTIME(%d)"
+
+
+local SQL = buffer.new()
+local STM_DELIM = ";\n"
+local VAL_DELIM = ", "
+
+
+local CLEANUP_PROBABILITY = 0.001 -- 1 / 1000
+
+
+local function exec(self, query)
+ local my = mysql:new()
+
+ local connect_timeout = self.connect_timeout
+ local send_timeout = self.send_timeout
+ local read_timeout = self.read_timeout
+ if connect_timeout or send_timeout or read_timeout then
+ if my.sock and my.sock.settimeouts then
+ my.sock:settimeouts(connect_timeout, send_timeout, read_timeout)
+ else
+ my:set_timeout(connect_timeout)
+ end
+ end
+
+ local ok, err = my:connect(self.options)
+ if not ok then
+ return nil, err
+ end
+
+ ok, err = my:query(query)
+
+ if not my:set_keepalive(self.keepalive_timeout) then
+ my:close()
+ end
+
+ return ok, err
+end
+
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam boolean remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+ local table = self.table
+ local exp = ttl + current_time
+
+ if not metadata and not old_key then
+ return exec(self, fmt(SET, table, key, name, value, exp))
+ end
+
+ SQL:reset():putf(SET, table, key, name, value, exp)
+
+ if old_key then
+ if remember then
+ SQL:put(STM_DELIM):putf(DELETE, table, old_key)
+ else
+ local stale_exp = stale_ttl + current_time
+ SQL:put(STM_DELIM):putf(EXPIRE, table, stale_exp, old_key, stale_exp)
+ end
+ end
+
+ local table_meta = self.table_meta
+ if metadata then
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+
+ SQL:put(STM_DELIM):putf(SET_META_PREFIX, table_meta)
+
+ for i = 1, count do
+ if i > 1 then
+ SQL:put(VAL_DELIM)
+ end
+ SQL:putf(SET_META_VALUES, audiences[i], subjects[i], key)
+ end
+
+ SQL:putf(SET_META_SUFFIX)
+ end
+
+ if random() < CLEANUP_PROBABILITY then
+ SQL:put(STM_DELIM):putf(CLEANUP, self.table, current_time)
+ end
+
+ return exec(self, SQL:get())
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(name, key, current_time) -- luacheck: ignore
+ local res, err = exec(self, fmt(GET, self.table, key, current_time))
+ if not res then
+ return nil, err
+ end
+
+ local row = res[1]
+ if not row then
+ return nil, "session not found"
+ end
+
+ local data = row.data
+ if not row.data then
+ return nil, "session not found"
+ end
+
+ return data
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(name, key, current_time, metadata) -- luacheck: ignore
+ SQL:reset():putf(DELETE, self.table, key)
+
+ if random() < CLEANUP_PROBABILITY then
+ SQL:put(STM_DELIM):putf(CLEANUP, self.table, current_time)
+ end
+
+ return exec(self, SQL:get())
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function instance:read_metadata
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+function metatable:read_metadata(name, audience, subject, current_time) -- luacheck: ignore
+ local res = {}
+ local t = exec(self, fmt(GET_META, self.table_meta, self.table, audience, subject, current_time))
+ if not t then
+ return nil, "not found"
+ end
+
+ for _, v in ipairs(t) do
+ local key = v.sid
+ if key then
+ res[key] = v.exp
+ end
+ end
+
+ return res
+end
+
+local storage = {}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- Postgres storage backend configuration
+-- @field host The host to connect (defaults to `"127.0.0.1"`).
+-- @field port The port to connect (defaults to `3306`).
+-- @field socket The socket file to connect to (defaults to `nil`).
+-- @field username The database username to authenticate (defaults to `nil`).
+-- @field password Password for authentication, may be required depending on server configuration.
+-- @field charset The character set used on the MySQL connection (defaults to `"ascii"`).
+-- @field database The database name to connect.
+-- @field table_name Name of database table to which to store session data (defaults to `"sessions"`).
+-- @field table_name_meta Name of database meta data table to which to store session meta data (defaults to `"sessions_meta"`).
+-- @field max_packet_size The upper limit for the reply packets sent from the MySQL server (defaults to 1 MB).
+-- @field connect_timeout Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.
+-- @field send_timeout Controls the default timeout value used in TCP/unix-domain socket object's `send` method.
+-- @field read_timeout Controls the default timeout value used in TCP/unix-domain socket object's `receive` method.
+-- @field keepalive_timeout Controls the default maximal idle time of the connections in the connection pool.
+-- @field pool A custom name for the connection pool being used.
+-- @field pool_size The size of the connection pool.
+-- @field backlog A queue size to use when the connection pool is full (configured with @pool_size).
+-- @field ssl Enable SSL (defaults to `false`).
+-- @field ssl_verify Verify server certificate (defaults to `nil`).
+-- @table configuration
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a MySQL / MariaDB storage.
+--
+-- This creates a new MySQL / MariaDB storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration mysql/mariadb storage @{configuration}
+-- @treturn table mysql/mariadb storage instance
+function storage.new(configuration)
+ local host = configuration and configuration.host or DEFAULT_HOST
+ local port = configuration and configuration.port or DEFAULT_PORT
+ local socket = configuration and configuration.socket
+
+ local username = configuration and configuration.username
+ local password = configuration and configuration.password
+ local charset = configuration and configuration.charset or DEFAULT_CHARSET
+ local database = configuration and configuration.database
+ local max_packet_size = configuration and configuration.max_packet_size
+
+ local table_name = configuration and configuration.table or DEFAULT_TABLE
+ local table_name_meta = configuration and configuration.table_meta
+
+ local connect_timeout = configuration and configuration.connect_timeout
+ local send_timeout = configuration and configuration.send_timeout
+ local read_timeout = configuration and configuration.read_timeout
+ local keepalive_timeout = configuration and configuration.keepalive_timeout
+
+ local pool = configuration and configuration.pool
+ local pool_size = configuration and configuration.pool_size
+ local backlog = configuration and configuration.backlog
+ local ssl = configuration and configuration.ssl
+ local ssl_verify = configuration and configuration.ssl_verify
+
+ if socket then
+ return setmetatable({
+ table = table_name,
+ table_meta = table_name_meta or (table_name .. "_meta"),
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ options = {
+ path = socket,
+ user = username,
+ password = password,
+ charset = charset,
+ database = database,
+ max_packet_size = max_packet_size,
+ pool = pool,
+ pool_size = pool_size,
+ backlog = backlog,
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ }
+ }, metatable)
+ end
+
+ return setmetatable({
+ table = table_name,
+ table_meta = table_name_meta or (table_name .. "_meta"),
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ options = {
+ host = host,
+ port = port,
+ user = username,
+ password = password,
+ charset = charset,
+ database = database,
+ max_packet_size = max_packet_size,
+ pool = pool,
+ pool_size = pool_size,
+ backlog = backlog,
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ }
+ }, metatable)
+end
+
+
+return storage
diff --git a/lib/resty/session/postgres.lua b/lib/resty/session/postgres.lua
new file mode 100644
index 000000000..25e603920
--- /dev/null
+++ b/lib/resty/session/postgres.lua
@@ -0,0 +1,354 @@
+---
+-- Postgres backend for session library.
+--
+-- @module resty.session.postgres
+
+
+---
+-- Database
+-- @section database
+
+
+---
+-- Sessions table.
+--
+-- Database table that stores session data.
+--
+-- @usage
+-- CREATE TABLE IF NOT EXISTS sessions (
+-- sid TEXT PRIMARY KEY,
+-- name TEXT,
+-- data TEXT,
+-- exp TIMESTAMP WITH TIME ZONE
+-- );
+-- CREATE INDEX ON sessions (exp);
+-- @table sessions
+
+
+---
+-- Sessions metadata table.
+--
+-- This is only needed if you want to store session metadata.
+--
+-- @usage
+-- CREATE TABLE IF NOT EXISTS sessions_meta (
+-- aud TEXT,
+-- sub TEXT,
+-- sid TEXT REFERENCES sessions (sid) ON DELETE CASCADE ON UPDATE CASCADE,
+-- PRIMARY KEY (aud, sub, sid)
+-- );
+-- @table metadata
+
+
+local buffer = require "string.buffer"
+local pgmoon = require "pgmoon"
+
+
+local setmetatable = setmetatable
+local random = math.random
+local ipairs = ipairs
+local error = error
+local fmt = string.format
+
+
+local DEFAULT_HOST = "127.0.0.1"
+local DEFAULT_PORT = 5432
+local DEFAULT_TABLE = "sessions"
+
+
+local SET = "INSERT INTO %s (sid, name, data, exp) VALUES ('%s', '%s', '%s', TO_TIMESTAMP(%d) AT TIME ZONE 'UTC') ON CONFLICT (sid) DO UPDATE SET data = EXCLUDED.data, exp = EXCLUDED.exp"
+local SET_META_PREFIX = "INSERT INTO %s (aud, sub, sid) VALUES "
+local SET_META_VALUES = "('%s', '%s', '%s')"
+local SET_META_SUFFIX = " ON CONFLICT DO NOTHING"
+local GET_META = "SELECT sid, exp FROM %s JOIN %s USING (sid) WHERE aud = '%s' AND sub = '%s' AND exp >= TO_TIMESTAMP(%d)"
+local GET = "SELECT data FROM %s WHERE sid = '%s' AND exp >= TO_TIMESTAMP(%d) AT TIME ZONE 'UTC'"
+local EXPIRE = "UPDATE %s SET exp = TO_TIMESTAMP(%d) AT TIME ZONE 'UTC' WHERE sid = '%s' AND exp > TO_TIMESTAMP(%d) AT TIME ZONE 'UTC'"
+local DELETE = "DELETE FROM %s WHERE sid = '%s'"
+local CLEANUP = "DELETE FROM %s WHERE exp < TO_TIMESTAMP(%d)"
+
+
+local SQL = buffer.new()
+local STM_DELIM = ";\n"
+local VAL_DELIM = ", "
+
+
+local CLEANUP_PROBABILITY = 0.001 -- 1 / 1000
+
+
+local function exec(self, query)
+ local pg = pgmoon.new(self.options)
+
+ local connect_timeout = self.connect_timeout
+ local send_timeout = self.send_timeout
+ local read_timeout = self.read_timeout
+ if connect_timeout or send_timeout or read_timeout then
+ if pg.sock and pg.sock.settimeouts then
+ pg.sock:settimeouts(connect_timeout, send_timeout, read_timeout)
+ else
+ pg:settimeout(connect_timeout)
+ end
+ end
+
+ local ok, err = pg:connect()
+ if not ok then
+ return nil, err
+ end
+
+ ok, err = pg:query(query)
+
+ if not pg:keepalive(self.keepalive_timeout) then
+ pg:close()
+ end
+
+ return ok, err
+end
+
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam boolean remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+ local table = self.table
+ local exp = ttl + current_time
+
+ if not metadata and not old_key then
+ return exec(self, fmt(SET, table, key, name, value, exp))
+ end
+
+ SQL:reset():putf(SET, table, key, name, value, exp)
+
+ if old_key then
+ if remember then
+ SQL:put(STM_DELIM):putf(DELETE, table, old_key)
+ else
+ local stale_exp = stale_ttl + current_time
+ SQL:put(STM_DELIM):putf(EXPIRE, table, stale_exp, old_key, stale_exp)
+ end
+ end
+
+ local table_meta = self.table_meta
+ if metadata then
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+
+ SQL:put(STM_DELIM):putf(SET_META_PREFIX, table_meta)
+
+ for i = 1, count do
+ if i > 1 then
+ SQL:put(VAL_DELIM)
+ end
+ SQL:putf(SET_META_VALUES, audiences[i], subjects[i], key)
+ end
+
+ SQL:putf(SET_META_SUFFIX)
+ end
+
+ if random() < CLEANUP_PROBABILITY then
+ SQL:put(STM_DELIM):putf(CLEANUP, self.table, current_time)
+ end
+
+ return exec(self, SQL:get())
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(name, key, current_time) -- luacheck: ignore
+ local res, err = exec(self, fmt(GET, self.table, key, current_time))
+ if not res then
+ return nil, err
+ end
+
+ local row = res[1]
+ if not row then
+ return nil
+ end
+
+ local data = row.data
+ if not row.data then
+ return nil
+ end
+
+ return data
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(name, key, current_time, metadata) -- luacheck: ignore
+ SQL:reset():putf(DELETE, self.table, key)
+
+ if random() < CLEANUP_PROBABILITY then
+ SQL:put(STM_DELIM):putf(CLEANUP, self.table, current_time)
+ end
+
+ return exec(self, SQL:get())
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function instance:read_metadata
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+function metatable:read_metadata(name, audience, subject, current_time) -- luacheck: ignore
+ local res = {}
+
+ local t = exec(self, fmt(GET_META, self.table_meta, self.table, audience, subject, current_time))
+ if not t then
+ return nil, "not found"
+ end
+
+ for _, v in ipairs(t) do
+ local key = v.sid
+ if key then
+ res[key] = v.exp
+ end
+ end
+
+ return res
+end
+
+
+local storage = {}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- Postgres storage backend configuration
+-- @field host The host to connect (defaults to `"127.0.0.1"`).
+-- @field port The port to connect (defaults to `5432`).
+-- @field application Set the name of the connection as displayed in pg_stat_activity (defaults to `"pgmoon"`).
+-- @field username The database username to authenticate (defaults to `"postgres"`).
+-- @field password Password for authentication, may be required depending on server configuration.
+-- @field database The database name to connect.
+-- @field table_name Name of database table to which to store session data (can be `database schema` prefixed) (defaults to `"sessions"`).
+-- @field table_name_meta Name of database meta data table to which to store session meta data (can be `database schema` prefixed) (defaults to `"sessions_meta"`).
+-- @field connect_timeout Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.
+-- @field send_timeout Controls the default timeout value used in TCP/unix-domain socket object's `send` method.
+-- @field read_timeout Controls the default timeout value used in TCP/unix-domain socket object's `receive` method.
+-- @field keepalive_timeout Controls the default maximal idle time of the connections in the connection pool.
+-- @field pool A custom name for the connection pool being used.
+-- @field pool_size The size of the connection pool.
+-- @field backlog A queue size to use when the connection pool is full (configured with @pool_size).
+-- @field ssl Enable SSL (defaults to `false`).
+-- @field ssl_verify Verify server certificate (defaults to `nil`).
+-- @field ssl_required Abort the connection if the server does not support SSL connections (defaults to `nil`).
+-- @table configuration
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a Postgres storage.
+--
+-- This creates a new Postgres storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration postgres storage @{configuration}
+-- @treturn table postgres storage instance
+function storage.new(configuration)
+ local host = configuration and configuration.host or DEFAULT_HOST
+ local port = configuration and configuration.port or DEFAULT_PORT
+
+ local application = configuration and configuration.application
+ local username = configuration and configuration.username
+ local password = configuration and configuration.password
+ local database = configuration and configuration.database
+
+ local table_name = configuration and configuration.table or DEFAULT_TABLE
+ local table_name_meta = configuration and configuration.table_meta
+
+ local connect_timeout = configuration and configuration.connect_timeout
+ local send_timeout = configuration and configuration.send_timeout
+ local read_timeout = configuration and configuration.read_timeout
+ local keepalive_timeout = configuration and configuration.keepalive_timeout
+
+ local pool = configuration and configuration.pool
+ local pool_size = configuration and configuration.pool_size
+ local backlog = configuration and configuration.backlog
+ local ssl = configuration and configuration.ssl
+ local ssl_verify = configuration and configuration.ssl_verify
+ local ssl_required = configuration and configuration.ssl_required
+
+ return setmetatable({
+ table = table_name,
+ table_meta = table_name_meta or (table_name .. "_meta"),
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ options = {
+ host = host,
+ port = port,
+ application_name = application,
+ user = username,
+ password = password,
+ database = database,
+ socket_type = "nginx",
+ pool = pool,
+ pool_size = pool_size,
+ backlog = backlog,
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ ssl_required = ssl_required,
+ }
+ }, metatable)
+end
+
+
+return storage
diff --git a/lib/resty/session/redis.lua b/lib/resty/session/redis.lua
new file mode 100644
index 000000000..baffcce33
--- /dev/null
+++ b/lib/resty/session/redis.lua
@@ -0,0 +1,279 @@
+---
+-- Redis backend for session library
+--
+-- @module resty.session.redis
+
+
+local common = require "resty.session.redis.common"
+local redis = require "resty.redis"
+
+
+local setmetatable = setmetatable
+local error = error
+local null = ngx.null
+
+
+local DEFAULT_HOST = "127.0.0.1"
+local DEFAULT_PORT = 6379
+
+
+local SET = common.SET
+local GET = common.GET
+local UNLINK = common.UNLINK
+local READ_METADATA = common.READ_METADATA
+
+
+local function exec(self, func, ...)
+ local red = redis:new()
+
+ local connect_timeout = self.connect_timeout
+ local send_timeout = self.send_timeout
+ local read_timeout = self.read_timeout
+ if connect_timeout or send_timeout or read_timeout then
+ red:set_timeouts(connect_timeout, send_timeout, read_timeout)
+ end
+
+ local ok, err do
+ local socket = self.socket
+ if socket then
+ ok, err = red:connect(socket, self.options)
+ else
+ ok, err = red:connect(self.host, self.port, self.options)
+ end
+ end
+ if not ok then
+ return nil, err
+ end
+
+ if red:get_reused_times() == 0 then
+ local password = self.password
+ if password then
+ local username = self.username
+ if username then
+ ok, err = red:auth(username, password)
+ else
+ ok, err = red:auth(password)
+ end
+
+ if not ok then
+ red:close()
+ return nil, err
+ end
+ end
+ end
+
+ local database = self.database
+ if database then
+ ok, err = red:select(database)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ ok, err = func(self, red, ...)
+ if err then
+ red:close()
+ return nil, err
+ end
+
+ if not red:set_keepalive(self.keepalive_timeout) then
+ red:close()
+ end
+
+ if ok == null then
+ ok = nil
+ end
+
+ return ok, err
+end
+
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam boolean remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(...)
+ return exec(self, SET, ...)
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(...)
+ return exec(self, GET, ...)
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(...)
+ return exec(self, UNLINK, ...)
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function instance:read_metadata
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+function metatable:read_metadata(...)
+ return exec(self, READ_METADATA, ...)
+end
+
+
+local storage = {}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- Redis storage backend configuration
+-- @field prefix Prefix for the keys stored in Redis.
+-- @field suffix Suffix for the keys stored in Redis.
+-- @field host The host to connect (defaults to `"127.0.0.1"`).
+-- @field port The port to connect (defaults to `6379`).
+-- @field socket The socket file to connect to (defaults to `nil`).
+-- @field username The database username to authenticate.
+-- @field password Password for authentication.
+-- @field database The database to connect.
+-- @field connect_timeout Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.
+-- @field send_timeout Controls the default timeout value used in TCP/unix-domain socket object's `send` method.
+-- @field read_timeout Controls the default timeout value used in TCP/unix-domain socket object's `receive` method.
+-- @field keepalive_timeout Controls the default maximal idle time of the connections in the connection pool.
+-- @field pool A custom name for the connection pool being used.
+-- @field pool_size The size of the connection pool.
+-- @field backlog A queue size to use when the connection pool is full (configured with @pool_size).
+-- @field ssl Enable SSL (defaults to `false`).
+-- @field ssl_verify Verify server certificate (defaults to `nil`).
+-- @field server_name The server name for the new TLS extension Server Name Indication (SNI).
+-- @table configuration
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a Redis storage.
+--
+-- This creates a new Redis storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration redis storage @{configuration}
+-- @treturn table redis storage instance
+function storage.new(configuration)
+ local prefix = configuration and configuration.prefix
+ local suffix = configuration and configuration.suffix
+
+ local host = configuration and configuration.host or DEFAULT_HOST
+ local port = configuration and configuration.port or DEFAULT_PORT
+ local socket = configuration and configuration.socket
+
+ local username = configuration and configuration.username
+ local password = configuration and configuration.password
+ local database = configuration and configuration.database
+
+ local connect_timeout = configuration and configuration.connect_timeout
+ local send_timeout = configuration and configuration.send_timeout
+ local read_timeout = configuration and configuration.read_timeout
+ local keepalive_timeout = configuration and configuration.keepalive_timeout
+
+ local pool = configuration and configuration.pool
+ local pool_size = configuration and configuration.pool_size
+ local backlog = configuration and configuration.backlog
+ local ssl = configuration and configuration.ssl
+ local ssl_verify = configuration and configuration.ssl_verify
+ local server_name = configuration and configuration.server_name
+
+ if ssl ~= nil or ssl_verify ~= nil or server_name or pool or pool_size or backlog then
+ return setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ host = host,
+ port = port,
+ socket = socket,
+ username = username,
+ password = password,
+ database = database,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ options = {
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ server_name = server_name,
+ pool = pool,
+ pool_size = pool_size,
+ backlog = backlog,
+ }
+ }, metatable)
+ end
+
+ return setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ host = host,
+ port = port,
+ socket = socket,
+ username = username,
+ password = password,
+ database = database,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ }, metatable)
+end
+
+
+return storage
diff --git a/lib/resty/session/redis/cluster.lua b/lib/resty/session/redis/cluster.lua
new file mode 100644
index 000000000..adc0daacf
--- /dev/null
+++ b/lib/resty/session/redis/cluster.lua
@@ -0,0 +1,271 @@
+---
+-- Redis Cluster backend for session library
+--
+-- @module resty.session.redis.cluster
+
+
+local common = require "resty.session.redis.common"
+local redis = require "resty.rediscluster"
+
+
+local setmetatable = setmetatable
+local error = error
+local null = ngx.null
+
+
+local SET = common.SET
+local GET = common.GET
+local UNLINK = common.UNLINK
+local READ_METADATA = common.READ_METADATA
+
+
+local function exec(self, func, ...)
+ local red, err = redis:new(self.options)
+ if err then
+ return nil, err
+ end
+
+ local ok, err = func(self, red, ...)
+ if err then
+ red:close()
+ return nil, err
+ end
+
+ if ok == null then
+ ok = nil
+ end
+
+ return ok, err
+end
+
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam boolean remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(...)
+ return exec(self, SET, ...)
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(...)
+ return exec(self, GET, ...)
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(...)
+ return exec(self, UNLINK, ...)
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function instance:read_metadata
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+function metatable:read_metadata(...)
+ return exec(self, READ_METADATA, ...)
+end
+
+
+local storage = {}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- Redis Cluster storage backend configuration
+-- @field prefix Prefix for the keys stored in redis.
+-- @field suffix Suffix for the keys stored in redis.
+-- @field name Redis cluster name.
+-- @field nodes Redis cluster nodes.
+-- @field lock_zone Shared dictionary name for locks.
+-- @field lock_prefix Shared dictionary name prefix for lock.
+-- @field max_redirections Maximum retry attempts for redirection.
+-- @field max_connection_attempts Maximum retry attempts for connection.
+-- @field max_connection_timeout Maximum connection timeout in total among the retries.
+-- @field username The database username to authenticate.
+-- @field password Password for authentication.
+-- @field connect_timeout Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.
+-- @field send_timeout controls The default timeout value used in TCP/unix-domain socket object's `send` method.
+-- @field read_timeout controls The default timeout value used in TCP/unix-domain socket object's `receive` method.
+-- @field keepalive_timeout Controls the default maximal idle time of the connections in the connection pool.
+-- @field pool A custom name for the connection pool being used.
+-- @field pool_size The size of the connection pool.
+-- @field backlog A queue size to use when the connection pool is full (configured with @pool_size).
+-- @field ssl Enable SSL (defaults to `false`).
+-- @field ssl_verify Verify server certificate (defaults to `nil`).
+-- @field server_name The server name for the new TLS extension Server Name Indication (SNI).
+-- @table configuration
+
+
+---
+-- Cluster Nodes
+--
+-- An array of cluster nodes.
+--
+-- @table nodes
+
+
+---
+-- Cluster Node
+-- @field ip The IP address to connect (defaults to `"127.0.0.1"`).
+-- @field port The port to connect (defaults to `6379`).
+-- @table node
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a Redis Cluster storage.
+--
+-- This creates a new Redis Cluster storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration redis cluster storage @{configuration}
+-- @treturn table redis cluster storage instance
+function storage.new(configuration)
+ local prefix = configuration and configuration.prefix
+ local suffix = configuration and configuration.suffix
+
+ local name = configuration and configuration.name
+ local nodes = configuration and configuration.nodes
+
+ local lock_zone = configuration and configuration.lock_zone
+ local lock_prefix = configuration and configuration.lock_prefix
+ local max_redirections = configuration and configuration.max_redirections
+ local max_connection_attempts = configuration and configuration.max_connection_attempts
+ local max_connection_timeout = configuration and configuration.max_connection_timeout
+
+ local username = configuration and configuration.username
+ local password = configuration and configuration.password
+
+ local connect_timeout = configuration and configuration.connect_timeout
+ local send_timeout = configuration and configuration.send_timeout
+ local read_timeout = configuration and configuration.read_timeout
+ local keepalive_timeout = configuration and configuration.keepalive_timeout
+
+ local pool = configuration and configuration.pool
+ local pool_size = configuration and configuration.pool_size
+ local backlog = configuration and configuration.backlog
+ local ssl = configuration and configuration.ssl
+ local ssl_verify = configuration and configuration.ssl_verify
+ local server_name = configuration and configuration.server_name
+
+ local auth
+ if password then
+ if username then
+ auth = username .. " " .. password
+ else
+ auth = password
+ end
+ end
+
+ if ssl ~= nil or ssl_verify ~= nil or server_name or pool or pool_size or backlog then
+ return setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ options = {
+ name = name,
+ dict_name = lock_zone,
+ refresh_lock_key = lock_prefix,
+ serv_list = nodes,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ keepalive_cons = pool_size,
+ max_redirection = max_redirections,
+ max_connection_attempts = max_connection_attempts,
+ max_connection_timeout = max_connection_timeout,
+ auth = auth,
+ connect_opts = {
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ server_name = server_name,
+ pool = pool,
+ pool_size = pool_size,
+ backlog = backlog,
+ },
+ },
+ }, metatable)
+ end
+
+ return setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ options = {
+ name = name,
+ dict_name = lock_zone,
+ refresh_lock_key = lock_prefix,
+ serv_list = nodes,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ keepalive_cons = pool_size,
+ max_redirection = max_redirections,
+ max_connection_attempts = max_connection_attempts,
+ max_connection_timeout = max_connection_timeout,
+ auth = auth,
+ },
+ }, metatable)
+end
+
+
+return storage
diff --git a/lib/resty/session/redis/common.lua b/lib/resty/session/redis/common.lua
new file mode 100644
index 000000000..993da5624
--- /dev/null
+++ b/lib/resty/session/redis/common.lua
@@ -0,0 +1,172 @@
+---
+-- Common Redis functions shared between Redis,
+-- Redis Cluster and Redis Sentinel implementations.
+--
+-- @module resty.session.redis.common
+
+
+local utils = require "resty.session.utils"
+
+
+local get_name = utils.get_name
+local ipairs = ipairs
+
+
+---
+-- Store session data.
+--
+-- @function module.SET
+-- @tparam table storage the storage
+-- @tparam table red the redis instance
+-- @tparam string name the cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam table remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+local function SET(storage, red, name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+ if not metadata and not old_key then
+ return red:set(get_name(storage, name, key), value, "EX", ttl)
+ end
+
+ local old_name
+ local old_ttl
+ if old_key then
+ old_name = get_name(storage, name, old_key)
+ if not remember then
+ -- redis < 7.0
+ old_ttl = red:ttl(old_name)
+ end
+ end
+
+ red:init_pipeline()
+ red:set(get_name(storage, name, key), value, "EX", ttl)
+
+ -- redis < 7.0
+ if old_name then
+ if remember then
+ red:unlink(old_name)
+ elseif not old_ttl or old_ttl > stale_ttl then
+ red:expire(old_name, stale_ttl)
+ end
+ end
+
+ -- redis >= 7.0
+ --if old_key then
+ -- if remember then
+ -- red:unlink(get_name(storage, name, old_key))
+ -- else
+ -- red:expire(get_name(storage, name, old_key), stale_ttl, "LT")
+ -- end
+ --end
+
+ if metadata then
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local score = current_time - 1
+ local new_score = current_time + ttl
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = get_name(storage, name, audiences[i], subjects[i])
+ red:zremrangebyscore(meta_key, 0, score)
+ red:zadd(meta_key, new_score, key)
+ if old_key then
+ red:zrem(meta_key, old_key)
+ end
+ red:expire(meta_key, ttl)
+ end
+ end
+
+ return red:commit_pipeline()
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function module.GET
+-- @tparam table storage the storage
+-- @tparam table red the redis instance
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+local function GET(storage, red, name, key)
+ return red:get(get_name(storage, name, key))
+end
+
+
+---
+-- Delete session data.
+--
+-- @function module.UNLINK
+-- @tparam table storage the storage
+-- @tparam table red the redis instance
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam number current_time current time
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+local function UNLINK(storage, red, name, key, current_time, metadata)
+ if not metadata then
+ return red:unlink(get_name(storage, name, key))
+ end
+
+ red:init_pipeline()
+ red:unlink(get_name(storage, name, key))
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local score = current_time - 1
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = get_name(storage, name, audiences[i], subjects[i])
+ red:zremrangebyscore(meta_key, 0, score)
+ red:zrem(meta_key, key)
+ end
+
+ return red:commit_pipeline()
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function module.READ_METADATA
+-- @tparam table storage the storage
+-- @tparam table red the redis instance
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+local function READ_METADATA(storage, red, name, audience, subject, current_time)
+ local sessions = {}
+ local meta_key = get_name(storage, name, audience, subject)
+ local res, err = red:zrange(meta_key, current_time, "+inf", "BYSCORE", "WITHSCORES")
+ if not res then
+ return nil, err
+ end
+
+ for i, v in ipairs(res) do
+ if i % 2 ~= 0 then
+ sessions[v] = res[i + 1]
+ end
+ end
+
+ return sessions
+end
+
+
+return {
+ SET = SET,
+ GET = GET,
+ UNLINK = UNLINK,
+ READ_METADATA = READ_METADATA,
+}
diff --git a/lib/resty/session/redis/sentinel.lua b/lib/resty/session/redis/sentinel.lua
new file mode 100644
index 000000000..73db463fd
--- /dev/null
+++ b/lib/resty/session/redis/sentinel.lua
@@ -0,0 +1,260 @@
+---
+-- Redis Sentinel backend for session library
+--
+-- @module resty.session.redis.sentinel
+
+
+local common = require "resty.session.redis.common"
+local redis = require "resty.redis.connector"
+
+
+local setmetatable = setmetatable
+local error = error
+local null = ngx.null
+
+
+local SET = common.SET
+local GET = common.GET
+local UNLINK = common.UNLINK
+local READ_METADATA = common.READ_METADATA
+
+
+local function exec(self, func, ...)
+ local red, err = self.connector:connect()
+ if not red then
+ return nil, err
+ end
+
+ local ok, err = func(self, red, ...)
+ if err then
+ red:close()
+ return nil, err
+ end
+
+ if ok == null then
+ ok = nil
+ end
+
+ self.connector:set_keepalive(red)
+
+ return ok, err
+end
+
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam table remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(...)
+ return exec(self, SET, ...)
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(...)
+ return exec(self, GET, ...)
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(...)
+ return exec(self, UNLINK, ...)
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function instance:read_metadata
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+function metatable:read_metadata(...)
+ return exec(self, READ_METADATA, ...)
+end
+
+
+local storage = {}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- Redis Sentinel storage backend configuration
+-- @field prefix Prefix for the keys stored in redis.
+-- @field suffix Suffix for the keys stored in redis.
+-- @field master Name of master.
+-- @field role `"master"` or `"slave"`.
+-- @field sentinels Redis Sentinels.
+-- @field sentinel_username Optional sentinel username.
+-- @field sentinel_password Optional sentinel password.
+-- @field username The database username to authenticate.
+-- @field password Password for authentication.
+-- @field database The database to connect.
+-- @field connect_timeout Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.
+-- @field send_timeout Controls the default timeout value used in TCP/unix-domain socket object's `send` method.
+-- @field read_timeout Controls the default timeout value used in TCP/unix-domain socket object's `receive` method.
+-- @field keepalive_timeout Controls the default maximal idle time of the connections in the connection pool.
+-- @field pool A custom name for the connection pool being used.
+-- @field pool_size The size of the connection pool.
+-- @field backlog A queue size to use when the connection pool is full (configured with @pool_size).
+-- @field ssl Enable SSK (defaults to `false`).
+-- @field ssl_verify Verify server certificate (defaults to `nil`).
+-- @field server_name The server name for the new TLS extension Server Name Indication (SNI).
+-- @table configuration
+
+
+---
+-- Sentinels
+--
+-- An array of sentinels.
+--
+-- @table sentinels
+
+
+---
+-- Sentinel
+-- @field host The host to connect.
+-- @field port The port to connect.
+-- @table sentinel
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a Redis Sentinel storage.
+--
+-- This creates a new Redis Sentinel storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration redis sentinel storage @{configuration}
+-- @treturn table redis sentinel storage instance
+function storage.new(configuration)
+ local prefix = configuration and configuration.prefix
+ local suffix = configuration and configuration.suffix
+
+ local master = configuration and configuration.master
+ local role = configuration and configuration.role
+ local sentinels = configuration and configuration.sentinels
+ local sentinel_username = configuration and configuration.sentinel_username
+ local sentinel_password = configuration and configuration.sentinel_password
+
+ local username = configuration and configuration.username
+ local password = configuration and configuration.password
+ local database = configuration and configuration.database
+
+ local connect_timeout = configuration and configuration.connect_timeout
+ local send_timeout = configuration and configuration.send_timeout
+ local read_timeout = configuration and configuration.read_timeout
+ local keepalive_timeout = configuration and configuration.keepalive_timeout
+
+ local pool = configuration and configuration.pool
+ local pool_size = configuration and configuration.pool_size
+ local backlog = configuration and configuration.backlog
+ local ssl = configuration and configuration.ssl
+ local ssl_verify = configuration and configuration.ssl_verify
+ local server_name = configuration and configuration.server_name
+
+ local connector
+ if ssl ~= nil or ssl_verify ~= nil or server_name or pool or pool_size or backlog then
+ connector = redis.new({
+ master_name = master,
+ role = role,
+ sentinels = sentinels,
+ sentinel_username = sentinel_username,
+ sentinel_password = sentinel_password,
+ username = username,
+ password = password,
+ db = database,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ keepalive_poolsize = pool_size,
+ connection_options = {
+ ssl = ssl,
+ ssl_verify = ssl_verify,
+ server_name = server_name,
+ pool = pool,
+ pool_size = pool_size,
+ backlog = backlog,
+ }
+ })
+ else
+ connector = redis.new({
+ master_name = master,
+ role = role,
+ sentinels = sentinels,
+ sentinel_username = sentinel_username,
+ sentinel_password = sentinel_password,
+ username = username,
+ password = password,
+ db = database,
+ connect_timeout = connect_timeout,
+ send_timeout = send_timeout,
+ read_timeout = read_timeout,
+ keepalive_timeout = keepalive_timeout,
+ keepalive_poolsize = pool_size,
+ })
+ end
+
+ return setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ connector = connector,
+ }, metatable)
+end
+
+
+return storage
diff --git a/lib/resty/session/shm.lua b/lib/resty/session/shm.lua
new file mode 100644
index 000000000..1f5690932
--- /dev/null
+++ b/lib/resty/session/shm.lua
@@ -0,0 +1,315 @@
+---
+-- Shared Memory (SHM) backend for session library
+--
+-- @module resty.session.shm
+
+
+local table_new = require "table.new"
+local utils = require "resty.session.utils"
+
+
+local meta_get_value = utils.meta_get_value
+local meta_get_next = utils.meta_get_next
+local get_name = utils.get_name
+local errmsg = utils.errmsg
+
+
+local setmetatable = setmetatable
+local shared = ngx.shared
+local random = math.random
+local assert = assert
+local error = error
+local pairs = pairs
+local max = math.max
+local log = ngx.log
+
+
+local WARN = ngx.WARN
+
+
+local DEFAULT_ZONE = "sessions"
+local CLEANUP_PROBABILITY = 0.1 -- 1 / 10
+
+
+local function get_and_clean_metadata(dict, meta_key, current_time)
+ local size = dict:llen(meta_key)
+ if not size or size == 0 then
+ return
+ end
+
+ local max_expiry = current_time
+ local sessions = table_new(0, size)
+
+ for _ = 1, size do
+ local meta_value, err = dict:lpop(meta_key)
+ if not meta_value then
+ log(WARN, "[session] ", errmsg(err, "failed read meta value"))
+ break
+ end
+
+ local key, err, exp = meta_get_next(meta_value, 1)
+ if err then
+ return nil, err
+ end
+
+ if exp and exp > current_time then
+ sessions[key] = exp
+ max_expiry = max(max_expiry, exp)
+
+ else
+ sessions[key] = nil
+ end
+ end
+
+ for key, exp in pairs(sessions) do
+ local meta_value = meta_get_value(key, exp)
+ local ok, err = dict:rpush(meta_key, meta_value)
+ if not ok then
+ log(WARN, "[session] ", errmsg(err, "failed to update metadata"))
+ end
+ end
+
+ local exp = max_expiry - current_time
+ if exp > 0 then
+ local ok, err = dict:expire(meta_key, max_expiry - current_time)
+ if not ok and err ~= "not found" then
+ log(WARN, "[session] ", errmsg(err, "failed to touch metadata"))
+ end
+
+ else
+ dict:delete(meta_key)
+ end
+
+ return sessions
+end
+
+
+local function cleanup(dict, meta_key, current_time)
+ get_and_clean_metadata(dict, meta_key, current_time)
+end
+
+
+local function read_metadata(self, meta_key, current_time)
+ return get_and_clean_metadata(self.dict, meta_key, current_time)
+end
+
+---
+-- Storage
+-- @section instance
+
+
+local metatable = {}
+
+
+metatable.__index = metatable
+
+
+function metatable.__newindex()
+ error("attempt to update a read-only table", 2)
+end
+
+
+---
+-- Store session data.
+--
+-- @function instance:set
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam string value session value
+-- @tparam number ttl session ttl
+-- @tparam number current_time current time
+-- @tparam[opt] string old_key old session id
+-- @tparam string stale_ttl stale ttl
+-- @tparam[opt] table metadata table of metadata
+-- @tparam boolean remember whether storing persistent session or not
+-- @treturn true|nil ok
+-- @treturn string error message
+function metatable:set(name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
+ local dict = self.dict
+ if not metadata and not old_key then
+ local ok, err = dict:set(get_name(self, name, key), value, ttl)
+ if not ok then
+ return nil, err
+ end
+
+ return true
+ end
+
+ local old_name, old_ttl
+ if old_key then
+ old_name = get_name(self, name, old_key)
+ if not remember then
+ old_ttl = dict:ttl(old_name)
+ end
+ end
+
+ local ok, err = dict:set(get_name(self, name, key), value, ttl)
+ if not ok then
+ return nil, err
+ end
+
+ if old_name then
+ if remember then
+ dict:delete(old_name)
+
+ elseif (not old_ttl or old_ttl > stale_ttl) then
+ local ok, err = dict:expire(old_name, stale_ttl)
+ if not ok then
+ log(WARN, "[session] ", errmsg(err, "failed to touch old session"))
+ end
+ end
+ end
+
+ if not metadata then
+ return true
+ end
+
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = get_name(self, name, audiences[i], subjects[i])
+ local meta_value = meta_get_value(key, current_time + ttl)
+
+ local ok, err = dict:rpush(meta_key, meta_value)
+ if not ok then
+ log(WARN, "[session] ", errmsg(err, "failed to update metadata"))
+ end
+
+ if old_key then
+ meta_value = meta_get_value(old_key, 0)
+ local ok, err = dict:rpush(meta_key, meta_value)
+ if not ok then
+ log(WARN, "[session] ", errmsg(err, "failed to update old metadata"))
+ end
+ end
+
+ -- no need to clean up every time we write
+ -- it is just beneficial when a key is used a lot
+ if random() < CLEANUP_PROBABILITY then
+ cleanup(dict, meta_key, current_time)
+ end
+ end
+
+ return true
+end
+
+
+---
+-- Retrieve session data.
+--
+-- @function instance:get
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @treturn string|nil session data
+-- @treturn string error message
+function metatable:get(name, key)
+ local value, err = self.dict:get(get_name(self, name, key))
+ if not value then
+ return nil, err
+ end
+
+ return value
+end
+
+
+---
+-- Delete session data.
+--
+-- @function instance:delete
+-- @tparam string name cookie name
+-- @tparam string key session key
+-- @tparam[opt] table metadata session meta data
+-- @treturn boolean|nil session data
+-- @treturn string error message
+function metatable:delete(name, key, current_time, metadata)
+ local dict = self.dict
+
+ dict:delete(get_name(self, name, key))
+
+ if not metadata then
+ return true
+ end
+
+ local audiences = metadata.audiences
+ local subjects = metadata.subjects
+ local count = #audiences
+ for i = 1, count do
+ local meta_key = get_name(self, name, audiences[i], subjects[i])
+ local meta_value = meta_get_value(key, 0)
+
+ local ok, err = dict:rpush(meta_key, meta_value)
+ if not ok then
+ if not ok then
+ log(WARN, "[session] ", errmsg(err, "failed to update metadata"))
+ end
+ end
+
+ cleanup(dict, meta_key, current_time)
+ end
+
+ return true
+end
+
+
+---
+-- Read session metadata.
+--
+-- @function instance:read_metadata
+-- @tparam string name cookie name
+-- @tparam string audience session key
+-- @tparam string subject session key
+-- @tparam number current_time current time
+-- @treturn table|nil session metadata
+-- @treturn string error message
+function metatable:read_metadata(name, audience, subject, current_time)
+ local meta_key = get_name(self, name, audience, subject)
+ return read_metadata(self, meta_key, current_time)
+end
+
+
+local storage = {}
+
+
+---
+-- Configuration
+-- @section configuration
+
+
+---
+-- Shared memory storage backend configuration
+-- @field prefix Prefix for the keys stored in SHM.
+-- @field suffix Suffix for the keys stored in SHM.
+-- @field zone A name of shared memory zone (defaults to `sessions`).
+-- @table configuration
+
+
+---
+-- Constructors
+-- @section constructors
+
+
+---
+-- Create a SHM storage.
+--
+-- This creates a new shared memory storage instance.
+--
+-- @function module.new
+-- @tparam[opt] table configuration shm storage @{configuration}
+-- @treturn table shm storage instance
+function storage.new(configuration)
+ local prefix = configuration and configuration.prefix
+ local suffix = configuration and configuration.suffix
+ local zone = configuration and configuration.zone or DEFAULT_ZONE
+
+ local dict = assert(shared[zone], "lua_shared_dict " .. zone .. " is missing")
+
+ return setmetatable({
+ prefix = prefix,
+ suffix = suffix,
+ dict = dict,
+ }, metatable)
+end
+
+
+return storage
diff --git a/lib/resty/session/utils.lua b/lib/resty/session/utils.lua
new file mode 100644
index 000000000..662e4967c
--- /dev/null
+++ b/lib/resty/session/utils.lua
@@ -0,0 +1,1121 @@
+---
+-- Common utilities for session library and storage backends
+--
+-- @module resty.session.utils
+
+
+local require = require
+
+
+local buffer = require "string.buffer"
+local bit = require "bit"
+
+
+local select = select
+local floor = math.floor
+local ceil = math.ceil
+local byte = string.byte
+local char = string.char
+local band = bit.band
+local bnot = bit.bnot
+local bor = bit.bor
+local fmt = string.format
+local sub = string.sub
+
+
+local is_fips_mode do
+ local IS_FIPS
+
+ local function is_fips_mode_real()
+ return IS_FIPS == true
+ end
+
+ ---
+ -- Returns whether OpenSSL is in FIPS-mode.
+ --
+ -- @function utils.is_fips_mode
+ -- @treturn boolean `true` if OpenSSL is in FIPS-mode, otherwise `false`
+ --
+ -- @usage
+ -- local is_fips = require "resty.session.utils".is_fips_mode()
+ is_fips_mode = function()
+ IS_FIPS = require("resty.openssl").get_fips_mode()
+ is_fips_mode = is_fips_mode_real
+ return is_fips_mode()
+ end
+end
+
+
+local bpack, bunpack do
+ local buf = buffer.new()
+
+ ---
+ -- Binary pack unsigned integer.
+ --
+ -- Returns binary packed version of an integer in little endian unsigned format.
+ --
+ -- Size can be:
+ --
+ -- * `1`, pack input as a little endian unsigned char (` 0 then
+ msg = fmt(msg, ...)
+ end
+
+ if err then
+ return fmt("%s (%s)", msg, err)
+ end
+
+ return msg
+end
+
+
+---
+-- Helper to create a delimited key.
+--
+-- @function utils.get_name
+-- @tparam table storage a storage implementation
+-- @tparam string name name
+-- @tparam string key key
+-- @tparam string subject subject
+-- @treturn string formatted and delimited name
+local function get_name(storage, name, key, subject)
+ local prefix = storage.prefix
+ local suffix = storage.suffix
+ if prefix and suffix and subject then
+ return fmt("%s:%s:%s:%s:%s", prefix, name, key, subject, suffix)
+ elseif prefix and suffix then
+ return fmt("%s:%s:%s:%s", prefix, name, key, suffix)
+ elseif prefix and subject then
+ return fmt("%s:%s:%s:%s", prefix, name, key, subject)
+ elseif suffix and subject then
+ return fmt("%s:%s:%s:%s", name, key, subject, suffix)
+ elseif prefix then
+ return fmt("%s:%s:%s", prefix, name, key)
+ elseif suffix then
+ return fmt("%s:%s:%s", name, key, suffix)
+ elseif subject then
+ return fmt("%s:%s:%s", name, key, subject)
+ else
+ return fmt("%s:%s", name, key)
+ end
+end
+
+
+---
+-- Helper to turn on a flag.
+--
+-- @function utils.set_flag
+-- @tparam number flags flags on which the flag is applied
+-- @tparam number flag flag that is applied
+-- @treturn number flags with the flag applied
+--
+-- @usage
+-- local flags = 0x0000
+-- local FLAG_DOG = 0x001
+-- flags = utils.set_flag(flags, FLAG_DOG)
+local function set_flag(options, flag)
+ return bor(options, flag)
+end
+
+
+---
+-- Helper to turn off a flag.
+--
+-- @function utils.unset_flag
+-- @tparam number flags flags on which the flag is removed
+-- @tparam number flag flag that is removed
+-- @treturn number flags with the flag removed
+--
+-- @usage
+-- local options = 0x0000
+-- local FLAG_DOG = 0x001
+-- flags = utils.set_flag(options, FLAG_DOG)
+-- flags = utils.unset_flag(options, FLAG_DOG)
+local function unset_flag(flags, flag)
+ return band(flags, bnot(flag))
+end
+
+
+---
+-- Helper to check if flag is enabled.
+--
+-- @function utils.has_flag
+-- @tparam number flags flags on which the flag is checked
+-- @tparam number flag flag that is checked
+-- @treturn boolean true if flag has is present, otherwise false
+--
+-- @usage
+-- local flags = 0x0000
+-- local FLAG_DOG = 0x001
+-- local FLAG_BONE = 0x010
+-- flags = utils.set_flag(flags, FLAG_DOG)
+-- flags = utils.set_flag(flags, FLAG_BONE)
+-- print(utils.has_flag(flags, FLAG_BONE)
+local function has_flag(flags, flag)
+ return band(flags, flag) ~= 0
+end
+
+
+---
+-- Helper to get the value used to store metadata for a certain aud and sub
+-- Empty exp means the session id has been invalidated
+--
+-- @function utils.meta_get_value
+-- @tparam string key storage key
+-- @tparam string exp expiration
+-- @treturn string the value to store in the metadata collection
+local function meta_get_value(key, exp)
+ if not exp or exp == 0 then
+ return fmt("%s;", key)
+ end
+ return fmt("%s:%s;", key, encode_base64url(bpack(5, exp)))
+end
+
+
+local meta_get_next do
+ local KEY_OFFSET = 43
+ local DEL_OFFSET = 1
+ local EXP_OFFSET = 7
+
+ local COLON = byte(":")
+
+ ---
+ -- Function to extract the next key and exp from a serialized
+ -- metadata list, starting from index
+ --
+ -- @function utils.meta_get_next
+ -- @tparam string val list of key:exp;
+ -- @tparam number index start index
+ -- @treturn key string session id
+ -- @treturn err string error
+ -- @treturn exp number expiration
+ -- @treturn index number|nil index of the cursor
+ meta_get_next = function(val, index)
+ local key = sub(val, index, index + KEY_OFFSET - 1)
+ index = index + KEY_OFFSET
+ local del = byte(val, index + DEL_OFFSET - 1)
+ index = index + DEL_OFFSET
+
+ if del ~= COLON then
+ return key, nil, nil, index
+ end
+
+ local exp = sub(val, index, index + EXP_OFFSET - 1)
+ index = index + EXP_OFFSET + DEL_OFFSET
+ local exp, err = decode_base64url(exp)
+ if err then
+ return nil, err
+ end
+
+ exp = bunpack(5, exp)
+ return key, nil, exp, index
+ end
+end
+
+
+---
+-- Function to filter out the latest valid key:exp from a
+-- serialized list, used to store session metadata
+--
+-- @function utils.meta_get_latest
+-- @tparam string sessions list of key:exp;
+-- @treturn table valid sessions and their exp
+local function meta_get_latest(sessions, current_time)
+ current_time = current_time
+
+ local latest = {}
+ local index = 1
+ local length = #sessions
+ while index < length do
+ local key, err, exp
+ key, err, exp, index = meta_get_next(sessions, index)
+ if err then
+ return nil, err
+ end
+
+ if exp and exp > current_time then
+ latest[key] = exp
+ else
+ latest[key] = nil
+ end
+ end
+
+ return latest
+end
+
+
+return {
+ is_fips_mode = is_fips_mode,
+ bpack = bpack,
+ bunpack = bunpack,
+ trim = trim,
+ encode_json = encode_json,
+ decode_json = decode_json,
+ encode_base64url = encode_base64url,
+ decode_base64url = decode_base64url,
+ base64_size = base64_size,
+ inflate = inflate,
+ deflate = deflate,
+ rand_bytes = rand_bytes,
+ sha256 = sha256,
+ derive_hkdf_sha256 = derive_hkdf_sha256,
+ derive_pbkdf2_hmac_sha256 = derive_pbkdf2_hmac_sha256,
+ derive_aes_gcm_256_key_and_iv = derive_aes_gcm_256_key_and_iv,
+ derive_hmac_sha256_key = derive_hmac_sha256_key,
+ encrypt_aes_256_gcm = encrypt_aes_256_gcm,
+ decrypt_aes_256_gcm = decrypt_aes_256_gcm,
+ hmac_sha256 = hmac_sha256,
+ load_storage = load_storage,
+ errmsg = errmsg,
+ get_name = get_name,
+ set_flag = set_flag,
+ unset_flag = unset_flag,
+ has_flag = has_flag,
+ meta_get_value = meta_get_value,
+ meta_get_next = meta_get_next,
+ meta_get_latest = meta_get_latest,
+}
diff --git a/lua-resty-session-4.0.4-1.rockspec b/lua-resty-session-4.0.4-1.rockspec
new file mode 100644
index 000000000..d080bbe3b
--- /dev/null
+++ b/lua-resty-session-4.0.4-1.rockspec
@@ -0,0 +1,37 @@
+package = "lua-resty-session"
+version = "4.0.4-1"
+source = {
+ url = "git+https://github.com/bungle/lua-resty-session.git",
+ tag = "v4.0.4",
+}
+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 , Samuele Illuminati ",
+ license = "BSD",
+}
+dependencies = {
+ "lua >= 5.1",
+ "lua-ffi-zlib >= 0.5",
+ "lua-resty-openssl >= 0.8.0",
+}
+build = {
+ type = "builtin",
+ modules = {
+ ["resty.session"] = "lib/resty/session.lua",
+ ["resty.session.dshm"] = "lib/resty/session/dshm.lua",
+ ["resty.session.file"] = "lib/resty/session/file.lua",
+ ["resty.session.file.thread"] = "lib/resty/session/file/thread.lua",
+ ["resty.session.file.utils"] = "lib/resty/session/file/utils.lua",
+ ["resty.session.memcached"] = "lib/resty/session/memcached.lua",
+ ["resty.session.mysql"] = "lib/resty/session/mysql.lua",
+ ["resty.session.postgres"] = "lib/resty/session/postgres.lua",
+ ["resty.session.redis"] = "lib/resty/session/redis.lua",
+ ["resty.session.redis.cluster"] = "lib/resty/session/redis/cluster.lua",
+ ["resty.session.redis.sentinel"] = "lib/resty/session/redis/sentinel.lua",
+ ["resty.session.redis.common"] = "lib/resty/session/redis/common.lua",
+ ["resty.session.shm"] = "lib/resty/session/shm.lua",
+ ["resty.session.utils"] = "lib/resty/session/utils.lua",
+ },
+}
diff --git a/spec/01-utils_spec.lua b/spec/01-utils_spec.lua
new file mode 100644
index 000000000..bb96cba51
--- /dev/null
+++ b/spec/01-utils_spec.lua
@@ -0,0 +1,139 @@
+local utils = require "resty.session.utils"
+
+
+local describe = describe
+local tostring = tostring
+local random = math.random
+local assert = assert
+local ipairs = ipairs
+local match = string.match
+local it = it
+
+
+describe("Testing utils", function()
+ describe("pack/unpack data", function()
+ local function range_max(bytes)
+ if bytes == 8 then
+ bytes = bytes - 1
+ end
+ return 2 ^ (8 * bytes) - 1
+ end
+
+ it("bpack and bunpack produce consistent output", function()
+ for _, i in ipairs{ 1, 2, 4, 8 } do
+ local input = random(1, range_max(i))
+ local packed = utils.bpack(i, input)
+ local unpacked = utils.bunpack(i, packed)
+
+ assert.equals(i, #packed)
+ assert.equals(unpacked, input)
+ end
+ end)
+ end)
+
+ describe("trim", function()
+ it("characters are trimmed as expected", function()
+ local input = " \t\t\r\n\n\v\f\f\fyay\ntrim!\f\f\v\n\r\t "
+ local expected_output = "yay\ntrim!"
+ local output = utils.trim(input)
+
+ assert.equals(output, expected_output)
+ end)
+ end)
+
+ describe("encode/decode json", function()
+ it("produce consistent output", function()
+ local input = {
+ foo = "bar",
+ test = 123
+ }
+ local encoded = utils.encode_json(input)
+ local decoded = utils.decode_json(encoded)
+
+ assert.same(decoded, input)
+ end)
+ end)
+
+ describe("encode/decode base64url", function()
+ it("produce consistent output", function()
+ local input = "<<>>?!"
+ local encoded = utils.encode_base64url(input)
+
+ assert.is_nil(match(encoded, "[/=+]"))
+ local decoded = utils.decode_base64url(encoded)
+ assert.equals(input, decoded)
+ end)
+ end)
+
+ describe("deflate/inflate", function()
+ it("produce consistent output", function()
+ local input = utils.rand_bytes(1024)
+ local deflated = utils.deflate(input)
+ local inflated = utils.inflate(deflated)
+
+ assert.equals(input, inflated)
+ end)
+ end)
+
+ describe("Derive keys, encrypt and decrypt", function()
+ local ikm = "some key material"
+ local nonce = "0000000000000000"
+ local usage = "encryption"
+ local size = 44
+
+ it("derives key of expected size with derive_hkdf_sha256", function()
+ local k_bytes, err = utils.derive_hkdf_sha256(ikm, nonce, usage, size)
+ assert.is_not_nil(k_bytes)
+ assert.is_nil(err)
+ assert.equals(size, #tostring(k_bytes))
+ end)
+
+ it("derives key of expected size with derive_pbkdf2_hmac_sha256", function()
+ local k_bytes, err = utils.derive_pbkdf2_hmac_sha256(ikm, nonce, usage, size)
+ assert.is_not_nil(k_bytes)
+ assert.is_nil(err)
+ assert.equals(size, #tostring(k_bytes))
+ end)
+
+ it("derives a valid key and calculates mac with it", function()
+ local key, err, mac
+ key, err = utils.derive_hmac_sha256_key(ikm, nonce)
+ assert.is_not_nil(key)
+ assert.is_nil(err)
+ assert.equals(32, #key)
+
+ mac, err = utils.hmac_sha256(key, "some message")
+ assert.is_not_nil(mac)
+ assert.is_nil(err)
+ end)
+
+ it("successfully derives key and iv; encryption and decryption succeeds", function()
+ local data = {
+ foo = "bar",
+ tab = { "val" },
+ num = 123,
+ }
+ local aad = "some_aad"
+ local input_data = utils.encode_json(data)
+
+ local key, err, iv, ciphertext, tag, plaintext
+ for _, slow in ipairs({ true, false }) do
+ key, err, iv = utils.derive_aes_gcm_256_key_and_iv(ikm, nonce, slow)
+ assert.is_not_nil(key)
+ assert.is_not_nil(iv)
+ assert.is_nil(err)
+
+ ciphertext, err, tag = utils.encrypt_aes_256_gcm(key, iv, input_data, aad)
+ assert.is_not_nil(ciphertext)
+ assert.is_not_nil(tag)
+ assert.is_nil(err)
+
+ plaintext, err = utils.decrypt_aes_256_gcm(key, iv, ciphertext, aad, tag)
+ assert.is_not_nil(plaintext)
+ assert.is_nil(err)
+
+ assert.equals(plaintext, input_data)
+ end
+ end)
+ end)
+end)
diff --git a/spec/02-file-utils_spec.lua b/spec/02-file-utils_spec.lua
new file mode 100644
index 000000000..cca9b76c0
--- /dev/null
+++ b/spec/02-file-utils_spec.lua
@@ -0,0 +1,108 @@
+local file_utils = require "resty.session.file.utils"
+
+
+local fmt = string.format
+local describe = describe
+local assert = assert
+local it = it
+
+
+describe("Testing file utils", function()
+ describe("validate file name", function()
+ local validate_file_name = file_utils.validate_file_name
+ local name = "name"
+ local sid1 = "ABCdef123_-iJqwertYUIOpasdfgHJKLzxcvbnmJuis"
+ local aud_sub1 = "some-aud:some-sub"
+ local simple_prefix = "pref"
+ local special_prefix = "@-_-"
+ local simple_suffix = "suff"
+ local special_suffix = "-_-@"
+ local filename_inv1 = "some.conf"
+ local filename_inv2 = "abc"
+ local filename_inv3 = name.."_".. "abc"
+
+ it("validation fails with invalid files with prefix and suffix", function()
+ assert.is_false(validate_file_name(simple_prefix, simple_suffix, name, filename_inv1))
+ assert.is_false(validate_file_name(simple_prefix, simple_suffix, name, filename_inv2))
+ assert.is_false(validate_file_name(simple_prefix, simple_suffix, name, filename_inv3))
+ end)
+
+
+ it("validation fails with invalid files with prefix only", function()
+ assert.is_false(validate_file_name(simple_prefix, nil, name, filename_inv1))
+ assert.is_false(validate_file_name(simple_prefix, nil, name, filename_inv2))
+ assert.is_false(validate_file_name(simple_prefix, nil, name, filename_inv3))
+ end)
+
+ it("validation fails with invalid files with suffix only", function()
+ assert.is_false(validate_file_name(nil, simple_suffix, name, filename_inv1))
+ assert.is_false(validate_file_name(nil, simple_suffix, name, filename_inv2))
+ assert.is_false(validate_file_name(nil, simple_suffix, name, filename_inv3))
+ end)
+
+ it("validation fails with invalid files with no prefix or suffix", function()
+ assert.is_false(validate_file_name(nil, nil, name, filename_inv1))
+ assert.is_false(validate_file_name(nil, nil, name, filename_inv2))
+ assert.is_false(validate_file_name(nil, nil, name, filename_inv3))
+ end)
+
+ it("validation passes with prefix and suffix", function()
+ local filename_sess = fmt("%s_%s_%s.%s", simple_prefix, name, sid1, simple_suffix)
+ local filename_meta = fmt("%s_%s_%s.%s", simple_prefix, name, aud_sub1, simple_suffix)
+
+ assert.is_true(validate_file_name(simple_prefix, simple_suffix, name, filename_sess))
+ assert.is_true(validate_file_name(simple_prefix, simple_suffix, name, filename_meta))
+ assert.is_false(validate_file_name(simple_prefix, simple_suffix, name, filename_inv1))
+ assert.is_false(validate_file_name(simple_prefix, simple_suffix, name, filename_inv2))
+ end)
+
+ it("validation passes with special prefix and suffix", function()
+ local sp_filename_sess = fmt("%s_%s_%s.%s", special_prefix, name, sid1, special_suffix)
+ local sp_filename_meta = fmt("%s_%s_%s.%s", special_prefix, name, aud_sub1, special_suffix)
+
+ assert.is_true(validate_file_name(special_prefix, special_suffix, name, sp_filename_sess))
+ assert.is_true(validate_file_name(special_prefix, special_suffix, name, sp_filename_meta))
+ end)
+
+
+ it("validation passes with prefix", function()
+ local filename_sess = fmt("%s_%s_%s", simple_prefix, name, sid1)
+ local filename_meta = fmt("%s_%s_%s", simple_prefix, name, aud_sub1)
+
+ assert.is_true(validate_file_name(simple_prefix, nil, name, filename_sess))
+ assert.is_true(validate_file_name(simple_prefix, nil, name, filename_meta))
+ end)
+
+ it("validation passes with special prefix", function()
+ local sp_filename_sess = fmt("%s_%s_%s", special_prefix, name, sid1)
+ local sp_filename_meta = fmt("%s_%s_%s", special_prefix, name, aud_sub1)
+
+ assert.is_true(validate_file_name(special_prefix, nil, name, sp_filename_sess))
+ assert.is_true(validate_file_name(special_prefix, nil, name, sp_filename_meta))
+ end)
+
+ it("#only validation passes with suffix", function()
+ local filename_sess = fmt("%s_%s.%s", name, sid1, simple_suffix)
+ local filename_meta = fmt("%s_%s.%s", name, aud_sub1, simple_suffix)
+
+ assert.is_true(validate_file_name(nil, simple_suffix, name, filename_sess))
+ assert.is_true(validate_file_name(nil, simple_suffix, name, filename_meta))
+ end)
+
+ it("validation passes with special suffix", function()
+ local sp_filename_sess = fmt("%s_%s.%s", name, sid1, special_suffix)
+ local sp_filename_meta = fmt("%s_%s.%s", name, aud_sub1, special_suffix)
+
+ assert.is_true(validate_file_name(nil, special_suffix, name, sp_filename_sess))
+ assert.is_true(validate_file_name(nil, special_suffix, name, sp_filename_meta))
+ end)
+
+ it("validation passes with no prefix or suffix", function()
+ local filename_sess = fmt("%s_%s", name, sid1)
+ local filename_meta = fmt("%s_%s", name, aud_sub1)
+
+ assert.is_true(validate_file_name(nil, nil, name, filename_sess))
+ assert.is_true(validate_file_name(nil, nil, name, filename_meta))
+ end)
+ end)
+end)
diff --git a/spec/03-session_spec.lua b/spec/03-session_spec.lua
new file mode 100644
index 000000000..78e7dbebf
--- /dev/null
+++ b/spec/03-session_spec.lua
@@ -0,0 +1,405 @@
+local session = require "resty.session"
+
+
+local before_each = before_each
+local describe = describe
+local assert = assert
+local pcall = pcall
+local it = it
+
+
+local function extract_cookie(cookie_name, cookies)
+ local session_cookie
+ if type(cookies) == "table" then
+ for _, v in ipairs(cookies) do
+ session_cookie = ngx.re.match(v, cookie_name .. "=([\\w-]+);")
+ if session_cookie then
+ return session_cookie[1]
+ end
+ end
+ return ""
+ end
+ session_cookie = ngx.re.match(cookies, cookie_name .. "=([\\w-]+);")
+ return session_cookie and session_cookie[1] or ""
+end
+
+
+describe("Session", function()
+ local configuration = {}
+
+ describe("instance methods behave as expected", function()
+ local cookie_name = "session_cookie"
+ local remember_cookie_name = "remember_cookie"
+ local test_key = "test_key"
+ local data = "test_data"
+ local test_subject = "test_subject"
+ local test_audience = "test_audience"
+ local lout_subject = "lout_subject"
+ local lout_audience = "lout_audience"
+
+ local function test_session_set_get(s)
+ assert.is_nil(
+ s:get(test_key) or
+ s:get(test_subject) or
+ s:get(test_audience)
+ )
+
+ s:set(test_key, data)
+ s:set_subject(test_subject)
+ s:set_audience(test_audience)
+
+ assert.equals(s:get(test_key), data)
+ assert.equals(s:get_subject(), test_subject)
+ assert.equals(s:get_audience(), test_audience)
+ end
+
+ local function test_session_save(s, cookies)
+ session.__set_ngx_header(cookies)
+
+ local ok, err = s:save()
+
+ assert.equals(s.state, "open")
+ assert.is_true(ok)
+ assert.is_nil(err)
+ assert.is_not_nil(s.meta)
+ assert.is_not_nil(s.meta.data_size)
+ assert(s.meta.data_size > 0)
+
+ local session_cookie = extract_cookie(cookie_name, cookies["Set-Cookie"])
+ return session_cookie
+ end
+
+ local function test_session_close_open(s, session_cookie)
+ s:close()
+
+ assert.equals(s.state, "closed")
+
+ local ok, err = pcall(s.get, s, "anything")
+ assert.is_false(ok)
+ assert.matches("unable to get session data", err)
+
+ session.__set_ngx_var({
+ ["cookie_" .. cookie_name] = session_cookie
+ })
+
+ ok, err = s:open()
+ assert.is_true(ok)
+ assert.is_nil(err)
+ assert.equals(s.state, "open")
+ assert.equals(data, s:get(test_key))
+ end
+
+ local function test_session_get_property(s)
+ assert.equals(43, #s:get_property("id"))
+ assert.equals(32, #s:get_property("nonce"))
+ assert.equals(test_audience, s:get_property("audience"))
+ assert.equals(test_subject, s:get_property("subject"))
+ assert.match.is_number(s:get_property("timeout"))
+ assert.match.is_number(s:get_property("idling-timeout"))
+ assert.match.is_number(s:get_property("rolling-timeout"))
+ assert.match.is_number(s:get_property("absolute-timeout"))
+ end
+
+ local function test_session_touch(s)
+ local ok, err = s:touch()
+ assert.is_true(ok)
+ assert.is_nil(err)
+ assert.equals(s.state, "open")
+ end
+
+ local function test_session_destroy_open(s)
+ local cookies = {}
+
+ session.__set_ngx_header(cookies)
+
+ local ok, err = s:destroy()
+ assert.is_true(ok)
+ assert.is_nil(err)
+ assert.equals(s.state, "closed")
+
+ ok, err = pcall(s.get, s, "anything")
+ assert.is_false(ok)
+ assert.matches("unable to get session data", err)
+
+ local session_cookie = extract_cookie(cookie_name, cookies["Set-Cookie"]) -- empty
+
+ session.__set_ngx_var({
+ ["cookie_" .. cookie_name] = session_cookie
+ })
+
+ ok, err = s:open()
+ assert.is_nil(ok)
+ assert.equals("invalid session header", err)
+ assert.equals(s.state, "closed")
+
+ ok, err = pcall(s.get, s, "anything")
+ assert.is_false(ok)
+ assert.matches("unable to get session data", err)
+ end
+
+ local function test_session(s)
+ local session_cookie
+ local cookies = {}
+
+ test_session_set_get(s)
+ session_cookie = test_session_save(s, cookies)
+ test_session_close_open(s, session_cookie)
+ test_session_get_property(s)
+ test_session_touch(s)
+ test_session_destroy_open(s)
+ end
+
+ before_each(function()
+ configuration = {
+ cookie_name = cookie_name,
+ remember_cookie_name = remember_cookie_name
+ }
+ end)
+
+ it("with default values", function()
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ end)
+
+ it("with custom secret", function()
+ configuration.secret = "t"
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ end)
+
+ it("custom ikm takes precedence on secret", function()
+ configuration.secret = "t"
+ configuration.ikm = "00000000000000000000000000000000"
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+
+ assert.equals(configuration.ikm, s.meta.ikm)
+ end)
+
+ it("logout individual audience and subject", function()
+ local cookies = {}
+ session.__set_ngx_header(cookies)
+ session.init(configuration)
+
+ configuration.audience = test_audience
+ configuration.subject = test_subject
+ local s1 = session.new(configuration)
+ assert.is_not_nil(s1)
+ test_session_save(s1, cookies)
+ local session_cookie = extract_cookie(cookie_name, cookies["Set-Cookie"])
+ session.__set_ngx_var({
+ ["cookie_" .. cookie_name] = session_cookie
+ })
+ assert.is_not_nil(session_cookie)
+ assert.is_not_equal("", session_cookie)
+ assert.match(s1:get_audience(), configuration.audience)
+
+ configuration.audience = lout_audience
+ configuration.subject = lout_subject
+ local s2 = session.open(configuration)
+ assert.is_not_nil(s2)
+ test_session_save(s2, cookies)
+ session_cookie = extract_cookie(cookie_name, cookies["Set-Cookie"])
+ session.__set_ngx_var({
+ ["cookie_" .. cookie_name] = session_cookie
+ })
+ assert.is_not_nil(session_cookie)
+ assert.is_not_equal("", session_cookie)
+ assert.equals(configuration.audience, s2:get_audience())
+
+ s2:logout()
+ assert.equals(s1.state, "open")
+ assert.equals(s2.state, "closed")
+ session_cookie = extract_cookie(cookie_name, cookies["Set-Cookie"])
+ session.__set_ngx_var({
+ ["cookie_" .. cookie_name] = session_cookie
+ })
+ assert.is_not_nil(session_cookie)
+ assert.is_not_equal("", session_cookie)
+
+ s1:logout()
+ assert.equals(s1.state, "closed")
+ session_cookie = extract_cookie(cookie_name, cookies["Set-Cookie"])
+ assert.is_not_nil(session_cookie)
+ assert.equals("", session_cookie)
+ end)
+
+ it("set_remember=true produces remember cookie, get_remember returns expected values", function()
+ local cookies = {}
+ session.__set_ngx_header(cookies)
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ assert.is_false(s:get_remember())
+ s:save()
+ assert.equals(s.state, "open")
+ local session_cookie = extract_cookie(cookie_name, cookies["Set-Cookie"])
+ local remember_cookie = extract_cookie(remember_cookie_name, cookies["Set-Cookie"])
+ assert.is_not_nil(remember_cookie)
+ assert.equals("", remember_cookie)
+
+ session.__set_ngx_var({
+ ["cookie_" .. cookie_name] = session_cookie,
+ ["cookie_" .. remember_cookie_name] = remember_cookie,
+ })
+ s:set_remember(true)
+ assert.is_true(s:get_remember())
+ s:save()
+ assert.equals(s.state, "open")
+ remember_cookie = extract_cookie(remember_cookie_name, cookies["Set-Cookie"])
+ assert.is_not_nil(remember_cookie)
+ assert.is_not_equal(remember_cookie, "")
+ end)
+
+ describe("with custom cookie attribute", function()
+ it("Domain", function()
+ configuration.cookie_domain = "example.org"
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ assert.matches("Domain=example.org", s.cookie_flags)
+ end)
+
+ it("Path", function()
+ configuration.cookie_path = "/test"
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ assert.matches("Path=/test", s.cookie_flags)
+ end)
+
+ it("SameSite", function()
+ configuration.cookie_same_site = "Default"
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ assert.matches("SameSite=Default", s.cookie_flags)
+ end)
+
+ it("HttpOnly", function()
+ configuration.cookie_http_only = false
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ assert.does_not.match("HttpOnly", s.cookie_flags)
+ end)
+
+ it("Secure", function()
+ configuration.cookie_secure = true
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ assert.matches("Secure", s.cookie_flags)
+ end)
+
+ it("Priority", function()
+ configuration.cookie_priority = "High"
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ assert.matches("Priority=High", s.cookie_flags)
+ end)
+
+ it("Partitioned", function()
+ configuration.cookie_partitioned = true
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ assert.matches("Partitioned", s.cookie_flags)
+ end)
+
+ it("SameParty", function()
+ configuration.cookie_same_party = true
+ session.init(configuration)
+
+ local s = session.new()
+ assert.is_not_nil(s)
+ test_session(s)
+ assert.matches("SameParty", s.cookie_flags)
+ end)
+ end)
+ end)
+
+ describe("Fields validation", function()
+ describe("init validates fields", function()
+ before_each(function()
+ configuration = {}
+ end)
+
+ it("custom ikm must be 32 bytes", function()
+ configuration.ikm = "12345"
+
+ local ok, err = pcall(session.init,configuration)
+ assert.is_false(ok)
+ assert.matches("invalid ikm size", err)
+ end)
+
+ it("custom ikm_fallbacks must be 32 bytes", function()
+ configuration.ikm_fallbacks = {
+ "00000000000000000000000000000000",
+ "123456",
+ }
+
+ local ok, err = pcall(session.init,configuration)
+ assert.is_false(ok)
+ assert.matches("invalid ikm size in ikm_fallbacks", err)
+ end)
+ end)
+
+ describe("new validates fields", function()
+ before_each(function()
+ configuration = {}
+ end)
+
+ it("custom ikm must be 32 bytes", function()
+ configuration.ikm = "12345"
+ local ok, err = pcall(session.new, configuration)
+ assert.is_false(ok)
+ assert.matches("invalid ikm size", err)
+ end)
+
+ it("custom ikm_fallbacks must be 32 bytes", function()
+ configuration.ikm_fallbacks = {
+ "00000000000000000000000000000000",
+ "123456",
+ }
+ local ok, err = pcall(session.new, configuration)
+ assert.is_false(ok)
+ assert.matches("invalid ikm size", err)
+ end)
+
+ it("SameParty and SameSite=strict fails to instantiate session", function()
+ configuration.cookie_same_party = true
+ configuration.cookie_same_site = "Strict"
+
+ local ok, err = pcall(session.new, configuration)
+ assert.is_false(ok)
+ assert.matches("SameParty session cookies cannot use SameSite=Strict", err)
+ end)
+ end)
+ end)
+end)
diff --git a/spec/04-storage-1_spec.lua b/spec/04-storage-1_spec.lua
new file mode 100644
index 000000000..6271622ea
--- /dev/null
+++ b/spec/04-storage-1_spec.lua
@@ -0,0 +1,223 @@
+---
+-- Ensure to keep the tests consistent with those in 05-storage-1_spec.lua
+
+
+local utils = require "resty.session.utils"
+
+
+local before_each = before_each
+local after_each = after_each
+local lazy_setup = lazy_setup
+local describe = describe
+local ipairs = ipairs
+local assert = assert
+local sleep = ngx.sleep
+local time = ngx.time
+local it = it
+
+
+local storage_configs = {
+ file = {
+ suffix = "session",
+ },
+ shm = {
+ prefix = "sessions",
+ connect_timeout = 10000,
+ send_timeout = 10000,
+ read_timeout = 10000,
+ },
+ redis = {
+ prefix = "sessions",
+ password = "password",
+ },
+ memcached = {
+ prefix = "sessions",
+ connect_timeout = 10000,
+ send_timeout = 10000,
+ read_timeout = 10000,
+ },
+}
+
+
+for _, st in ipairs({
+ "file",
+ "shm",
+ "redis",
+ "memcached",
+}) do
+ describe("Storage tests 1", function()
+ local current_time
+ local storage
+ local long_ttl = 60
+ local short_ttl = 2
+ local key = "test_key_1iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
+ local key1 = "test_key_2iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
+ local key2 = "test_key_3iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
+ local old_key = "old_key_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
+ local name = "test_name"
+ local value = "test_value"
+
+ lazy_setup(function()
+ local conf = {
+ remember = true,
+ store_metadata = true,
+ }
+ conf[st] = storage_configs[st]
+ storage = utils.load_storage(st, conf)
+ assert.is_not_nil(storage)
+ end)
+
+ before_each(function()
+ current_time = time()
+ end)
+
+ describe("[#" .. st .. "] storage: SET + GET", function()
+ local audiences = { "foo", "bar" }
+ local subjects = { "john", "jane" }
+
+ local metadata = {
+ audiences = audiences,
+ subjects = subjects,
+ }
+
+ after_each(function()
+ storage:delete(name, key, current_time, metadata)
+ storage:delete(name, key1, current_time, metadata)
+ storage:delete(name, key2, current_time, metadata)
+ end)
+
+ it("SET: simple set does not return errors, GET fetches value correctly", function()
+ local ok = storage:set(name, key, value, long_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ local v, err = storage:get(name, key, current_time)
+ assert.is_not_nil(v)
+ assert.is_nil(err)
+ assert.equals(v, value)
+ end)
+
+ it("SET: with metadata and remember works correctly", function()
+ local ok = storage:set(name, key, value, long_ttl, current_time, nil, nil, metadata, true)
+ assert.is_not_nil(ok)
+
+ sleep(1)
+
+ local v, err = storage:get(name, key, time())
+ assert.is_not_nil(v)
+ assert.is_nil(err)
+ assert.equals(v, value)
+ end)
+
+ it("SET: with metadata (long ttl) correctly appends metadata to collection", function()
+ local ok = storage:set(name, key, value, long_ttl, current_time, nil, nil, metadata, true)
+ ok = ok and storage:set(name, key1, value, long_ttl, current_time, nil, nil, metadata, true)
+ ok = ok and storage:set(name, key2, value, long_ttl, current_time, nil, nil, metadata, true)
+ assert.is_not_nil(ok)
+
+ sleep(1)
+
+ for i = 1, #audiences do
+ local meta_values = storage:read_metadata(name, audiences[i], subjects[i], time())
+ assert.is_not_nil(meta_values)
+ assert.truthy(meta_values[key ])
+ assert.truthy(meta_values[key1])
+ assert.truthy(meta_values[key2])
+ end
+ end)
+
+ it("SET: with metadata (short ttl) correctly expires metadata", function()
+ local ok = storage:set(name, key, value, short_ttl, current_time, nil, nil, metadata, true)
+
+ sleep(short_ttl + 1)
+
+ ok = ok and storage:set(name, key1, value, long_ttl, time(), nil, nil, metadata, true)
+ assert.is_not_nil(ok)
+
+ sleep(1)
+
+ for i = 1, #audiences do
+ local meta_values = storage:read_metadata(name, audiences[i], subjects[i], time())
+ assert.falsy(meta_values[key])
+ assert.truthy(meta_values[key1])
+ end
+ end)
+
+ it("SET: with old_key correctly applies stale ttl on old key", function()
+ local stale_ttl = 1
+
+ local ok = storage:set(name, old_key, value, long_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ ok = storage:set(name, key, value, long_ttl, current_time, old_key, stale_ttl, nil, false)
+ assert.is_not_nil(ok)
+
+ sleep(3)
+
+ local v = storage:get(name, old_key, time())
+ assert.is_nil(v)
+ end)
+
+ it("SET: remember deletes file in old_key", function()
+ local stale_ttl = long_ttl
+
+ local ok = storage:set(name, old_key, value, long_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ ok = storage:set(name, key, value, long_ttl, current_time, old_key, stale_ttl, nil, true)
+ assert.is_not_nil(ok)
+
+ local v = storage:get(name, old_key, current_time)
+ assert.is_nil(v)
+ end)
+
+ it("SET: ttl works as expected", function()
+ local ok = storage:set(name, key, value, short_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ sleep(3)
+
+ local v = storage:get(name, key, time())
+ assert.is_nil(v)
+ end)
+ end)
+
+ describe("[#" .. st .. "] storage: DELETE", function()
+ local audiences = { "foo" }
+ local subjects = { "john" }
+
+ local metadata = {
+ audiences = audiences,
+ subjects = subjects,
+ }
+
+ it("deleted file is really deleted", function()
+ local ok = storage:set(name, key, value, short_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ storage:delete(name, key, current_time, nil)
+
+ local v = storage:get(name, key, current_time)
+ assert.is_nil(v)
+ end)
+
+ it("with metadata correctly deletes metadata collection", function()
+ local ok = storage:set(name, key1, value, long_ttl, current_time, nil, nil, metadata, true)
+ assert.is_not_nil(ok)
+
+ sleep(1)
+
+ for i = 1, #audiences do
+ local meta_values = storage:read_metadata(name, audiences[i], subjects[i], time())
+ assert.truthy(meta_values[key1])
+ ok = storage:delete(name, key1, time(), metadata)
+ assert.is_not_nil(ok)
+
+ sleep(2)
+
+ meta_values = storage:read_metadata(name, audiences[i], subjects[i], time()) or {}
+ assert.falsy(meta_values[key1])
+ end
+ end)
+ end)
+ end)
+end
diff --git a/spec/05-storage-2_spec.lua b/spec/05-storage-2_spec.lua
new file mode 100644
index 000000000..f6eb4c265
--- /dev/null
+++ b/spec/05-storage-2_spec.lua
@@ -0,0 +1,252 @@
+---
+-- For now these tests don't run on CI.
+-- Ensure to keep the tests consistent with those in 04-storage-1_spec.lua
+
+
+local utils = require "resty.session.utils"
+
+
+local before_each = before_each
+local after_each = after_each
+local lazy_setup = lazy_setup
+local describe = describe
+local ipairs = ipairs
+local assert = assert
+local sleep = ngx.sleep
+local time = ngx.time
+local it = it
+
+
+local storage_configs = {
+ mysql = {
+ username = "root",
+ password = "password",
+ database = "test",
+ },
+ postgres = {
+ username = "postgres",
+ password = "password",
+ database = "test",
+ },
+ redis_sentinel = {
+ prefix = "sessions",
+ password = "password",
+ sentinels = {
+ { host = "127.0.0.1", port = "26379" }
+ },
+ connect_timeout = 10000,
+ send_timeout = 10000,
+ read_timeout = 10000,
+ },
+ redis_cluster = {
+ password = "password",
+ nodes = {
+ { ip = "127.0.0.1", port = "6380" }
+ },
+ name = "somecluster",
+ lock_zone = "sessions",
+ connect_timeout = 10000,
+ send_timeout = 10000,
+ read_timeout = 10000,
+ },
+ dshm = {
+ prefix = "sessions",
+ connect_timeout = 10000,
+ send_timeout = 10000,
+ read_timeout = 10000,
+ },
+}
+
+
+local function storage_type(ty)
+ if ty == "redis_cluster" or ty == "redis_sentinel" then
+ return "redis"
+ end
+ return ty
+end
+
+
+for _, st in ipairs({
+ "mysql",
+ "postgres",
+ "redis_cluster",
+ "redis_sentinel",
+ "dshm"
+}) do
+ describe("Storage tests 2 #noci", function()
+ local current_time
+ local storage
+ local long_ttl = 60
+ local short_ttl = 2
+ local key = "test_key_1iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
+ local key1 = "test_key_2iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
+ local key2 = "test_key_3iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
+ local old_key = "old_key_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
+ local name = "test_name"
+ local value = "test_value"
+
+ lazy_setup(function()
+ local conf = {
+ remember = true,
+ store_metadata = true,
+ }
+ conf[storage_type(st)] = storage_configs[st]
+ storage = utils.load_storage(storage_type(st), conf)
+ assert.is_not_nil(storage)
+ end)
+
+ before_each(function()
+ current_time = time()
+ end)
+
+ describe("[#" .. st .. "] storage: SET + GET", function()
+ local audiences = { "foo", "bar" }
+ local subjects = { "john", "jane" }
+
+ local metadata = {
+ audiences = audiences,
+ subjects = subjects,
+ }
+
+ after_each(function()
+ current_time = time()
+ storage:delete(name, key, current_time, metadata)
+ storage:delete(name, key1, current_time, metadata)
+ storage:delete(name, key2, current_time, metadata)
+ end)
+
+ it("SET: simple set does not return errors, GET fetches value correctly", function()
+ local ok = storage:set(name, key, value, long_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ local v, err = storage:get(name, key, current_time)
+ assert.is_not_nil(v)
+ assert.is_nil(err)
+ assert.equals(v, value)
+ end)
+
+ it("SET: with metadata and remember works correctly", function()
+ local ok = storage:set(name, key, value, long_ttl, time(), nil, nil, metadata, true)
+ assert.is_not_nil(ok)
+
+ sleep(1)
+
+ local v, err = storage:get(name, key, time())
+ assert.is_not_nil(v)
+ assert.is_nil(err)
+ assert.equals(v, value)
+ end)
+
+ it("SET: with metadata (long ttl) correctly appends metadata to collection", function()
+ local ok = storage:set(name, key, value, long_ttl, current_time, nil, nil, metadata, true)
+ ok = ok and storage:set(name, key1, value, long_ttl, current_time, nil, nil, metadata, true)
+ ok = ok and storage:set(name, key2, value, long_ttl, current_time, nil, nil, metadata, true)
+ assert.is_not_nil(ok)
+
+ sleep(1)
+
+ for i = 1, #audiences do
+ local meta_values = storage:read_metadata(name, audiences[i], subjects[i], time())
+ assert.is_not_nil(meta_values)
+ assert.truthy(meta_values[key ])
+ assert.truthy(meta_values[key1])
+ assert.truthy(meta_values[key2])
+ end
+ end)
+
+ it("SET: with metadata (short ttl) correctly expires metadata", function()
+ local ok = storage:set(name, key, value, short_ttl, current_time, nil, nil, metadata, true)
+
+ sleep(short_ttl + 1)
+
+ ok = ok and storage:set(name, key1, value, long_ttl, time(), nil, nil, metadata, true)
+ assert.is_not_nil(ok)
+
+ sleep(1)
+
+ for i = 1, #audiences do
+ local meta_values = storage:read_metadata(name, audiences[i], subjects[i], time())
+ assert.falsy(meta_values[key])
+ assert.truthy(meta_values[key1])
+ end
+ end)
+
+ it("SET: with old_key correctly applies stale ttl on old key", function()
+ local stale_ttl = 1
+
+ local ok = storage:set(name, old_key, value, long_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ ok = storage:set(name, key, value, long_ttl, current_time, old_key, stale_ttl, nil, false)
+ assert.is_not_nil(ok)
+
+ sleep(3)
+
+ local v = storage:get(name, old_key, time())
+ assert.is_nil(v)
+ end)
+
+ it("SET: remember deletes file in old_key", function()
+ local stale_ttl = long_ttl
+ local ok = storage:set(name, old_key, value, long_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ ok = storage:set(name, key, value, long_ttl, current_time, old_key, stale_ttl, nil, true)
+ assert.is_not_nil(ok)
+
+ local v = storage:get(name, old_key, current_time)
+ assert.is_nil(v)
+ end)
+
+ it("SET: ttl works as expected", function()
+ local ok = storage:set(name, key, value, short_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ sleep(3)
+
+ local v = storage:get(name, key, time())
+ assert.is_nil(v)
+ end)
+ end)
+
+ describe("[#" .. st .. "] storage: DELETE", function()
+ local audiences = { "foo" }
+ local subjects = { "john" }
+
+ local metadata = {
+ audiences = audiences,
+ subjects = subjects,
+ }
+
+ it("deleted file is really deleted", function()
+ local ok = storage:set(name, key, value, short_ttl, current_time)
+ assert.is_not_nil(ok)
+
+ storage:delete(name, key, current_time)
+
+ local v = storage:get(name, key, current_time)
+ assert.is_nil(v)
+ end)
+
+ it("with metadata correctly deletes metadata collection", function()
+ local ok = storage:set(name, key1, value, long_ttl, current_time, nil, nil, metadata, true)
+ assert.is_not_nil(ok)
+
+ sleep(1)
+
+ for i = 1, #audiences do
+ local meta_values = storage:read_metadata(name, audiences[i], subjects[i], time())
+ assert.truthy(meta_values[key1])
+
+ ok = storage:delete(name, key1, time(), metadata)
+ assert.is_not_nil(ok)
+
+ sleep(2)
+
+ meta_values = storage:read_metadata(name, audiences[i], subjects[i], time()) or {}
+ assert.falsy(meta_values[key1])
+ end
+ end)
+ end)
+ end)
+end
diff --git a/t/01-cookies.t b/t/01-cookies.t
new file mode 100644
index 000000000..df7fc1308
--- /dev/null
+++ b/t/01-cookies.t
@@ -0,0 +1,149 @@
+use Test::Nginx::Socket;
+repeat_each(2);
+
+$ENV{TEST_NGINX_NXSOCK} ||= html_dir();
+plan tests => repeat_each() * blocks() * 3 + 4;
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: session cookie is returned
+--- http_config
+ init_by_lua_block {
+ require("resty.session").init({
+ storage = "cookie",
+ })
+ }
+--- config
+location = /test {
+ content_by_lua_block {
+ local session = require "resty.session".new()
+ local ok = session:save()
+ if ok then
+ ngx.say("yay")
+ end
+ }
+}
+
+--- request
+GET /test
+--- response_body
+yay
+--- response_headers_like
+Set-Cookie: .*session=.+;.*
+--- error_code: 200
+--- no_error_log
+[error]
+
+
+=== TEST 2: remember cookie is returned when remember=true
+--- http_config
+ init_by_lua_block {
+ require("resty.session").init({
+ remember = true,
+ storage = "cookie",
+ })
+ }
+--- config
+location = /test {
+ content_by_lua_block {
+ local session = require "resty.session".new()
+ local ok = session:save()
+
+ if ok then
+ ngx.say("yay")
+ end
+ }
+}
+
+--- request
+GET /test
+--- response_body
+yay
+--- response_headers_like
+Set-Cookie: .*remember=.+;.*
+--- error_code: 200
+--- no_error_log
+[error]
+
+
+=== TEST 3: session.open() opens a session from a valid cookie and data is
+extracted correctly
+--- http_config
+ init_by_lua_block {
+ require("resty.session").init({
+ secret = "RaJKp8UQW1",
+ storage = "cookie",
+ audience = "my_application",
+ idling_timeout = 0,
+ rolling_timeout = 0,
+ absolute_timeout = 0,
+ })
+ }
+--- config
+location = /test {
+ content_by_lua_block {
+ local session = require "resty.session".open()
+ local sub = session:get_subject()
+ local aud = session:get_audience()
+ local quote = session:get("quote")
+
+ ngx.say(sub .. "|" .. aud .. "|" .. quote)
+ }
+}
+
+--- request
+GET /test
+--- more_headers
+Cookie: session=AQAAS3ZGU0k8tUKsWSci9Fb6PM5xbm469FlR5g_B5HWZ6KYGSOZjAAAAAABcAABTCuHjqpE7B6Ux7m4GCylZAAAAzcWnTvzG51whooR_4QQwDgGdMOOa5W7tG4JWiDFU3zuYLFzakWEi-y-ogrwTpnt24zQXP_uJK7r5lMPNzRSMJM9H1a_MIegzEMm-QSgVRaoZVJq3Oo; Path=/; SameSite=Lax; HttpOnly
+--- response_body
+Lua Fan|my_application|Lorem ipsum dolor sit amet
+--- error_code: 200
+--- no_error_log
+[error]
+
+
+=== TEST 4: clear_request_cookie() clears session Cookie from request to
+upstream
+--- http_config
+ init_by_lua_block {
+ require("resty.session").init({
+ secret = "RaJKp8UQW1",
+ storage = "cookie",
+ audience = "my_application",
+ idling_timeout = 0,
+ rolling_timeout = 0,
+ absolute_timeout = 0,
+ })
+ }
+
+ server {
+ listen unix:/$TEST_NGINX_NXSOCK/nginx.sock;
+
+ location /t {
+ content_by_lua_block {
+ local headers = ngx.req.get_headers()
+ ngx.say("session_cookie: [", tostring(headers["Cookie"]), "]")
+ }
+ }
+ }
+
+--- config
+location = /test {
+ access_by_lua_block {
+ local session = require "resty.session".open()
+ session:clear_request_cookie()
+ }
+ proxy_pass http://unix:/$TEST_NGINX_NXSOCK/nginx.sock;
+}
+
+--- request
+GET /test
+--- more_headers
+Cookie: session=AQAAS3ZGU0k8tUKsWSci9Fb6PM5xbm469FlR5g_B5HWZ6KYGSOZjAAAAAABcAABTCuHjqpE7B6Ux7m4GCylZAAAAzcWnTvzG51whooR_4QQwDgGdMOOa5W7tG4JWiDFU3zuYLFzakWEi-y-ogrwTpnt24zQXP_uJK7r5lMPNzRSMJM9H1a_MIegzEMm-QSgVRaoZVJq3Oo; Path=/; SameSite=Lax; HttpOnly
+--- response_body
+session_cookie: [Path=/; SameSite=Lax; HttpOnly]
+--- error_code: 200
+--- no_error_log
+[error]