mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 01:04:23 +08:00
30990006a9
This reduces chances of errors where consumers of strings mutate inputs and reduces memory usage of the app. Test suite passes now, but there may be some stuff left, so we will run a few sites on a branch prior to merging
195 lines
6.0 KiB
Ruby
195 lines
6.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_dependency 'screening_model'
|
|
require_dependency 'ip_addr'
|
|
|
|
# A ScreenedIpAddress record represents an IP address or subnet that is being watched,
|
|
# and possibly blocked from creating accounts.
|
|
class ScreenedIpAddress < ActiveRecord::Base
|
|
|
|
include ScreeningModel
|
|
|
|
default_action :block
|
|
|
|
validates :ip_address, ip_address_format: true, presence: true
|
|
after_validation :check_for_match
|
|
|
|
def self.watch(ip_address, opts = {})
|
|
match_for_ip_address(ip_address) || create(opts.slice(:action_type).merge(ip_address: ip_address))
|
|
end
|
|
|
|
def check_for_match
|
|
unless self.errors[:ip_address].present?
|
|
matched = self.class.match_for_ip_address(self.ip_address)
|
|
if matched && matched.action_type == self.action_type
|
|
self.errors.add(:ip_address, :ip_address_already_screened)
|
|
end
|
|
end
|
|
end
|
|
|
|
# In Rails 4.0.0, validators are run to handle invalid assignments to inet columns (as they should).
|
|
# In Rails 4.0.1, an exception is raised before validation happens, so we need this hack for
|
|
# inet/cidr columns:
|
|
def ip_address=(val)
|
|
if val.nil?
|
|
self.errors.add(:ip_address, :invalid)
|
|
return
|
|
end
|
|
|
|
if val.is_a?(IPAddr)
|
|
write_attribute(:ip_address, val)
|
|
return
|
|
end
|
|
|
|
v = IPAddr.handle_wildcards(val)
|
|
|
|
if v.nil?
|
|
self.errors.add(:ip_address, :invalid)
|
|
return
|
|
end
|
|
|
|
write_attribute(:ip_address, v)
|
|
|
|
# this gets even messier, Ruby 1.9.2 raised a different exception to Ruby 2.0.0
|
|
# handle both exceptions
|
|
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
self.errors.add(:ip_address, :invalid)
|
|
end
|
|
|
|
# Return a string with the ip address and mask in standard format. e.g., "127.0.0.0/8".
|
|
def ip_address_with_mask
|
|
ip_address.try(:to_cidr_s)
|
|
end
|
|
|
|
def self.match_for_ip_address(ip_address)
|
|
# The <<= operator on inet columns means "is contained within or equal to".
|
|
#
|
|
# Read more about PostgreSQL's inet data type here:
|
|
#
|
|
# http://www.postgresql.org/docs/9.1/static/datatype-net-types.html
|
|
# http://www.postgresql.org/docs/9.1/static/functions-net.html
|
|
find_by("? <<= ip_address", ip_address.to_s)
|
|
end
|
|
|
|
def self.should_block?(ip_address)
|
|
exists_for_ip_address_and_action?(ip_address, actions[:block])
|
|
end
|
|
|
|
def self.is_whitelisted?(ip_address)
|
|
exists_for_ip_address_and_action?(ip_address, actions[:do_nothing])
|
|
end
|
|
|
|
def self.exists_for_ip_address_and_action?(ip_address, action_type, opts = {})
|
|
b = match_for_ip_address(ip_address)
|
|
found = (!!b && b.action_type == (action_type))
|
|
b.record_match! if found && opts[:record_match] != (false)
|
|
found
|
|
end
|
|
|
|
def self.block_admin_login?(user, ip_address)
|
|
return false unless SiteSetting.use_admin_ip_whitelist
|
|
return false if user.nil?
|
|
return false if !user.admin?
|
|
return false if ScreenedIpAddress.where(action_type: actions[:allow_admin]).count == 0
|
|
return true if ip_address.nil?
|
|
!exists_for_ip_address_and_action?(ip_address, actions[:allow_admin], record_match: false)
|
|
end
|
|
|
|
def self.star_subnets_query
|
|
@star_subnets_query ||= <<~SQL
|
|
SELECT network(inet(host(ip_address) || '/24'))::text AS ip_range
|
|
FROM screened_ip_addresses
|
|
WHERE action_type = #{ScreenedIpAddress.actions[:block]}
|
|
AND family(ip_address) = 4
|
|
AND masklen(ip_address) = 32
|
|
GROUP BY ip_range
|
|
HAVING COUNT(*) >= :min_count
|
|
SQL
|
|
end
|
|
|
|
def self.star_star_subnets_query
|
|
@star_star_subnets_query ||= <<~SQL
|
|
WITH weighted_subnets AS (
|
|
SELECT network(inet(host(ip_address) || '/16'))::text AS ip_range,
|
|
CASE masklen(ip_address)
|
|
WHEN 32 THEN 1
|
|
WHEN 24 THEN :roll_up_weight
|
|
ELSE 0
|
|
END AS weight
|
|
FROM screened_ip_addresses
|
|
WHERE action_type = #{ScreenedIpAddress.actions[:block]}
|
|
AND family(ip_address) = 4
|
|
)
|
|
SELECT ip_range
|
|
FROM weighted_subnets
|
|
GROUP BY ip_range
|
|
HAVING SUM(weight) >= :min_count
|
|
SQL
|
|
end
|
|
|
|
def self.star_subnets
|
|
min_count = SiteSetting.min_ban_entries_for_roll_up
|
|
DB.query_single(star_subnets_query, min_count: min_count)
|
|
end
|
|
|
|
def self.star_star_subnets
|
|
weight = SiteSetting.min_ban_entries_for_roll_up
|
|
DB.query_single(star_star_subnets_query, min_count: 10, roll_up_weight: weight)
|
|
end
|
|
|
|
def self.roll_up(current_user = Discourse.system_user)
|
|
subnets = [star_subnets, star_star_subnets].flatten
|
|
|
|
StaffActionLogger.new(current_user).log_roll_up(subnets) unless subnets.blank?
|
|
|
|
subnets.each do |subnet|
|
|
ScreenedIpAddress.create(ip_address: subnet) unless ScreenedIpAddress.where("? <<= ip_address", subnet).exists?
|
|
|
|
sql = <<~SQL
|
|
UPDATE screened_ip_addresses
|
|
SET match_count = sum_match_count
|
|
, created_at = min_created_at
|
|
, last_match_at = max_last_match_at
|
|
FROM (
|
|
SELECT SUM(match_count) AS sum_match_count
|
|
, MIN(created_at) AS min_created_at
|
|
, MAX(last_match_at) AS max_last_match_at
|
|
FROM screened_ip_addresses
|
|
WHERE action_type = #{ScreenedIpAddress.actions[:block]}
|
|
AND family(ip_address) = 4
|
|
AND ip_address << :ip_address
|
|
) s
|
|
WHERE ip_address = :ip_address
|
|
SQL
|
|
|
|
DB.exec(sql, ip_address: subnet)
|
|
|
|
ScreenedIpAddress.where(action_type: ScreenedIpAddress.actions[:block])
|
|
.where("family(ip_address) = 4")
|
|
.where("ip_address << ?", subnet)
|
|
.delete_all
|
|
end
|
|
|
|
subnets
|
|
end
|
|
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: screened_ip_addresses
|
|
#
|
|
# id :integer not null, primary key
|
|
# ip_address :inet not null
|
|
# action_type :integer not null
|
|
# match_count :integer default(0), not null
|
|
# last_match_at :datetime
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_screened_ip_addresses_on_ip_address (ip_address) UNIQUE
|
|
# index_screened_ip_addresses_on_last_match_at (last_match_at)
|
|
#
|