commit a3cd342f3e1fffd7b16b83a24e03bb9ed501b319
Author: Théophile Diot
Date: Fri Jun 30 15:38:54 2023 -0400
Squashed 'src/deps/src/lua-resty-session/' content from commit 8b5f8752f
git-subtree-dir: src/deps/src/lua-resty-session
git-subtree-split: 8b5f8752f3046396c414c5b97850e784c07e1641
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]