Module: BetterAuth::RequestIP

Defined in:
lib/better_auth/request_ip.rb

Constant Summary collapse

LOCALHOST_IP =
"127.0.0.1"

Class Method Summary collapse

Class Method Details

.client_ip(source, options) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/better_auth/request_ip.rb', line 11

def client_ip(source, options)
  ip_options = options.advanced[:ip_address] || {}
  return nil if ip_options[:disable_ip_tracking]

  request = unwrap_request(source)
  configured_headers = ip_options[:ip_address_headers]
  if configured_headers
    Array(configured_headers).each do |header|
      value = header_value(source, header)
      next unless value.is_a?(String)

      ip = ip_from_header(
        value,
        trusted_proxies: ip_options[:trusted_proxies],
        peer_ip: peer_ip(request),
        ipv6_subnet: ip_options[:ipv6_subnet]
      )
      return ip if ip
    end
  end

  ip = fallback_ip(request, allow_forwarded_headers: configured_headers.nil?)
  return normalize_ip(ip, ipv6_subnet: ip_options[:ipv6_subnet]) if valid_ip?(ip)

  LOCALHOST_IP if test_or_development?
end

.fallback_ip(request, allow_forwarded_headers:) ⇒ Object



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

def fallback_ip(request, allow_forwarded_headers:)
  return nil unless request

  remote_ip = request.remote_ip.to_s if request.respond_to?(:remote_ip)
  return remote_ip if valid_ip?(remote_ip)

  direct_peer_ip = peer_ip(request)
  return nil unless valid_ip?(direct_peer_ip)

  if allow_forwarded_headers && request.respond_to?(:ip)
    ip = request.ip.to_s
    return ip if valid_ip?(ip)
  end

  direct_peer_ip
end

.header_value(request, header) ⇒ Object



65
66
67
68
69
70
71
# File 'lib/better_auth/request_ip.rb', line 65

def header_value(request, header)
  return request.get_header(rack_header_name(header)) if request.respond_to?(:get_header)
  return request.headers[header.to_s.downcase] if request.respond_to?(:headers)
  return request[header.to_s.downcase] || request[header.to_s] || request[header.to_sym] if request.is_a?(Hash)

  nil
end

.ip_from_header(value, trusted_proxies: nil, peer_ip: nil, ipv6_subnet: nil) ⇒ Object



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
# File 'lib/better_auth/request_ip.rb', line 38

def ip_from_header(value, trusted_proxies: nil, peer_ip: nil, ipv6_subnet: nil)
  forwarded_ips = value.split(",").map(&:strip).reject(&:empty?)
  return nil if forwarded_ips.empty?

  proxy_entries = Array(trusted_proxies).map(&:to_s).reject(&:empty?)
  if proxy_entries.any?
    proxies = proxy_entries.map { |entry| parse_trusted_proxy(entry) }
    return nil if proxies.any?(&:nil?)

    peer_address = parse_ip(peer_ip)
    return nil unless peer_address && proxies.any? { |proxy| proxy.include?(native_address(peer_address)) }

    forwarded_ips.reverse_each do |ip|
      address = parse_ip(ip)
      return nil unless address
      next if proxies.any? { |proxy| proxy.include?(native_address(address)) }

      return normalize_ip(ip, ipv6_subnet: ipv6_subnet)
    end
    return nil
  end

  return nil unless forwarded_ips.one? && valid_ip?(forwarded_ips.first)

  normalize_ip(forwarded_ips.first, ipv6_subnet: ipv6_subnet)
end

.native_address(address) ⇒ Object



158
159
160
161
162
163
164
# File 'lib/better_auth/request_ip.rb', line 158

def native_address(address)
  if address.respond_to?(:ipv4_mapped?) && address.ipv4_mapped?
    address.native
  else
    address
  end
end

.normalize_ip(ip, ipv6_subnet: nil) ⇒ Object



166
167
168
169
170
171
172
# File 'lib/better_auth/request_ip.rb', line 166

def normalize_ip(ip, ipv6_subnet: nil)
  address = IPAddr.new(ip)
  return address.native.to_s if address.respond_to?(:ipv4_mapped?) && address.ipv4_mapped?
  return address.to_s if address.ipv4?

  address.mask((ipv6_subnet || 64).to_i).to_s
end

.parse_ip(ip) ⇒ Object



132
133
134
135
136
# File 'lib/better_auth/request_ip.rb', line 132

def parse_ip(ip)
  IPAddr.new(ip)
rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
  nil
end

.parse_trusted_proxy(entry) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/better_auth/request_ip.rb', line 138

def parse_trusted_proxy(entry)
  value = entry.to_s
  return nil if value.empty? || value.match?(/\s/)

  if value.include?("/")
    address, prefix = value.split("/", -1)
    return nil unless value.count("/") == 1 && prefix.match?(/\A\d+\z/)

    parsed_address = parse_ip(address)
    return nil unless parsed_address

    max_prefix = parsed_address.ipv4? ? 32 : 128
    return nil if prefix.to_i > max_prefix
  end

  IPAddr.new(value)
rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
  nil
end

.peer_ip(request) ⇒ Object



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

def peer_ip(request)
  if request.respond_to?(:get_header)
    value = request.get_header("REMOTE_ADDR")
    return value.to_s unless value.nil?
  end
  if request.respond_to?(:env)
    value = request.env["REMOTE_ADDR"]
    return value.to_s unless value.nil?
  end
  if request.is_a?(Hash)
    value = request["REMOTE_ADDR"] || request[:REMOTE_ADDR] || request[:remote_addr]
    return value.to_s unless value.nil?
  end

  nil
end

.rack_header_name(header) ⇒ Object



122
123
124
# File 'lib/better_auth/request_ip.rb', line 122

def rack_header_name(header)
  "HTTP_#{header.to_s.upcase.tr("-", "_")}"
end

.test_or_development?Boolean

Returns:

  • (Boolean)


174
175
176
177
178
# File 'lib/better_auth/request_ip.rb', line 174

def test_or_development?
  ["test", "development"].include?(ENV["RACK_ENV"]) ||
    ["test", "development"].include?(ENV["RAILS_ENV"]) ||
    ["test", "development"].include?(ENV["APP_ENV"])
end

.unwrap_request(source) ⇒ Object



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

def unwrap_request(source)
  current = source
  seen = {}
  while current&.respond_to?(:request)
    break if seen[current.object_id]

    seen[current.object_id] = true
    nested = current.request
    break unless nested && !nested.equal?(current)

    current = nested
  end
  current
end

.valid_ip?(ip) ⇒ Boolean

Returns:

  • (Boolean)


126
127
128
129
130
# File 'lib/better_auth/request_ip.rb', line 126

def valid_ip?(ip)
  return false if ip.to_s.empty? || ip.to_s.match?(/\s/)

  !parse_ip(ip).nil?
end