app/models/proxy_rule.rb
# frozen_string_literal: true
require 'addressable/template'
class ProxyRule < ApplicationRecord
acts_as_list scope: %i[owner_id owner_type], add_new_at: :bottom
scope :ordered, -> { order(position: :asc) }
include ProxyConfigAffectingChanges::ModelExtension
belongs_to :proxy, inverse_of: :proxy_rules
belongs_to :owner, polymorphic: true # FIXME: we should touch the owner here, but it will raise ActiveRecord::StaleObjectError
belongs_to :metric, inverse_of: :proxy_rules
validates :http_method, :pattern, :owner_id, :owner_type, :metric_id, presence: true
validates :owner_type, length: { maximum: 255 }
validates :delta, numericality: { :only_integer => true, :greater_than => 0 }
before_validation :fill_owner
include Searchable
self.allowed_sort_columns = %w[proxy_rules.http_method proxy_rules.pattern proxy_rules.last proxy_rules.position metrics.friendly_name]
self.default_sort_column = :position
self.default_sort_direction = :asc
ALLOWED_HTTP_METHODS = %w[GET POST DELETE PUT PATCH HEAD OPTIONS].freeze
class PatternParser
REGEX_LITERAL = /[_\w]+/i
REGEX_VARIABLE = /\{#{REGEX_LITERAL}\}/
# pchar = unreserved | escaped |
# ":" | "@" | "&" | "=" | "+" | "$" | ","
param = /
(?:
[#{URI::REGEXP::PATTERN::UNRESERVED}:@&=+,] # note that $ is in the RFC but is removed for our purpposes
|
#{URI::REGEXP::PATTERN::ESCAPED}
|
#{REGEX_VARIABLE}
)*
/x
segment = /
#{param}
(?:;#{param})*
/x
REGEX_PATH = %r{
/#{segment}(?:/#{segment})* # normal URI path segments like /foo/bar
}x
query = /
(?:
[#{URI::REGEXP::PATTERN::UNRESERVED}#{URI::REGEXP::PATTERN::RESERVED}]
|
#{URI::REGEXP::PATTERN::ESCAPED}
|
#{REGEX_LITERAL}=#{REGEX_VARIABLE}
)*
/x
ABSOLUTE_PATH = /\A
#{REGEX_PATH} # absolute path
[$]? # optionally forcing to match the end of path
(?:\?(?:#{query}))? # optionally followed by a query string
\Z/x
def initialize
@pattern = ABSOLUTE_PATH
end
attr_reader :pattern
def =~(other)
other =~ pattern
end
def call(_)
pattern
end
end
validates :pattern, format: { with: PatternParser.new, allow_blank: true }
# a valid pattern is: slash alone or a path => maybe a dollar sign => maybe a query string
validates :pattern, length: { maximum: 255 }
validates :http_method, inclusion: { in: ALLOWED_HTTP_METHODS }
validate :non_repeated_parameters
validate :no_vars_in_keys
validates :redirect_url, format: URI.regexp(%w[http https]), allow_blank: true, length: { maximum: 10000 }
def parameters
Addressable::Template.new(path_pattern).variables
end
def querystring_parameters
query = query_pattern or return {}
Hash[query.split('&').map { |kv| kv.split('=', 2) }]
end
def metric_system_name
metric.try!(:system_name)
end
def path_pattern
pattern_uri.path
end
def query_pattern
pattern_uri.query
end
def pattern_uri
Addressable::URI.parse(pattern || '')
rescue Addressable::URI::InvalidURIError
Addressable::URI.new
end
def backend_api_owner?
owner_type == 'BackendApi'
end
delegate :account, to: :owner, allow_nil: true
def proxies
backend_api_owner? ? owner.proxies : [proxy]
end
def scheduled_for_deletion?
!owner || !account || owner.scheduled_for_deletion?
end
def act_as_list_no_update?
super || scheduled_for_deletion?
end
protected
def querystring_parameter_keys
query = query_pattern or return []
query.split('&').flat_map { |kv| kv.split('=').first }
end
def non_repeated_parameters
pattern = path_pattern
return if pattern.blank?
duplicated = Addressable::Template.new(pattern).named_captures.values.any? { |captures| captures.size > 1}
unless duplicated
params = parameters + querystring_parameter_keys
duplicated |= params != params.uniq
end
errors.add(:pattern, "Can't have repeated variable names") if duplicated
end
def no_vars_in_keys
if querystring_parameters.keys.any?(&PatternParser::REGEX_VARIABLE.method(:match))
errors.add(:pattern, "Can't use variables as keys in the querystring")
end
end
def fill_owner
return true if owner_type?
self.owner_id = proxy_id
self.owner_type = 'Proxy'
end
end