diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb index e786bb535..46b1682ee 100644 --- a/app/logical/concerns/searchable.rb +++ b/app/logical/concerns/searchable.rb @@ -79,10 +79,10 @@ module Searchable def where_inet_matches(attr, value) if value.match?(/[, ]/) - ips = value.split(/[, ]+/).map { |ip| IPAddress.parse(ip).to_string } + ips = value.split(/[, ]+/).map { |ip| Danbooru::IpAddress.new(ip).to_string } where("#{qualified_column_for(attr)} = ANY(ARRAY[?]::inet[])", ips) else - ip = IPAddress.parse(value) + ip = Danbooru::IpAddress.new(value) where("#{qualified_column_for(attr)} <<= ?", ip.to_string) end end diff --git a/app/logical/danbooru/ip_address.rb b/app/logical/danbooru/ip_address.rb index 5c9a575da..c1d25df37 100644 --- a/app/logical/danbooru/ip_address.rb +++ b/app/logical/danbooru/ip_address.rb @@ -5,10 +5,23 @@ module Danbooru class IpAddress attr_reader :ip_address - delegate_missing_to :ip_address + delegate :ipv4?, :ipv6?, :loopback?, :link_local?, :unique_local?, :private?, :to_string, :prefix, :multicast?, :unspecified?, to: :ip_address + delegate :ip_info, :is_proxy?, to: :ip_lookup def initialize(string) - @ip_address = ::IPAddress.parse(string) + @ip_address = ::IPAddress.parse(string.to_s) + end + + def ip_lookup + @ip_lookup ||= IpLookup.new(self) + end + + def is_local? + if ipv4? + loopback? || link_local? || multicast? || private? + elsif ipv6? + loopback? || link_local? || unique_local? || unspecified? + end end # "1.2.3.4/24" if the address is a subnet, "1.2.3.4" otherwise. @@ -19,5 +32,17 @@ module Danbooru def inspect "#" end + + def ==(other) + self.class == other.class && to_s == other.to_s + end + + # This is needed to be able to correctly treat IpAddresses as hash keys, + # which Rails does internally when preloading associations. + def hash + to_s.hash + end + + alias_method :eql?, :== end end diff --git a/app/logical/ip_lookup.rb b/app/logical/ip_lookup.rb index f8dae9747..8c1eb7430 100644 --- a/app/logical/ip_lookup.rb +++ b/app/logical/ip_lookup.rb @@ -10,12 +10,13 @@ class IpLookup end def initialize(ip_addr, api_key: Danbooru.config.ip_registry_api_key, cache_duration: 3.days) - @ip_addr = IPAddress.parse(ip_addr.to_s) + @ip_addr = Danbooru::IpAddress.new(ip_addr) @api_key = api_key @cache_duration = cache_duration end def ip_info + return {} if ip_addr.is_local? return {} if response.blank? { @@ -43,14 +44,6 @@ class IpLookup json end - def is_local? - if ip_addr.ipv4? - ip_addr.loopback? || ip_addr.link_local? || ip_addr.private? - elsif ip_addr.ipv6? - ip_addr.loopback? || ip_addr.link_local? || ip_addr.unique_local? - end - end - def is_proxy? response[:security].present? && response[:security].values.any? end diff --git a/app/logical/user_verifier.rb b/app/logical/user_verifier.rb index fdc7965e0..27e655785 100644 --- a/app/logical/user_verifier.rb +++ b/app/logical/user_verifier.rb @@ -12,7 +12,7 @@ class UserVerifier def requires_verification? return false if !Danbooru.config.new_user_verification? - return false if is_local_ip? + return false if ip_address.is_local? # we check for IP bans first to make sure we bump the IP ban hit count is_ip_banned? || is_logged_in? || is_recent_signup? || is_proxy? @@ -33,11 +33,7 @@ class UserVerifier private def ip_address - @ip_address ||= IPAddress.parse(request.remote_ip) - end - - def is_local_ip? - IpLookup.new(ip_address).is_local? + @ip_address ||= Danbooru::IpAddress.new(request.remote_ip) end def is_logged_in? @@ -56,7 +52,7 @@ class UserVerifier end def is_proxy? - IpLookup.new(ip_address).is_proxy? + ip_address.is_proxy? end def to_h diff --git a/app/logical/validating_socket.rb b/app/logical/validating_socket.rb index 446609073..9e3dc803c 100644 --- a/app/logical/validating_socket.rb +++ b/app/logical/validating_socket.rb @@ -12,16 +12,8 @@ class ValidatingSocket < TCPSocket end def validate_hostname!(hostname) - ip = IPAddress.parse(::Resolv.getaddress(hostname)) - raise ProhibitedIpError, "Connection to #{hostname} failed; #{ip} is a prohibited IP" if prohibited_ip?(ip) + ip = Danbooru::IpAddress.new(::Resolv.getaddress(hostname)) + raise ProhibitedIpError, "Connection to #{hostname} failed; #{ip} is a prohibited IP" if ip.is_local? ip.to_s end - - def prohibited_ip?(ip) - if ip.ipv4? - ip.loopback? || ip.link_local? || ip.multicast? || ip.private? - elsif ip.ipv6? - ip.loopback? || ip.link_local? || ip.unique_local? || ip.unspecified? - end - end end diff --git a/app/models/ip_address.rb b/app/models/ip_address.rb index ab70f37d6..8898cb715 100644 --- a/app/models/ip_address.rb +++ b/app/models/ip_address.rb @@ -37,7 +37,7 @@ class IpAddress < ApplicationRecord end def lookup - @lookup ||= IpLookup.new(ip_addr) + @lookup ||= ip_addr.ip_lookup end def to_s diff --git a/app/models/ip_geolocation.rb b/app/models/ip_geolocation.rb index 266ce25b0..9cd4920fe 100644 --- a/app/models/ip_geolocation.rb +++ b/app/models/ip_geolocation.rb @@ -1,6 +1,9 @@ # An IpGeolocation contains metadata associated with an IP address, primarily geolocation data. class IpGeolocation < ApplicationRecord + attribute :ip_addr, :ip_address + attribute :network, :ip_address + has_many :user_sessions, foreign_key: :ip_addr, primary_key: :ip_addr def self.visible(user) @@ -17,13 +20,12 @@ class IpGeolocation < ApplicationRecord q end - def self.create_or_update!(ip_addr) - ip_lookup = IpLookup.new(ip_addr) - return nil if ip_lookup.is_local? - return nil if ip_lookup.ip_info.blank? + def self.create_or_update!(ip) + ip_address = Danbooru::IpAddress.new(ip) + return nil if ip_address.ip_info.blank? - ip_geolocation = IpGeolocation.create_with(**ip_lookup.ip_info).create_or_find_by!(ip_addr: ip_addr) - ip_geolocation.update!(**ip_lookup.ip_info) + ip_geolocation = IpGeolocation.create_with(**ip_address.ip_info).create_or_find_by!(ip_addr: ip_address) + ip_geolocation.update!(**ip_address.ip_info) ip_geolocation end end diff --git a/app/models/user_session.rb b/app/models/user_session.rb index 9ecaaaab7..7044426b3 100644 --- a/app/models/user_session.rb +++ b/app/models/user_session.rb @@ -3,6 +3,8 @@ # and their browser user agent. This is used to track logins and other events. class UserSession < ApplicationRecord + attribute :ip_addr, :ip_address + belongs_to :ip_geolocation, foreign_key: :ip_addr, primary_key: :ip_addr, optional: true def self.visible(user)