From fa73a0aacd27a42f3f7ef1dc372c62dff9df4f54 Mon Sep 17 00:00:00 2001 From: Artur Beljajev Date: Mon, 9 Sep 2019 16:14:20 +0300 Subject: [PATCH] Introduce module --- app/controllers/epp/base_controller.rb | 410 +++++++++++++++ app/controllers/epp/contacts_controller.rb | 378 +++++++------- app/controllers/epp/domains_controller.rb | 534 ++++++++++---------- app/controllers/epp/errors_controller.rb | 20 +- app/controllers/epp/keyrelays_controller.rb | 86 ++-- app/controllers/epp/polls_controller.rb | 92 ++-- app/controllers/epp/sessions_controller.rb | 235 ++++----- app/controllers/epp_controller.rb | 410 --------------- doc/controllers_brief.svg | 5 - doc/controllers_complete.svg | 29 -- 10 files changed, 1089 insertions(+), 1110 deletions(-) create mode 100644 app/controllers/epp/base_controller.rb delete mode 100644 app/controllers/epp_controller.rb diff --git a/app/controllers/epp/base_controller.rb b/app/controllers/epp/base_controller.rb new file mode 100644 index 000000000..bc19670fe --- /dev/null +++ b/app/controllers/epp/base_controller.rb @@ -0,0 +1,410 @@ +module Epp + class BaseController < ApplicationController + layout false + skip_before_action :verify_authenticity_token + + before_action :ensure_session_id_passed + before_action :generate_svtrid + before_action :latin_only + before_action :validate_against_schema + before_action :validate_request + before_action :update_epp_session, if: 'signed_in?' + + around_action :catch_epp_errors + + helper_method :current_user + helper_method :resource + + def validate_against_schema + return if ['hello', 'error', 'keyrelay'].include?(params[:action]) + schema.validate(params[:nokogiri_frame]).each do |error| + epp_errors << { + code: 2001, + msg: error + } + end + handle_errors and return if epp_errors.any? + end + + def catch_epp_errors + err = catch(:epp_error) do + yield + nil + end + return unless err + @errors = [err] + handle_errors + end + + rescue_from StandardError do |e| + @errors ||= [] + + if e.class == CanCan::AccessDenied + if @errors.blank? + @errors = [{ + msg: t('errors.messages.epp_authorization_error'), + code: '2201' + }] + end + else + if @errors.blank? + @errors = [{ + msg: 'Internal error.', + code: '2400' + }] + end + + if Rails.env.test? || Rails.env.development? + puts e.backtrace.reverse.join("\n") + puts "\n BACKTRACE REVERSED!\n" + puts "\n FROM-EPP-RESCUE: #{e.message}\n\n\n" + else + logger.error "FROM-EPP-RESCUE: #{e.message}" + logger.error e.backtrace.join("\n") + end + end + + render_epp_response '/epp/error' + end + + def schema + EPP_ALL_SCHEMA + end + + def generate_svtrid + @svTRID = "ccReg-#{format('%010d', rand(10 ** 10))}" + end + + def params_hash # TODO: THIS IS DEPRECATED AND WILL BE REMOVED IN FUTURE + @params_hash ||= Hash.from_xml(params[:frame]).with_indifferent_access + end + + def epp_session + EppSession.find_by(session_id: epp_session_id) + end + + def current_user + return unless signed_in? + epp_session.user + end + + # ERROR + RESPONSE HANDLING + def epp_errors + @errors ||= [] + end + + def handle_errors(obj = nil) + @errors ||= [] + + if obj + obj.construct_epp_errors + @errors += obj.errors[:epp_errors] + end + + if params[:parsed_frame].at_css('update') + @errors.each_with_index do |errors, index| + if errors[:code] == '2304' && + errors[:value].present? && + errors[:value][:val] == DomainStatus::SERVER_DELETE_PROHIBITED && + errors[:value][:obj] == 'status' + @errors[index][:value][:val] = DomainStatus::PENDING_UPDATE + end + end + end + + # for debugging + if @errors.blank? + @errors << { + code: '1', + msg: 'handle_errors was executed when there were actually no errors' + } + end + + @errors.uniq! + + logger.error "\nFOLLOWING ERRORS OCCURRED ON EPP QUERY:" + logger.error @errors.inspect + logger.error "\n" + + render_epp_response '/epp/error' + end + + def render_epp_response(*args) + @response = render_to_string(*args) + render xml: @response + write_to_epp_log + end + + # VALIDATION + def latin_only + return true if params['frame'].blank? + if params['frame'].match?(/\A[\p{Latin}\p{Z}\p{P}\p{S}\p{Cc}\p{Cf}\w_\'\+\-\.\(\)\/]*\Z/i) + return true + end + + epp_errors << { + msg: 'Parameter value policy error. Allowed only Latin characters.', + code: '2306' + } + + handle_errors and return false + end + + # VALIDATION + def validate_request + validation_method = "validate_#{params[:action]}" + return unless respond_to?(validation_method, true) + send(validation_method) + + # validate legal document's type here because it may be in most of the requests + @prefix = nil + if element_count('extdata > legalDocument').positive? + requires_attribute('extdata > legalDocument', 'type', values: LegalDocument::TYPES, policy: true) + end + + handle_errors and return if epp_errors.any? + end + + # let's follow grape's validations: https://github.com/intridea/grape/#parameter-validation-and-coercion + + # Adds error to epp_errors if element is missing or blank + # Returns last element of selectors if it exists + # + # requires 'transfer' + # + # TODO: Add possibility to pass validations / options in the method + + def requires(*selectors) + options = selectors.extract_options! + allow_blank = options[:allow_blank] ||= false # allow_blank is false by default + + el, missing = nil, nil + selectors.each do |selector| + full_selector = [@prefix, selector].compact.join(' ') + attr = selector.split('>').last.strip.underscore + el = params[:parsed_frame].css(full_selector).first + + if allow_blank + missing = el.nil? + else + missing = el.present? ? el.text.blank? : true + end + epp_errors << { + code: '2003', + msg: I18n.t('errors.messages.required_parameter_missing', key: "#{full_selector} [#{attr}]") + } if missing + end + + missing ? false : el # return last selector if it was present + end + + # Adds error to epp_errors if element or attribute is missing or attribute attribute is not one + # of the values + # + # requires_attribute 'transfer', 'op', values: %(approve, query, reject) + + def requires_attribute(element_selector, attribute_selector, options) + element = requires(element_selector, allow_blank: options[:allow_blank]) + return unless element + + attribute = element[attribute_selector] + + unless attribute + epp_errors << { + code: '2003', + msg: I18n.t('errors.messages.required_parameter_missing', key: attribute_selector) + } + return + end + + return if options[:values].include?(attribute) + + if options[:policy] + epp_errors << { + code: '2306', + msg: I18n.t('attribute_is_invalid', attribute: attribute_selector) + } + else + epp_errors << { + code: '2004', + msg: I18n.t('parameter_value_range_error', key: attribute_selector) + } + end + end + + def optional_attribute(element_selector, attribute_selector, options) + full_selector = [@prefix, element_selector].compact.join(' ') + element = params[:parsed_frame].css(full_selector).first + return unless element + + attribute = element[attribute_selector] + return if (attribute && options[:values].include?(attribute)) || !attribute + + epp_errors << { + code: '2306', + msg: I18n.t('attribute_is_invalid', attribute: attribute_selector) + } + end + + def exactly_one_of(*selectors) + full_selectors = create_full_selectors(*selectors) + return if element_count(*full_selectors, use_prefix: false) == 1 + + epp_errors << { + code: '2306', + msg: I18n.t(:exactly_one_parameter_required, params: full_selectors.join(' OR ')) + } + end + + def mutually_exclusive(*selectors) + full_selectors = create_full_selectors(*selectors) + return if element_count(*full_selectors, use_prefix: false) <= 1 + + epp_errors << { + code: '2306', + msg: I18n.t(:mutally_exclusive_params, params: full_selectors.join(', ')) + } + end + + def optional(selector, *validations) + full_selector = [@prefix, selector].compact.join(' ') + el = params[:parsed_frame].css(full_selector).first + return unless el&.text.present? + value = el.text + + validations.each do |x| + validator = "#{x.first[0]}_validator".camelize.constantize + err = validator.validate_epp(selector.split(' ').last, value) + epp_errors << err if err + end + end + + # Returns how many elements were present in the request + # if use_prefix is true, @prefix will be prepended to selectors e.g create > create > name + # default is true + # + # @prefix = 'create > create >' + # element_count 'name', 'registrar', use_prefix: false + # => 2 + + def element_count(*selectors) + options = selectors.extract_options! + use_prefix = options[:use_prefix] != false # use_prefix is true by default + + present_count = 0 + selectors.each do |selector| + full_selector = use_prefix ? [@prefix, selector].compact.join(' ') : selector + el = params[:parsed_frame].css(full_selector).first + present_count += 1 if el && el.text.present? + end + present_count + end + + def create_full_selectors(*selectors) + selectors.map { |x| [@prefix, x].compact.join(' ') } + end + + def xml_attrs_present?(ph, attributes) # TODO: THIS IS DEPRECATED AND WILL BE REMOVED IN FUTURE + attributes.each do |x| + epp_errors << { + code: '2003', + msg: I18n.t('errors.messages.required_parameter_missing', key: x.last) + } unless has_attribute(ph, x) + end + epp_errors.empty? + end + + def has_attribute(ph, path) # TODO: THIS IS DEPRECATED AND WILL BE REMOVED IN FUTURE + path.reduce(ph) do |location, key| + location.respond_to?(:keys) ? location[key] : nil + end + end + + def write_to_epp_log + request_command = params[:command] || params[:action] # error receives :command, other methods receive :action + frame = params[:raw_frame] || params[:frame] + + # filter pw + if request_command == 'login' && frame.present? + frame.gsub!(/pw>.+<\//, 'pw>[FILTERED]]+)>([^<])+<\/eis:legalDocument>/, "[FILTERED]") if frame.present? + + ApiLog::EppLog.create({ + request: trimmed_request, + request_command: request_command, + request_successful: epp_errors.empty?, + request_object: resource ? "#{params[:epp_object_type]}: #{resource.class} - #{resource.id} - #{resource.name}" : params[:epp_object_type], + response: @response, + api_user_name: @api_user.try(:username) || current_user.try(:username) || 'api-public', + api_user_registrar: @api_user.try(:registrar).try(:to_s) || current_user.try(:registrar).try(:to_s), + ip: request.ip, + uuid: request.uuid + }) + end + + def resource + name = self.class.to_s.sub("Epp::", "").sub("Controller", "").underscore.singularize + instance_variable_get("@#{name}") + end + + private + + def signed_in? + epp_session + end + + def epp_session_id + cookies[:session] # Passed by mod_epp https://github.com/mod-epp/mod-epp#requestscript-interface + end + + def ensure_session_id_passed + raise 'EPP session id is empty' unless epp_session_id.present? + end + + def update_epp_session + iptables_counter_update + + if session_timeout_reached? + @api_user = current_user # cache current_user for logging + epp_session.destroy + + epp_errors << { + msg: t('session_timeout'), + code: '2201' + } + + handle_errors and return + else + epp_session.update_column(:updated_at, Time.zone.now) + end + end + + def session_timeout_reached? + timeout = 5.minutes + epp_session.updated_at < (Time.zone.now - timeout) + end + + def iptables_counter_update + return if ENV['iptables_counter_enabled'].blank? && ENV['iptables_counter_enabled'] != 'true' + return if current_user.blank? + counter_update(current_user.registrar_code, ENV['iptables_server_ip']) + end + + def counter_update(registrar_code, ip) + counter_proc = "/proc/net/xt_recent/#{registrar_code}" + + begin + File.open(counter_proc, 'a') do |f| + f.puts "+#{ip}" + end + rescue Errno::ENOENT => e + logger.error "IPTABLES COUNTER UPDATE: cannot open #{counter_proc}: #{e}" + rescue Errno::EACCES => e + logger.error "IPTABLES COUNTER UPDATE: no permission #{counter_proc}: #{e}" + rescue IOError => e + logger.error "IPTABLES COUNTER UPDATE: cannot write #{ip} to #{counter_proc}: #{e}" + end + end + end +end diff --git a/app/controllers/epp/contacts_controller.rb b/app/controllers/epp/contacts_controller.rb index 7f28961f6..ff5dc982f 100644 --- a/app/controllers/epp/contacts_controller.rb +++ b/app/controllers/epp/contacts_controller.rb @@ -1,210 +1,212 @@ -class Epp::ContactsController < EppController - before_action :find_contact, only: [:info, :update, :delete] - before_action :find_password, only: [:info, :update, :delete] - helper_method :address_processing? +module Epp + class ContactsController < BaseController + before_action :find_contact, only: [:info, :update, :delete] + before_action :find_password, only: [:info, :update, :delete] + helper_method :address_processing? - def info - authorize! :info, @contact, @password - render_epp_response 'epp/contacts/info' - end + def info + authorize! :info, @contact, @password + render_epp_response 'epp/contacts/info' + end - def check - authorize! :check, Epp::Contact + def check + authorize! :check, Epp::Contact - ids = params[:parsed_frame].css('id').map(&:text) - @results = Epp::Contact.check_availability(ids) - render_epp_response '/epp/contacts/check' - end + ids = params[:parsed_frame].css('id').map(&:text) + @results = Epp::Contact.check_availability(ids) + render_epp_response '/epp/contacts/check' + end - def create - authorize! :create, Epp::Contact - frame = params[:parsed_frame] - @contact = Epp::Contact.new(frame, current_user.registrar) + def create + authorize! :create, Epp::Contact + frame = params[:parsed_frame] + @contact = Epp::Contact.new(frame, current_user.registrar) - @contact.add_legal_file_to_new(frame) - @contact.generate_code + @contact.add_legal_file_to_new(frame) + @contact.generate_code - if @contact.save - if !address_processing? && address_given? - @response_code = 1100 - @response_description = t('epp.contacts.completed_without_address') + if @contact.save + if !address_processing? && address_given? + @response_code = 1100 + @response_description = t('epp.contacts.completed_without_address') + else + @response_code = 1000 + @response_description = t('epp.contacts.completed') + end + + render_epp_response '/epp/contacts/save' else - @response_code = 1000 - @response_description = t('epp.contacts.completed') + handle_errors(@contact) + end + end + + def update + authorize! :update, @contact, @password + + frame = params[:parsed_frame] + + if @contact.update_attributes(frame, current_user) + if !address_processing? && address_given? + @response_code = 1100 + @response_description = t('epp.contacts.completed_without_address') + else + @response_code = 1000 + @response_description = t('epp.contacts.completed') + end + + render_epp_response 'epp/contacts/save' + else + handle_errors(@contact) + end + end + + def delete + authorize! :delete, @contact, @password + + if @contact.destroy_and_clean(params[:parsed_frame]) + render_epp_response '/epp/contacts/delete' + else + handle_errors(@contact) + end + end + + def renew + authorize! :renew, Epp::Contact + epp_errors << { code: '2101', msg: t(:'errors.messages.unimplemented_command') } + handle_errors + end + + private + + def find_password + @password = params[:parsed_frame].css('authInfo pw').text + end + + def find_contact + code = params[:parsed_frame].css('id').text.strip.upcase + + @contact = Epp::Contact.find_by_epp_code(code) + + if @contact.blank? + epp_errors << { + code: '2303', + msg: t('errors.messages.epp_obj_does_not_exist'), + value: { obj: 'id', val: code } + } + fail CanCan::AccessDenied + end + @contact + end + + # + # Validations + # + def validate_info + @prefix = 'info > info >' + requires 'id' + end + + def validate_check + @prefix = 'check > check >' + requires 'id' + end + + def validate_create + @prefix = 'create > create >' + + required_attributes = [ + 'postalInfo > name', + 'voice', + 'email' + ] + + address_attributes = [ + 'postalInfo > addr > street', + 'postalInfo > addr > city', + 'postalInfo > addr > pc', + 'postalInfo > addr > cc', + ] + + required_attributes.concat(address_attributes) if address_processing? + + requires(*required_attributes) + ident = params[:parsed_frame].css('ident') + + if ident.present? && ident.attr('type').blank? + epp_errors << { + code: '2003', + msg: I18n.t('errors.messages.required_ident_attribute_missing', key: 'type') + } end - render_epp_response '/epp/contacts/save' - else - handle_errors(@contact) - end - end - - def update - authorize! :update, @contact, @password - - frame = params[:parsed_frame] - - if @contact.update_attributes(frame, current_user) - if !address_processing? && address_given? - @response_code = 1100 - @response_description = t('epp.contacts.completed_without_address') - else - @response_code = 1000 - @response_description = t('epp.contacts.completed') + if ident.present? && ident.text != 'birthday' && ident.attr('cc').blank? + epp_errors << { + code: '2003', + msg: I18n.t('errors.messages.required_ident_attribute_missing', key: 'cc') + } end - - render_epp_response 'epp/contacts/save' - else - handle_errors(@contact) - end - end - - def delete - authorize! :delete, @contact, @password - - if @contact.destroy_and_clean(params[:parsed_frame]) - render_epp_response '/epp/contacts/delete' - else - handle_errors(@contact) - end - end - - def renew - authorize! :renew, Epp::Contact - epp_errors << { code: '2101', msg: t(:'errors.messages.unimplemented_command') } - handle_errors - end - - private - - def find_password - @password = params[:parsed_frame].css('authInfo pw').text - end - - def find_contact - code = params[:parsed_frame].css('id').text.strip.upcase - - @contact = Epp::Contact.find_by_epp_code(code) - - if @contact.blank? - epp_errors << { - code: '2303', - msg: t('errors.messages.epp_obj_does_not_exist'), - value: { obj: 'id', val: code } - } - fail CanCan::AccessDenied - end - @contact - end - - # - # Validations - # - def validate_info - @prefix = 'info > info >' - requires 'id' - end - - def validate_check - @prefix = 'check > check >' - requires 'id' - end - - def validate_create - @prefix = 'create > create >' - - required_attributes = [ - 'postalInfo > name', - 'voice', - 'email' - ] - - address_attributes = [ - 'postalInfo > addr > street', - 'postalInfo > addr > city', - 'postalInfo > addr > pc', - 'postalInfo > addr > cc', - ] - - required_attributes.concat(address_attributes) if address_processing? - - requires(*required_attributes) - ident = params[:parsed_frame].css('ident') - - if ident.present? && ident.attr('type').blank? - epp_errors << { - code: '2003', - msg: I18n.t('errors.messages.required_ident_attribute_missing', key: 'type') - } - end - - if ident.present? && ident.text != 'birthday' && ident.attr('cc').blank? - epp_errors << { - code: '2003', - msg: I18n.t('errors.messages.required_ident_attribute_missing', key: 'cc') - } - end - # if ident.present? && ident.attr('cc').blank? + # if ident.present? && ident.attr('cc').blank? # epp_errors << { - # code: '2003', - # msg: I18n.t('errors.messages.required_ident_attribute_missing', key: 'cc') + # code: '2003', + # msg: I18n.t('errors.messages.required_ident_attribute_missing', key: 'cc') # } - # end - contact_org_disabled - fax_disabled - status_editing_disabled - @prefix = nil - requires 'extension > extdata > ident' - end + # end + contact_org_disabled + fax_disabled + status_editing_disabled + @prefix = nil + requires 'extension > extdata > ident' + end - def validate_update - @prefix = 'update > update >' - contact_org_disabled - fax_disabled - status_editing_disabled - requires 'id' - @prefix = nil - end + def validate_update + @prefix = 'update > update >' + contact_org_disabled + fax_disabled + status_editing_disabled + requires 'id' + @prefix = nil + end - def validate_delete - @prefix = 'delete > delete >' - requires 'id' - @prefix = nil - end + def validate_delete + @prefix = 'delete > delete >' + requires 'id' + @prefix = nil + end - def contact_org_disabled - return true if ENV['contact_org_enabled'] == 'true' - return true if params[:parsed_frame].css('postalInfo org').text.blank? + def contact_org_disabled + return true if ENV['contact_org_enabled'] == 'true' + return true if params[:parsed_frame].css('postalInfo org').text.blank? - epp_errors << { - code: '2306', - msg: "#{I18n.t(:contact_org_error)}: postalInfo > org [org]" - } - end + epp_errors << { + code: '2306', + msg: "#{I18n.t(:contact_org_error)}: postalInfo > org [org]" + } + end - def fax_disabled - return true if ENV['fax_enabled'] == 'true' - return true if params[:parsed_frame].css('fax').text.blank? - epp_errors << { - code: '2306', - msg: "#{I18n.t(:contact_fax_error)}: fax [fax]" - } - end + def fax_disabled + return true if ENV['fax_enabled'] == 'true' + return true if params[:parsed_frame].css('fax').text.blank? + epp_errors << { + code: '2306', + msg: "#{I18n.t(:contact_fax_error)}: fax [fax]" + } + end - def status_editing_disabled - return true if Setting.client_status_editing_enabled - return true if params[:parsed_frame].css('status').empty? - epp_errors << { - code: '2306', - msg: "#{I18n.t(:client_side_status_editing_error)}: status [status]" - } - end + def status_editing_disabled + return true if Setting.client_status_editing_enabled + return true if params[:parsed_frame].css('status').empty? + epp_errors << { + code: '2306', + msg: "#{I18n.t(:client_side_status_editing_error)}: status [status]" + } + end - def address_given? - params[:parsed_frame].css('postalInfo addr').size != 0 - end + def address_given? + params[:parsed_frame].css('postalInfo addr').size != 0 + end - def address_processing? - Contact.address_processing? + def address_processing? + Contact.address_processing? + end end end diff --git a/app/controllers/epp/domains_controller.rb b/app/controllers/epp/domains_controller.rb index ecee7ae9d..64d4e972e 100644 --- a/app/controllers/epp/domains_controller.rb +++ b/app/controllers/epp/domains_controller.rb @@ -1,92 +1,118 @@ -class Epp::DomainsController < EppController - before_action :find_domain, only: %i[info renew update transfer delete] - before_action :find_password, only: %i[info update transfer delete] +module Epp + class DomainsController < BaseController + before_action :find_domain, only: %i[info renew update transfer delete] + before_action :find_password, only: %i[info update transfer delete] - def info - authorize! :info, @domain, @password + def info + authorize! :info, @domain, @password - @hosts = params[:parsed_frame].css('name').first['hosts'] || 'all' + @hosts = params[:parsed_frame].css('name').first['hosts'] || 'all' - case @hosts - when 'del' - @nameservers = @domain.delegated_nameservers.sort - when 'sub' - @nameservers = @domain.subordinate_nameservers.sort - when 'all' - @nameservers = @domain.nameservers.sort + case @hosts + when 'del' + @nameservers = @domain.delegated_nameservers.sort + when 'sub' + @nameservers = @domain.subordinate_nameservers.sort + when 'all' + @nameservers = @domain.nameservers.sort + end + + render_epp_response '/epp/domains/info' end - render_epp_response '/epp/domains/info' - end + def create + authorize! :create, Epp::Domain - def create - authorize! :create, Epp::Domain + if Domain.release_to_auction + request_domain_name = params[:parsed_frame].css('name').text.strip.downcase + domain_name = DNS::DomainName.new(SimpleIDN.to_unicode(request_domain_name)) - if Domain.release_to_auction - request_domain_name = params[:parsed_frame].css('name').text.strip.downcase - domain_name = DNS::DomainName.new(SimpleIDN.to_unicode(request_domain_name)) - - if domain_name.at_auction? - throw :epp_error, - code: '2306', - msg: 'Parameter value policy error: domain is at auction' - elsif domain_name.awaiting_payment? - throw :epp_error, - code: '2003', - msg: 'Required parameter missing; reserved>pw element required for reserved domains' - elsif domain_name.pending_registration? - registration_code = params[:parsed_frame].css('reserved > pw').text - - if registration_code.empty? + if domain_name.at_auction? + throw :epp_error, + code: '2306', + msg: 'Parameter value policy error: domain is at auction' + elsif domain_name.awaiting_payment? throw :epp_error, code: '2003', - msg: 'Required parameter missing; reserved>pw element is required' - end + msg: 'Required parameter missing; reserved>pw element required for reserved domains' + elsif domain_name.pending_registration? + registration_code = params[:parsed_frame].css('reserved > pw').text - unless domain_name.available_with_code?(registration_code) - throw :epp_error, - code: '2202', - msg: 'Invalid authorization information; invalid reserved>pw value' + if registration_code.empty? + throw :epp_error, + code: '2003', + msg: 'Required parameter missing; reserved>pw element is required' + end + + unless domain_name.available_with_code?(registration_code) + throw :epp_error, + code: '2202', + msg: 'Invalid authorization information; invalid reserved>pw value' + end + end + end + + @domain = Epp::Domain.new_from_epp(params[:parsed_frame], current_user) + handle_errors(@domain) and return if @domain.errors.any? + @domain.valid? + @domain.errors.delete(:name_dirty) if @domain.errors[:puny_label].any? + handle_errors(@domain) and return if @domain.errors.any? + handle_errors and return unless balance_ok?('create') # loads pricelist in this method + + ActiveRecord::Base.transaction do + @domain.add_legal_file_to_new(params[:parsed_frame]) + + if @domain.save # TODO: Maybe use validate: false here because we have already validated the domain? + current_user.registrar.debit!({ + sum: @domain_pricelist.price.amount, + description: "#{I18n.t('create')} #{@domain.name}", + activity_type: AccountActivity::CREATE, + price: @domain_pricelist + }) + + if Domain.release_to_auction && domain_name.pending_registration? + active_auction = Auction.find_by(domain: domain_name.to_s, + status: Auction.statuses[:payment_received]) + active_auction.domain_registered! + end + + render_epp_response '/epp/domains/create' + else + handle_errors(@domain) end end end - @domain = Epp::Domain.new_from_epp(params[:parsed_frame], current_user) - handle_errors(@domain) and return if @domain.errors.any? - @domain.valid? - @domain.errors.delete(:name_dirty) if @domain.errors[:puny_label].any? - handle_errors(@domain) and return if @domain.errors.any? - handle_errors and return unless balance_ok?('create') # loads pricelist in this method - - ActiveRecord::Base.transaction do - @domain.add_legal_file_to_new(params[:parsed_frame]) - - if @domain.save # TODO: Maybe use validate: false here because we have already validated the domain? - current_user.registrar.debit!({ - sum: @domain_pricelist.price.amount, - description: "#{I18n.t('create')} #{@domain.name}", - activity_type: AccountActivity::CREATE, - price: @domain_pricelist - }) - - if Domain.release_to_auction && domain_name.pending_registration? - active_auction = Auction.find_by(domain: domain_name.to_s, - status: Auction.statuses[:payment_received]) - active_auction.domain_registered! + def update + authorize! :update, @domain, @password + begin + if @domain.update(params[:parsed_frame], current_user) + if @domain.epp_pending_update.present? + render_epp_response '/epp/domains/success_pending' + else + render_epp_response '/epp/domains/success' + end + else + handle_errors(@domain) + end + rescue => e + if @domain.errors.any? + handle_errors(@domain) + else + throw e end - - render_epp_response '/epp/domains/create' - else - handle_errors(@domain) end end - end - def update - authorize! :update, @domain, @password - begin - if @domain.update(params[:parsed_frame], current_user) - if @domain.epp_pending_update.present? + def delete + authorize! :delete, @domain, @password + # all includes for bullet + @domain = Epp::Domain.where(id: @domain.id).includes(nameservers: :versions).first + + handle_errors(@domain) and return unless @domain.can_be_deleted? + + if @domain.epp_destroy(params[:parsed_frame], current_user.id) + if @domain.epp_pending_delete.present? render_epp_response '/epp/domains/success_pending' else render_epp_response '/epp/domains/success' @@ -94,227 +120,203 @@ class Epp::DomainsController < EppController else handle_errors(@domain) end - rescue => e - if @domain.errors.any? - handle_errors(@domain) - else - throw e - end end - end - def delete - authorize! :delete, @domain, @password - # all includes for bullet - @domain = Epp::Domain.where(id: @domain.id).includes(nameservers: :versions).first + def check + authorize! :check, Epp::Domain - handle_errors(@domain) and return unless @domain.can_be_deleted? - - if @domain.epp_destroy(params[:parsed_frame], current_user.id) - if @domain.epp_pending_delete.present? - render_epp_response '/epp/domains/success_pending' - else - render_epp_response '/epp/domains/success' - end - else - handle_errors(@domain) + domain_names = params[:parsed_frame].css('name').map(&:text) + @domains = Epp::Domain.check_availability(domain_names) + render_epp_response '/epp/domains/check' end - end - def check - authorize! :check, Epp::Domain + def renew + authorize! :renew, @domain - domain_names = params[:parsed_frame].css('name').map(&:text) - @domains = Epp::Domain.check_availability(domain_names) - render_epp_response '/epp/domains/check' - end + period_element = params[:parsed_frame].css('period').text + period = (period_element.to_i == 0) ? 1 : period_element.to_i + period_unit = Epp::Domain.parse_period_unit_from_frame(params[:parsed_frame]) || 'y' - def renew - authorize! :renew, @domain + balance_ok?('renew', period, period_unit) # loading pricelist - period_element = params[:parsed_frame].css('period').text - period = (period_element.to_i == 0) ? 1 : period_element.to_i - period_unit = Epp::Domain.parse_period_unit_from_frame(params[:parsed_frame]) || 'y' + begin + ActiveRecord::Base.transaction(isolation: :serializable) do + @domain.reload - balance_ok?('renew', period, period_unit) # loading pricelist + success = @domain.renew( + params[:parsed_frame].css('curExpDate').text, + period, period_unit + ) - begin - ActiveRecord::Base.transaction(isolation: :serializable) do - @domain.reload + if success + unless balance_ok?('renew', period, period_unit) + handle_errors + fail ActiveRecord::Rollback + end - success = @domain.renew( - params[:parsed_frame].css('curExpDate').text, - period, period_unit - ) + current_user.registrar.debit!({ + sum: @domain_pricelist.price.amount, + description: "#{I18n.t('renew')} #{@domain.name}", + activity_type: AccountActivity::RENEW, + price: @domain_pricelist + }) - if success - unless balance_ok?('renew', period, period_unit) - handle_errors - fail ActiveRecord::Rollback + render_epp_response '/epp/domains/renew' + else + handle_errors(@domain) end - - current_user.registrar.debit!({ - sum: @domain_pricelist.price.amount, - description: "#{I18n.t('renew')} #{@domain.name}", - activity_type: AccountActivity::RENEW, - price: @domain_pricelist - }) - - render_epp_response '/epp/domains/renew' - else - handle_errors(@domain) end + rescue ActiveRecord::StatementInvalid => e + sleep rand / 100 + retry end - rescue ActiveRecord::StatementInvalid => e - sleep rand / 100 - retry - end - end - - def transfer - authorize! :transfer, @domain, @password - action = params[:parsed_frame].css('transfer').first[:op] - - if @domain.non_transferable? - throw :epp_error, { - code: '2304', - msg: I18n.t(:object_status_prohibits_operation) - } end - @domain_transfer = @domain.transfer(params[:parsed_frame], action, current_user) + def transfer + authorize! :transfer, @domain, @password + action = params[:parsed_frame].css('transfer').first[:op] - if @domain_transfer - render_epp_response '/epp/domains/transfer' - else - epp_errors << { - code: '2303', - msg: I18n.t('no_transfers_found') - } - handle_errors - end - end + if @domain.non_transferable? + throw :epp_error, { + code: '2304', + msg: I18n.t(:object_status_prohibits_operation) + } + end - private + @domain_transfer = @domain.transfer(params[:parsed_frame], action, current_user) - def validate_info - @prefix = 'info > info >' - requires('name') - optional_attribute 'name', 'hosts', values: %(all, sub, del, none) - end - - def validate_create - if Domain.nameserver_required? - @prefix = 'create > create >' - requires 'name', 'ns', 'registrant', 'ns > hostAttr' - end - - @prefix = 'extension > create >' - mutually_exclusive 'keyData', 'dsData' - - @prefix = nil - requires 'extension > extdata > legalDocument' - - optional_attribute 'period', 'unit', values: %w(d m y) - - status_editing_disabled - end - - def validate_update - if element_count('update > chg > registrant') > 0 - requires 'extension > extdata > legalDocument' - end - - @prefix = 'update > update >' - requires 'name' - - status_editing_disabled - end - - def validate_delete - requires 'extension > extdata > legalDocument' - - @prefix = 'delete > delete >' - requires 'name' - end - - def validate_check - @prefix = 'check > check >' - requires('name') - end - - def validate_renew - @prefix = 'renew > renew >' - requires 'name', 'curExpDate' - - optional_attribute 'period', 'unit', values: %w(d m y) - end - - def validate_transfer - # period element is disabled for now - if params[:parsed_frame].css('period').any? - epp_errors << { - code: '2307', - msg: I18n.t(:unimplemented_object_service), - value: { obj: 'period' } - } - end - - requires 'transfer > transfer' - - @prefix = 'transfer > transfer >' - requires 'name' - - @prefix = nil - requires_attribute 'transfer', 'op', values: %(approve, query, reject, request, cancel) - end - - def find_domain - domain_name = params[:parsed_frame].css('name').text.strip.downcase - @domain = Epp::Domain.find_by_idn domain_name - - unless @domain - epp_errors << { - code: '2303', - msg: I18n.t('errors.messages.epp_domain_not_found'), - value: { obj: 'name', val: domain_name } - } - fail CanCan::AccessDenied - end - - @domain - end - - def find_password - @password = params[:parsed_frame].css('authInfo pw').text - end - - def status_editing_disabled - return true if Setting.client_status_editing_enabled - return true if params[:parsed_frame].css('status').empty? - epp_errors << { - code: '2306', - msg: "#{I18n.t(:client_side_status_editing_error)}: status [status]" - } - end - - def balance_ok?(operation, period = nil, unit = nil) - @domain_pricelist = @domain.pricelist(operation, period.try(:to_i), unit) - if @domain_pricelist.try(:price) # checking if price list is not found - if current_user.registrar.balance < @domain_pricelist.price.amount + if @domain_transfer + render_epp_response '/epp/domains/transfer' + else epp_errors << { + code: '2303', + msg: I18n.t('no_transfers_found') + } + handle_errors + end + end + + private + + def validate_info + @prefix = 'info > info >' + requires('name') + optional_attribute 'name', 'hosts', values: %(all, sub, del, none) + end + + def validate_create + if Domain.nameserver_required? + @prefix = 'create > create >' + requires 'name', 'ns', 'registrant', 'ns > hostAttr' + end + + @prefix = 'extension > create >' + mutually_exclusive 'keyData', 'dsData' + + @prefix = nil + requires 'extension > extdata > legalDocument' + + optional_attribute 'period', 'unit', values: %w(d m y) + + status_editing_disabled + end + + def validate_update + if element_count('update > chg > registrant') > 0 + requires 'extension > extdata > legalDocument' + end + + @prefix = 'update > update >' + requires 'name' + + status_editing_disabled + end + + def validate_delete + requires 'extension > extdata > legalDocument' + + @prefix = 'delete > delete >' + requires 'name' + end + + def validate_check + @prefix = 'check > check >' + requires('name') + end + + def validate_renew + @prefix = 'renew > renew >' + requires 'name', 'curExpDate' + + optional_attribute 'period', 'unit', values: %w(d m y) + end + + def validate_transfer + # period element is disabled for now + if params[:parsed_frame].css('period').any? + epp_errors << { + code: '2307', + msg: I18n.t(:unimplemented_object_service), + value: { obj: 'period' } + } + end + + requires 'transfer > transfer' + + @prefix = 'transfer > transfer >' + requires 'name' + + @prefix = nil + requires_attribute 'transfer', 'op', values: %(approve, query, reject, request, cancel) + end + + def find_domain + domain_name = params[:parsed_frame].css('name').text.strip.downcase + @domain = Epp::Domain.find_by_idn domain_name + + unless @domain + epp_errors << { + code: '2303', + msg: I18n.t('errors.messages.epp_domain_not_found'), + value: { obj: 'name', val: domain_name } + } + fail CanCan::AccessDenied + end + + @domain + end + + def find_password + @password = params[:parsed_frame].css('authInfo pw').text + end + + def status_editing_disabled + return true if Setting.client_status_editing_enabled + return true if params[:parsed_frame].css('status').empty? + epp_errors << { + code: '2306', + msg: "#{I18n.t(:client_side_status_editing_error)}: status [status]" + } + end + + def balance_ok?(operation, period = nil, unit = nil) + @domain_pricelist = @domain.pricelist(operation, period.try(:to_i), unit) + if @domain_pricelist.try(:price) # checking if price list is not found + if current_user.registrar.balance < @domain_pricelist.price.amount + epp_errors << { code: '2104', msg: I18n.t('billing_failure_credit_balance_low') + } + return false + end + else + epp_errors << { + code: '2104', + msg: I18n.t(:active_price_missing_for_this_operation) } return false end - else - epp_errors << { - code: '2104', - msg: I18n.t(:active_price_missing_for_this_operation) - } - return false + true end - true end end diff --git a/app/controllers/epp/errors_controller.rb b/app/controllers/epp/errors_controller.rb index 05618f5f7..2711b5907 100644 --- a/app/controllers/epp/errors_controller.rb +++ b/app/controllers/epp/errors_controller.rb @@ -1,13 +1,15 @@ -class Epp::ErrorsController < EppController - skip_authorization_check +module Epp + class ErrorsController < BaseController + skip_authorization_check - def error - epp_errors << { code: params[:code], msg: params[:msg] } - render_epp_response '/epp/error' - end + def error + epp_errors << { code: params[:code], msg: params[:msg] } + render_epp_response '/epp/error' + end - def not_found - epp_errors << { code: 2400, msg: t(:could_not_determine_object_type_check_xml_format_and_namespaces) } - render_epp_response '/epp/error' + def not_found + epp_errors << { code: 2400, msg: t(:could_not_determine_object_type_check_xml_format_and_namespaces) } + render_epp_response '/epp/error' + end end end diff --git a/app/controllers/epp/keyrelays_controller.rb b/app/controllers/epp/keyrelays_controller.rb index c4d7ef395..b4159e796 100644 --- a/app/controllers/epp/keyrelays_controller.rb +++ b/app/controllers/epp/keyrelays_controller.rb @@ -1,61 +1,63 @@ -class Epp::KeyrelaysController < EppController - skip_authorization_check # TODO: move authorization under ability +module Epp + class KeyrelaysController < BaseController + skip_authorization_check # TODO: move authorization under ability - def keyrelay - # keyrelay temp turned off - @domain = find_domain + def keyrelay + # keyrelay temp turned off + @domain = find_domain - handle_errors(@domain) and return unless @domain - handle_errors(@domain) and return unless @domain.authenticate(params[:parsed_frame].css('pw').text) - handle_errors(@domain) and return unless @domain.keyrelay(params[:parsed_frame], current_user.registrar) + handle_errors(@domain) and return unless @domain + handle_errors(@domain) and return unless @domain.authenticate(params[:parsed_frame].css('pw').text) + handle_errors(@domain) and return unless @domain.keyrelay(params[:parsed_frame], current_user.registrar) - render_epp_response '/epp/shared/success' - end + render_epp_response '/epp/shared/success' + end - private + private - def validate_keyrelay - @prefix = 'keyrelay >' + def validate_keyrelay + @prefix = 'keyrelay >' - requires( - 'name', - 'keyData', 'keyData > pubKey', 'keyData > flags', 'keyData > protocol', 'keyData > alg', - 'authInfo', 'authInfo > pw' - ) + requires( + 'name', + 'keyData', 'keyData > pubKey', 'keyData > flags', 'keyData > protocol', 'keyData > alg', + 'authInfo', 'authInfo > pw' + ) - optional 'expiry > relative', duration_iso8601: true - optional 'expiry > absolute', date_time_iso8601: true + optional 'expiry > relative', duration_iso8601: true + optional 'expiry > absolute', date_time_iso8601: true - exactly_one_of 'expiry > relative', 'expiry > absolute' - end + exactly_one_of 'expiry > relative', 'expiry > absolute' + end - def find_domain - domain_name = params[:parsed_frame].css('name').text.strip.downcase + def find_domain + domain_name = params[:parsed_frame].css('name').text.strip.downcase - # keyrelay temp turned off - epp_errors << { - code: '2307', - msg: I18n.t(:unimplemented_object_service), - value: { obj: 'name', val: domain_name } - } - nil - # end of keyrelay temp turned off + # keyrelay temp turned off + epp_errors << { + code: '2307', + msg: I18n.t(:unimplemented_object_service), + value: { obj: 'name', val: domain_name } + } + nil + # end of keyrelay temp turned off - # domain = Epp::Domain.includes(:registrant).find_by(name: domain_name) + # domain = Epp::Domain.includes(:registrant).find_by(name: domain_name) - # unless domain + # unless domain # epp_errors << { - # code: '2303', - # msg: I18n.t('errors.messages.epp_domain_not_found'), - # value: { obj: 'name', val: domain_name } + # code: '2303', + # msg: I18n.t('errors.messages.epp_domain_not_found'), + # value: { obj: 'name', val: domain_name } # } # return nil - # end + # end - # domain - end + # domain + end - def resource - @domain + def resource + @domain + end end end diff --git a/app/controllers/epp/polls_controller.rb b/app/controllers/epp/polls_controller.rb index 57961eb82..c971d7d85 100644 --- a/app/controllers/epp/polls_controller.rb +++ b/app/controllers/epp/polls_controller.rb @@ -1,61 +1,63 @@ -class Epp::PollsController < EppController - skip_authorization_check # TODO: move authorization under ability +module Epp + class PollsController < BaseController + skip_authorization_check # TODO: move authorization under ability - def poll - req_poll if params[:parsed_frame].css('poll').first['op'] == 'req' - ack_poll if params[:parsed_frame].css('poll').first['op'] == 'ack' - end + def poll + req_poll if params[:parsed_frame].css('poll').first['op'] == 'req' + ack_poll if params[:parsed_frame].css('poll').first['op'] == 'ack' + end - private + private - def req_poll - @notification = current_user.unread_notifications.order('created_at DESC').take + def req_poll + @notification = current_user.unread_notifications.order('created_at DESC').take - render_epp_response 'epp/poll/poll_no_messages' and return unless @notification - if @notification.attached_obj_type && @notification.attached_obj_id - begin - @object = Object.const_get(@notification.attached_obj_type).find(@notification.attached_obj_id) - rescue => problem - # the data model might be inconsistent; or ... - # this could happen if the registrar does not dequeue messages, and then the domain was deleted + render_epp_response 'epp/poll/poll_no_messages' and return unless @notification + if @notification.attached_obj_type && @notification.attached_obj_id + begin + @object = Object.const_get(@notification.attached_obj_type).find(@notification.attached_obj_id) + rescue => problem + # the data model might be inconsistent; or ... + # this could happen if the registrar does not dequeue messages, and then the domain was deleted - # SELECT messages.id, domains.name, messages.body FROM messages LEFT OUTER - # JOIN domains ON attached_obj_id::INTEGER = domains.id - # WHERE attached_obj_type = 'Epp::Domain' AND name IS NULL; + # SELECT messages.id, domains.name, messages.body FROM messages LEFT OUTER + # JOIN domains ON attached_obj_id::INTEGER = domains.id + # WHERE attached_obj_type = 'Epp::Domain' AND name IS NULL; - Rails.logger.error 'orphan message, error ignored: ' + problem.to_s - # now we should dequeue or delete the messages avoid duplicate log alarms + Rails.logger.error 'orphan message, error ignored: ' + problem.to_s + # now we should dequeue or delete the messages avoid duplicate log alarms + end + end + + if @notification.attached_obj_type == 'Keyrelay' + render_epp_response 'epp/poll/poll_keyrelay' + else + render_epp_response 'epp/poll/poll_req' end end - if @notification.attached_obj_type == 'Keyrelay' - render_epp_response 'epp/poll/poll_keyrelay' - else - render_epp_response 'epp/poll/poll_req' - end - end + def ack_poll + @notification = current_user.unread_notifications.find_by(id: params[:parsed_frame].css('poll').first['msgID']) - def ack_poll - @notification = current_user.unread_notifications.find_by(id: params[:parsed_frame].css('poll').first['msgID']) + unless @notification + epp_errors << { + code: '2303', + msg: I18n.t('message_was_not_found'), + value: { obj: 'msgID', val: params[:parsed_frame].css('poll').first['msgID'] } + } + handle_errors and return + end - unless @notification - epp_errors << { - code: '2303', - msg: I18n.t('message_was_not_found'), - value: { obj: 'msgID', val: params[:parsed_frame].css('poll').first['msgID'] } - } - handle_errors and return + handle_errors(@notification) and return unless @notification.mark_as_read + render_epp_response 'epp/poll/poll_ack' end - handle_errors(@notification) and return unless @notification.mark_as_read - render_epp_response 'epp/poll/poll_ack' - end + def validate_poll + requires_attribute 'poll', 'op', values: %(ack req), allow_blank: true + end - def validate_poll - requires_attribute 'poll', 'op', values: %(ack req), allow_blank: true - end - - def resource - @notification + def resource + @notification + end end end diff --git a/app/controllers/epp/sessions_controller.rb b/app/controllers/epp/sessions_controller.rb index 449d1feef..2175f7281 100644 --- a/app/controllers/epp/sessions_controller.rb +++ b/app/controllers/epp/sessions_controller.rb @@ -1,135 +1,138 @@ -class Epp::SessionsController < EppController - skip_authorization_check only: [:hello, :login, :logout] +module Epp + class SessionsController < BaseController + skip_authorization_check only: [:hello, :login, :logout] - def hello - render_epp_response('greeting') - end - - def login - success = true - @api_user = ApiUser.find_by(login_params) - - webclient_request = ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip) - if webclient_request && !Rails.env.test? && !Rails.env.development? - client_md5 = Certificate.parse_md_from_string(request.env['HTTP_SSL_CLIENT_CERT']) - server_md5 = Certificate.parse_md_from_string(File.read(ENV['cert_path'])) - if client_md5 != server_md5 - epp_errors << { - msg: 'Authentication error; server closing connection (certificate is not valid)', - code: '2501' - } - - success = false - end + def hello + render_epp_response('greeting') end - if !Rails.env.development? && (!webclient_request && @api_user) - unless @api_user.api_pki_ok?(request.env['HTTP_SSL_CLIENT_CERT'], request.env['HTTP_SSL_CLIENT_S_DN_CN']) - epp_errors << { - msg: 'Authentication error; server closing connection (certificate is not valid)', - code: '2501' - } + def login + success = true + @api_user = ApiUser.find_by(login_params) - success = false - end - end + webclient_request = ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip) + if webclient_request && !Rails.env.test? && !Rails.env.development? + client_md5 = Certificate.parse_md_from_string(request.env['HTTP_SSL_CLIENT_CERT']) + server_md5 = Certificate.parse_md_from_string(File.read(ENV['cert_path'])) + if client_md5 != server_md5 + epp_errors << { + msg: 'Authentication error; server closing connection (certificate is not valid)', + code: '2501' + } - if success && !@api_user - epp_errors << { - msg: 'Authentication error; server closing connection (API user not found)', - code: '2501' - } - - success = false - end - - if success && !@api_user.try(:active) - epp_errors << { - msg: 'Authentication error; server closing connection (API user is not active)', - code: '2501' - } - - success = false - end - - if success && @api_user.cannot?(:create, :epp_login) - epp_errors << { - msg: 'Authentication error; server closing connection (API user does not have epp role)', - code: '2501' - } - - success = false - end - - if success && !ip_white? - epp_errors << { - msg: 'Authentication error; server closing connection (IP is not whitelisted)', - code: '2501' - } - - success = false - end - - if success && EppSession.limit_reached?(@api_user.registrar) - epp_errors << { - msg: 'Authentication error; server closing connection (connection limit reached)', - code: '2501' - } - - success = false - end - - if success - if params[:parsed_frame].css('newPW').first - unless @api_user.update(plain_text_password: params[:parsed_frame].css('newPW').first.text) - handle_errors(@api_user) and return + success = false end end - epp_session = EppSession.new - epp_session.session_id = epp_session_id - epp_session.user = @api_user - epp_session.save! - render_epp_response('login_success') - else - handle_errors + if !Rails.env.development? && (!webclient_request && @api_user) + unless @api_user.api_pki_ok?(request.env['HTTP_SSL_CLIENT_CERT'], request.env['HTTP_SSL_CLIENT_S_DN_CN']) + epp_errors << { + msg: 'Authentication error; server closing connection (certificate is not valid)', + code: '2501' + } + + success = false + end + end + + if success && !@api_user + epp_errors << { + msg: 'Authentication error; server closing connection (API user not found)', + code: '2501' + } + + success = false + end + + if success && !@api_user.try(:active) + epp_errors << { + msg: 'Authentication error; server closing connection (API user is not active)', + code: '2501' + } + + success = false + end + + if success && @api_user.cannot?(:create, :epp_login) + epp_errors << { + msg: 'Authentication error; server closing connection (API user does not have epp role)', + code: '2501' + } + + success = false + end + + if success && !ip_white? + epp_errors << { + msg: 'Authentication error; server closing connection (IP is not whitelisted)', + code: '2501' + } + + success = false + end + + if success && EppSession.limit_reached?(@api_user.registrar) + epp_errors << { + msg: 'Authentication error; server closing connection (connection limit reached)', + code: '2501' + } + + success = false + end + + if success + if params[:parsed_frame].css('newPW').first + unless @api_user.update(plain_text_password: params[:parsed_frame].css('newPW').first.text) + handle_errors(@api_user) and return + end + end + + epp_session = EppSession.new + epp_session.session_id = epp_session_id + epp_session.user = @api_user + epp_session.save! + render_epp_response('login_success') + else + handle_errors end end - def ip_white? - webclient_request = ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip) - return true if webclient_request - if @api_user - return false unless @api_user.registrar.api_ip_white?(request.ip) - end - true - end - - def logout - unless signed_in? - epp_errors << { - code: 2201, - msg: 'Authorization error' - } - handle_errors - return + def ip_white? + webclient_request = ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip) + return true if webclient_request + if @api_user + return false unless @api_user.registrar.api_ip_white?(request.ip) + end + true end - @api_user = current_user # cache current_user for logging - epp_session.destroy - render_epp_response('logout') + def logout + unless signed_in? + epp_errors << { + code: 2201, + msg: 'Authorization error' + } + handle_errors + return + end + + @api_user = current_user # cache current_user for logging + epp_session.destroy + render_epp_response('logout') end - ### HELPER METHODS ### + ### HELPER METHODS ### - def login_params - user = params[:parsed_frame].css('clID').first.text - pw = params[:parsed_frame].css('pw').first.text - { username: user, plain_text_password: pw } - end + def login_params + user = params[:parsed_frame].css('clID').first.text + pw = params[:parsed_frame].css('pw').first.text + { username: user, plain_text_password: pw } + end - private - def resource - @api_user + private + + def resource + @api_user + end end end diff --git a/app/controllers/epp_controller.rb b/app/controllers/epp_controller.rb deleted file mode 100644 index 15af1cd73..000000000 --- a/app/controllers/epp_controller.rb +++ /dev/null @@ -1,410 +0,0 @@ -class EppController < ApplicationController - layout false - skip_before_action :verify_authenticity_token - - before_action :ensure_session_id_passed - before_action :generate_svtrid - before_action :latin_only - before_action :validate_against_schema - before_action :validate_request - before_action :update_epp_session, if: 'signed_in?' - - around_action :catch_epp_errors - - helper_method :current_user - helper_method :resource - - def validate_against_schema - return if ['hello', 'error', 'keyrelay'].include?(params[:action]) - schema.validate(params[:nokogiri_frame]).each do |error| - epp_errors << { - code: 2001, - msg: error - } - end - handle_errors and return if epp_errors.any? - end - - - def catch_epp_errors - err = catch(:epp_error) do - yield - nil - end - return unless err - @errors = [err] - handle_errors - end - - - rescue_from StandardError do |e| - @errors ||= [] - - if e.class == CanCan::AccessDenied - if @errors.blank? - @errors = [{ - msg: t('errors.messages.epp_authorization_error'), - code: '2201' - }] - end - else - if @errors.blank? - @errors = [{ - msg: 'Internal error.', - code: '2400' - }] - end - - if Rails.env.test? || Rails.env.development? - puts e.backtrace.reverse.join("\n") - puts "\n BACKTRACE REVERSED!\n" - puts "\n FROM-EPP-RESCUE: #{e.message}\n\n\n" - else - logger.error "FROM-EPP-RESCUE: #{e.message}" - logger.error e.backtrace.join("\n") - end - end - - render_epp_response '/epp/error' - end - - def schema - EPP_ALL_SCHEMA - end - - def generate_svtrid - @svTRID = "ccReg-#{format('%010d', rand(10**10))}" - end - - def params_hash # TODO: THIS IS DEPRECATED AND WILL BE REMOVED IN FUTURE - @params_hash ||= Hash.from_xml(params[:frame]).with_indifferent_access - end - - def epp_session - EppSession.find_by(session_id: epp_session_id) - end - - def current_user - return unless signed_in? - epp_session.user - end - - # ERROR + RESPONSE HANDLING - def epp_errors - @errors ||= [] - end - - def handle_errors(obj = nil) - @errors ||= [] - - if obj - obj.construct_epp_errors - @errors += obj.errors[:epp_errors] - end - - if params[:parsed_frame].at_css('update') - @errors.each_with_index do |errors, index| - if errors[:code] == '2304' && - errors[:value].present? && - errors[:value][:val] == DomainStatus::SERVER_DELETE_PROHIBITED && - errors[:value][:obj] == 'status' - @errors[index][:value][:val] = DomainStatus::PENDING_UPDATE - end - end - end - - # for debugging - if @errors.blank? - @errors << { - code: '1', - msg: 'handle_errors was executed when there were actually no errors' - } - end - - @errors.uniq! - - logger.error "\nFOLLOWING ERRORS OCCURRED ON EPP QUERY:" - logger.error @errors.inspect - logger.error "\n" - - render_epp_response '/epp/error' - end - - def render_epp_response(*args) - @response = render_to_string(*args) - render xml: @response - write_to_epp_log - end - - # VALIDATION - def latin_only - return true if params['frame'].blank? - if params['frame'].match?(/\A[\p{Latin}\p{Z}\p{P}\p{S}\p{Cc}\p{Cf}\w_\'\+\-\.\(\)\/]*\Z/i) - return true - end - - epp_errors << { - msg: 'Parameter value policy error. Allowed only Latin characters.', - code: '2306' - } - - handle_errors and return false - end - - # VALIDATION - def validate_request - validation_method = "validate_#{params[:action]}" - return unless respond_to?(validation_method, true) - send(validation_method) - - # validate legal document's type here because it may be in most of the requests - @prefix = nil - if element_count('extdata > legalDocument').positive? - requires_attribute('extdata > legalDocument', 'type', values: LegalDocument::TYPES, policy: true) - end - - handle_errors and return if epp_errors.any? - end - - # let's follow grape's validations: https://github.com/intridea/grape/#parameter-validation-and-coercion - - # Adds error to epp_errors if element is missing or blank - # Returns last element of selectors if it exists - # - # requires 'transfer' - # - # TODO: Add possibility to pass validations / options in the method - - def requires(*selectors) - options = selectors.extract_options! - allow_blank = options[:allow_blank] ||= false # allow_blank is false by default - - el, missing = nil, nil - selectors.each do |selector| - full_selector = [@prefix, selector].compact.join(' ') - attr = selector.split('>').last.strip.underscore - el = params[:parsed_frame].css(full_selector).first - - if allow_blank - missing = el.nil? - else - missing = el.present? ? el.text.blank? : true - end - epp_errors << { - code: '2003', - msg: I18n.t('errors.messages.required_parameter_missing', key: "#{full_selector} [#{attr}]") - } if missing - end - - missing ? false : el # return last selector if it was present - end - - # Adds error to epp_errors if element or attribute is missing or attribute attribute is not one - # of the values - # - # requires_attribute 'transfer', 'op', values: %(approve, query, reject) - - def requires_attribute(element_selector, attribute_selector, options) - element = requires(element_selector, allow_blank: options[:allow_blank]) - return unless element - - attribute = element[attribute_selector] - - unless attribute - epp_errors << { - code: '2003', - msg: I18n.t('errors.messages.required_parameter_missing', key: attribute_selector) - } - return - end - - return if options[:values].include?(attribute) - - if options[:policy] - epp_errors << { - code: '2306', - msg: I18n.t('attribute_is_invalid', attribute: attribute_selector) - } - else - epp_errors << { - code: '2004', - msg: I18n.t('parameter_value_range_error', key: attribute_selector) - } - end - end - - def optional_attribute(element_selector, attribute_selector, options) - full_selector = [@prefix, element_selector].compact.join(' ') - element = params[:parsed_frame].css(full_selector).first - return unless element - - attribute = element[attribute_selector] - return if (attribute && options[:values].include?(attribute)) || !attribute - - epp_errors << { - code: '2306', - msg: I18n.t('attribute_is_invalid', attribute: attribute_selector) - } - end - - def exactly_one_of(*selectors) - full_selectors = create_full_selectors(*selectors) - return if element_count(*full_selectors, use_prefix: false) == 1 - - epp_errors << { - code: '2306', - msg: I18n.t(:exactly_one_parameter_required, params: full_selectors.join(' OR ')) - } - end - - def mutually_exclusive(*selectors) - full_selectors = create_full_selectors(*selectors) - return if element_count(*full_selectors, use_prefix: false) <= 1 - - epp_errors << { - code: '2306', - msg: I18n.t(:mutally_exclusive_params, params: full_selectors.join(', ')) - } - end - - def optional(selector, *validations) - full_selector = [@prefix, selector].compact.join(' ') - el = params[:parsed_frame].css(full_selector).first - return unless el&.text.present? - value = el.text - - validations.each do |x| - validator = "#{x.first[0]}_validator".camelize.constantize - err = validator.validate_epp(selector.split(' ').last, value) - epp_errors << err if err - end - end - - # Returns how many elements were present in the request - # if use_prefix is true, @prefix will be prepended to selectors e.g create > create > name - # default is true - # - # @prefix = 'create > create >' - # element_count 'name', 'registrar', use_prefix: false - # => 2 - - def element_count(*selectors) - options = selectors.extract_options! - use_prefix = options[:use_prefix] != false # use_prefix is true by default - - present_count = 0 - selectors.each do |selector| - full_selector = use_prefix ? [@prefix, selector].compact.join(' ') : selector - el = params[:parsed_frame].css(full_selector).first - present_count += 1 if el && el.text.present? - end - present_count - end - - def create_full_selectors(*selectors) - selectors.map { |x| [@prefix, x].compact.join(' ') } - end - - def xml_attrs_present?(ph, attributes) # TODO: THIS IS DEPRECATED AND WILL BE REMOVED IN FUTURE - attributes.each do |x| - epp_errors << { - code: '2003', - msg: I18n.t('errors.messages.required_parameter_missing', key: x.last) - } unless has_attribute(ph, x) - end - epp_errors.empty? - end - - def has_attribute(ph, path) # TODO: THIS IS DEPRECATED AND WILL BE REMOVED IN FUTURE - path.reduce(ph) do |location, key| - location.respond_to?(:keys) ? location[key] : nil - end - end - - def write_to_epp_log - request_command = params[:command] || params[:action] # error receives :command, other methods receive :action - frame = params[:raw_frame] || params[:frame] - - # filter pw - if request_command == 'login' && frame.present? - frame.gsub!(/pw>.+<\//, 'pw>[FILTERED]]+)>([^<])+<\/eis:legalDocument>/, "[FILTERED]") if frame.present? - - ApiLog::EppLog.create({ - request: trimmed_request, - request_command: request_command, - request_successful: epp_errors.empty?, - request_object: resource ? "#{params[:epp_object_type]}: #{resource.class} - #{resource.id} - #{resource.name}" : params[:epp_object_type], - response: @response, - api_user_name: @api_user.try(:username) || current_user.try(:username) || 'api-public', - api_user_registrar: @api_user.try(:registrar).try(:to_s) || current_user.try(:registrar).try(:to_s), - ip: request.ip, - uuid: request.uuid - }) - end - - def resource - name = self.class.to_s.sub("Epp::","").sub("Controller","").underscore.singularize - instance_variable_get("@#{name}") - end - - private - - def signed_in? - epp_session - end - - def epp_session_id - cookies[:session] # Passed by mod_epp https://github.com/mod-epp/mod-epp#requestscript-interface - end - - def ensure_session_id_passed - raise 'EPP session id is empty' unless epp_session_id.present? - end - - def update_epp_session - iptables_counter_update - - if session_timeout_reached? - @api_user = current_user # cache current_user for logging - epp_session.destroy - - epp_errors << { - msg: t('session_timeout'), - code: '2201' - } - - handle_errors and return - else - epp_session.update_column(:updated_at, Time.zone.now) - end - end - - def session_timeout_reached? - timeout = 5.minutes - epp_session.updated_at < (Time.zone.now - timeout) - end - - def iptables_counter_update - return if ENV['iptables_counter_enabled'].blank? && ENV['iptables_counter_enabled'] != 'true' - return if current_user.blank? - counter_update(current_user.registrar_code, ENV['iptables_server_ip']) - end - - def counter_update(registrar_code, ip) - counter_proc = "/proc/net/xt_recent/#{registrar_code}" - - begin - File.open(counter_proc, 'a') do |f| - f.puts "+#{ip}" - end - rescue Errno::ENOENT => e - logger.error "IPTABLES COUNTER UPDATE: cannot open #{counter_proc}: #{e}" - rescue Errno::EACCES => e - logger.error "IPTABLES COUNTER UPDATE: no permission #{counter_proc}: #{e}" - rescue IOError => e - logger.error "IPTABLES COUNTER UPDATE: cannot write #{ip} to #{counter_proc}: #{e}" - end - end -end diff --git a/doc/controllers_brief.svg b/doc/controllers_brief.svg index 63fc964ba..1ae503845 100644 --- a/doc/controllers_brief.svg +++ b/doc/controllers_brief.svg @@ -247,10 +247,5 @@ Registrant::DomainDeleteConfirmsController - -EppController - -EppController - diff --git a/doc/controllers_complete.svg b/doc/controllers_complete.svg index 0501ceac9..23ab7f9ec 100644 --- a/doc/controllers_complete.svg +++ b/doc/controllers_complete.svg @@ -646,34 +646,5 @@ _layout - -EppController - -EppController - -create_full_selectors -current_user -element_count -epp_errors -epp_session -exactly_one_of -generate_svtrid -handle_errors -has_attribute -latin_only -mutually_exclusive -optional -optional_attribute -params_hash -render_epp_response -requires -requires_attribute -validate_request -write_to_epp_log -xml_attrs_present? - - -_layout -