discourse/app/models/global_setting.rb
Alan Guo Xiang Tan 69af29cc40
DEV: Add a test to ensure that our SMTP settings are correct (#26410)
Why this change?

This is a follow up to 897be75941.

When updating `net-smtp` from `0.4.x` to `0.5.x`, our test suite passed
but the error `ArgumentError: SMTP-AUTH requested but missing user name`
was being thrown in production leading to emails being failed to send
out via SMTP.

This commit adds a test to ensure that our production SMTP settings will
at least attemp to connect to an SMTP server.
2024-03-28 10:18:19 +08:00

386 lines
10 KiB
Ruby

# frozen_string_literal: true
class GlobalSetting
def self.register(key, default)
define_singleton_method(key) { provider.lookup(key, default) }
end
VALID_SECRET_KEY ||= /\A[0-9a-f]{128}\z/
# this is named SECRET_TOKEN as opposed to SECRET_KEY_BASE
# for legacy reasons
REDIS_SECRET_KEY ||= "SECRET_TOKEN"
REDIS_VALIDATE_SECONDS ||= 30
# In Rails secret_key_base is used to encrypt the cookie store
# the cookie store contains session data
# Discourse also uses this secret key to digest user auth tokens
# This method will
# - use existing token if already set in ENV or discourse.conf
# - generate a token on the fly if needed and cache in redis
# - enforce rules about token format falling back to redis if needed
def self.safe_secret_key_base
if @safe_secret_key_base && @token_in_redis &&
(@token_last_validated + REDIS_VALIDATE_SECONDS) < Time.now
@token_last_validated = Time.now
token = Discourse.redis.without_namespace.get(REDIS_SECRET_KEY)
Discourse.redis.without_namespace.set(REDIS_SECRET_KEY, @safe_secret_key_base) if token.nil?
end
@safe_secret_key_base ||=
begin
token = secret_key_base
if token.blank? || token !~ VALID_SECRET_KEY
@token_in_redis = true
@token_last_validated = Time.now
token = Discourse.redis.without_namespace.get(REDIS_SECRET_KEY)
unless token && token =~ VALID_SECRET_KEY
token = SecureRandom.hex(64)
Discourse.redis.without_namespace.set(REDIS_SECRET_KEY, token)
end
end
if !secret_key_base.blank? && token != secret_key_base
STDERR.puts "WARNING: DISCOURSE_SECRET_KEY_BASE is invalid, it was re-generated"
end
token
end
rescue Redis::CommandError => e
@safe_secret_key_base = SecureRandom.hex(64) if e.message =~ /READONLY/
end
def self.load_defaults
default_provider =
FileProvider.from(File.expand_path("../../../config/discourse_defaults.conf", __FILE__))
default_provider
.keys
.concat(@provider.keys)
.uniq
.each do |key|
default = default_provider.lookup(key, nil)
instance_variable_set("@#{key}_cache", nil)
define_singleton_method(key) do
val = instance_variable_get("@#{key}_cache")
if val.nil?
val = provider.lookup(key, default)
val = :missing if val.nil?
instance_variable_set("@#{key}_cache", val)
end
val == :missing ? nil : val
end
end
end
def self.skip_db=(v)
@skip_db = v
end
def self.skip_db?
@skip_db
end
def self.skip_redis=(v)
@skip_redis = v
end
def self.skip_redis?
@skip_redis
end
# rubocop:disable Lint/BooleanSymbol
def self.use_s3?
(
@use_s3 ||=
begin
if s3_bucket && s3_region &&
(s3_use_iam_profile || (s3_access_key_id && s3_secret_access_key))
:true
else
:false
end
end
) == :true
end
# rubocop:enable Lint/BooleanSymbol
def self.s3_bucket_name
@s3_bucket_name ||= s3_bucket.downcase.split("/")[0]
end
# for testing
def self.reset_s3_cache!
@use_s3 = nil
end
def self.cdn_hostnames
hostnames = []
hostnames << URI.parse(cdn_url).host if cdn_url.present?
hostnames << cdn_origin_hostname if cdn_origin_hostname.present?
hostnames
end
def self.database_config
hash = { "adapter" => "postgresql" }
%w[
pool
connect_timeout
socket
host
backup_host
port
backup_port
username
password
replica_host
replica_port
].each do |s|
if val = self.public_send("db_#{s}")
hash[s] = val
end
end
hostnames = [hostname]
hostnames << backup_hostname if backup_hostname.present?
hostnames << URI.parse(cdn_url).host if cdn_url.present?
hostnames << cdn_origin_hostname if cdn_origin_hostname.present?
hash["host_names"] = hostnames
hash["database"] = db_name
hash["prepared_statements"] = !!self.db_prepared_statements
hash["idle_timeout"] = connection_reaper_age if connection_reaper_age.present?
hash["reaping_frequency"] = connection_reaper_interval if connection_reaper_interval.present?
hash["advisory_locks"] = !!self.db_advisory_locks
db_variables = provider.keys.filter { |k| k.to_s.starts_with? "db_variables_" }
if db_variables.length > 0
hash["variables"] = {}
db_variables.each do |k|
hash["variables"][k.slice(("db_variables_".length)..)] = self.public_send(k)
end
end
{ "production" => hash }
end
# For testing purposes
def self.reset_redis_config!
@config = nil
@message_bus_config = nil
end
def self.get_redis_replica_host
return redis_replica_host if redis_replica_host.present?
redis_slave_host if respond_to?(:redis_slave_host) && redis_slave_host.present?
end
def self.get_redis_replica_port
return redis_replica_port if redis_replica_port.present?
redis_slave_port if respond_to?(:redis_slave_port) && redis_slave_port.present?
end
def self.get_message_bus_redis_replica_host
return message_bus_redis_replica_host if message_bus_redis_replica_host.present?
if respond_to?(:message_bus_redis_slave_host) && message_bus_redis_slave_host.present?
message_bus_redis_slave_host
end
end
def self.get_message_bus_redis_replica_port
return message_bus_redis_replica_port if message_bus_redis_replica_port.present?
if respond_to?(:message_bus_redis_slave_port) && message_bus_redis_slave_port.present?
message_bus_redis_slave_port
end
end
def self.redis_config
@config ||=
begin
c = {}
c[:host] = redis_host if redis_host
c[:port] = redis_port if redis_port
if get_redis_replica_host && get_redis_replica_port && defined?(RailsFailover)
c[:replica_host] = get_redis_replica_host
c[:replica_port] = get_redis_replica_port
c[:connector] = RailsFailover::Redis::Connector
end
c[:password] = redis_password if redis_password.present?
c[:db] = redis_db if redis_db != 0
c[:db] = 1 if Rails.env == "test"
c[:id] = nil if redis_skip_client_commands
c[:ssl] = true if redis_use_ssl
c.freeze
end
end
def self.message_bus_redis_config
return redis_config unless message_bus_redis_enabled
@message_bus_config ||=
begin
c = {}
c[:host] = message_bus_redis_host if message_bus_redis_host
c[:port] = message_bus_redis_port if message_bus_redis_port
if get_message_bus_redis_replica_host && get_message_bus_redis_replica_port
c[:replica_host] = get_message_bus_redis_replica_host
c[:replica_port] = get_message_bus_redis_replica_port
c[:connector] = RailsFailover::Redis::Connector
end
c[:password] = message_bus_redis_password if message_bus_redis_password.present?
c[:db] = message_bus_redis_db if message_bus_redis_db != 0
c[:db] = 1 if Rails.env == "test"
c[:id] = nil if message_bus_redis_skip_client_commands
c[:ssl] = true if redis_use_ssl
c.freeze
end
end
def self.add_default(name, default)
define_singleton_method(name) { default } unless self.respond_to? name
end
def self.smtp_settings
if GlobalSetting.smtp_address
settings = {
address: GlobalSetting.smtp_address,
port: GlobalSetting.smtp_port,
domain: GlobalSetting.smtp_domain,
user_name: GlobalSetting.smtp_user_name,
password: GlobalSetting.smtp_password,
enable_starttls_auto: GlobalSetting.smtp_enable_start_tls,
open_timeout: GlobalSetting.smtp_open_timeout,
read_timeout: GlobalSetting.smtp_read_timeout,
}
if settings[:password] || settings[:user_name]
settings[:authentication] = GlobalSetting.smtp_authentication
end
settings[
:openssl_verify_mode
] = GlobalSetting.smtp_openssl_verify_mode if GlobalSetting.smtp_openssl_verify_mode
settings[:tls] = true if GlobalSetting.smtp_force_tls
settings.compact
settings
end
end
class BaseProvider
def self.coerce(setting)
return setting == "true" if setting == "true" || setting == "false"
return $1.to_i if setting.to_s.strip =~ /\A([0-9]+)\z/
setting
end
def resolve(current, default)
BaseProvider.coerce(
if current.present?
current
else
default.present? ? default : nil
end,
)
end
end
class FileProvider < BaseProvider
attr_reader :data
def self.from(file)
parse(file) if File.exist?(file)
end
def initialize(file)
@file = file
@data = {}
end
def read
ERB
.new(File.read(@file))
.result()
.split("\n")
.each do |line|
if line =~ /\A\s*([a-z_]+[a-z0-9_]*)\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|[^#]*)/
@data[$1.strip.to_sym] = ($4 || $3 || $2).strip
end
end
end
def lookup(key, default)
var = @data[key]
resolve(var, var.nil? ? default : "")
end
def keys
@data.keys
end
def self.parse(file)
provider = self.new(file)
provider.read
provider
end
private_class_method :parse
end
class EnvProvider < BaseProvider
def lookup(key, default)
var = ENV["DISCOURSE_" + key.to_s.upcase]
resolve(var, var.nil? ? default : nil)
end
def keys
ENV.keys.select { |k| k =~ /\ADISCOURSE_/ }.map { |k| k[10..-1].downcase.to_sym }
end
end
class BlankProvider < BaseProvider
def lookup(key, default)
if key == :redis_port
return ENV["DISCOURSE_REDIS_PORT"] if ENV["DISCOURSE_REDIS_PORT"]
end
default
end
def keys
[]
end
end
class << self
attr_accessor :provider
end
def self.configure!
if Rails.env == "test"
@provider = BlankProvider.new
else
@provider =
FileProvider.from(File.expand_path("../../../config/discourse.conf", __FILE__)) ||
EnvProvider.new
end
end
def self.load_plugins?
if ENV["LOAD_PLUGINS"] == "1"
true
elsif ENV["LOAD_PLUGINS"] == "0"
false
elsif Rails.env.test?
false
else
true
end
end
end