mirror of
https://github.com/internetee/registry.git
synced 2025-07-25 12:08:27 +02:00
parent 64e3bc885a2cb8b46a1aaa4bf4f121ee7f5d44a6
author Karl Erik Õunapuu <karlerik@kreative.ee> 1591359032 +0300 committer Alex Sherman <yul.golem@gmail.com> 1617029320 +0500 CsyncJob: Don't respect IPv6 if nessecary
This commit is contained in:
parent
e46fdd57af
commit
88e1bc3727
33 changed files with 1475 additions and 119 deletions
81
app/models/actions/contact_create.rb
Normal file
81
app/models/actions/contact_create.rb
Normal file
|
@ -0,0 +1,81 @@
|
|||
module Actions
|
||||
class ContactCreate
|
||||
attr_reader :contact, :legal_document, :ident
|
||||
|
||||
def initialize(contact, legal_document, ident)
|
||||
@contact = contact
|
||||
@legal_document = legal_document
|
||||
@ident = ident
|
||||
end
|
||||
|
||||
def call
|
||||
maybe_remove_address
|
||||
maybe_attach_legal_doc
|
||||
validate_ident
|
||||
commit
|
||||
end
|
||||
|
||||
def maybe_remove_address
|
||||
return if Contact.address_processing?
|
||||
|
||||
contact.city = nil
|
||||
contact.zip = nil
|
||||
contact.street = nil
|
||||
contact.state = nil
|
||||
contact.country_code = nil
|
||||
end
|
||||
|
||||
def validate_ident
|
||||
validate_ident_integrity
|
||||
validate_ident_birthday
|
||||
|
||||
identifier = ::Contact::Ident.new(code: ident[:ident], type: ident[:ident_type],
|
||||
country_code: ident[:ident_country_code])
|
||||
|
||||
identifier.validate
|
||||
contact.identifier = identifier
|
||||
end
|
||||
|
||||
def validate_ident_integrity
|
||||
return if ident.blank?
|
||||
|
||||
if ident[:ident_type].blank?
|
||||
contact.add_epp_error('2003', nil, 'ident_type',
|
||||
I18n.t('errors.messages.required_ident_attribute_missing'))
|
||||
@error = true
|
||||
elsif !%w[priv org birthday].include?(ident[:ident_type])
|
||||
contact.add_epp_error('2003', nil, 'ident_type', 'Invalid ident type')
|
||||
@error = true
|
||||
end
|
||||
end
|
||||
|
||||
def validate_ident_birthday
|
||||
return if ident.blank?
|
||||
return unless ident[:ident_type] != 'birthday' && ident[:ident_country_code].blank?
|
||||
|
||||
contact.add_epp_error('2003', nil, 'ident_country_code',
|
||||
I18n.t('errors.messages.required_ident_attribute_missing'))
|
||||
@error = true
|
||||
end
|
||||
|
||||
def maybe_attach_legal_doc
|
||||
return unless legal_document
|
||||
|
||||
doc = LegalDocument.create(
|
||||
documentable_type: Contact,
|
||||
document_type: legal_document[:type], body: legal_document[:body]
|
||||
)
|
||||
|
||||
contact.legal_documents = [doc]
|
||||
contact.legal_document_id = doc.id
|
||||
end
|
||||
|
||||
def commit
|
||||
contact.id = nil # new record
|
||||
return false if @error
|
||||
|
||||
contact.generate_code
|
||||
contact.save
|
||||
end
|
||||
end
|
||||
end
|
46
app/models/actions/contact_delete.rb
Normal file
46
app/models/actions/contact_delete.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
module Actions
|
||||
class ContactDelete
|
||||
attr_reader :contact
|
||||
attr_reader :new_attributes
|
||||
attr_reader :legal_document
|
||||
attr_reader :ident
|
||||
attr_reader :user
|
||||
|
||||
def initialize(contact, legal_document = nil)
|
||||
@legal_document = legal_document
|
||||
@contact = contact
|
||||
end
|
||||
|
||||
def call
|
||||
maybe_attach_legal_doc
|
||||
|
||||
if contact.linked?
|
||||
contact.errors.add(:domains, :exist)
|
||||
return
|
||||
end
|
||||
|
||||
if contact.delete_prohibited?
|
||||
contact.errors.add(:statuses, :delete_prohibited)
|
||||
return
|
||||
end
|
||||
|
||||
commit
|
||||
end
|
||||
|
||||
def maybe_attach_legal_doc
|
||||
return unless legal_document
|
||||
|
||||
document = contact.legal_documents.create(
|
||||
document_type: legal_document[:type],
|
||||
body: legal_document[:body]
|
||||
)
|
||||
|
||||
contact.legal_document_id = document.id
|
||||
contact.save
|
||||
end
|
||||
|
||||
def commit
|
||||
contact.destroy
|
||||
end
|
||||
end
|
||||
end
|
109
app/models/actions/contact_update.rb
Normal file
109
app/models/actions/contact_update.rb
Normal file
|
@ -0,0 +1,109 @@
|
|||
module Actions
|
||||
class ContactUpdate
|
||||
attr_reader :contact
|
||||
attr_reader :new_attributes
|
||||
attr_reader :legal_document
|
||||
attr_reader :ident
|
||||
attr_reader :user
|
||||
|
||||
def initialize(contact, new_attributes, legal_document, ident, user)
|
||||
@contact = contact
|
||||
@new_attributes = new_attributes
|
||||
@legal_document = legal_document
|
||||
@ident = ident
|
||||
@user = user
|
||||
end
|
||||
|
||||
def call
|
||||
maybe_remove_address
|
||||
maybe_update_statuses
|
||||
maybe_update_ident if ident.present?
|
||||
maybe_attach_legal_doc
|
||||
commit
|
||||
end
|
||||
|
||||
def maybe_remove_address
|
||||
return if Contact.address_processing?
|
||||
|
||||
new_attributes.delete(:city)
|
||||
new_attributes.delete(:zip)
|
||||
new_attributes.delete(:street)
|
||||
new_attributes.delete(:state)
|
||||
new_attributes.delete(:country_code)
|
||||
end
|
||||
|
||||
def maybe_update_statuses
|
||||
return unless Setting.client_status_editing_enabled
|
||||
|
||||
new_statuses =
|
||||
contact.statuses - new_attributes[:statuses_to_remove] + new_attributes[:statuses_to_add]
|
||||
|
||||
new_attributes[:statuses] = new_statuses
|
||||
end
|
||||
|
||||
def maybe_attach_legal_doc
|
||||
return unless legal_document
|
||||
|
||||
document = contact.legal_documents.create(
|
||||
document_type: legal_document[:type],
|
||||
body: legal_document[:body]
|
||||
)
|
||||
|
||||
contact.legal_document_id = document.id
|
||||
end
|
||||
|
||||
def maybe_update_ident
|
||||
unless ident.is_a?(Hash)
|
||||
contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.valid_ident'))
|
||||
@error = true
|
||||
return
|
||||
end
|
||||
|
||||
if contact.identifier.valid?
|
||||
submitted_ident = ::Contact::Ident.new(code: ident[:ident],
|
||||
type: ident[:ident_type],
|
||||
country_code: ident[:ident_country_code])
|
||||
|
||||
if submitted_ident != contact.identifier
|
||||
contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.valid_ident'))
|
||||
@error = true
|
||||
end
|
||||
else
|
||||
ident_update_attempt = ident[:ident] != contact.ident
|
||||
|
||||
if ident_update_attempt
|
||||
contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.ident_update'))
|
||||
@error = true
|
||||
end
|
||||
|
||||
identifier = ::Contact::Ident.new(code: ident[:ident],
|
||||
type: ident[:ident_type],
|
||||
country_code: ident[:ident_country_code])
|
||||
|
||||
identifier.validate
|
||||
|
||||
contact.identifier = identifier
|
||||
contact.ident_updated_at ||= Time.zone.now
|
||||
end
|
||||
end
|
||||
|
||||
def commit
|
||||
return false if @error
|
||||
|
||||
contact.upid = user.registrar&.id
|
||||
contact.up_date = Time.zone.now
|
||||
|
||||
contact.attributes = new_attributes
|
||||
|
||||
email_changed = contact.will_save_change_to_email?
|
||||
old_email = contact.email_was
|
||||
updated = contact.save
|
||||
|
||||
if updated && email_changed && contact.registrant?
|
||||
ContactMailer.email_changed(contact: contact, old_email: old_email).deliver_now
|
||||
end
|
||||
|
||||
updated
|
||||
end
|
||||
end
|
||||
end
|
74
app/models/actions/domain_transfer.rb
Normal file
74
app/models/actions/domain_transfer.rb
Normal file
|
@ -0,0 +1,74 @@
|
|||
module Actions
|
||||
class DomainTransfer
|
||||
attr_reader :domain
|
||||
attr_reader :transfer_code
|
||||
attr_reader :legal_document
|
||||
attr_reader :ident
|
||||
attr_reader :user
|
||||
|
||||
def initialize(domain, transfer_code, user)
|
||||
@domain = domain
|
||||
@transfer_code = transfer_code
|
||||
@user = user
|
||||
end
|
||||
|
||||
def call
|
||||
return unless domain_exists?
|
||||
return unless valid_transfer_code?
|
||||
|
||||
run_validations
|
||||
|
||||
# return domain.pending_transfer if domain.pending_transfer
|
||||
# attach_legal_document(::Deserializers::Xml::LegalDocument.new(frame).call)
|
||||
|
||||
return if domain.errors[:epp_errors].any?
|
||||
|
||||
commit
|
||||
end
|
||||
|
||||
def domain_exists?
|
||||
return true if domain.persisted?
|
||||
|
||||
domain.add_epp_error('2303', nil, nil, 'Object does not exist')
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def run_validations
|
||||
validate_registrar
|
||||
validate_eligilibty
|
||||
validate_not_discarded
|
||||
end
|
||||
|
||||
def valid_transfer_code?
|
||||
return true if transfer_code == domain.transfer_code
|
||||
|
||||
domain.add_epp_error('2202', nil, nil, 'Invalid authorization information')
|
||||
false
|
||||
end
|
||||
|
||||
def validate_registrar
|
||||
return unless user == domain.registrar
|
||||
|
||||
domain.add_epp_error('2002', nil, nil,
|
||||
I18n.t(:domain_already_belongs_to_the_querying_registrar))
|
||||
end
|
||||
|
||||
def validate_eligilibty
|
||||
return unless domain.non_transferable?
|
||||
|
||||
domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation')
|
||||
end
|
||||
|
||||
def validate_not_discarded
|
||||
return unless domain.discarded?
|
||||
|
||||
domain.add_epp_error('2106', nil, nil, 'Object is not eligible for transfer')
|
||||
end
|
||||
|
||||
def commit
|
||||
bare_domain = Domain.find(domain.id)
|
||||
::DomainTransfer.request(bare_domain, user)
|
||||
end
|
||||
end
|
||||
end
|
45
app/models/concerns/csync_record/diggable.rb
Normal file
45
app/models/concerns/csync_record/diggable.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
module CsyncRecord::Diggable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def valid_security_level?(post: false)
|
||||
res = post ? valid_post_action? : valid_pre_action?
|
||||
|
||||
log_dnssec_entry(valid: res, post: post)
|
||||
res
|
||||
rescue Dnsruby::NXDomain
|
||||
log.info("CsyncRecord: #{domain.name}: Could not resolve (NXDomain)")
|
||||
false
|
||||
end
|
||||
|
||||
def valid_pre_action?
|
||||
case domain.dnssec_security_level
|
||||
when Dnsruby::Message::SecurityLevel.SECURE
|
||||
return true if %w[rollover deactivate].include?(action)
|
||||
when Dnsruby::Message::SecurityLevel.INSECURE, Dnsruby::Message::SecurityLevel.BOGUS
|
||||
return true if action == 'initialized'
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def valid_post_action?
|
||||
secure_msg = Dnsruby::Message::SecurityLevel.SECURE
|
||||
security_level = domain.dnssec_security_level(stubber: dnskey)
|
||||
return true if action == 'deactivate' && security_level != secure_msg
|
||||
return true if %w[rollover initialized].include?(action) && security_level == secure_msg
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def dnssec_validates?
|
||||
return false unless dnskey.valid?
|
||||
return true if valid_security_level? && valid_security_level?(post: true)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def log_dnssec_entry(valid:, post:)
|
||||
log.info("#{domain.name}: #{post ? 'Post' : 'Pre'} DNSSEC validation " \
|
||||
"#{valid ? 'PASSED' : 'FAILED'} for action '#{action}'")
|
||||
end
|
||||
end
|
132
app/models/csync_record.rb
Normal file
132
app/models/csync_record.rb
Normal file
|
@ -0,0 +1,132 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CsyncRecord < ApplicationRecord
|
||||
include CsyncRecord::Diggable
|
||||
belongs_to :domain, optional: false
|
||||
validates :domain, uniqueness: true
|
||||
validates :cdnskey, :action, :last_scan, presence: true
|
||||
validate :validate_unique_pub_key
|
||||
validate :validate_csync_action
|
||||
validate :validate_cdnskey_format
|
||||
after_save :process_new_dnskey, if: proc { pushable? && !disable_requested? }
|
||||
after_save :remove_dnskeys, if: proc { pushable? && disable_requested? }
|
||||
|
||||
SCAN_CYCLES = 3
|
||||
|
||||
def record_new_scan(result)
|
||||
assign_scanner_data!(result)
|
||||
prefix = "CsyncRecord: #{domain.name}:"
|
||||
|
||||
if save
|
||||
log.info "#{prefix} Cycle done."
|
||||
else
|
||||
log.info "#{prefix}: not processing. Reason: #{errors.full_messages.join(' .')}"
|
||||
CsyncRecord.where(domain: domain).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def assign_scanner_data!(result)
|
||||
state = result[:type]
|
||||
self.last_scan = Time.zone.now
|
||||
self.times_scanned = (persisted? && cdnskey != result[:cdnskey] ? 1 : times_scanned + 1)
|
||||
self.cdnskey = result[:cdnskey]
|
||||
self.action = initializes_dnssec?(state) ? 'initialized' : determine_csync_intention(state)
|
||||
end
|
||||
|
||||
def dnskey
|
||||
key = Dnskey.new_from_csync(domain: domain, cdnskey: cdnskey)
|
||||
log.info "DNSKEY not valid. #{key.errors.full_messages.join('. ')}." unless key.valid?
|
||||
|
||||
key
|
||||
end
|
||||
|
||||
def destroy_all_but_last_one
|
||||
domain.dnskeys.order(id: :desc).offset(1).destroy_all
|
||||
end
|
||||
|
||||
def process_new_dnskey
|
||||
return unless dnssec_validates?
|
||||
|
||||
if dnskey.save
|
||||
destroy_all_but_last_one
|
||||
finalize_and_notify
|
||||
else
|
||||
log.info "Failed to save DNSKEY. Errors: #{dnskey.errors.full_messages.join('. ')}"
|
||||
end
|
||||
end
|
||||
|
||||
def finalize_and_notify
|
||||
CsyncMailer.dnssec_updated(domain: domain).deliver_now
|
||||
notify_registrar_about_csync
|
||||
CsyncRecord.where(domain: domain).destroy_all
|
||||
log.info "CsyncRecord: #{domain.name}: DNSKEYs updated."
|
||||
end
|
||||
|
||||
def pushable?
|
||||
return true if domain.dnskeys.any? || times_scanned >= SCAN_CYCLES
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def disable_requested?
|
||||
['0 3 0 AA==', '0 3 0 0'].include? cdnskey
|
||||
end
|
||||
|
||||
def remove_dnskeys
|
||||
log.info "CsyncJob: Removing DNSKEYs for domain '#{domain.name}'"
|
||||
domain.dnskeys.destroy_all
|
||||
CsyncMailer.dnssec_deleted(domain: domain).deliver_now
|
||||
notify_registrar_about_csync
|
||||
|
||||
destroy
|
||||
end
|
||||
|
||||
def notify_registrar_about_csync
|
||||
domain.update_whois_record
|
||||
domain.registrar.notifications.create!(text: I18n.t('notifications.texts.csync',
|
||||
domain: domain.name, action: action))
|
||||
end
|
||||
|
||||
def validate_unique_pub_key
|
||||
return false unless domain
|
||||
return true if disable_requested?
|
||||
return true unless domain.dnskeys.where(public_key: dnskey.public_key).any?
|
||||
|
||||
errors.add(:public_key, 'already tied to this domain')
|
||||
end
|
||||
|
||||
def self.by_domain_name(domain_name)
|
||||
domain = Domain.find_by(name: domain_name)
|
||||
log.info "CsyncRecord: '#{domain_name}' not in zone. Not initializing record." unless domain
|
||||
CsyncRecord.find_or_initialize_by(domain: domain) if domain
|
||||
end
|
||||
|
||||
def determine_csync_intention(scan_state)
|
||||
return unless domain.dnskeys.any? && scan_state == 'secure'
|
||||
|
||||
disable_requested? ? 'deactivate' : 'rollover'
|
||||
end
|
||||
|
||||
def initializes_dnssec?(scan_state)
|
||||
true if domain.dnskeys.empty? && !disable_requested? && scan_state == 'insecure'
|
||||
end
|
||||
|
||||
def log
|
||||
@log ||= Rails.env.test? ? logger : Logger.new(STDOUT)
|
||||
@log
|
||||
end
|
||||
|
||||
def validate_csync_action
|
||||
return true if %w[initialized rollover].include? action
|
||||
return true if action == 'deactivate' && disable_requested?
|
||||
|
||||
errors.add(:action, :invalid)
|
||||
end
|
||||
|
||||
def validate_cdnskey_format
|
||||
return true if disable_requested?
|
||||
return true if dnskey.valid?
|
||||
|
||||
errors.add(:cdnskey, :invalid)
|
||||
end
|
||||
end
|
|
@ -28,7 +28,7 @@ class Dnskey < ApplicationRecord
|
|||
PROTOCOLS = %w(3)
|
||||
FLAGS = %w(0 256 257) # 256 = ZSK, 257 = KSK
|
||||
DS_DIGEST_TYPE = [1,2]
|
||||
|
||||
RESOLVERS = ENV['dnssec_resolver_ips'].to_s.strip.split(', ').freeze
|
||||
self.ignored_columns = %w[legacy_domain_id]
|
||||
|
||||
def epp_code_map
|
||||
|
@ -122,6 +122,26 @@ class Dnskey < ApplicationRecord
|
|||
errors.add(:public_key, :invalid)
|
||||
end
|
||||
|
||||
def self.new_from_csync(cdnskey:, domain:)
|
||||
cdnskey ||= '' # avoid strip() issues for gibberish key
|
||||
|
||||
flags, proto, alg, pub = cdnskey.strip.split(' ')
|
||||
Dnskey.new(domain: domain, flags: flags, protocol: proto, alg: alg, public_key: pub)
|
||||
end
|
||||
|
||||
def ds_rr
|
||||
# Break the DNSSEC trust chain as we are not able to fake RRSIG's
|
||||
Dnsruby::Dnssec.clear_trust_anchors
|
||||
Dnsruby::Dnssec.clear_trusted_keys
|
||||
|
||||
# Basically let's configure domain as root anchor. We can still verify
|
||||
# RRSIG's / DNSKEY targeted by DS of this domain
|
||||
generate_digest
|
||||
generate_ds_key_tag
|
||||
Dnsruby::RR.create("#{domain.name}. 3600 IN DS #{ds_key_tag} #{ds_alg} " \
|
||||
"#{ds_digest_type} #{ds_digest}")
|
||||
end
|
||||
|
||||
class << self
|
||||
def int_to_hex(s)
|
||||
s = s.to_s(16)
|
||||
|
|
|
@ -58,6 +58,7 @@ class Domain < ApplicationRecord
|
|||
|
||||
has_many :legal_documents, as: :documentable
|
||||
has_many :registrant_verifications, dependent: :destroy
|
||||
has_one :csync_record, dependent: :destroy
|
||||
|
||||
after_initialize do
|
||||
self.pending_json = {} if pending_json.blank?
|
||||
|
@ -167,6 +168,35 @@ class Domain < ApplicationRecord
|
|||
validate :validate_nameserver_ips
|
||||
|
||||
validate :statuses_uniqueness
|
||||
|
||||
def security_level_resolver
|
||||
resolver = Dnsruby::Resolver.new(nameserver: Dnskey::RESOLVERS)
|
||||
resolver.do_validation = true
|
||||
resolver.do_caching = false
|
||||
resolver.dnssec = true
|
||||
resolver
|
||||
end
|
||||
|
||||
def dnssec_security_level(stubber: nil)
|
||||
Dnsruby::Dnssec.reset
|
||||
resolver = security_level_resolver
|
||||
Dnsruby::Recursor.clear_caches(resolver)
|
||||
if Rails.env.staging?
|
||||
clear_dnssec_trusted_anchors_and_keys
|
||||
elsif stubber
|
||||
Dnsruby::Dnssec.add_trust_anchor(stubber.ds_rr)
|
||||
end
|
||||
recursor = Dnsruby::Recursor.new(resolver)
|
||||
recursor.dnssec = true
|
||||
recursor.query(name, 'A', 'IN').security_level
|
||||
end
|
||||
|
||||
def clear_dnssec_trusted_anchors_and_keys
|
||||
Dnsruby::Dnssec.clear_trust_anchors
|
||||
Dnsruby::Dnssec.clear_trusted_keys
|
||||
Dnsruby::Dnssec.add_trust_anchor(Dnsruby::RR.create(ENV['trusted_dnskey']))
|
||||
end
|
||||
|
||||
def statuses_uniqueness
|
||||
return if statuses.uniq == statuses
|
||||
errors.add(:statuses, :taken)
|
||||
|
|
|
@ -44,6 +44,8 @@ class Epp::Contact < Contact
|
|||
|
||||
def check_availability(codes, reg:)
|
||||
codes = [codes] if codes.is_a?(String)
|
||||
codes = codes.map { |c| c.include?(':') ? c : "#{reg}:#{c}" }
|
||||
|
||||
res = []
|
||||
codes.map { |c| c.include?(':') ? c : "#{reg}:#{c}" }.map { |c| c.strip.upcase }.each do |x|
|
||||
c = find_by_epp_code(x)
|
||||
|
|
|
@ -58,8 +58,8 @@ class Nameserver < ApplicationRecord
|
|||
end
|
||||
|
||||
def hostname=(hostname)
|
||||
self[:hostname] = SimpleIDN.to_unicode(hostname)
|
||||
self[:hostname_puny] = SimpleIDN.to_ascii(hostname)
|
||||
self[:hostname] = SimpleIDN.to_unicode(hostname).gsub(/\.+$/, '')
|
||||
self[:hostname_puny] = SimpleIDN.to_ascii(hostname).gsub(/\.+$/, '')
|
||||
end
|
||||
|
||||
class << self
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue