3scale/porta

View on GitHub
lib/three_scale/middleware/cors.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module ThreeScale::Middleware
  class Cors < Rack::Cors
    def initialize(app, opts = {}, &block)
      super(app, opts) { set_config }
      set_excludes

      return unless block_given?

      if block.arity == 1
        yield(self)
      else
        instance_eval(&block)
      end
    end

    def call(env)
      return @app.call(env) if excluded?(env)

      super
    end

    private

    def config
      Rails.configuration.three_scale.cors
    end

    def set_config # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
      rules = config.allow.presence || []

      rules.map(&:symbolize_keys).each do |rule|
        origins = rule[:origins].presence || '*'
        resources = rule[:resources].presence || '*'
        methods = rule[:methods].presence || :get

        allow do
          self.origins origins
          [*resources].each do |resource|
            self.resource resource, headers: rule[:headers].presence, methods: methods, credentials: rule[:credentials].present?, max_age: rule[:max_age].presence, vary: rule[:vary], expose: rule[:expose].presence
          end
        end
      end
    end

    def set_excludes
      rules = config.exclude.presence || []

      @excludes = rules.map do |rule|
        ExcludeMatcher.for(rule.symbolize_keys)
      end
    end

    def excluded?(env)
      @excludes.any? { |matcher| matcher.matches?(env) }
    end
  end

  module ExcludeMatcher
    class << self
      def matchers
        @matchers ||= Set.new
      end

      def add_matchers(*matcher_classes)
        matchers.merge matcher_classes.flatten
      end

      def for(rule)
        matcher = matchers.detect { |klass| klass.supports?(rule) }

        raise ThreeScale::ConfigurationError, "Unsupported exclude specification: #{rule}" unless matcher

        matcher.new(rule)
      end
    end

    class PathRegexpMatcher
      attr_reader :regexp

      def initialize(rule)
        regexp = rule[:path_regexp]
        @regexp = regexp.is_a?(Regexp) ? regexp : /#{regexp}/
      end

      def matches?(env)
        regexp.match? env["PATH_INFO"]
      end

      def self.supports?(rule)
        rule[:path_regexp].present?
      end
    end

    class PathPrefixMatcher < PathRegexpMatcher
      def initialize(rule)
        path = rule[:path_prefix]
        segment_end_check = path.end_with?("/") ? "" : "(?:/|$)"
        super({path_regexp: /^#{path}#{segment_end_check}/})
      end

      def self.supports?(rule)
        rule[:path_prefix].present?
      end
    end

    add_matchers PathPrefixMatcher, PathRegexpMatcher
  end
end