diff --git a/app/controllers/epp/contacts_controller.rb b/app/controllers/epp/contacts_controller.rb index ad46acc95..0d0d3df34 100644 --- a/app/controllers/epp/contacts_controller.rb +++ b/app/controllers/epp/contacts_controller.rb @@ -1,9 +1,197 @@ class Epp::ContactsController < ApplicationController - protect_from_forgery with: :null_session + include Epp::Common + include Shared::UserStamper ## Refactor this? + helper WhodunnitHelper ## Refactor this? + + def user_for_paper_trail ## Refactor this? + current_epp_user ? "#{current_epp_user.id}-EppUser" : nil + end def create + @contact = Contact.new(contact_and_address_attributes) + @contact.registrar = current_epp_user.registrar + render_epp_response '/epp/contacts/create' and return if stamp(@contact) && @contact.save + handle_errors(@contact) + end + + def update + # FIXME: Update returns 2303 update multiple times + code = params_hash['epp']['command']['update']['update'][:id] + @contact = Contact.where(code: code).first + # if update_rights? && stamp(@contact) && @contact.update_attributes(contact_and_address_attributes(:update)) + if owner? && stamp(@contact) && @contact.update_attributes(contact_and_address_attributes(:update)) + render_epp_response 'epp/contacts/update' + else + contact_exists?(code) + handle_errors(@contact) and return + end + end + + # rubocop:disable Metrics/CyclomaticComplexity + def delete + @contact = find_contact + handle_errors(@contact) and return unless rights? # owner? + handle_errors(@contact) and return unless @contact + handle_errors(@contact) and return unless @contact.destroy_and_clean + + render_epp_response '/epp/contacts/delete' + end + # rubocop:enable Metrics/CyclomaticComplexity + + def check + ph = params_hash['epp']['command']['check']['check'] + @contacts = Contact.check_availability(ph[:id]) + render_epp_response '/epp/contacts/check' end def info + handle_errors(@contact) and return unless @contact && rights? + # handle_errors(@contact) and return unless rights? + @disclosure = ContactDisclosure.default_values.merge(@contact.disclosure.try(:as_hash) || {}) + @disclosure_policy = @contact.disclosure.try(:attributes_with_flag) + @owner = owner?(false) + # need to reload contact eagerly + @contact = find_contact('with eager load') if @owner # for clarity, could just be true + render_epp_response 'epp/contacts/info' + end + + def renew + epp_errors << { code: '2101', msg: t(:'errors.messages.unimplemented_command') } + handle_errors + end + + ## HELPER METHODS + + private + + ## CREATE + def validate_create + @ph = params_hash['epp']['command']['create']['create'] + return false unless validate_params + xml_attrs_present?(@ph, [%w(postalInfo name), %w(postalInfo addr city), %w(postalInfo addr cc), + %w(ident), %w(voice), %w(email)]) + + epp_errors.empty? + end + + ## UPDATE + def validate_update + @ph = params_hash['epp']['command']['update']['update'] + update_attrs_present? + # xml_attrs_present?(@ph, [['id'], %w(authInfo pw)]) + xml_attrs_present?(@ph, [['id']]) + end + + def contact_exists?(code) + return true if @contact.is_a?(Contact) + epp_errors << { code: '2303', msg: t('errors.messages.epp_obj_does_not_exist'), + value: { obj: 'id', val: code } } + end + + def update_attrs_present? + return true if parsed_frame.css('add').present? + return true if parsed_frame.css('rem').present? + return true if parsed_frame.css('chg').present? + epp_errors << { code: '2003', msg: I18n.t('errors.messages.required_parameter_missing', key: 'add, rem or chg') } + end + + ## DELETE + def validate_delete + @ph = params_hash['epp']['command']['delete']['delete'] + xml_attrs_present?(@ph, [['id']]) + end + + ## check + def validate_check + @ph = params_hash['epp']['command']['check']['check'] + xml_attrs_present?(@ph, [['id']]) + end + + ## info + def validate_info # and process + @ph = params_hash['epp']['command']['info']['info'] + return false unless xml_attrs_present?(@ph, [['id']]) + @contact = find_contact + return false unless @contact + return true if current_epp_user.registrar == @contact.registrar || xml_attrs_present?(@ph, [%w(authInfo pw)]) + false + end + + ## SHARED + + def find_contact(eager_load = nil) + if eager_load + contact = Contact.includes(address: :country).find_by(code: @ph[:id]) + else + contact = Contact.find_by(code: @ph[:id]) + end + unless contact + epp_errors << { code: '2303', + msg: t('errors.messages.epp_obj_does_not_exist'), + value: { obj: 'id', val: @ph[:id] } } + end + contact + end + + def owner?(with_errors = true) + return false unless find_contact + return true if @contact.registrar == current_epp_user.registrar + return false unless with_errors + epp_errors << { code: '2201', msg: t('errors.messages.epp_authorization_error') } + false + end + + def rights? + pw = @ph.try(:[], :authInfo).try(:[], :pw) + + return true if current_epp_user.try(:registrar) == @contact.try(:registrar) + return true if pw && @contact.auth_info_matches(pw) # @contact.try(:auth_info_matches, pw) + + epp_errors << { code: '2200', msg: t('errors.messages.epp_authentication_error') } + false + end + + def update_rights? + pw = @ph.try(:[], :authInfo).try(:[], :pw) + return true if pw && @contact.auth_info_matches(pw) + epp_errors << { code: '2200', msg: t('errors.messages.epp_authentication_error') } + false + end + + def contact_and_address_attributes(type = :create) + case type + when :update + # TODO: support for rem/add + contact_hash = merge_attribute_hash(@ph[:chg], type).delete_if { |_k, v| v.empty? } + else + contact_hash = merge_attribute_hash(@ph, type) + end + contact_hash[:ident_type] = ident_type unless ident_type.nil? + contact_hash + end + + def merge_attribute_hash(prms, type) + contact_hash = Contact.extract_attributes(prms, type) + contact_hash = contact_hash.merge( + Address.extract_attributes((prms.try(:[], :postalInfo) || [])) + ) + contact_hash[:disclosure_attributes] = + ContactDisclosure.extract_attributes(parsed_frame) + + contact_hash + end + + def ident_type + result = parsed_frame.css('ident').first.try(:attributes).try(:[], 'type').try(:value) + return nil unless result + + Contact::IDENT_TYPES.any? { |type| return type if result.include?(type) } + nil + end + + def validate_params + return true if @ph + epp_errors << { code: '2001', msg: t(:'errors.messages.epp_command_syntax_error') } + false end end diff --git a/app/controllers/epp/domains_controller.rb b/app/controllers/epp/domains_controller.rb index e66f455cc..02c84bb42 100644 --- a/app/controllers/epp/domains_controller.rb +++ b/app/controllers/epp/domains_controller.rb @@ -2,6 +2,16 @@ class Epp::DomainsController < ApplicationController include Epp::Common def create + @domain = Epp::EppDomain.new(domain_create_params) + + @domain.parse_and_attach_domain_dependencies(params[:parsed_frame]) + @domain.parse_and_attach_ds_data(params[:parsed_frame].css('extension create')) + + if @domain.errors.any? || !@domain.save + handle_errors(@domain) + else + render_epp_response '/epp/domains/create' + end end def info @@ -16,12 +26,178 @@ class Epp::DomainsController < ApplicationController render_epp_response '/epp/domains/check' end + def renew + # TODO: support period unit + @domain = find_domain + + handle_errors(@domain) and return unless @domain + handle_errors(@domain) and return unless @domain.renew( + params[:parsed_frame].css('curExpDate').text, + params[:parsed_frame].css('period').text, + params[:parsed_frame].css('period').first['unit'] + ) + + render_epp_response '/epp/domains/renew' + end + + # rubocop:disable Metrics/CyclomaticComplexity + def update + @domain = find_domain + + handle_errors(@domain) and return unless @domain + + @domain.parse_and_detach_domain_dependencies(params[:parsed_frame].css('rem')) + @domain.parse_and_detach_ds_data(params[:parsed_frame].css('extension rem')) + @domain.parse_and_attach_domain_dependencies(params[:parsed_frame].css('add')) + @domain.parse_and_attach_ds_data(params[:parsed_frame].css('extension add')) + @domain.parse_and_update_domain_dependencies(params[:parsed_frame].css('chg')) + @domain.attach_legal_document(Epp::EppDomain.parse_legal_document_from_frame(params[:parsed_frame])) + + if @domain.errors.any? || !@domain.save + handle_errors(@domain) + else + render_epp_response '/epp/domains/success' + end + end + + # rubocop: disable Metrics/PerceivedComplexity + # rubocop: disable Metrics/MethodLength + + def transfer + @domain = find_domain(secure: false) + handle_errors(@domain) and return unless @domain + handle_errors(@domain) and return unless @domain.authenticate(domain_transfer_params[:pw]) + + if domain_transfer_params[:action] == 'query' + if @domain.pending_transfer + @domain_transfer = @domain.pending_transfer + else + @domain_transfer = @domain.query_transfer(domain_transfer_params, params[:parsed_frame]) + handle_errors(@domain) and return unless @domain_transfer + end + elsif domain_transfer_params[:action] == 'approve' + if @domain.pending_transfer + @domain_transfer = @domain.approve_transfer(domain_transfer_params, params[:parsed_frame]) + handle_errors(@domain) and return unless @domain_transfer + else + epp_errors << { code: '2303', msg: I18n.t('pending_transfer_was_not_found') } + handle_errors(@domain) and return + end + elsif domain_transfer_params[:action] == 'reject' + if @domain.pending_transfer + @domain_transfer = @domain.reject_transfer(domain_transfer_params, params[:parsed_frame]) + handle_errors(@domain) and return unless @domain_transfer + else + epp_errors << { code: '2303', msg: I18n.t('pending_transfer_was_not_found') } + handle_errors(@domain) and return + end + end + + render_epp_response '/epp/domains/transfer' + end + + # rubocop: enable Metrics/MethodLength + # rubocop: enable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/CyclomaticComplexity + + def delete + @domain = find_domain + + handle_errors(@domain) and return unless @domain + handle_errors(@domain) and return unless @domain.can_be_deleted? + + @domain.attach_legal_document(Epp::EppDomain.parse_legal_document_from_frame(params[:parsed_frame])) + @domain.save(validate: false) + + handle_errors(@domain) and return unless @domain.destroy + + render_epp_response '/epp/domains/success' + end + # rubocop:enbale Metrics/CyclomaticComplexity + private + def validate_info + @ph = params_hash['epp']['command']['info']['info'] + xml_attrs_present?(@ph, [['name']]) + end + def validate_check epp_request_valid?('name') end + def validate_create + ret = true + + # TODO: Verify contact presence if registrant is juridical + attrs_present = epp_request_valid?('name', 'ns', 'registrant', 'legalDocument') + ret = false unless attrs_present + + if params[:parsed_frame].css('hostObj').any? + epp_errors << { code: '2306', msg: I18n.t('host_obj_is_not_allowed') } + ret = false + end + + if params[:parsed_frame].css('dsData').count > 0 && params[:parsed_frame].css('create > keyData').count > 0 + epp_errors << { code: '2306', msg: I18n.t('ds_data_and_key_data_must_not_exists_together') } + ret = false + end + ret + end + + def validate_renew + @ph = params_hash['epp']['command']['renew']['renew'] + xml_attrs_present?(@ph, [['name'], ['curExpDate'], ['period']]) + end + + def validate_update + @ph = params_hash['epp']['command']['update']['update'] + + if params[:parsed_frame].css('chg registrant').present? && params[:parsed_frame].css('legalDocument').blank? + xml_attrs_present?(@ph, [['name'], ['legalDocument']]) + else + xml_attrs_present?(@ph, [['name']]) + end + end + + ## TRANSFER + def validate_transfer + @ph = params_hash['epp']['command']['transfer']['transfer'] + attrs_present = xml_attrs_present?(@ph, [['name']]) + return false unless attrs_present + + op = params[:parsed_frame].css('transfer').first[:op] + return true if %w(approve query reject).include?(op) + epp_errors << { code: '2306', msg: I18n.t('errors.messages.attribute_op_is_invalid') } + false + end + + ## DELETE + def validate_delete + epp_request_valid?('name', 'legalDocument') + end + + def domain_create_params + name = params[:parsed_frame].css('name').text + period = params[:parsed_frame].css('period').text + + { + name: name, + registrar_id: current_epp_user.registrar.try(:id), + registered_at: Time.now, + period: (period.to_i == 0) ? 1 : period.to_i, + period_unit: Epp::EppDomain.parse_period_unit_from_frame(params[:parsed_frame]) || 'y' + } + end + + def domain_transfer_params + res = {} + res[:pw] = parsed_frame.css('pw').first.try(:text) + res[:action] = parsed_frame.css('transfer').first[:op] + res[:current_user] = current_epp_user + res + end + def find_domain(secure = { secure: true }) domain_name = params[:parsed_frame].css('name').text.strip.downcase domain = Epp::EppDomain.find_by(name: domain_name) diff --git a/app/controllers/epp/keyrelays_controller.rb b/app/controllers/epp/keyrelays_controller.rb new file mode 100644 index 000000000..d822f2618 --- /dev/null +++ b/app/controllers/epp/keyrelays_controller.rb @@ -0,0 +1,51 @@ +class Epp::KeyrelaysController < ApplicationController + include Epp::Common + # rubocop: disable Metrics/PerceivedComplexity + # rubocop: disable Metrics/CyclomaticComplexity + def keyrelay + @domain = find_domain + + handle_errors(@domain) and return unless @domain + handle_errors(@domain) and return unless @domain.authenticate(parsed_frame.css('pw').text) + handle_errors(@domain) and return unless @domain.keyrelay(parsed_frame, current_epp_user.registrar) + + render_epp_response '/epp/shared/success' + end + + private + + def validate_keyrelay + epp_request_valid?('pubKey', 'flags', 'protocol', 'alg', 'name', 'pw') + + begin + abs_datetime = parsed_frame.css('absolute').text + abs_datetime = DateTime.parse(abs_datetime) if abs_datetime.present? + rescue => _e + epp_errors << { + code: '2005', + msg: I18n.t('unknown_expiry_absolute_pattern'), + value: { obj: 'expiry_absolute', val: abs_datetime } + } + end + + epp_errors.empty? + end + # rubocop: enable Metrics/PerceivedComplexity + # rubocop: enable Metrics/CyclomaticComplexity + + def find_domain + domain_name = parsed_frame.css('name').text.strip.downcase + domain = Epp::EppDomain.find_by(name: domain_name) + + unless domain + epp_errors << { + code: '2303', + msg: I18n.t('errors.messages.epp_domain_not_found'), + value: { obj: 'name', val: domain_name } + } + return nil + end + + domain + end +end diff --git a/app/controllers/epp/polls_controller.rb b/app/controllers/epp/polls_controller.rb new file mode 100644 index 000000000..c0a8383d1 --- /dev/null +++ b/app/controllers/epp/polls_controller.rb @@ -0,0 +1,48 @@ +class Epp::PollsController < ApplicationController + include Epp::Common + + def poll + req_poll if parsed_frame.css('poll').first['op'] == 'req' + ack_poll if parsed_frame.css('poll').first['op'] == 'ack' + end + + def req_poll + @message = current_epp_user.queued_messages.last + render_epp_response 'epp/poll/poll_no_messages' and return unless @message + + if @message.attached_obj_type && @message.attached_obj_id + @object = Object.const_get(@message.attached_obj_type).find(@message.attached_obj_id) + end + + if @message.attached_obj_type == 'Keyrelay' + render_epp_response 'epp/poll/poll_keyrelay' + else + render_epp_response 'epp/poll/poll_req' + end + end + + def ack_poll + @message = current_epp_user.queued_messages.find_by(id: parsed_frame.css('poll').first['msgID']) + + unless @message + epp_errors << { + code: '2303', + msg: I18n.t('message_was_not_found'), + value: { obj: 'msgID', val: parsed_frame.css('poll').first['msgID'] } + } + handle_errors and return + end + + handle_errors(@message) and return unless @message.dequeue + render_epp_response 'epp/poll/poll_ack' + end + + private + + def validate_poll + op = parsed_frame.css('poll').first[:op] + return true if %w(ack req).include?(op) + epp_errors << { code: '2306', msg: I18n.t('errors.messages.attribute_op_is_invalid') } + false + end +end diff --git a/config/routes.rb b/config/routes.rb index 79ac521be..de6e9406e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,12 +20,11 @@ class EppConstraint end Rails.application.routes.draw do - namespace(:epp) do - post 'command/info', to: 'domains#info', defaults: { format: :xml }, constraints: EppConstraint.new(:domain) - post 'command/info', to: 'contacts#info', defaults: { format: :xml }, constraints: EppConstraint.new(:contact) - - post 'command/check', to: 'domains#check', defaults: { format: :xml }, constraints: EppConstraint.new(:domain) - post 'command/check', to: 'contacts#check', defaults: { format: :xml }, constraints: EppConstraint.new(:contact) + namespace(:epp, defaults: { format: :xml }) do + post 'command/:action', controller: 'domains', constraints: EppConstraint.new(:domain) + post 'command/:action', controller: 'contacts', constraints: EppConstraint.new(:contact) + post 'command/poll', to: 'polls#poll' + post 'command/keyrelay', to: 'keyrelays#keyrelay' match 'session/:command', to: 'sessions#proxy', defaults: { format: :xml }, via: [:get, :post] # match 'command/:command', to: 'commands#proxy', defaults: { format: :xml }, via: [:post, :get]