tina4ruby 3.13.78 → 3.13.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/tina4/rack_app.rb +18 -3
- data/lib/tina4/request.rb +30 -1
- data/lib/tina4/session.rb +75 -11
- data/lib/tina4/session_handlers/mongo_handler.rb +11 -5
- data/lib/tina4/session_handlers/redis_handler.rb +8 -4
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6426e235ce3c5433ca29549aa6435f791526c806f37714cdb42db9a9796072f9
|
|
4
|
+
data.tar.gz: 3fbe6777cb881a67e8a1fbcc65ecd1f3203470b4ecbe89ceec02e26817274487
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1af6247e51829dfd829d35b28dfc0f04269d394733a44fdf2505e8629fc393128b6ed05e1429a09e3c0222d7bf23589602d89a73bd6e668d1a604dad30ff2412
|
|
7
|
+
data.tar.gz: fda705c023c9b0196626a957378f55ae9dfbc61cfa65145c1561dae5f70ff6625aa34f109900419aa3fb05843b6aad5af5c87043e4994aafea92d939a8428950
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -268,10 +268,25 @@ module Tina4
|
|
|
268
268
|
end
|
|
269
269
|
|
|
270
270
|
sid = sess.id
|
|
271
|
-
|
|
271
|
+
# Read the INCOMING session cookie by the SAME configured name the
|
|
272
|
+
# write side emits — via the one shared resolver (Session.cookie_name),
|
|
273
|
+
# exact `name=` prefix. A hardcoded "tina4_session=" here never matches
|
|
274
|
+
# a cookie renamed through TINA4_SESSION_NAME, so the auto-Set-Cookie
|
|
275
|
+
# would be needlessly re-emitted on every request that already carries
|
|
276
|
+
# the renamed session cookie. Parity with Python's
|
|
277
|
+
# core/server._init_session cookie_prefix.
|
|
278
|
+
cookie_prefix = "#{Tina4::Session.cookie_name}="
|
|
279
|
+
cookie_val = (env["HTTP_COOKIE"] || "").split(";").map(&:strip)
|
|
280
|
+
.find { |part| part.start_with?(cookie_prefix) }
|
|
281
|
+
&.slice(cookie_prefix.length..)
|
|
272
282
|
if sid && sid != cookie_val
|
|
273
|
-
|
|
274
|
-
|
|
283
|
+
# Route through Session#cookie_header rather than hand-writing the
|
|
284
|
+
# header, so TINA4_SESSION_SECURE / _SAMESITE / _HTTPONLY / _NAME /
|
|
285
|
+
# _TTL are all honoured and Secure reflects the request scheme. The
|
|
286
|
+
# old hand-written literal ignored every attribute except TTL and
|
|
287
|
+
# hardcoded SameSite=Lax + HttpOnly, making the security env vars
|
|
288
|
+
# silent no-ops (issue #31).
|
|
289
|
+
headers["set-cookie"] = sess.cookie_header
|
|
275
290
|
end
|
|
276
291
|
rack_response = [status, headers, body_parts]
|
|
277
292
|
end
|
data/lib/tina4/request.rb
CHANGED
|
@@ -152,11 +152,40 @@ module Tina4
|
|
|
152
152
|
@body_parsed = nil
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
# Is this request HTTPS from the CLIENT's point of view?
|
|
156
|
+
#
|
|
157
|
+
# TLS is normally terminated at a proxy (nginx, HAProxy, ALB, Cloudflare,
|
|
158
|
+
# most container deploys) that then forwards plain HTTP to the app, so
|
|
159
|
+
# rack.url_scheme is "http" on exactly the deployments that ARE encrypted.
|
|
160
|
+
# x-forwarded-proto carries the scheme the client actually used, and a
|
|
161
|
+
# chain of proxies appends each hop ("https, http") — the FIRST is the
|
|
162
|
+
# client-facing one. Falls back to the native rack scheme, then the CGI
|
|
163
|
+
# HTTPS var.
|
|
164
|
+
#
|
|
165
|
+
# This is the single source of truth for BOTH the URL scheme (#url) and the
|
|
166
|
+
# session-cookie Secure flag (Session#cookie_header). They used to decide it
|
|
167
|
+
# independently and could disagree — url() said https while the cookie
|
|
168
|
+
# concluded plain HTTP and dropped Secure (ruby#31, parity with PHP
|
|
169
|
+
# Request::isSecureScheme / tina4-php#175).
|
|
170
|
+
#
|
|
171
|
+
# @param env [Hash] the Rack environment.
|
|
172
|
+
# @return [Boolean] true when the client's scheme is https.
|
|
173
|
+
def self.secure_scheme?(env)
|
|
174
|
+
forwarded = (env["HTTP_X_FORWARDED_PROTO"] || "").to_s
|
|
175
|
+
unless forwarded.strip.empty?
|
|
176
|
+
return forwarded.split(",").first.to_s.strip.casecmp("https").zero?
|
|
177
|
+
end
|
|
178
|
+
scheme = (env["rack.url_scheme"] || "").to_s
|
|
179
|
+
return true if scheme.casecmp("https").zero?
|
|
180
|
+
https = (env["HTTPS"] || "").to_s
|
|
181
|
+
!https.empty? && https.casecmp("off") != 0
|
|
182
|
+
end
|
|
183
|
+
|
|
155
184
|
# Full absolute URL — scheme://host[:port]/path[?query].
|
|
156
185
|
# Honours X-Forwarded-Proto / X-Forwarded-Host so apps behind a proxy
|
|
157
186
|
# still see the URL the client used. Matches Python/PHP/Node parity.
|
|
158
187
|
def url
|
|
159
|
-
scheme = env
|
|
188
|
+
scheme = self.class.secure_scheme?(env) ? "https" : "http"
|
|
160
189
|
host = env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
|
|
161
190
|
port = env["SERVER_PORT"]
|
|
162
191
|
url_str = "#{scheme}://#{host}"
|
data/lib/tina4/session.rb
CHANGED
|
@@ -5,7 +5,6 @@ require "json"
|
|
|
5
5
|
module Tina4
|
|
6
6
|
class Session
|
|
7
7
|
DEFAULT_OPTIONS = {
|
|
8
|
-
cookie_name: "tina4_session",
|
|
9
8
|
secret: nil,
|
|
10
9
|
max_age: 3600,
|
|
11
10
|
handler: :file,
|
|
@@ -14,23 +13,59 @@ module Tina4
|
|
|
14
13
|
|
|
15
14
|
attr_reader :id, :data
|
|
16
15
|
|
|
16
|
+
# The session cookie name — the SINGLE source of truth shared by the WRITE
|
|
17
|
+
# side (#cookie_header) and the READ side (#extract_session_id AND RackApp's
|
|
18
|
+
# incoming-cookie parse), so a cookie written under a renamed name is read
|
|
19
|
+
# back on the next request. Keeping the default literal in ONE place means it
|
|
20
|
+
# can never drift between the emit and parse paths. Parity with Python's
|
|
21
|
+
# module-level session_cookie_name() (tina4_python/session/__init__.py).
|
|
22
|
+
#
|
|
23
|
+
# TINA4_SESSION_NAME Cookie name (default: tina4_session)
|
|
24
|
+
def self.cookie_name
|
|
25
|
+
name = ENV["TINA4_SESSION_NAME"]
|
|
26
|
+
name.nil? || name.empty? ? "tina4_session" : name
|
|
27
|
+
end
|
|
28
|
+
|
|
17
29
|
def initialize(env, options = {})
|
|
18
30
|
@options = DEFAULT_OPTIONS.merge(options)
|
|
19
|
-
# TINA4_SESSION_NAME —
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
# TINA4_SESSION_NAME — resolved by the ONE shared resolver (Session.cookie_name)
|
|
32
|
+
# that BOTH the write path (#cookie_header) and the read paths
|
|
33
|
+
# (#extract_session_id + RackApp incoming-cookie parse) go through, so a
|
|
34
|
+
# renamed cookie is emitted and read back under the same name. An explicit
|
|
35
|
+
# :cookie_name option still wins (same precedence as :handler below).
|
|
36
|
+
@options[:cookie_name] = self.class.cookie_name unless options.key?(:cookie_name)
|
|
24
37
|
# No guessable built-in secret. The session never signs with this value
|
|
25
38
|
# (IDs are SecureRandom.hex(32)), so we resolve it from TINA4_SECRET only
|
|
26
39
|
# — nil when unset. This honours the framework's blank-secret discipline
|
|
27
40
|
# (Auth.ensure_dev_secret never uses a guessable default); Python/Node
|
|
28
41
|
# sessions carry no secret field at all.
|
|
29
42
|
@options[:secret] ||= ENV["TINA4_SECRET"]
|
|
43
|
+
# TINA4_SESSION_TTL — cookie Max-Age (parity with Python's Session._ttl,
|
|
44
|
+
# which reads the same var). Only consulted when the caller did NOT pass
|
|
45
|
+
# an explicit :max_age (an explicit option always wins). Keeps the cookie
|
|
46
|
+
# honouring TINA4_SESSION_TTL now that rack_app routes the Set-Cookie
|
|
47
|
+
# through #cookie_header instead of hand-writing it (issue #31).
|
|
48
|
+
unless options.key?(:max_age)
|
|
49
|
+
ttl_env = ENV["TINA4_SESSION_TTL"]
|
|
50
|
+
@options[:max_age] = Integer(ttl_env) if ttl_env && !ttl_env.strip.empty?
|
|
51
|
+
end
|
|
52
|
+
# TINA4_SESSION_BACKEND — selects the storage handler unless the caller
|
|
53
|
+
# explicitly passed :handler (same precedence as :cookie_name above; an
|
|
54
|
+
# explicit option always wins over the environment). Without this the
|
|
55
|
+
# shipped redis/valkey/mongo/database handlers are unreachable by config.
|
|
56
|
+
# Parity with Python's Session._resolve_handler.
|
|
57
|
+
@options[:handler] = env_session_backend || @options[:handler] unless options.key?(:handler)
|
|
30
58
|
# Backend-failure policy strict flag (parity with Python's
|
|
31
59
|
# TINA4_SESSION_STRICT). When truthy, read/write/destroy/gc failures
|
|
32
60
|
# RE-RAISE instead of logging + degrading.
|
|
33
61
|
@strict = Tina4::Env.is_truthy(ENV["TINA4_SESSION_STRICT"])
|
|
62
|
+
# Whether THIS request reached us over https, decided PROXY-AWARE from the
|
|
63
|
+
# Rack env at construction (x-forwarded-proto first hop, else rack scheme /
|
|
64
|
+
# HTTPS). #cookie_header ORs this into the Secure flag so a cookie set on an
|
|
65
|
+
# https request is Secure even when TINA4_SESSION_SECURE is unset — a
|
|
66
|
+
# session cookie for an encrypted request must never be sent in the clear.
|
|
67
|
+
# Uses the SAME detector as Request#url so the two never disagree (issue #31).
|
|
68
|
+
@request_secure = Tina4::Request.secure_scheme?(env || {})
|
|
34
69
|
@handler = create_handler
|
|
35
70
|
@id = extract_session_id(env) || SecureRandom.hex(32)
|
|
36
71
|
@data = load_session
|
|
@@ -184,8 +219,14 @@ module Tina4
|
|
|
184
219
|
samesite = ENV["TINA4_SESSION_SAMESITE"] || "Lax"
|
|
185
220
|
# HttpOnly defaults to true (existing behaviour); flip off only when explicitly false.
|
|
186
221
|
httponly = !%w[false 0 no off].include?((ENV["TINA4_SESSION_HTTPONLY"] || "true").to_s.strip.downcase)
|
|
187
|
-
# Secure
|
|
188
|
-
|
|
222
|
+
# Secure is set when ANY of the unified-contract conditions hold (parity
|
|
223
|
+
# with tina4-php#175/#179): TINA4_SESSION_SECURE is truthy, OR SameSite is
|
|
224
|
+
# None (browsers reject a None cookie without Secure per RFC 6265bis), OR
|
|
225
|
+
# the request itself is https (proxy-aware, from @request_secure). A plain
|
|
226
|
+
# HTTP request with the flag unset and SameSite != None stays non-Secure.
|
|
227
|
+
secure = %w[true 1 yes on].include?((ENV["TINA4_SESSION_SECURE"] || "false").to_s.strip.downcase) ||
|
|
228
|
+
samesite.to_s.strip.casecmp("none").zero? ||
|
|
229
|
+
@request_secure == true
|
|
189
230
|
|
|
190
231
|
parts = ["#{name}=#{@id}", "Path=/"]
|
|
191
232
|
parts << "HttpOnly" if httponly
|
|
@@ -262,9 +303,24 @@ module Tina4
|
|
|
262
303
|
warn("Session #{operation} failed: #{error.message}")
|
|
263
304
|
end
|
|
264
305
|
|
|
306
|
+
# The configured TINA4_SESSION_BACKEND name, or nil when unset/blank (so the
|
|
307
|
+
# caller keeps the DEFAULT_OPTIONS handler). The value is normalised at
|
|
308
|
+
# dispatch in #create_handler, not here.
|
|
309
|
+
def env_session_backend
|
|
310
|
+
value = ENV["TINA4_SESSION_BACKEND"]
|
|
311
|
+
value && !value.strip.empty? ? value : nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Build the storage handler for the resolved backend name.
|
|
315
|
+
#
|
|
316
|
+
# The accepted names — and the aliases — mirror Python's
|
|
317
|
+
# Session._resolve_handler exactly: file|filesystem, redis, valkey,
|
|
318
|
+
# mongodb|mongo, database|db. The name is normalised (downcase + strip) so
|
|
319
|
+
# "Redis" / " mongodb " from a .env line resolve, and an UNKNOWN value falls
|
|
320
|
+
# back to the file handler SILENTLY (never raises) — same as Python.
|
|
265
321
|
def create_handler
|
|
266
|
-
case @options[:handler].to_sym
|
|
267
|
-
when :file
|
|
322
|
+
case @options[:handler].to_s.downcase.strip.to_sym
|
|
323
|
+
when :file, :filesystem
|
|
268
324
|
Tina4::SessionHandlers::FileHandler.new(@options[:handler_options])
|
|
269
325
|
when :redis
|
|
270
326
|
Tina4::SessionHandlers::RedisHandler.new(@options[:handler_options])
|
|
@@ -273,7 +329,15 @@ module Tina4
|
|
|
273
329
|
when :valkey
|
|
274
330
|
Tina4::SessionHandlers::ValkeyHandler.new(@options[:handler_options])
|
|
275
331
|
when :database, :db
|
|
276
|
-
|
|
332
|
+
# Parity with Python: the database backend "uses whatever DB is
|
|
333
|
+
# connected", so reuse the single ORM resolver (named binding → global
|
|
334
|
+
# Tina4.bind_database → TINA4_DATABASE_URL auto-discovery) rather than
|
|
335
|
+
# opening a second, divergent connection. An explicit
|
|
336
|
+
# handler_options[:db] still wins; a nil here leaves the handler's own
|
|
337
|
+
# env-derived default untouched.
|
|
338
|
+
Tina4::SessionHandlers::DatabaseHandler.new(
|
|
339
|
+
{ db: Tina4::ORM.db }.merge(@options[:handler_options] || {})
|
|
340
|
+
)
|
|
277
341
|
else
|
|
278
342
|
Tina4::SessionHandlers::FileHandler.new(@options[:handler_options])
|
|
279
343
|
end
|
|
@@ -4,14 +4,20 @@ require "json"
|
|
|
4
4
|
module Tina4
|
|
5
5
|
module SessionHandlers
|
|
6
6
|
class MongoHandler
|
|
7
|
+
# Connection is configured from TINA4_SESSION_MONGO_* env vars (parity with
|
|
8
|
+
# Python's MongoDBSessionHandler) so TINA4_SESSION_BACKEND=mongodb can be
|
|
9
|
+
# pointed at a server by env. TINA4_SESSION_MONGO_URI is canonical;
|
|
10
|
+
# TINA4_SESSION_MONGO_URL is a legacy alias. The database default is
|
|
11
|
+
# "tina4" (Python's default — Ruby previously drifted to "tina4_sessions").
|
|
12
|
+
# An explicit constructor option always wins over the environment.
|
|
7
13
|
def initialize(options = {})
|
|
8
14
|
require "mongo"
|
|
9
15
|
@ttl = options[:ttl] || 86400
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
)
|
|
14
|
-
@collection = client[
|
|
16
|
+
@uri = options[:uri] || ENV["TINA4_SESSION_MONGO_URI"] || ENV["TINA4_SESSION_MONGO_URL"] || "mongodb://localhost:27017"
|
|
17
|
+
@database = options[:database] || ENV["TINA4_SESSION_MONGO_DB"] || "tina4"
|
|
18
|
+
@collection_name = options[:collection] || ENV["TINA4_SESSION_MONGO_COLLECTION"] || "sessions"
|
|
19
|
+
client = Mongo::Client.new(@uri, database: @database)
|
|
20
|
+
@collection = client[@collection_name]
|
|
15
21
|
ensure_ttl_index
|
|
16
22
|
rescue LoadError
|
|
17
23
|
raise "MongoDB session handler requires the 'mongo' gem. Install with: gem install mongo"
|
|
@@ -9,13 +9,17 @@ module Tina4
|
|
|
9
9
|
# speaks raw RESP over a TCP socket via RespClient — zero dependencies, so a
|
|
10
10
|
# Tina4 app stores sessions in Redis with no extra gem.
|
|
11
11
|
class RedisHandler
|
|
12
|
+
# Connection is configured from TINA4_SESSION_REDIS_* env vars (parity with
|
|
13
|
+
# Python's RedisSessionHandler and the Ruby ValkeyHandler shape) so
|
|
14
|
+
# TINA4_SESSION_BACKEND=redis can actually be pointed at a server by env.
|
|
15
|
+
# An explicit constructor option always wins over the environment.
|
|
12
16
|
def initialize(options = {})
|
|
13
17
|
@prefix = options[:prefix] || "tina4:session:"
|
|
14
18
|
@ttl = options[:ttl] || 86400
|
|
15
|
-
@host = options[:host] || "localhost"
|
|
16
|
-
@port = options[:port] || 6379
|
|
17
|
-
@db = options[:db] || 0
|
|
18
|
-
@password = options[:password]
|
|
19
|
+
@host = options[:host] || ENV["TINA4_SESSION_REDIS_HOST"] || "localhost"
|
|
20
|
+
@port = options[:port] || (ENV["TINA4_SESSION_REDIS_PORT"] ? ENV["TINA4_SESSION_REDIS_PORT"].to_i : 6379)
|
|
21
|
+
@db = options[:db] || (ENV["TINA4_SESSION_REDIS_DB"] ? ENV["TINA4_SESSION_REDIS_DB"].to_i : 0)
|
|
22
|
+
@password = options[:password] || ENV["TINA4_SESSION_REDIS_PASSWORD"]
|
|
19
23
|
@redis = build_gem_client
|
|
20
24
|
@resp = @redis ? nil : RespClient.new(host: @host, port: @port, password: @password, db: @db)
|
|
21
25
|
end
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.79
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|