require 'resolv'
require 'ipaddr'
require 'uri'

module Preview
  class UrlSafety
    class UnsafeUrl < StandardError; end

    FORBIDDEN_NETWORKS = [
      '0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8',
      '169.254.0.0/16', '172.16.0.0/12', '192.0.0.0/24',
      '192.0.2.0/24', '192.168.0.0/16', '198.18.0.0/15',
      '198.51.100.0/24', '203.0.113.0/24', '224.0.0.0/4',
      '240.0.0.0/4', '::/128', '::1/128', 'fc00::/7', 'fe80::/10',
      'ff00::/8', '2001:db8::/32', '::ffff:0:0/96'
    ].map { IPAddr.new(_1) }.freeze

    def self.validate(raw_url)
      value = raw_url.to_s.strip
      if value.match?(/\A[a-z][a-z0-9+\-.]*:/i)
        unless value.match?(/\Ahttps?:\/\//i)
          raise UnsafeUrl, 'http または https の URL を指定してください．'
        end
      else
        value = "http://#{ value }"
      end
      uri = URI.parse(value)

      unless ['http', 'https'].include?(uri.scheme&.downcase) && uri.host.present?
        raise UnsafeUrl, 'http または https の URL を指定してください．'
      end
      raise UnsafeUrl, 'userinfo つき URL は使用できません．' if uri.userinfo.present?

      addresses = Resolv.getaddresses(uri.host)
      raise UnsafeUrl, 'URL のホストを解決できません．' if addresses.empty?

      parsed_addresses = addresses.map { IPAddr.new(_1) }
      if parsed_addresses.any? { |address| forbidden?(address) }
        raise UnsafeUrl, '安全でない接続先は使用できません．'
      end

      [uri, parsed_addresses.map(&:to_s)]
    rescue Resolv::ResolvError
      raise UnsafeUrl, 'URL のホストを解決できません．'
    rescue URI::InvalidURIError, IPAddr::InvalidAddressError
      raise UnsafeUrl, 'URL が不正です．'
    end

    def self.forbidden?(address)
      FORBIDDEN_NETWORKS.any? { _1.include?(address) }
    end

    private_class_method :forbidden?
  end
end
