From 0f3c08adbc30ac13b860640a9a1db6b42c35eb17 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Thu, 18 Jun 2026 10:20:55 +0530 Subject: [PATCH 1/2] fix(request): normalize SERVER_NAME fallback Signed-off-by: Mridankan Mandal --- CHANGELOG.md | 1 + lib/rage/cookies.rb | 1 + lib/rage/internal.rb | 13 +++++++++++ lib/rage/request.rb | 10 +++++++- lib/rage/router/constrainer.rb | 2 +- spec/controller/api/cookies_spec.rb | 36 +++++++++++++++++++++++++++++ spec/rage/request_spec.rb | 32 +++++++++++++++++++++++++ spec/router/constraints_spec.rb | 17 ++++++++++++++ 8 files changed, 110 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f605c379..2c8e817c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - [API] Ignore `If-Modified-Since` when `If-None-Match` is present. - [API] Use weak comparison for `If-None-Match` validation. +- [Request] Normalize malformed `SERVER_NAME` fallback hosts that already include a port. - [Request] Treat IPv6 literals as non-domain hosts. - [Router] Fall back to `SERVER_NAME` when deriving exact host constraints. - [Cookies] Use request host fallback when resolving cookie domains. diff --git a/lib/rage/cookies.rb b/lib/rage/cookies.rb index 2a0e702a..0f9bd8d5 100644 --- a/lib/rage/cookies.rb +++ b/lib/rage/cookies.rb @@ -202,6 +202,7 @@ def []=(key, value) if (domain = value[:domain]) host = Rack::Request.new(@env).host + host ||= Rage::Internal.extract_host(@env["SERVER_NAME"]) unless @env["HTTP_HOST"] processed_domain = if domain.is_a?(String) domain diff --git a/lib/rage/internal.rb b/lib/rage/internal.rb index 721447c5..36c34610 100644 --- a/lib/rage/internal.rb +++ b/lib/rage/internal.rb @@ -44,6 +44,19 @@ def build_arguments(method, arguments) }.join(", ") end + # Extract the host from a host:port authority while leaving bare IPv6 literals unchanged. + # @param authority [String, nil] + # @return [String, nil] + def extract_host(authority) + if authority&.start_with?("[") + authority.sub(/\]:\d+\z/, "]") + elsif authority&.count(":") == 1 + authority.sub(/:\d+\z/, "") + else + authority + end + end + # Generate a stream name based on the provided object. # @param streamables [#id, String, Symbol, Numeric, Array] an object that will be used to generate the stream name # @return [String] the generated stream name diff --git a/lib/rage/request.rb b/lib/rage/request.rb index 6dafa1ac..b3239001 100644 --- a/lib/rage/request.rb +++ b/lib/rage/request.rb @@ -226,7 +226,15 @@ def route_uri_pattern private def rack_request - @rack_request ||= Rack::Request.new(@env) + @rack_request ||= begin + request_env = @env + + if !request_env["HTTP_HOST"] && (server_name = Rage::Internal.extract_host(request_env["SERVER_NAME"])) != request_env["SERVER_NAME"] + request_env = request_env.merge("SERVER_NAME" => server_name) + end + + Rack::Request.new(request_env) + end end def check_method(name) diff --git a/lib/rage/router/constrainer.rb b/lib/rage/router/constrainer.rb index 99998f05..13581ca3 100644 --- a/lib/rage/router/constrainer.rb +++ b/lib/rage/router/constrainer.rb @@ -69,7 +69,7 @@ def __build_derive_constraints # Optimization: inline the derivation for the common built in constraints if !strategy.custom? if key == :host - lines << " host: env['HTTP_HOST'.freeze]&.sub(/:\\d+\\z/, ''.freeze) || env['SERVER_NAME'.freeze]," + lines << " host: env['HTTP_HOST'.freeze]&.sub(/:\\d+\\z/, ''.freeze) || Rage::Internal.extract_host(env['SERVER_NAME'.freeze])," else raise ArgumentError, "unknown non-custom strategy for compiling constraint derivation function" end diff --git a/spec/controller/api/cookies_spec.rb b/spec/controller/api/cookies_spec.rb index 0facdf1a..de98e6e4 100644 --- a/spec/controller/api/cookies_spec.rb +++ b/spec/controller/api/cookies_spec.rb @@ -353,6 +353,24 @@ expect(response_cookies[:user_id]).to eq("120; domain=cookie.test.com") end end + + context "when request uses malformed SERVER_NAME fallback" do + let(:request_env) do + { + "SERVER_NAME" => "cookie.test.com:3000", + "SERVER_PORT" => "3000" + } + end + + it "correctly sets domain value" do + subject.cookies[:user_id] = { + domain: %w(api.test.com cookie.test.com), + value: 120 + } + + expect(response_cookies[:user_id]).to eq("120; domain=cookie.test.com") + end + end end context "with :all domain" do @@ -382,6 +400,24 @@ expect(response_cookies[:user_id]).to eq("120; domain=test.com") end end + + context "when request uses malformed SERVER_NAME fallback" do + let(:request_env) do + { + "SERVER_NAME" => "cookie.test.com:3000", + "SERVER_PORT" => "3000" + } + end + + it "correctly sets domain value" do + subject.cookies[:user_id] = { + domain: :all, + value: 120 + } + + expect(response_cookies[:user_id]).to eq("120; domain=test.com") + end + end end context "with permanent cookies" do diff --git a/spec/rage/request_spec.rb b/spec/rage/request_spec.rb index 9da06167..2f35300a 100644 --- a/spec/rage/request_spec.rb +++ b/spec/rage/request_spec.rb @@ -35,6 +35,18 @@ expect(request.url).to eq("http://localhost:3000/users?show_archived=true") end + context "when HTTP_HOST is missing and SERVER_NAME contains a port" do + before do + env.delete("HTTP_HOST") + env["SERVER_NAME"] = "api.foo.bar.com:3000" + env["SERVER_PORT"] = "3000" + end + + it "normalizes the URL" do + expect(request.url).to eq("http://api.foo.bar.com:3000/users?show_archived=true") + end + end + it "returns the path" do expect(request.path).to eq("/users") end @@ -137,6 +149,14 @@ it "falls back to SERVER_NAME" do expect(subject).to eq("fallback.example") end + + context "when SERVER_NAME contains a port" do + before { env["SERVER_NAME"] = "api.foo.bar.com:3000" } + + it "falls back to the normalized SERVER_NAME" do + expect(subject).to eq("api.foo.bar.com") + end + end end end @@ -179,6 +199,18 @@ expect(request.domain).to be_nil end end + + context "without HTTP_HOST and with SERVER_NAME containing a port" do + before do + env.delete("HTTP_HOST") + env["SERVER_NAME"] = "api.foo.bar.com:3000" + env["SERVER_PORT"] = "3000" + end + + it "returns the correct domain" do + expect(request.domain).to eq("bar.com") + end + end end describe "HTTP method handling" do diff --git a/spec/router/constraints_spec.rb b/spec/router/constraints_spec.rb index 8baad00e..f8aa5aa2 100644 --- a/spec/router/constraints_spec.rb +++ b/spec/router/constraints_spec.rb @@ -38,6 +38,23 @@ expect(handler[:handler].call(env, handler[:params])).to eq("get photos") end + it "correctly processes a constrained url when malformed SERVER_NAME contains a port" do + router.on("GET", "/photos", ->(_) { "get photos" }, constraints: { host: "google.com" }) + + env = { + "REQUEST_METHOD" => "GET", + "PATH_INFO" => "/photos", + "SERVER_NAME" => "google.com:3000", + "SERVER_PORT" => "3000", + "rack.input" => StringIO.new + } + + handler = router.lookup(env) + + expect(handler).not_to be_nil + expect(handler[:handler].call(env, handler[:params])).to eq("get photos") + end + it "correctly processes urls with multiple constraints" do router.on("GET", "/photos", ->(_) { "US photos" }, constraints: { host: "google.com" }) router.on("GET", "/photos", ->(_) { "CA photos" }, constraints: { host: "google.ca" }) From 633bc86fe790d12298abd0ca74906a7cdbe7b75c Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Wed, 24 Jun 2026 19:14:11 +0530 Subject: [PATCH 2/2] refactor(request): move host normalization Signed-off-by: Mridankan Mandal --- lib/rage/cookies.rb | 3 +-- lib/rage/internal.rb | 13 ------------- lib/rage/request.rb | 15 ++++++++++++++- lib/rage/router/constrainer.rb | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/rage/cookies.rb b/lib/rage/cookies.rb index 0f9bd8d5..e0dbd738 100644 --- a/lib/rage/cookies.rb +++ b/lib/rage/cookies.rb @@ -201,8 +201,7 @@ def []=(key, value) end if (domain = value[:domain]) - host = Rack::Request.new(@env).host - host ||= Rage::Internal.extract_host(@env["SERVER_NAME"]) unless @env["HTTP_HOST"] + host = Rage::Request.new(@env).host processed_domain = if domain.is_a?(String) domain diff --git a/lib/rage/internal.rb b/lib/rage/internal.rb index 36c34610..721447c5 100644 --- a/lib/rage/internal.rb +++ b/lib/rage/internal.rb @@ -44,19 +44,6 @@ def build_arguments(method, arguments) }.join(", ") end - # Extract the host from a host:port authority while leaving bare IPv6 literals unchanged. - # @param authority [String, nil] - # @return [String, nil] - def extract_host(authority) - if authority&.start_with?("[") - authority.sub(/\]:\d+\z/, "]") - elsif authority&.count(":") == 1 - authority.sub(/:\d+\z/, "") - else - authority - end - end - # Generate a stream name based on the provided object. # @param streamables [#id, String, Symbol, Numeric, Array] an object that will be used to generate the stream name # @return [String] the generated stream name diff --git a/lib/rage/request.rb b/lib/rage/request.rb index b3239001..1c29e18d 100644 --- a/lib/rage/request.rb +++ b/lib/rage/request.rb @@ -36,6 +36,19 @@ class Rage::Request # Set data structure of all RFC defined HTTP headers KNOWN_HTTP_METHODS = (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).to_set + # Extract the host from a host:port authority while leaving bare IPv6 literals unchanged. + # @param authority [String, nil] + # @return [String, nil] + def self.extract_host(authority) + if authority&.start_with?("[") + authority.sub(/\]:\d+\z/, "]") + elsif authority&.count(":") == 1 + authority.sub(/:\d+\z/, "") + else + authority + end + end + # @private # @param env [Hash] Rack env # @param controller [RageController::API] @@ -229,7 +242,7 @@ def rack_request @rack_request ||= begin request_env = @env - if !request_env["HTTP_HOST"] && (server_name = Rage::Internal.extract_host(request_env["SERVER_NAME"])) != request_env["SERVER_NAME"] + if !request_env["HTTP_HOST"] && (server_name = self.class.extract_host(request_env["SERVER_NAME"])) != request_env["SERVER_NAME"] request_env = request_env.merge("SERVER_NAME" => server_name) end diff --git a/lib/rage/router/constrainer.rb b/lib/rage/router/constrainer.rb index 13581ca3..3298f210 100644 --- a/lib/rage/router/constrainer.rb +++ b/lib/rage/router/constrainer.rb @@ -69,7 +69,7 @@ def __build_derive_constraints # Optimization: inline the derivation for the common built in constraints if !strategy.custom? if key == :host - lines << " host: env['HTTP_HOST'.freeze]&.sub(/:\\d+\\z/, ''.freeze) || Rage::Internal.extract_host(env['SERVER_NAME'.freeze])," + lines << " host: env['HTTP_HOST'.freeze]&.sub(/:\\d+\\z/, ''.freeze) || Rage::Request.extract_host(env['SERVER_NAME'.freeze])," else raise ArgumentError, "unknown non-custom strategy for compiling constraint derivation function" end