gitlab-org--gitlab-foss/app/services/google_cloud/fetch_google_ip_list_servic...

90 lines
2.9 KiB
Ruby

# frozen_string_literal: true
module GoogleCloud
class FetchGoogleIpListService
include BaseServiceUtility
GOOGLE_IP_RANGES_URL = 'https://www.gstatic.com/ipranges/cloud.json'
RESPONSE_BODY_LIMIT = 1.megabyte
EXPECTED_CONTENT_TYPE = 'application/json'
IpListNotRetrievedError = Class.new(StandardError)
def execute
# Prevent too many workers from hitting the same HTTP endpoint
if ::Gitlab::ApplicationRateLimiter.throttled?(:fetch_google_ip_list, scope: nil)
return error("#{self.class} was rate limited")
end
subnets = fetch_and_update_cache!
Gitlab::AppJsonLogger.info(class: self.class.name,
message: 'Successfully retrieved Google IP list',
subnet_count: subnets.count)
success({ subnets: subnets })
rescue IpListNotRetrievedError => err
Gitlab::ErrorTracking.log_exception(err)
error('Google IP list not retrieved')
end
private
# Attempts to retrieve and parse the list of IPs from Google. Updates
# the internal cache so that the data is accessible.
#
# Returns an array of IPAddr objects consisting of subnets.
def fetch_and_update_cache!
parsed_response = fetch_google_ip_list
parse_google_prefixes(parsed_response).tap do |subnets|
::ObjectStorage::CDN::GoogleIpCache.update!(subnets)
end
end
def fetch_google_ip_list
response = Gitlab::HTTP.get(GOOGLE_IP_RANGES_URL, follow_redirects: false, allow_local_requests: false)
validate_response!(response)
response.parsed_response
end
def validate_response!(response)
raise IpListNotRetrievedError, "response was #{response.code}" unless response.code == 200
raise IpListNotRetrievedError, "response was nil" unless response.body
parsed_response = response.parsed_response
unless response.content_type == EXPECTED_CONTENT_TYPE && parsed_response.is_a?(Hash)
raise IpListNotRetrievedError, "response was not JSON"
end
if response.body&.bytesize.to_i > RESPONSE_BODY_LIMIT
raise IpListNotRetrievedError, "response was too large: #{response.body.bytesize}"
end
prefixes = parsed_response['prefixes']
raise IpListNotRetrievedError, "JSON was type #{prefixes.class}, expected Array" unless prefixes.is_a?(Array)
raise IpListNotRetrievedError, "#{GOOGLE_IP_RANGES_URL} did not return any IP ranges" if prefixes.empty?
response.parsed_response
end
def parse_google_prefixes(parsed_response)
ranges = parsed_response['prefixes'].map do |prefix|
ip_range = prefix['ipv4Prefix'] || prefix['ipv6Prefix']
next unless ip_range
IPAddr.new(ip_range)
end.compact
raise IpListNotRetrievedError, "#{GOOGLE_IP_RANGES_URL} did not return any IP ranges" if ranges.empty?
ranges
end
end
end