diff options
Diffstat (limited to 'server/resty/session')
-rw-r--r-- | server/resty/session/ciphers/aes.lua | 113 | ||||
-rw-r--r-- | server/resty/session/ciphers/none.lua | 15 | ||||
-rw-r--r-- | server/resty/session/compressors/none.lua | 15 | ||||
-rw-r--r-- | server/resty/session/compressors/zlib.lua | 43 | ||||
-rw-r--r-- | server/resty/session/encoders/base16.lua | 29 | ||||
-rw-r--r-- | server/resty/session/encoders/base64.lua | 39 | ||||
-rw-r--r-- | server/resty/session/encoders/hex.lua | 1 | ||||
-rw-r--r-- | server/resty/session/hmac/sha1.lua | 1 | ||||
-rw-r--r-- | server/resty/session/identifiers/random.lua | 13 | ||||
-rw-r--r-- | server/resty/session/serializers/json.lua | 6 | ||||
-rw-r--r-- | server/resty/session/storage/cookie.lua | 7 | ||||
-rw-r--r-- | server/resty/session/storage/dshm.lua | 163 | ||||
-rw-r--r-- | server/resty/session/storage/memcache.lua | 303 | ||||
-rw-r--r-- | server/resty/session/storage/memcached.lua | 1 | ||||
-rw-r--r-- | server/resty/session/storage/redis.lua | 478 | ||||
-rw-r--r-- | server/resty/session/storage/shm.lua | 125 | ||||
-rw-r--r-- | server/resty/session/strategies/default.lua | 232 | ||||
-rw-r--r-- | server/resty/session/strategies/regenerate.lua | 43 |
18 files changed, 1627 insertions, 0 deletions
diff --git a/server/resty/session/ciphers/aes.lua b/server/resty/session/ciphers/aes.lua new file mode 100644 index 0000000..9a088ad --- /dev/null +++ b/server/resty/session/ciphers/aes.lua @@ -0,0 +1,113 @@ +local aes = require "resty.aes" + +local setmetatable = setmetatable +local tonumber = tonumber +local ceil = math.ceil +local var = ngx.var +local sub = string.sub +local rep = string.rep + +local HASHES = aes.hash + +local CIPHER_MODES = { + ecb = "ecb", + cbc = "cbc", + cfb1 = "cfb1", + cfb8 = "cfb8", + cfb128 = "cfb128", + ofb = "ofb", + ctr = "ctr", + gcm = "gcm", +} + +local CIPHER_SIZES = { + [128] = 128, + [192] = 192, + [256] = 256, +} + +local defaults = { + size = CIPHER_SIZES[tonumber(var.session_aes_size, 10)] or 256, + mode = CIPHER_MODES[var.session_aes_mode] or "cbc", + hash = HASHES[var.session_aes_hash] or HASHES.sha512, + rounds = tonumber(var.session_aes_rounds, 10) or 1, +} + +local function adjust_salt(salt) + if not salt then + return nil + end + + local z = #salt + if z < 8 then + return sub(rep(salt, ceil(8 / z)), 1, 8) + end + if z > 8 then + return sub(salt, 1, 8) + end + + return salt +end + +local function get_cipher(self, key, salt) + local mode = aes.cipher(self.size, self.mode) + if not mode then + return nil, "invalid cipher mode " .. self.mode .. "(" .. self.size .. ")" + end + + return aes:new(key, adjust_salt(salt), mode, self.hash, self.rounds) +end + +local cipher = {} + +cipher.__index = cipher + +function cipher.new(session) + local config = session.aes or defaults + return setmetatable({ + size = CIPHER_SIZES[tonumber(config.size, 10)] or defaults.size, + mode = CIPHER_MODES[config.mode] or defaults.mode, + hash = HASHES[config.hash] or defaults.hash, + rounds = tonumber(config.rounds, 10) or defaults.rounds, + }, cipher) +end + +function cipher:encrypt(data, key, salt, _) + local cip, err = get_cipher(self, key, salt) + if not cip then + return nil, err or "unable to aes encrypt data" + end + + local encrypted_data + encrypted_data, err = cip:encrypt(data) + if not encrypted_data then + return nil, err or "aes encryption failed" + end + + if self.mode == "gcm" then + return encrypted_data[1], nil, encrypted_data[2] + end + + return encrypted_data +end + +function cipher:decrypt(data, key, salt, _, tag) + local cip, err = get_cipher(self, key, salt) + if not cip then + return nil, err or "unable to aes decrypt data" + end + + local decrypted_data + decrypted_data, err = cip:decrypt(data, tag) + if not decrypted_data then + return nil, err or "aes decryption failed" + end + + if self.mode == "gcm" then + return decrypted_data, nil, tag + end + + return decrypted_data +end + +return cipher diff --git a/server/resty/session/ciphers/none.lua b/server/resty/session/ciphers/none.lua new file mode 100644 index 0000000..b29bb88 --- /dev/null +++ b/server/resty/session/ciphers/none.lua @@ -0,0 +1,15 @@ +local cipher = {} + +function cipher.new() + return cipher +end + +function cipher.encrypt(_, data, _, _) + return data +end + +function cipher.decrypt(_, data, _, _, _) + return data +end + +return cipher diff --git a/server/resty/session/compressors/none.lua b/server/resty/session/compressors/none.lua new file mode 100644 index 0000000..3d14a5c --- /dev/null +++ b/server/resty/session/compressors/none.lua @@ -0,0 +1,15 @@ +local compressor = {} + +function compressor.new() + return compressor +end + +function compressor.compress(_, data) + return data +end + +function compressor.decompress(_, data) + return data +end + +return compressor diff --git a/server/resty/session/compressors/zlib.lua b/server/resty/session/compressors/zlib.lua new file mode 100644 index 0000000..1d23be0 --- /dev/null +++ b/server/resty/session/compressors/zlib.lua @@ -0,0 +1,43 @@ +local zlib = require "ffi-zlib" +local sio = require "pl.stringio" + +local concat = table.concat + +local function gzip(func, input) + local stream = sio.open(input) + local output = {} + local n = 0 + + local ok, err = func(function(size) + return stream:read(size) + end, function(data) + n = n + 1 + output[n] = data + end, 8192) + + if not ok then + return nil, err + end + + if n == 0 then + return "" + end + + return concat(output, nil, 1, n) +end + +local compressor = {} + +function compressor.new() + return compressor +end + +function compressor.compress(_, data) + return gzip(zlib.deflateGzip, data) +end + +function compressor.decompress(_, data) + return gzip(zlib.inflateGzip, data) +end + +return compressor diff --git a/server/resty/session/encoders/base16.lua b/server/resty/session/encoders/base16.lua new file mode 100644 index 0000000..552f50e --- /dev/null +++ b/server/resty/session/encoders/base16.lua @@ -0,0 +1,29 @@ +local to_hex = require "resty.string".to_hex + +local tonumber = tonumber +local gsub = string.gsub +local char = string.char + +local function chr(c) + return char(tonumber(c, 16) or 0) +end + +local encoder = {} + +function encoder.encode(value) + if not value then + return nil, "unable to base16 encode value" + end + + return to_hex(value) +end + +function encoder.decode(value) + if not value then + return nil, "unable to base16 decode value" + end + + return (gsub(value, "..", chr)) +end + +return encoder diff --git a/server/resty/session/encoders/base64.lua b/server/resty/session/encoders/base64.lua new file mode 100644 index 0000000..ddaf4e8 --- /dev/null +++ b/server/resty/session/encoders/base64.lua @@ -0,0 +1,39 @@ +local encode_base64 = ngx.encode_base64 +local decode_base64 = ngx.decode_base64 + +local gsub = string.gsub + +local ENCODE_CHARS = { + ["+"] = "-", + ["/"] = "_", +} + +local DECODE_CHARS = { + ["-"] = "+", + ["_"] = "/", +} + +local encoder = {} + +function encoder.encode(value) + if not value then + return nil, "unable to base64 encode value" + end + + local encoded = encode_base64(value, true) + if not encoded then + return nil, "unable to base64 encode value" + end + + return gsub(encoded, "[+/]", ENCODE_CHARS) +end + +function encoder.decode(value) + if not value then + return nil, "unable to base64 decode value" + end + + return decode_base64((gsub(value, "[-_]", DECODE_CHARS))) +end + +return encoder diff --git a/server/resty/session/encoders/hex.lua b/server/resty/session/encoders/hex.lua new file mode 100644 index 0000000..1b94a5a --- /dev/null +++ b/server/resty/session/encoders/hex.lua @@ -0,0 +1 @@ +return require "resty.session.encoders.base16"
\ No newline at end of file diff --git a/server/resty/session/hmac/sha1.lua b/server/resty/session/hmac/sha1.lua new file mode 100644 index 0000000..1753412 --- /dev/null +++ b/server/resty/session/hmac/sha1.lua @@ -0,0 +1 @@ +return ngx.hmac_sha1 diff --git a/server/resty/session/identifiers/random.lua b/server/resty/session/identifiers/random.lua new file mode 100644 index 0000000..a2f9739 --- /dev/null +++ b/server/resty/session/identifiers/random.lua @@ -0,0 +1,13 @@ +local tonumber = tonumber +local random = require "resty.random".bytes +local var = ngx.var + +local defaults = { + length = tonumber(var.session_random_length, 10) or 16 +} + +return function(session) + local config = session.random or defaults + local length = tonumber(config.length, 10) or defaults.length + return random(length, true) or random(length) +end diff --git a/server/resty/session/serializers/json.lua b/server/resty/session/serializers/json.lua new file mode 100644 index 0000000..960c4d8 --- /dev/null +++ b/server/resty/session/serializers/json.lua @@ -0,0 +1,6 @@ +local json = require "cjson.safe" + +return { + serialize = json.encode, + deserialize = json.decode, +} diff --git a/server/resty/session/storage/cookie.lua b/server/resty/session/storage/cookie.lua new file mode 100644 index 0000000..95e26d1 --- /dev/null +++ b/server/resty/session/storage/cookie.lua @@ -0,0 +1,7 @@ +local storage = {} + +function storage.new() + return storage +end + +return storage diff --git a/server/resty/session/storage/dshm.lua b/server/resty/session/storage/dshm.lua new file mode 100644 index 0000000..e6d887f --- /dev/null +++ b/server/resty/session/storage/dshm.lua @@ -0,0 +1,163 @@ +local dshm = require "resty.dshm" + +local setmetatable = setmetatable +local tonumber = tonumber +local concat = table.concat +local var = ngx.var + +local defaults = { + region = var.session_dshm_region or "sessions", + connect_timeout = tonumber(var.session_dshm_connect_timeout, 10), + read_timeout = tonumber(var.session_dshm_read_timeout, 10), + send_timeout = tonumber(var.session_dshm_send_timeout, 10), + host = var.session_dshm_host or "127.0.0.1", + port = tonumber(var.session_dshm_port, 10) or 4321, + pool = { + name = var.session_dshm_pool_name, + size = tonumber(var.session_dshm_pool_size, 10) or 100, + timeout = tonumber(var.session_dshm_pool_timeout, 10) or 1000, + backlog = tonumber(var.session_dshm_pool_backlog, 10), + }, +} + +local storage = {} + +storage.__index = storage + +function storage.new(session) + local config = session.dshm or defaults + local pool = config.pool or defaults.pool + + local connect_timeout = tonumber(config.connect_timeout, 10) or defaults.connect_timeout + + local store = dshm:new() + if store.set_timeouts then + local send_timeout = tonumber(config.send_timeout, 10) or defaults.send_timeout + local read_timeout = tonumber(config.read_timeout, 10) or defaults.read_timeout + + if connect_timeout then + if send_timeout and read_timeout then + store:set_timeouts(connect_timeout, send_timeout, read_timeout) + else + store:set_timeout(connect_timeout) + end + end + + elseif store.set_timeout and connect_timeout then + store:set_timeout(connect_timeout) + end + + + local self = { + store = store, + encoder = session.encoder, + region = config.region or defaults.region, + host = config.host or defaults.host, + port = tonumber(config.port, 10) or defaults.port, + pool_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout, + connect_opts = { + pool = pool.name or defaults.pool.name, + pool_size = tonumber(pool.size, 10) or defaults.pool.size, + backlog = tonumber(pool.backlog, 10) or defaults.pool.backlog, + }, + } + + return setmetatable(self, storage) +end + +function storage:connect() + return self.store:connect(self.host, self.port, self.connect_opts) +end + +function storage:set_keepalive() + return self.store:set_keepalive(self.pool_timeout) +end + +function storage:key(id) + return concat({ self.region, id }, "::") +end + +function storage:set(key, ttl, data) + local ok, err = self:connect() + if not ok then + return nil, err + end + + data, err = self.encoder.encode(data) + + if not data then + self:set_keepalive() + return nil, err + end + + ok, err = self.store:set(key, data, ttl) + + self:set_keepalive() + + return ok, err +end + +function storage:get(key) + local ok, err = self:connect() + if not ok then + return nil, err + end + + local data + data, err = self.store:get(key) + if data then + data, err = self.encoder.decode(data) + end + + self:set_keepalive() + + return data, err +end + +function storage:delete(key) + local ok, err = self:connect() + if not ok then + return nil, err + end + + ok, err = self.store:delete(key) + + self:set_keepalive() + + return ok, err +end + +function storage:touch(key, ttl) + local ok, err = self:connect() + if not ok then + return nil, err + end + + ok, err = self.store:touch(key, ttl) + + self:set_keepalive() + + return ok, err +end + +function storage:open(id) + local key = self:key(id) + return self:get(key) +end + +function storage:save(id, ttl, data) + local key = self:key(id) + return self:set(key, ttl, data) +end + +function storage:destroy(id) + local key = self:key(id) + return self:delete(key) +end + +function storage:ttl(id, ttl) + local key = self:key(id) + return self:touch(key, ttl) +end + +return storage diff --git a/server/resty/session/storage/memcache.lua b/server/resty/session/storage/memcache.lua new file mode 100644 index 0000000..da44ba7 --- /dev/null +++ b/server/resty/session/storage/memcache.lua @@ -0,0 +1,303 @@ +local memcached = require "resty.memcached" +local setmetatable = setmetatable +local tonumber = tonumber +local concat = table.concat +local sleep = ngx.sleep +local null = ngx.null +local var = ngx.var + +local function enabled(value) + if value == nil then + return nil + end + + return value == true + or value == "1" + or value == "true" + or value == "on" +end + +local function ifnil(value, default) + if value == nil then + return default + end + + return enabled(value) +end + +local defaults = { + prefix = var.session_memcache_prefix or "sessions", + socket = var.session_memcache_socket, + host = var.session_memcache_host or "127.0.0.1", + uselocking = enabled(var.session_memcache_uselocking or true), + connect_timeout = tonumber(var.session_memcache_connect_timeout, 10), + read_timeout = tonumber(var.session_memcache_read_timeout, 10), + send_timeout = tonumber(var.session_memcache_send_timeout, 10), + port = tonumber(var.session_memcache_port, 10) or 11211, + spinlockwait = tonumber(var.session_memcache_spinlockwait, 10) or 150, + maxlockwait = tonumber(var.session_memcache_maxlockwait, 10) or 30, + pool = { + name = var.session_memcache_pool_name, + timeout = tonumber(var.session_memcache_pool_timeout, 10), + size = tonumber(var.session_memcache_pool_size, 10), + backlog = tonumber(var.session_memcache_pool_backlog, 10), + }, +} + +local storage = {} + +storage.__index = storage + +function storage.new(session) + local config = session.memcache or defaults + local pool = config.pool or defaults.pool + local locking = ifnil(config.uselocking, defaults.uselocking) + + local connect_timeout = tonumber(config.connect_timeout, 10) or defaults.connect_timeout + + local memcache = memcached:new() + if memcache.set_timeouts then + local send_timeout = tonumber(config.send_timeout, 10) or defaults.send_timeout + local read_timeout = tonumber(config.read_timeout, 10) or defaults.read_timeout + + if connect_timeout then + if send_timeout and read_timeout then + memcache:set_timeouts(connect_timeout, send_timeout, read_timeout) + else + memcache:set_timeout(connect_timeout) + end + end + + elseif memcache.set_timeout and connect_timeout then + memcache:set_timeout(connect_timeout) + end + + local self = { + memcache = memcache, + prefix = config.prefix or defaults.prefix, + uselocking = locking, + spinlockwait = tonumber(config.spinlockwait, 10) or defaults.spinlockwait, + maxlockwait = tonumber(config.maxlockwait, 10) or defaults.maxlockwait, + pool_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout, + connect_opts = { + pool = pool.name or defaults.pool.name, + pool_size = tonumber(pool.size, 10) or defaults.pool.size, + backlog = tonumber(pool.backlog, 10) or defaults.pool.backlog, + }, + } + + local socket = config.socket or defaults.socket + if socket and socket ~= "" then + self.socket = socket + else + self.host = config.host or defaults.host + self.port = config.port or defaults.port + end + + return setmetatable(self, storage) +end + +function storage:connect() + local socket = self.socket + if socket then + return self.memcache:connect(socket, self.connect_opts) + end + return self.memcache:connect(self.host, self.port, self.connect_opts) +end + +function storage:set_keepalive() + return self.memcache:set_keepalive(self.pool_timeout) +end + +function storage:key(id) + return concat({ self.prefix, id }, ":" ) +end + +function storage:lock(key) + if not self.uselocking or self.locked then + return true + end + + if not self.token then + self.token = var.request_id + end + + local lock_key = concat({ key, "lock" }, "." ) + local lock_ttl = self.maxlockwait + 1 + local attempts = (1000 / self.spinlockwait) * self.maxlockwait + local waittime = self.spinlockwait / 1000 + + for _ = 1, attempts do + local ok = self.memcache:add(lock_key, self.token, lock_ttl) + if ok then + self.locked = true + return true + end + + sleep(waittime) + end + + return false, "unable to acquire a session lock" +end + +function storage:unlock(key) + if not self.uselocking or not self.locked then + return true + end + + local lock_key = concat({ key, "lock" }, "." ) + local token = self:get(lock_key) + + if token == self.token then + self.memcache:delete(lock_key) + self.locked = nil + end +end + +function storage:get(key) + local data, err = self.memcache:get(key) + if not data then + return nil, err + end + + if data == null then + return nil + end + + return data +end + +function storage:set(key, data, ttl) + return self.memcache:set(key, data, ttl) +end + +function storage:expire(key, ttl) + return self.memcache:touch(key, ttl) +end + +function storage:delete(key) + return self.memcache:delete(key) +end + +function storage:open(id, keep_lock) + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + ok, err = self:lock(key) + if not ok then + self:set_keepalive() + return nil, err + end + + local data + data, err = self:get(key) + + if err or not data or not keep_lock then + self:unlock(key) + end + + self:set_keepalive() + + return data, err +end + +function storage:start(id) + if not self.uselocking or not self.locked then + return true + end + + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + ok, err = self:lock(key) + + self:set_keepalive() + + return ok, err +end + +function storage:save(id, ttl, data, close) + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + ok, err = self:set(key, data, ttl) + + if close then + self:unlock(key) + end + + self:set_keepalive() + + if not ok then + return nil, err + end + + return true +end + +function storage:close(id) + if not self.uselocking or not self.locked then + return true + end + + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + self:unlock(key) + self:set_keepalive() + + return true +end + +function storage:destroy(id) + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + ok, err = self:delete(key) + + self:unlock(key) + self:set_keepalive() + + return ok, err +end + +function storage:ttl(id, ttl, close) + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + ok, err = self:expire(key, ttl) + + if close then + self:unlock(key) + end + + self:set_keepalive() + + return ok, err +end + +return storage diff --git a/server/resty/session/storage/memcached.lua b/server/resty/session/storage/memcached.lua new file mode 100644 index 0000000..0ecc508 --- /dev/null +++ b/server/resty/session/storage/memcached.lua @@ -0,0 +1 @@ +return require "resty.session.storage.memcache" diff --git a/server/resty/session/storage/redis.lua b/server/resty/session/storage/redis.lua new file mode 100644 index 0000000..3de0472 --- /dev/null +++ b/server/resty/session/storage/redis.lua @@ -0,0 +1,478 @@ +local setmetatable = setmetatable +local tonumber = tonumber +local type = type +local reverse = string.reverse +local gmatch = string.gmatch +local find = string.find +local byte = string.byte +local sub = string.sub +local concat = table.concat +local sleep = ngx.sleep +local null = ngx.null +local var = ngx.var + +local LB = byte("[") +local RB = byte("]") + +local function parse_cluster_nodes(nodes) + if not nodes or nodes == "" then + return nil + end + + if type(nodes) == "table" then + return nodes + end + + local addrs + local i + for node in gmatch(nodes, "%S+") do + local ip = node + local port = 6379 + local pos = find(reverse(ip), ":", 2, true) + if pos then + local p = tonumber(sub(ip, -pos + 1), 10) + if p >= 1 and p <= 65535 then + local addr = sub(ip, 1, -pos - 1) + if find(addr, ":", 1, true) then + if byte(addr, -1) == RB then + ip = addr + port = p + end + + else + ip = addr + port = p + end + end + end + + if byte(ip, 1, 1) == LB then + ip = sub(ip, 2) + end + + if byte(ip, -1) == RB then + ip = sub(ip, 1, -2) + end + + if not addrs then + i = 1 + addrs = {{ + ip = ip, + port = port, + }} + else + i = i + 1 + addrs[i] = { + ip = ip, + port = port, + } + end + end + + if not i then + return + end + + return addrs +end + +local redis_single = require "resty.redis" +local redis_cluster +do + local pcall = pcall + local require = require + local ok + ok, redis_cluster = pcall(require, "resty.rediscluster") + if not ok then + ok, redis_cluster = pcall(require, "rediscluster") + if not ok then + redis_cluster = nil + end + end +end + +local UNLOCK = [[ +if redis.call("GET", KEYS[1]) == ARGV[1] then + return redis.call("DEL", KEYS[1]) +else + return 0 +end +]] + +local function enabled(value) + if value == nil then return nil end + return value == true or (value == "1" or value == "true" or value == "on") +end + +local function ifnil(value, default) + if value == nil then + return default + end + + return enabled(value) +end + +local defaults = { + prefix = var.session_redis_prefix or "sessions", + socket = var.session_redis_socket, + host = var.session_redis_host or "127.0.0.1", + username = var.session_redis_username, + password = var.session_redis_password or var.session_redis_auth, + server_name = var.session_redis_server_name, + ssl = enabled(var.session_redis_ssl) or false, + ssl_verify = enabled(var.session_redis_ssl_verify) or false, + uselocking = enabled(var.session_redis_uselocking or true), + port = tonumber(var.session_redis_port, 10) or 6379, + database = tonumber(var.session_redis_database, 10) or 0, + connect_timeout = tonumber(var.session_redis_connect_timeout, 10), + read_timeout = tonumber(var.session_redis_read_timeout, 10), + send_timeout = tonumber(var.session_redis_send_timeout, 10), + spinlockwait = tonumber(var.session_redis_spinlockwait, 10) or 150, + maxlockwait = tonumber(var.session_redis_maxlockwait, 10) or 30, + pool = { + name = var.session_redis_pool_name, + timeout = tonumber(var.session_redis_pool_timeout, 10), + size = tonumber(var.session_redis_pool_size, 10), + backlog = tonumber(var.session_redis_pool_backlog, 10), + }, +} + + +if redis_cluster then + defaults.cluster = { + name = var.session_redis_cluster_name, + dict = var.session_redis_cluster_dict, + maxredirections = tonumber(var.session_redis_cluster_maxredirections, 10), + nodes = parse_cluster_nodes(var.session_redis_cluster_nodes), + } +end + +local storage = {} + +storage.__index = storage + +function storage.new(session) + local config = session.redis or defaults + local pool = config.pool or defaults.pool + local cluster = config.cluster or defaults.cluster + local locking = ifnil(config.uselocking, defaults.uselocking) + + local self = { + prefix = config.prefix or defaults.prefix, + uselocking = locking, + spinlockwait = tonumber(config.spinlockwait, 10) or defaults.spinlockwait, + maxlockwait = tonumber(config.maxlockwait, 10) or defaults.maxlockwait, + } + + local username = config.username or defaults.username + if username == "" then + username = nil + end + local password = config.password or config.auth or defaults.password + if password == "" then + password = nil + end + + local connect_timeout = tonumber(config.connect_timeout, 10) or defaults.connect_timeout + + local cluster_nodes + if redis_cluster then + cluster_nodes = parse_cluster_nodes(cluster.nodes or defaults.cluster.nodes) + end + + local connect_opts = { + pool = pool.name or defaults.pool.name, + pool_size = tonumber(pool.size, 10) or defaults.pool.size, + backlog = tonumber(pool.backlog, 10) or defaults.pool.backlog, + server_name = config.server_name or defaults.server_name, + ssl = ifnil(config.ssl, defaults.ssl), + ssl_verify = ifnil(config.ssl_verify, defaults.ssl_verify), + } + + if cluster_nodes then + self.redis = redis_cluster:new({ + name = cluster.name or defaults.cluster.name, + dict_name = cluster.dict or defaults.cluster.dict, + username = var.session_redis_username, + password = var.session_redis_password or defaults.password, + connection_timout = connect_timeout, -- typo in library + connection_timeout = connect_timeout, + keepalive_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout, + keepalive_cons = tonumber(pool.size, 10) or defaults.pool.size, + max_redirection = tonumber(cluster.maxredirections, 10) or defaults.cluster.maxredirections, + serv_list = cluster_nodes, + connect_opts = connect_opts, + }) + self.cluster = true + + else + local redis = redis_single:new() + + if redis.set_timeouts then + local send_timeout = tonumber(config.send_timeout, 10) or defaults.send_timeout + local read_timeout = tonumber(config.read_timeout, 10) or defaults.read_timeout + + if connect_timeout then + if send_timeout and read_timeout then + redis:set_timeouts(connect_timeout, send_timeout, read_timeout) + else + redis:set_timeout(connect_timeout) + end + end + + elseif redis.set_timeout and connect_timeout then + redis:set_timeout(connect_timeout) + end + + self.redis = redis + self.username = username + self.password = password + self.database = tonumber(config.database, 10) or defaults.database + self.pool_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout + self.connect_opts = connect_opts + + local socket = config.socket or defaults.socket + if socket and socket ~= "" then + self.socket = socket + else + self.host = config.host or defaults.host + self.port = config.port or defaults.port + end + end + + return setmetatable(self, storage) +end + +function storage:connect() + if self.cluster then + return true -- cluster handles this on its own + end + + local ok, err + if self.socket then + ok, err = self.redis:connect(self.socket, self.connect_opts) + else + ok, err = self.redis:connect(self.host, self.port, self.connect_opts) + end + + if not ok then + return nil, err + end + + if self.password and self.redis:get_reused_times() == 0 then + -- usernames are supported only on Redis 6+, so use new AUTH form only when absolutely necessary + if self.username then + ok, err = self.redis:auth(self.username, self.password) + else + ok, err = self.redis:auth(self.password) + end + if not ok then + self.redis:close() + return nil, err + end + end + + if self.database ~= 0 then + ok, err = self.redis:select(self.database) + if not ok then + self.redis:close() + end + end + + return ok, err +end + +function storage:set_keepalive() + if self.cluster then + return true -- cluster handles this on its own + end + + return self.redis:set_keepalive(self.pool_timeout) +end + +function storage:key(id) + return concat({ self.prefix, id }, ":" ) +end + +function storage:lock(key) + if not self.uselocking or self.locked then + return true + end + + if not self.token then + self.token = var.request_id + end + + local lock_key = concat({ key, "lock" }, "." ) + local lock_ttl = self.maxlockwait + 1 + local attempts = (1000 / self.spinlockwait) * self.maxlockwait + local waittime = self.spinlockwait / 1000 + + for _ = 1, attempts do + local ok = self.redis:set(lock_key, self.token, "EX", lock_ttl, "NX") + if ok ~= null then + self.locked = true + return true + end + + sleep(waittime) + end + + return false, "unable to acquire a session lock" +end + +function storage:unlock(key) + if not self.uselocking or not self.locked then + return + end + + local lock_key = concat({ key, "lock" }, "." ) + + self.redis:eval(UNLOCK, 1, lock_key, self.token) + self.locked = nil +end + +function storage:get(key) + local data, err = self.redis:get(key) + if not data then + return nil, err + end + + if data == null then + return nil + end + + return data +end + +function storage:set(key, data, lifetime) + return self.redis:setex(key, lifetime, data) +end + +function storage:expire(key, lifetime) + return self.redis:expire(key, lifetime) +end + +function storage:delete(key) + return self.redis:del(key) +end + +function storage:open(id, keep_lock) + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + ok, err = self:lock(key) + if not ok then + self:set_keepalive() + return nil, err + end + + local data + data, err = self:get(key) + + if err or not data or not keep_lock then + self:unlock(key) + end + self:set_keepalive() + + return data, err +end + +function storage:start(id) + if not self.uselocking or not self.locked then + return true + end + + local ok, err = self:connect() + if not ok then + return nil, err + end + + ok, err = self:lock(self:key(id)) + + self:set_keepalive() + + return ok, err +end + +function storage:save(id, ttl, data, close) + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + ok, err = self:set(key, data, ttl) + + if close then + self:unlock(key) + end + + self:set_keepalive() + + if not ok then + return nil, err + end + + return true +end + +function storage:close(id) + if not self.uselocking or not self.locked then + return true + end + + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + self:unlock(key) + self:set_keepalive() + + return true +end + +function storage:destroy(id) + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + ok, err = self:delete(key) + + self:unlock(key) + self:set_keepalive() + + return ok, err +end + +function storage:ttl(id, ttl, close) + local ok, err = self:connect() + if not ok then + return nil, err + end + + local key = self:key(id) + + ok, err = self:expire(key, ttl) + + if close then + self:unlock(key) + end + + self:set_keepalive() + + return ok, err +end + +return storage diff --git a/server/resty/session/storage/shm.lua b/server/resty/session/storage/shm.lua new file mode 100644 index 0000000..6f81435 --- /dev/null +++ b/server/resty/session/storage/shm.lua @@ -0,0 +1,125 @@ +local lock = require "resty.lock" + +local setmetatable = setmetatable +local tonumber = tonumber +local concat = table.concat +local var = ngx.var +local shared = ngx.shared + +local function enabled(value) + if value == nil then return nil end + return value == true or (value == "1" or value == "true" or value == "on") +end + +local function ifnil(value, default) + if value == nil then + return default + end + + return enabled(value) +end + +local defaults = { + store = var.session_shm_store or "sessions", + uselocking = enabled(var.session_shm_uselocking or true), + lock = { + exptime = tonumber(var.session_shm_lock_exptime, 10) or 30, + timeout = tonumber(var.session_shm_lock_timeout, 10) or 5, + step = tonumber(var.session_shm_lock_step, 10) or 0.001, + ratio = tonumber(var.session_shm_lock_ratio, 10) or 2, + max_step = tonumber(var.session_shm_lock_max_step, 10) or 0.5, + } +} + +local storage = {} + +storage.__index = storage + +function storage.new(session) + local config = session.shm or defaults + local store = config.store or defaults.store + local locking = ifnil(config.uselocking, defaults.uselocking) + + local self = { + store = shared[store], + uselocking = locking, + } + + if locking then + local lock_opts = config.lock or defaults.lock + local opts = { + exptime = tonumber(lock_opts.exptime, 10) or defaults.exptime, + timeout = tonumber(lock_opts.timeout, 10) or defaults.timeout, + step = tonumber(lock_opts.step, 10) or defaults.step, + ratio = tonumber(lock_opts.ratio, 10) or defaults.ratio, + max_step = tonumber(lock_opts.max_step, 10) or defaults.max_step, + } + self.lock = lock:new(store, opts) + end + + return setmetatable(self, storage) +end + +function storage:open(id, keep_lock) + if self.uselocking then + local ok, err = self.lock:lock(concat{ id, ".lock" }) + if not ok then + return nil, err + end + end + + local data, err = self.store:get(id) + + if self.uselocking and (err or not data or not keep_lock) then + self.lock:unlock() + end + + return data, err +end + +function storage:start(id) + if self.uselocking then + return self.lock:lock(concat{ id, ".lock" }) + end + + return true +end + +function storage:save(id, ttl, data, close) + local ok, err = self.store:set(id, data, ttl) + if close and self.uselocking then + self.lock:unlock() + end + + return ok, err +end + +function storage:close() + if self.uselocking then + self.lock:unlock() + end + + return true +end + +function storage:destroy(id) + self.store:delete(id) + + if self.uselocking then + self.lock:unlock() + end + + return true +end + +function storage:ttl(id, lifetime, close) + local ok, err = self.store:expire(id, lifetime) + + if close and self.uselocking then + self.lock:unlock() + end + + return ok, err +end + +return storage diff --git a/server/resty/session/strategies/default.lua b/server/resty/session/strategies/default.lua new file mode 100644 index 0000000..a43ef5a --- /dev/null +++ b/server/resty/session/strategies/default.lua @@ -0,0 +1,232 @@ +local type = type +local concat = table.concat + +local strategy = {} + +function strategy.load(session, cookie, key, keep_lock) + local storage = session.storage + local id = cookie.id + local id_encoded = session.encoder.encode(id) + + local data, err, tag + if storage.open then + data, err = storage:open(id_encoded, keep_lock) + if not data then + return nil, err or "cookie data was not found" + end + + else + data = cookie.data + end + + local expires = cookie.expires + local usebefore = cookie.usebefore + local hash = cookie.hash + + if not key then + key = concat{ id, expires, usebefore } + end + + local hkey = session.hmac(session.secret, key) + + data, err, tag = session.cipher:decrypt(data, hkey, id, session.key, hash) + if not data then + if storage.close then + storage:close(id_encoded) + end + + return nil, err or "unable to decrypt data" + end + + if tag then + if tag ~= hash then + if storage.close then + storage:close(id_encoded) + end + + return nil, "cookie has invalid tag" + end + + else + local input = concat{ key, data, session.key } + if session.hmac(hkey, input) ~= hash then + if storage.close then + storage:close(id_encoded) + end + + return nil, "cookie has invalid signature" + end + end + + data, err = session.compressor:decompress(data) + if not data then + if storage.close then + storage:close(id_encoded) + end + + return nil, err or "unable to decompress data" + end + + data, err = session.serializer.deserialize(data) + if not data then + if storage.close then + storage:close(id_encoded) + end + + return nil, err or "unable to deserialize data" + end + + session.id = id + session.expires = expires + session.usebefore = usebefore + session.data = type(data) == "table" and data or {} + session.present = true + + return true +end + +function strategy.open(session, cookie, keep_lock) + return strategy.load(session, cookie, nil, keep_lock) +end + +function strategy.start(session) + local storage = session.storage + if not storage.start then + return true + end + + local id_encoded = session.encoder.encode(session.id) + + local ok, err = storage:start(id_encoded) + if not ok then + return nil, err or "unable to start session" + end + + return true +end + +function strategy.modify(session, action, close, key) + local id = session.id + local id_encoded = session.encoder.encode(id) + local storage = session.storage + local expires = session.expires + local usebefore = session.usebefore + local ttl = expires - session.now + + if ttl <= 0 then + if storage.close then + storage:close(id_encoded) + end + + return nil, "session is already expired" + end + + if not key then + key = concat{ id, expires, usebefore } + end + + local data, err = session.serializer.serialize(session.data) + if not data then + if close and storage.close then + storage:close(id_encoded) + end + + return nil, err or "unable to serialize data" + end + + data, err = session.compressor:compress(data) + if not data then + if close and storage.close then + storage:close(id_encoded) + end + + return nil, err or "unable to compress data" + end + + local hkey = session.hmac(session.secret, key) + + local encrypted_data, tag + encrypted_data, err, tag = session.cipher:encrypt(data, hkey, id, session.key) + if not encrypted_data then + if close and storage.close then + storage:close(id_encoded) + end + + return nil, err + end + + local hash + if tag then + hash = tag + else + -- it would be better to calculate signature from encrypted_data, + -- but this is kept for backward compatibility + hash = session.hmac(hkey, concat{ key, data, session.key }) + end + + if action == "save" and storage.save then + local ok + ok, err = storage:save(id_encoded, ttl, encrypted_data, close) + if not ok then + return nil, err + end + elseif close and storage.close then + local ok + ok, err = storage:close(id_encoded) + if not ok then + return nil, err + end + end + + if usebefore then + expires = expires .. ":" .. usebefore + end + + hash = session.encoder.encode(hash) + + local cookie + if storage.save then + cookie = concat({ id_encoded, expires, hash }, "|") + else + local encoded_data = session.encoder.encode(encrypted_data) + cookie = concat({ id_encoded, expires, encoded_data, hash }, "|") + end + + return cookie +end + +function strategy.touch(session, close) + return strategy.modify(session, "touch", close) +end + +function strategy.save(session, close) + return strategy.modify(session, "save", close) +end + +function strategy.destroy(session) + local id = session.id + if id then + local storage = session.storage + if storage.destroy then + return storage:destroy(session.encoder.encode(id)) + elseif storage.close then + return storage:close(session.encoder.encode(id)) + end + end + + return true +end + +function strategy.close(session) + local id = session.id + if id then + local storage = session.storage + if storage.close then + return storage:close(session.encoder.encode(id)) + end + end + + return true +end + +return strategy diff --git a/server/resty/session/strategies/regenerate.lua b/server/resty/session/strategies/regenerate.lua new file mode 100644 index 0000000..f2a97dd --- /dev/null +++ b/server/resty/session/strategies/regenerate.lua @@ -0,0 +1,43 @@ +local default = require "resty.session.strategies.default" + +local concat = table.concat + +local strategy = { + regenerate = true, + start = default.start, + destroy = default.destroy, + close = default.close, +} + +local function key(source) + if source.usebefore then + return concat{ source.id, source.usebefore } + end + + return source.id +end + +function strategy.open(session, cookie, keep_lock) + return default.load(session, cookie, key(cookie), keep_lock) +end + +function strategy.touch(session, close) + return default.modify(session, "touch", close, key(session)) +end + +function strategy.save(session, close) + if session.present then + local storage = session.storage + if storage.ttl then + storage:ttl(session.encoder.encode(session.id), session.cookie.discard, true) + elseif storage.close then + storage:close(session.encoder.encode(session.id)) + end + + session.id = session:identifier() + end + + return default.modify(session, "save", close, key(session)) +end + +return strategy |