Class: Tina4::Session
- Inherits:
-
Object
- Object
- Tina4::Session
- Defined in:
- lib/tina4/session.rb
Constant Summary collapse
- DEFAULT_OPTIONS =
{ secret: nil, max_age: 3600, handler: :file, handler_options: {} }.freeze
Instance Attribute Summary collapse
-
#data ⇒ Object
readonly
Returns the value of attribute data.
-
#id ⇒ Object
readonly
Returns the value of attribute id.
Class Method Summary collapse
-
.cookie_name ⇒ Object
The session cookie name — the SINGLE source of truth shared by the WRITE side (#cookie_header) and the READ side (#extract_session_id AND RackApp's incoming-cookie parse), so a cookie written under a renamed name is read back on the next request.
Instance Method Summary collapse
- #[](key) ⇒ Object
- #[]=(key, value) ⇒ Object
-
#all ⇒ Object
Return all session data.
- #clear ⇒ Object
- #cookie_header(cookie_name = nil) ⇒ Object
- #delete(key) ⇒ Object
-
#destroy ⇒ Object
Destroy the current session.
-
#flash(key, value = nil) ⇒ Object
Flash data: set a value that is removed after next read.
-
#gc(max_lifetime = nil) ⇒ Object
Garbage collection: remove expired sessions from the handler.
-
#get(key, default = nil) ⇒ Object
Get a session value with optional default.
-
#get_flash(key, default = nil) ⇒ Object
Get flash data by key (alias for flash(key) without value).
-
#get_session_id ⇒ Object
Returns the current session ID string.
-
#has?(key) ⇒ Boolean
Check if a key exists in the session.
-
#initialize(env, options = {}) ⇒ Session
constructor
A new instance of Session.
-
#read(session_id) ⇒ Object
Reads raw session data for a given session ID from backend storage.
-
#regenerate ⇒ Object
Regenerate the session ID while preserving data — returns the new ID.
-
#save ⇒ Object
Persist the session if dirty.
-
#set(key, value) ⇒ Object
Set a session value.
-
#start(session_id = nil) ⇒ Object
Start or resume a session.
- #to_hash ⇒ Object
-
#write(session_id, data, ttl = nil) ⇒ Object
Writes raw session data for a given session ID to backend storage.
Constructor Details
#initialize(env, options = {}) ⇒ Session
Returns a new instance of Session.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/tina4/session.rb', line 29 def initialize(env, = {}) @options = DEFAULT_OPTIONS.merge() # TINA4_SESSION_NAME — resolved by the ONE shared resolver (Session.cookie_name) # that BOTH the write path (#cookie_header) and the read paths # (#extract_session_id + RackApp incoming-cookie parse) go through, so a # renamed cookie is emitted and read back under the same name. An explicit # :cookie_name option still wins (same precedence as :handler below). @options[:cookie_name] = self.class. unless .key?(:cookie_name) # No guessable built-in secret. The session never signs with this value # (IDs are SecureRandom.hex(32)), so we resolve it from TINA4_SECRET only # — nil when unset. This honours the framework's blank-secret discipline # (Auth.ensure_dev_secret never uses a guessable default); Python/Node # sessions carry no secret field at all. @options[:secret] ||= ENV["TINA4_SECRET"] # TINA4_SESSION_TTL — cookie Max-Age (parity with Python's Session._ttl, # which reads the same var). Only consulted when the caller did NOT pass # an explicit :max_age (an explicit option always wins). Keeps the cookie # honouring TINA4_SESSION_TTL now that rack_app routes the Set-Cookie # through #cookie_header instead of hand-writing it (issue #31). unless .key?(:max_age) ttl_env = ENV["TINA4_SESSION_TTL"] @options[:max_age] = Integer(ttl_env) if ttl_env && !ttl_env.strip.empty? end # TINA4_SESSION_BACKEND — selects the storage handler unless the caller # explicitly passed :handler (same precedence as :cookie_name above; an # explicit option always wins over the environment). Without this the # shipped redis/valkey/mongo/database handlers are unreachable by config. # Parity with Python's Session._resolve_handler. @options[:handler] = env_session_backend || @options[:handler] unless .key?(:handler) # Backend-failure policy strict flag (parity with Python's # TINA4_SESSION_STRICT). When truthy, read/write/destroy/gc failures # RE-RAISE instead of logging + degrading. @strict = Tina4::Env.is_truthy(ENV["TINA4_SESSION_STRICT"]) # Whether THIS request reached us over https, decided PROXY-AWARE from the # Rack env at construction (x-forwarded-proto first hop, else rack scheme / # HTTPS). #cookie_header ORs this into the Secure flag so a cookie set on an # https request is Secure even when TINA4_SESSION_SECURE is unset — a # session cookie for an encrypted request must never be sent in the clear. # Uses the SAME detector as Request#url so the two never disagree (issue #31). @request_secure = Tina4::Request.secure_scheme?(env || {}) @handler = create_handler @id = extract_session_id(env) || SecureRandom.hex(32) @data = load_session @modified = false end |
Instance Attribute Details
#data ⇒ Object (readonly)
Returns the value of attribute data.
14 15 16 |
# File 'lib/tina4/session.rb', line 14 def data @data end |
#id ⇒ Object (readonly)
Returns the value of attribute id.
14 15 16 |
# File 'lib/tina4/session.rb', line 14 def id @id end |
Class Method Details
.cookie_name ⇒ Object
The session cookie name — the SINGLE source of truth shared by the WRITE side (#cookie_header) and the READ side (#extract_session_id AND RackApp's incoming-cookie parse), so a cookie written under a renamed name is read back on the next request. Keeping the default literal in ONE place means it can never drift between the emit and parse paths. Parity with Python's module-level session_cookie_name() (tina4_python/session/init.py).
TINA4_SESSION_NAME Cookie name (default: tina4_session)
24 25 26 27 |
# File 'lib/tina4/session.rb', line 24 def self. name = ENV["TINA4_SESSION_NAME"] name.nil? || name.empty? ? "tina4_session" : name end |
Instance Method Details
#[](key) ⇒ Object
75 76 77 |
# File 'lib/tina4/session.rb', line 75 def [](key) @data[key.to_s] end |
#[]=(key, value) ⇒ Object
79 80 81 82 |
# File 'lib/tina4/session.rb', line 79 def []=(key, value) @data[key.to_s] = value @modified = true end |
#all ⇒ Object
Return all session data
135 136 137 |
# File 'lib/tina4/session.rb', line 135 def all @data.dup end |
#clear ⇒ Object
89 90 91 92 |
# File 'lib/tina4/session.rb', line 89 def clear @data = {} @modified = true end |
#cookie_header(cookie_name = nil) ⇒ Object
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/tina4/session.rb', line 217 def ( = nil) name = || @options[:cookie_name] samesite = ENV["TINA4_SESSION_SAMESITE"] || "Lax" # HttpOnly defaults to true (existing behaviour); flip off only when explicitly false. httponly = !%w[false 0 no off].include?((ENV["TINA4_SESSION_HTTPONLY"] || "true").to_s.strip.downcase) # Secure is set when ANY of the unified-contract conditions hold (parity # with tina4-php#175/#179): TINA4_SESSION_SECURE is truthy, OR SameSite is # None (browsers reject a None cookie without Secure per RFC 6265bis), OR # the request itself is https (proxy-aware, from @request_secure). A plain # HTTP request with the flag unset and SameSite != None stays non-Secure. secure = %w[true 1 yes on].include?((ENV["TINA4_SESSION_SECURE"] || "false").to_s.strip.downcase) || samesite.to_s.strip.casecmp("none").zero? || @request_secure == true parts = ["#{name}=#{@id}", "Path=/"] parts << "HttpOnly" if httponly parts << "Secure" if secure parts << "SameSite=#{samesite}" parts << "Max-Age=#{@options[:max_age]}" parts.join("; ") end |
#delete(key) ⇒ Object
84 85 86 87 |
# File 'lib/tina4/session.rb', line 84 def delete(key) @data.delete(key.to_s) @modified = true end |
#destroy ⇒ Object
Destroy the current session. Should be called right after login or any privilege change to defend against session fixation (see #regenerate).
113 114 115 116 |
# File 'lib/tina4/session.rb', line 113 def destroy safe_destroy(@id) @data = {} end |
#flash(key, value = nil) ⇒ Object
Flash data: set a value that is removed after next read. Call with value to set, call without value to get (and remove).
141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/tina4/session.rb', line 141 def flash(key, value = nil) flash_key = "_flash_#{key}" if value.nil? val = @data.delete(flash_key.to_s) @modified = true if val val else @data[flash_key.to_s] = value @modified = true value end end |
#gc(max_lifetime = nil) ⇒ Object
Garbage collection: remove expired sessions from the handler. A backend failure is logged and swallowed (never crashes the request).
207 208 209 210 211 212 213 214 215 |
# File 'lib/tina4/session.rb', line 207 def gc(max_lifetime = nil) return unless @handler.respond_to?(:gc) max_lifetime ||= @options[:max_age] @handler.gc(max_lifetime) rescue StandardError => e log_backend_error("gc", e) raise if @strict nil end |
#get(key, default = nil) ⇒ Object
Get a session value with optional default
119 120 121 |
# File 'lib/tina4/session.rb', line 119 def get(key, default = nil) @data[key.to_s] || default end |
#get_flash(key, default = nil) ⇒ Object
Get flash data by key (alias for flash(key) without value)
155 156 157 158 |
# File 'lib/tina4/session.rb', line 155 def get_flash(key, default = nil) result = flash(key) result.nil? ? default : result end |
#get_session_id ⇒ Object
Returns the current session ID string.
189 190 191 |
# File 'lib/tina4/session.rb', line 189 def get_session_id @id end |
#has?(key) ⇒ Boolean
Check if a key exists in the session
130 131 132 |
# File 'lib/tina4/session.rb', line 130 def has?(key) @data.key?(key.to_s) end |
#read(session_id) ⇒ Object
Reads raw session data for a given session ID from backend storage. Returns the data hash, or {} on a backend failure (logged + degraded).
195 196 197 |
# File 'lib/tina4/session.rb', line 195 def read(session_id) safe_read(session_id) end |
#regenerate ⇒ Object
Regenerate the session ID while preserving data — returns the new ID. Call this right after login or any privilege change to defend against session fixation (a pre-auth session ID must not survive into the authenticated session). Destroys the old backend record (best-effort) and persists under the new ID.
165 166 167 168 169 170 171 172 |
# File 'lib/tina4/session.rb', line 165 def regenerate old_id = @id @id = SecureRandom.hex(32) safe_destroy(old_id) @modified = true save @id end |
#save ⇒ Object
Persist the session if dirty. On a backend write failure the error is logged and false is returned — the @modified (dirty) flag is RETAINED so a later save can retry. Returns true on a successful (or no-op) write.
101 102 103 104 105 106 107 108 109 |
# File 'lib/tina4/session.rb', line 101 def save return true unless @modified if safe_write(@id, @data) @modified = false true else false # dirty flag retained for retry end end |
#set(key, value) ⇒ Object
Set a session value
124 125 126 127 |
# File 'lib/tina4/session.rb', line 124 def set(key, value) @data[key.to_s] = value @modified = true end |
#start(session_id = nil) ⇒ Object
Start or resume a session. If session_id is given, load that session; otherwise generate a new ID. Returns the session ID string.
176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/tina4/session.rb', line 176 def start(session_id = nil) if session_id @id = session_id @data = load_session else @id = SecureRandom.hex(32) @data = {} end @modified = false @id end |
#to_hash ⇒ Object
94 95 96 |
# File 'lib/tina4/session.rb', line 94 def to_hash @data.dup end |
#write(session_id, data, ttl = nil) ⇒ Object
Writes raw session data for a given session ID to backend storage. Returns true on success, false on a backend failure (logged + degraded).
201 202 203 |
# File 'lib/tina4/session.rb', line 201 def write(session_id, data, ttl = nil) safe_write(session_id, data, ttl) end |