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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3a8f1d39ba3c70f76f17c7bc87f68045864dfc4715307ad0dd6a84575165391
4
- data.tar.gz: 8b60f21b3ad7cb1a5a0b04343abb03cc3cd63eb2f7e23fe01f84e75843c1dd40
3
+ metadata.gz: 6426e235ce3c5433ca29549aa6435f791526c806f37714cdb42db9a9796072f9
4
+ data.tar.gz: 3fbe6777cb881a67e8a1fbcc65ecd1f3203470b4ecbe89ceec02e26817274487
5
5
  SHA512:
6
- metadata.gz: eceebf601875d541f521a1ae1793085f9ef5657b8f87d47c1669a29129e6b7593f18d4f3f247114c9c88dc8e74ccd96269e38ba94024bd56511aa85fb8db65ee
7
- data.tar.gz: b8af90fd2ea93bc08b9a1770e2b17302c05cdb541bbcbb6e43e5a05f699aee5131546b9b95b6580218efa66285c666ffb347627e4a39b938aa72eec4937fb1e1
6
+ metadata.gz: 1af6247e51829dfd829d35b28dfc0f04269d394733a44fdf2505e8629fc393128b6ed05e1429a09e3c0222d7bf23589602d89a73bd6e668d1a604dad30ff2412
7
+ data.tar.gz: fda705c023c9b0196626a957378f55ae9dfbc61cfa65145c1561dae5f70ff6625aa34f109900419aa3fb05843b6aad5af5c87043e4994aafea92d939a8428950
@@ -268,10 +268,25 @@ module Tina4
268
268
  end
269
269
 
270
270
  sid = sess.id
271
- cookie_val = (env["HTTP_COOKIE"] || "")[/tina4_session=([^;]+)/, 1]
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
- ttl = Integer(ENV.fetch("TINA4_SESSION_TTL", 3600))
274
- headers["set-cookie"] = "tina4_session=#{sid}; Path=/; HttpOnly; SameSite=Lax; Max-Age=#{ttl}"
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["HTTP_X_FORWARDED_PROTO"] || env["rack.url_scheme"] || "http"
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 — overrides cookie_name unless caller explicitly passed one.
20
- env_name = ENV["TINA4_SESSION_NAME"]
21
- if !options.key?(:cookie_name) && env_name && !env_name.empty?
22
- @options[:cookie_name] = env_name
23
- end
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 defaults to false; flip on with truthy env var.
188
- secure = %w[true 1 yes on].include?((ENV["TINA4_SESSION_SECURE"] || "false").to_s.strip.downcase)
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
- Tina4::SessionHandlers::DatabaseHandler.new(@options[:handler_options])
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
- client = Mongo::Client.new(
11
- options[:uri] || "mongodb://localhost:27017",
12
- database: options[:database] || "tina4_sessions"
13
- )
14
- @collection = client[options[:collection] || "sessions"]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.78"
4
+ VERSION = "3.13.79"
5
5
  end
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.78
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-17 00:00:00.000000000 Z
11
+ date: 2026-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack