Module: BetterAuth::URLHelpers

Defined in:
lib/better_auth/url_helpers.rb

Class Method Summary collapse

Class Method Details

.dynamic_config?(config) ⇒ Boolean

Returns:

  • (Boolean)


173
174
175
# File 'lib/better_auth/url_helpers.rb', line 173

def dynamic_config?(config)
  config.is_a?(Hash) && (config.key?(:allowed_hosts) || config.key?("allowed_hosts") || config.key?(:allowedHosts) || config.key?("allowedHosts"))
end

.env_base_url(base_path) ⇒ Object



177
178
179
180
181
# File 'lib/better_auth/url_helpers.rb', line 177

def env_base_url(base_path)
  url = ENV["BETTER_AUTH_URL"] || ENV["NEXT_PUBLIC_BETTER_AUTH_URL"] || ENV["PUBLIC_BETTER_AUTH_URL"] || ENV["NUXT_PUBLIC_BETTER_AUTH_URL"] || ENV["NUXT_PUBLIC_AUTH_URL"]
  url ||= ENV["BASE_URL"] if ENV["BASE_URL"] && ENV["BASE_URL"] != "/"
  url ? with_path(url, base_path) : nil
end

.header_value(headers, key) ⇒ Object



161
162
163
164
165
166
167
# File 'lib/better_auth/url_helpers.rb', line 161

def header_value(headers, key)
  if headers.respond_to?(:get)
    headers.get(key)
  else
    headers[key] || headers[key.to_s] || headers[key.to_s.downcase] || headers[key.to_s.upcase] || headers[key.tr("-", "_").upcase]
  end
end

.headers_from_source(source) ⇒ Object



153
154
155
156
157
158
159
# File 'lib/better_auth/url_helpers.rb', line 153

def headers_from_source(source)
  return {} unless source
  return source.headers if source.respond_to?(:headers)
  return source if source.is_a?(Hash)

  {}
end

.host_from_source(source, trusted_proxy_headers: false) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/better_auth/url_helpers.rb', line 43

def host_from_source(source, trusted_proxy_headers: false)
  headers = headers_from_source(source)
  if trusted_proxy_headers
    forwarded_host = header_value(headers, "x-forwarded-host")
    return forwarded_host if forwarded_host && valid_proxy_header?(forwarded_host, :host)
  end

  host = header_value(headers, "host")
  return host if host && valid_proxy_header?(host, :host)

  uri_host(source_url(source))
end

.loopback_for_dev_scheme?(host) ⇒ Boolean

Returns:

  • (Boolean)


183
184
185
186
# File 'lib/better_auth/url_helpers.rb', line 183

def loopback_for_dev_scheme?(host)
  hostname = host.to_s.sub(/:\d+\z/, "").sub(/\A\[/, "").sub(/\]\z/, "").downcase
  hostname == "localhost" || hostname.end_with?(".localhost") || hostname == "::1" || hostname.start_with?("127.")
end

.matches_host_pattern?(host, pattern) ⇒ Boolean

Returns:

  • (Boolean)


32
33
34
35
36
37
38
39
40
41
# File 'lib/better_auth/url_helpers.rb', line 32

def matches_host_pattern?(host, pattern)
  return false if host.to_s.empty? || pattern.to_s.empty?

  normalized_host = normalize_host_pattern_value(host)
  normalized_pattern = normalize_host_pattern_value(pattern)
  regex = Regexp.escape(normalized_pattern)
    .gsub("\\*", ".*")
    .gsub("\\?", ".")
  !!normalized_host.match?(/\A#{regex}\z/i)
end

.normalize_host_pattern_value(value) ⇒ Object



149
150
151
# File 'lib/better_auth/url_helpers.rb', line 149

def normalize_host_pattern_value(value)
  value.to_s.sub(%r{\Ahttps?://}i, "").split("/").first.to_s.downcase
end

.origin(url) ⇒ Object



122
123
124
125
126
127
128
129
130
131
# File 'lib/better_auth/url_helpers.rb', line 122

def origin(url)
  parsed = URI.parse(url.to_s)
  return nil unless ["http", "https"].include?(parsed.scheme)

  port = parsed.port
  default_port = (parsed.scheme == "http" && port == 80) || (parsed.scheme == "https" && port == 443)
  default_port ? "#{parsed.scheme}://#{parsed.host}" : "#{parsed.scheme}://#{parsed.host}:#{port}"
rescue URI::InvalidURIError
  nil
end

.protocol_from_source(source, config_protocol: nil, trusted_proxy_headers: false) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/better_auth/url_helpers.rb', line 56

def protocol_from_source(source, config_protocol: nil, trusted_proxy_headers: false)
  return config_protocol if ["http", "https"].include?(config_protocol)

  headers = headers_from_source(source)
  if trusted_proxy_headers
    forwarded_proto = header_value(headers, "x-forwarded-proto")
    return forwarded_proto if forwarded_proto && valid_proxy_header?(forwarded_proto, :proto)
  end

  protocol = uri_scheme(source_url(source))
  return protocol if ["http", "https"].include?(protocol)

  host = host_from_source(source, trusted_proxy_headers: trusted_proxy_headers)
  return "http" if host && loopback_for_dev_scheme?(host)

  "https"
end

.resolve_base_url(config, base_path, source = nil, load_env: true, trusted_proxy_headers: false) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/better_auth/url_helpers.rb', line 74

def resolve_base_url(config, base_path, source = nil, load_env: true, trusted_proxy_headers: false)
  if dynamic_config?(config)
    return resolve_dynamic_base_url(config, source, base_path, trusted_proxy_headers: trusted_proxy_headers) if source
    return with_path(config[:fallback] || config["fallback"], base_path) if config[:fallback] || config["fallback"]

    return env_base_url(base_path) if load_env
    return nil
  end

  return with_path(config, base_path) if config.is_a?(String)
  return env_base_url(base_path) if load_env
  return with_path(origin(source_url(source)), base_path) if source

  nil
end

.resolve_dynamic_base_url(config, source, base_path, trusted_proxy_headers: false) ⇒ Object

Raises:



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/better_auth/url_helpers.rb', line 90

def resolve_dynamic_base_url(config, source, base_path, trusted_proxy_headers: false)
  host = host_from_source(source, trusted_proxy_headers: trusted_proxy_headers)
  fallback = config[:fallback] || config["fallback"]
  raise Error, "Could not determine host from request headers. Please provide a fallback URL in your baseURL config." unless host || fallback

  allowed_hosts = config[:allowed_hosts] || config["allowed_hosts"] || config[:allowedHosts] || config["allowedHosts"] || []
  if host && allowed_hosts.any? { |pattern| matches_host_pattern?(host, pattern) }
    protocol = protocol_from_source(source, config_protocol: config[:protocol] || config["protocol"], trusted_proxy_headers: trusted_proxy_headers)
    return with_path("#{protocol}://#{host}", base_path)
  end

  return with_path(fallback, base_path) if fallback

  raise Error, "Host \"#{host}\" is not in the allowed hosts list."
end

.source_url(source) ⇒ Object



169
170
171
# File 'lib/better_auth/url_helpers.rb', line 169

def source_url(source)
  source.url if source.respond_to?(:url)
end

.uri_host(url) ⇒ Object



133
134
135
136
137
138
139
140
141
# File 'lib/better_auth/url_helpers.rb', line 133

def uri_host(url)
  parsed = URI.parse(url.to_s)
  return nil unless parsed.host

  default_port = (parsed.scheme == "http" && parsed.port == 80) || (parsed.scheme == "https" && parsed.port == 443)
  default_port ? parsed.host : "#{parsed.host}:#{parsed.port}"
rescue URI::InvalidURIError
  nil
end

.uri_scheme(url) ⇒ Object



143
144
145
146
147
# File 'lib/better_auth/url_helpers.rb', line 143

def uri_scheme(url)
  URI.parse(url.to_s).scheme
rescue URI::InvalidURIError
  nil
end

.valid_port?(host) ⇒ Boolean

Returns:

  • (Boolean)


188
189
190
191
192
193
# File 'lib/better_auth/url_helpers.rb', line 188

def valid_port?(host)
  port = host[/:(\d{1,5})\z/, 1]
  return true unless port

  port.to_i.between?(1, 65_535)
end

.valid_proxy_header?(header, type) ⇒ Boolean

Returns:

  • (Boolean)


9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/better_auth/url_helpers.rb', line 9

def valid_proxy_header?(header, type)
  value = header.to_s
  return false if value.strip.empty?

  case type.to_sym
  when :proto
    ["http", "https"].include?(value)
  when :host
    return false if value.match?(/\.\.|\0|\s|\A[.]|[<>'"]|javascript:|file:|data:/i)
    return false if value.match?(%r{[/\\]})

    patterns = [
      /\A[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(:[0-9]{1,5})?\z/,
      /\A(\d{1,3}\.){3}\d{1,3}(:[0-9]{1,5})?\z/,
      /\A\[[0-9a-fA-F:]+\](:[0-9]{1,5})?\z/,
      /\Alocalhost(:[0-9]{1,5})?\z/i
    ]
    patterns.any? { |pattern| value.match?(pattern) } && valid_port?(value)
  else
    false
  end
end

.with_path(url, path = "/api/auth") ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/better_auth/url_helpers.rb', line 106

def with_path(url, path = "/api/auth")
  parsed = URI.parse(url.to_s)
  raise Error, "Invalid base URL: #{url}. URL must include 'http://' or 'https://'" unless ["http", "https"].include?(parsed.scheme)

  current_path = parsed.path.to_s.gsub(%r{/+\z}, "")
  return url.to_s if !current_path.empty? && current_path != "/"

  trimmed = url.to_s.gsub(%r{/+\z}, "")
  return trimmed if path.to_s.empty? || path == "/"

  suffix = path.start_with?("/") ? path : "/#{path}"
  "#{trimmed}#{suffix}"
rescue URI::InvalidURIError
  raise Error, "Invalid base URL: #{url}. Please provide a valid base URL."
end