1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
|
# frozen_string_literal: true
require 'httparty'
require 'json'
module Gitlab
# @private
class Request
include HTTParty
format :json
maintain_method_across_redirects true
headers 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded'
parser(proc { |body, _| parse(body) })
attr_accessor :private_token, :endpoint, :pat_prefix, :body_as_json
# Converts the response body to an ObjectifiedHash.
def self.parse(body)
body = decode(body)
if body.is_a? Hash
ObjectifiedHash.new body
elsif body.is_a? Array
PaginatedResponse.new(body.collect! { |e| ObjectifiedHash.new(e) })
elsif body
true
elsif !body
false
else
raise Error::Parsing, "Couldn't parse a response body"
end
end
# Decodes a JSON response into Ruby object.
def self.decode(response)
response ? JSON.load(response) : {}
rescue JSON::ParserError
raise Error::Parsing, 'The response is not a valid JSON'
end
%w[get post put patch delete].each do |method|
define_method method do |path, options = {}|
params = options.dup
httparty_config(params)
unless params[:unauthenticated]
params[:headers] ||= {}
params[:headers].merge!(authorization_header)
end
jsonify_body_content(params) if body_as_json
retries_left = params[:ratelimit_retries] || 3
begin
response = self.class.send(method, endpoint + path, params)
validate response
rescue Gitlab::Error::TooManyRequests => e
retries_left -= 1
raise e if retries_left.zero?
wait_time = response.headers['Retry-After'] || 2
sleep(wait_time.to_i)
retry
end
end
end
# Checks the response code for common errors.
# Returns parsed response for successful requests.
def validate(response)
error_klass = Error.klass(response)
raise error_klass, response if error_klass
parsed = response.parsed_response
parsed.client = self if parsed.respond_to?(:client=)
parsed.parse_headers!(response.headers) if parsed.respond_to?(:parse_headers!)
parsed
end
# Sets a base_uri and default_params for requests.
# @raise [Error::MissingCredentials] if endpoint not set.
def request_defaults(sudo = nil)
raise Error::MissingCredentials, 'Please set an endpoint to API' unless endpoint
self.class.default_params sudo: sudo
self.class.default_params.delete(:sudo) if sudo.nil?
end
private
# Returns an Authorization header hash
#
# @raise [Error::MissingCredentials] if private_token and auth_token are not set.
def authorization_header
raise Error::MissingCredentials, 'Please provide a private_token or auth_token for user' unless private_token
# The Personal Access Token prefix can be at most 20 characters, and the
# generated part is of length 20 characters. Personal Access Tokens, thus
# can have a maximum size of 40 characters. GitLab uses
# `Doorkeeper::OAuth::Helpers::UniqueToken.generate` for generating
# OAuth2 tokens, and specified `hex` as token generator method. Thus, the
# OAuth2 tokens are of length more than 64. If the token length is below
# that, it is probably a Personal Access Token or CI_JOB_TOKEN.
if private_token.size >= 64
{ 'Authorization' => "Bearer #{private_token}" }
elsif private_token.start_with?(pat_prefix.to_s)
{ 'PRIVATE-TOKEN' => private_token }
else
{ 'JOB-TOKEN' => private_token }
end
end
# Set HTTParty configuration
# @see https://github.com/jnunemaker/httparty
def httparty_config(options)
options.merge!(httparty) if httparty
end
# Handle 'body_as_json' configuration option
# Modifies passed params in place.
def jsonify_body_content(params)
# Only modify the content type if there is a body to process AND multipath
# was not explicitly requested. There are no uses of multipart in this code
# today, but file upload methods require it and someone might be manually
# crafting a post call with it:
return unless params[:body] && params[:multipart] != true
# If the caller explicitly requested a Content-Type during the call, assume
# they know best and have formatted the body as required:
return if params[:headers]&.key?('Content-Type')
# If we make it here, then we assume it is safe to JSON encode the body:
params[:headers] ||= {}
params[:headers]['Content-Type'] = 'application/json'
params[:body] = params[:body].to_json
end
end
end
|