class SafeFetch::RequestOptions DEFAULTS = { method: :get, body: nil, max_bytes: nil, open_timeout: SafeFetch::DEFAULT_OPEN_TIMEOUT, read_timeout: SafeFetch::DEFAULT_READ_TIMEOUT, headers: nil, http_basic_authentication: nil, allowed_content_type_prefixes: SafeFetch::DEFAULT_ALLOWED_CONTENT_TYPE_PREFIXES, allowed_content_types: SafeFetch::DEFAULT_ALLOWED_CONTENT_TYPES, validate_content_type: true }.freeze attr_reader :allowed_content_type_prefixes, :allowed_content_types, :body, :headers, :http_basic_authentication, :method, :open_timeout, :read_timeout, :uri, :url def initialize(url:, **options) config = DEFAULTS.merge(options) @url = url @uri = parse_and_validate_url!(url) @method = normalize_method(config[:method]) @body = config[:body] @max_bytes = config[:max_bytes] @open_timeout = config[:open_timeout] @read_timeout = config[:read_timeout] @headers = normalize_headers(config[:headers]) @http_basic_authentication = config[:http_basic_authentication] @allowed_content_type_prefixes = Array(config[:allowed_content_type_prefixes]) @allowed_content_types = Array(config[:allowed_content_types]) @validate_content_type = config[:validate_content_type] end def effective_max_bytes @effective_max_bytes ||= @max_bytes || default_max_bytes end def filename @filename ||= File.basename(uri.path).presence || "download-#{Time.current.to_i}-#{SecureRandom.hex(4)}" end def request_options { headers: headers, body: body, request_proc: request_proc, sensitive_headers: sensitive_headers, http_options: { open_timeout: open_timeout, read_timeout: read_timeout } } end def validate_content_type? @validate_content_type end def resolver SsrfFilter::DEFAULT_RESOLVER end private def default_max_bytes limit_mb = GlobalConfigService.load('MAXIMUM_FILE_UPLOAD_SIZE', SafeFetch::DEFAULT_MAX_BYTES_FALLBACK_MB).to_i limit_mb = SafeFetch::DEFAULT_MAX_BYTES_FALLBACK_MB if limit_mb <= 0 limit_mb.megabytes end def parse_and_validate_url!(value) parsed_uri = URI.parse(value) raise SafeFetch::InvalidUrlError, 'scheme must be http or https' unless parsed_uri.is_a?(URI::HTTP) || parsed_uri.is_a?(URI::HTTPS) raise SafeFetch::InvalidUrlError, 'missing host' if parsed_uri.host.blank? parsed_uri end def normalize_method(value) http_method = value.to_s.downcase.to_sym return http_method if SsrfFilter::VERB_MAP.key?(http_method) raise SafeFetch::UnsupportedMethodError, "unsupported method: #{value}" end def normalize_headers(value) value&.to_h end def request_proc proc do |request| credentials = http_basic_authentication.presence || basic_authentication_for(request.uri) request.basic_auth(*credentials) if credentials.present? end end def sensitive_headers SafeFetch::DEFAULT_SENSITIVE_HEADERS end def basic_authentication_for(request_uri) uri_basic_authentication(request_uri) || original_uri_basic_authentication(request_uri) end def original_uri_basic_authentication(request_uri) return unless same_origin?(request_uri, uri) uri_basic_authentication(uri) end def same_origin?(request_uri, other_uri) request_uri.scheme == other_uri.scheme && request_uri.hostname == other_uri.hostname && request_uri.port == other_uri.port end def uri_basic_authentication(value) return if value.user.blank? [ URI.decode_uri_component(value.user), URI.decode_uri_component(value.password.to_s) ] end end