Этот плагин предназначен для отправки предупреждения по электронной почте всем администраторам всякий раз, когда пользователь входит в систему с подозрительного IP-адреса. Он извлекает IP-адрес из последнего токена аутентификации (используя поле client_ip), проверяет этот адрес по списку заблокированных, определенному в настройках сайта (ip_alert_suspicious_ips), и если IP-адрес совпадает с каким-либо заблокированным шаблоном, отправляет предупреждающее письмо всем пользователям-администраторам.
Наблюдаемая ошибка:
Несмотря на то, что код плагина кажется корректным и следует стандартным шаблонам плагинов Discourse, при входе в систему я по-прежнему получаю общую ошибку «Упс — программное обеспечение, управляющее этим форумом для обсуждений, столкнулось с неожиданной проблемой». В логах ошибок нет четких указаний от наших сообщений логирования [DiscourseIpAlert], а трассировка стека не указывает напрямую на проблему в коде нашего плагина.
Текущий статус:
Я неоднократно проверял и дорабатывал плагин, включая проверку настроек сайта и обеспечение правильной регистрации хука (on(:user_logged_in)). Логика извлечения IP-адреса и отправки писем также, по-видимому, реализована правильно, однако ошибка 500 Internal Server Error сохраняется без четкой трассировки стека, связывающей её с нашим плагином.
Любая помощь?
plugin.rb:
# frozen_string_literal: true
# name: discourse-ip-alert
# about: Отправляет предупреждение по электронной почте при входе пользователя с подозрительного IP-адреса.
# version: 0.7
# authors: OrkoGrayskull (доработано)
# required_version: 3.5.0
enabled_site_setting :ip_alert_enabled
enabled_site_setting :ip_alert_suspicious_ips
module ::DiscourseIpAlert
PLUGIN_NAME = "discourse-ip-alert"
require 'ipaddr'
require 'mail'
# Извлекает IP-адрес из последнего токена аутентификации
def self.extract_ip(user)
token = UserAuthToken.where(user_id: user.id).order(created_at: :desc).first
ip = token&.attributes["client_ip"]
Rails.logger.info("[DiscourseIpAlert] extract_ip: Извлечен IP из UserAuthToken: #{ip.inspect}")
ip
rescue => e
Rails.logger.error("[DiscourseIpAlert] extract_ip error: #{e.message}")
nil
end
# Проверяет, присутствует ли данный IP-адрес в списке заблокированных
def self.ip_blocked?(ip_address)
return false if ip_address.blank?
blocked_ips = SiteSetting.ip_alert_suspicious_ips.split(",").map(&:strip)
Rails.logger.info("[DiscourseIpAlert] ip_blocked?: Заблокированные IP: #{blocked_ips.inspect}")
blocked_ips.any? do |blocked|
if blocked.include?('*')
regex = Regexp.new("^" + Regexp.escape(blocked).gsub('\\*', '\\d{1,3}') + "$")
Rails.logger.info("[DiscourseIpAlert] ip_blocked?: Сравнение #{ip_address} с регулярным выражением подстановочного знака #{regex.inspect}")
ip_address.match?(regex)
else
begin
result = IPAddr.new(blocked).include?(ip_address)
Rails.logger.info("[DiscourseIpAlert] ip_blocked?: Сравнение #{ip_address} с #{blocked}: #{result}")
result
rescue IPAddr::InvalidAddressError => e
Rails.logger.error("[DiscourseIpAlert] ip_blocked?: Неверный IP в списке заблокированных: #{blocked} (#{e.message})")
false
end
end
end
rescue => e
Rails.logger.error("[DiscourseIpAlert] ip_blocked? error: #{e.message}")
false
end
# Отправляет предупреждающее письмо всем администраторам
def self.send_warning_email(user, ip_address)
admin_emails = User.where(admin: true).pluck(:email)
return if admin_emails.empty?
subject = "⚠️ Предупреждение: Вход с подозрительного IP!"
body = <<~BODY
Внимание!
Пользователь #{user.username} (#{user.email}) вошел в систему с IP-адреса #{ip_address},
который был помечен как подозрительный.
Пожалуйста, проверьте эту учетную запись.
BODY
admin_emails.each do |email|
begin
mail = Mail.new do
from ENV['DISCOURSE_NOTIFICATION_EMAIL'] || 'no-reply@example.org'
to email
subject subject
body body
end
Rails.logger.info("[DiscourseIpAlert] Отправка предупреждающего письма на #{email}")
mail.deliver!
rescue => e
Rails.logger.error("[DiscourseIpAlert] Ошибка отправки письма на #{email}: #{e.message}")
end
end
rescue => e
Rails.logger.error("[DiscourseIpAlert] send_warning_email error: #{e.message}")
end
# Обрабатывает вход пользователя: извлекает IP, проверяет список заблокированных и отправляет предупреждение при необходимости
def self.process_user_login(user)
Rails.logger.info("[DiscourseIpAlert] process_user_login для пользователя: #{user.username}")
ip_address = extract_ip(user)
unless ip_address
Rails.logger.warn("[DiscourseIpAlert] IP-адрес не извлечен — пропуск дальнейшей обработки.")
return
end
if ip_blocked?(ip_address)
Rails.logger.warn("[DiscourseIpAlert] Обнаружен подозрительный IP: #{ip_address}")
send_warning_email(user, ip_address)
else
Rails.logger.info("[DiscourseIpAlert] IP #{ip_address} считается безопасным.")
end
rescue => e
Rails.logger.error("[DiscourseIpAlert] process_user_login error: #{e.message}")
end
end
after_initialize do
Rails.logger.info("[DiscourseIpAlert] after_initialize: Плагин интегрируется в процесс входа в систему.")
# Регистрируем хук, который вызывается при каждом входе пользователя
on(:user_logged_in) do |user|
begin
Rails.logger.info("[DiscourseIpAlert] Хук on(:user_logged_in) вызван для пользователя: #{user.username}")
::DiscourseIpAlert.process_user_login(user)
rescue => e
Rails.logger.error("[DiscourseIpAlert] Ошибка в хуке on(:user_logged_in): #{e.message}")
end
end
end
config/settings.yml:
ip_alert_enabled:
default: false
client: true
type: bool
description: "Включить предупреждение по IP"
ip_alert_suspicious_ips:
default: "192.168.1.1,203.0.113.0/24"
client: true
type: string
description: "Список подозрительных IP-адресов (через запятую)"