Module: BetterAuth::Host

Defined in:
lib/better_auth/host.rb

Constant Summary collapse

CLOUD_METADATA_HOSTS =
[
  "metadata.google.internal",
  "metadata.goog",
  "metadata",
  "instance-data",
  "instance-data.ec2.internal"
].freeze

Class Method Summary collapse

Class Method Details

.classify_host(host) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/better_auth/host.rb', line 17

def classify_host(host)
  canonical_input = normalize_input(host)
  lowered = canonical_input.downcase
  return {kind: :reserved, literal: :fqdn, canonical: ""} if lowered.empty?

  address = parse_ip(lowered)
  unless address
    return {kind: :localhost, literal: :fqdn, canonical: lowered} if lowered == "localhost" || lowered.end_with?(".localhost")
    return {kind: :cloud_metadata, literal: :fqdn, canonical: lowered} if CLOUD_METADATA_HOSTS.include?(lowered)

    return {kind: :public, literal: :fqdn, canonical: lowered}
  end

  native = address.respond_to?(:native) ? address.native : address
  if native.ipv4?
    canonical = native.to_s
    return {kind: classify_ipv4(canonical), literal: :ipv4, canonical: canonical}
  end

  canonical = expanded_ipv6(native)
  {kind: classify_ipv6(canonical), literal: :ipv6, canonical: canonical}
end

.classify_ipv4(ip) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/better_auth/host.rb', line 82

def classify_ipv4(ip)
  return :unspecified if ip == "0.0.0.0"
  return :broadcast if ip == "255.255.255.255"

  value = ipv4_to_i(ip)
  return :loopback if ipv4_range?(value, "127.0.0.0", 8)
  return :private if ipv4_range?(value, "10.0.0.0", 8)
  return :private if ipv4_range?(value, "172.16.0.0", 12)
  return :private if ipv4_range?(value, "192.168.0.0", 16)
  return :link_local if ipv4_range?(value, "169.254.0.0", 16)
  return :shared_address_space if ipv4_range?(value, "100.64.0.0", 10)
  return :documentation if ipv4_range?(value, "192.0.2.0", 24)
  return :documentation if ipv4_range?(value, "198.51.100.0", 24)
  return :documentation if ipv4_range?(value, "203.0.113.0", 24)
  return :benchmarking if ipv4_range?(value, "198.18.0.0", 15)
  return :multicast if ipv4_range?(value, "224.0.0.0", 4)
  return :reserved if ipv4_range?(value, "0.0.0.0", 8)
  return :reserved if ipv4_range?(value, "192.0.0.0", 24)
  return :reserved if ipv4_range?(value, "240.0.0.0", 4)

  :public
end

.classify_ipv6(expanded) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/better_auth/host.rb', line 114

def classify_ipv6(expanded)
  return :unspecified if expanded == "0000:0000:0000:0000:0000:0000:0000:0000"
  return :loopback if expanded == "0000:0000:0000:0000:0000:0000:0000:0001"

  first_byte = expanded[0, 2].to_i(16)
  second_byte = expanded[2, 2].to_i(16)

  return :multicast if first_byte == 0xff
  return :link_local if first_byte == 0xfe && (second_byte & 0xc0) == 0x80
  return :private if (first_byte & 0xfe) == 0xfc
  return :documentation if expanded.start_with?("2001:0db8:")

  if expanded.start_with?("2002:")
    embedded = embedded_ipv4(expanded, 1)
    return (classify_ipv4(embedded) == :public) ? :public : :reserved if embedded
  end

  if expanded.start_with?("0064:ff9b:0000:0000:0000:0000:")
    embedded = embedded_ipv4(expanded, 6)
    return :reserved if embedded
  end

  if expanded.start_with?("2001:0000:")
    embedded = embedded_ipv4(expanded, 6, xor: true)
    return :reserved if embedded
  end

  return :reserved if expanded.start_with?("0100:0000:0000:0000:")

  :public
end

.embedded_ipv4(expanded, start_group, xor: false) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/better_auth/host.rb', line 146

def embedded_ipv4(expanded, start_group, xor: false)
  groups = expanded.split(":")
  combined = (groups.fetch(start_group).to_i(16) << 16) | groups.fetch(start_group + 1).to_i(16)
  combined ^= 0xffffffff if xor
  [
    (combined >> 24) & 0xff,
    (combined >> 16) & 0xff,
    (combined >> 8) & 0xff,
    combined & 0xff
  ].join(".")
rescue IndexError
  nil
end

.expanded_ipv6(address) ⇒ Object



160
161
162
163
164
# File 'lib/better_auth/host.rb', line 160

def expanded_ipv6(address)
  address.hton.bytes.each_slice(2).map do |high, low|
    ((high << 8) + low).to_s(16).rjust(4, "0")
  end.join(":")
end

.ipv4_range?(value, prefix, length) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
112
# File 'lib/better_auth/host.rb', line 109

def ipv4_range?(value, prefix, length)
  mask = (length == 32) ? 0xffffffff : ((0xffffffff << (32 - length)) & 0xffffffff)
  (value & mask) == (ipv4_to_i(prefix) & mask)
end

.ipv4_to_i(ip) ⇒ Object



105
106
107
# File 'lib/better_auth/host.rb', line 105

def ipv4_to_i(ip)
  ip.split(".").map(&:to_i).reduce(0) { |sum, part| (sum << 8) + part }
end

.loopback_host?(host) ⇒ Boolean

Returns:

  • (Boolean)


44
45
46
# File 'lib/better_auth/host.rb', line 44

def loopback_host?(host)
  [:loopback, :localhost].include?(classify_host(host)[:kind])
end

.loopback_ip?(host) ⇒ Boolean

Returns:

  • (Boolean)


40
41
42
# File 'lib/better_auth/host.rb', line 40

def loopback_ip?(host)
  classify_host(host)[:kind] == :loopback
end

.normalize_input(host) ⇒ Object



52
53
54
55
56
57
58
# File 'lib/better_auth/host.rb', line 52

def normalize_input(host)
  value = host.to_s.strip
  value = strip_port(value)
  value = value[1...-1] if value.start_with?("[") && value.end_with?("]")
  value = value.split("%", 2).first || ""
  value.gsub(/\.+\z/, "")
end

.parse_ip(host) ⇒ Object



76
77
78
79
80
# File 'lib/better_auth/host.rb', line 76

def parse_ip(host)
  IPAddr.new(host)
rescue ArgumentError
  nil
end

.public_routable_host?(host) ⇒ Boolean

Returns:

  • (Boolean)


48
49
50
# File 'lib/better_auth/host.rb', line 48

def public_routable_host?(host)
  classify_host(host)[:kind] == :public
end

.strip_port(host) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/better_auth/host.rb', line 60

def strip_port(host)
  if host.start_with?("[")
    closing = host.index("]")
    return host unless closing

    return host[0..closing] if host[(closing + 1)..]&.match?(/\A:\d+\z/)
    return host
  end

  first_colon = host.index(":")
  return host unless first_colon
  return host if host.index(":", first_colon + 1)

  host[0...first_colon]
end