diff options
Diffstat (limited to 'app/uploaders')
-rw-r--r-- | app/uploaders/object_storage/cdn.rb | 46 | ||||
-rw-r--r-- | app/uploaders/object_storage/cdn/google_cdn.rb | 71 | ||||
-rw-r--r-- | app/uploaders/object_storage/cdn/google_ip_cache.rb | 60 | ||||
-rw-r--r-- | app/uploaders/packages/package_file_uploader.rb | 4 |
4 files changed, 178 insertions, 3 deletions
diff --git a/app/uploaders/object_storage/cdn.rb b/app/uploaders/object_storage/cdn.rb new file mode 100644 index 00000000000..0711ab0bd28 --- /dev/null +++ b/app/uploaders/object_storage/cdn.rb @@ -0,0 +1,46 @@ +# rubocop:disable Naming/FileName +# frozen_string_literal: true + +require_relative 'cdn/google_cdn' + +module ObjectStorage + module CDN + module Concern + extend ActiveSupport::Concern + + include Gitlab::Utils::StrongMemoize + + def use_cdn?(request_ip) + return false unless cdn_options.is_a?(Hash) && cdn_options['provider'] + return false unless cdn_provider + + cdn_provider.use_cdn?(request_ip) + end + + def cdn_signed_url + cdn_provider&.signed_url(path) + end + + private + + def cdn_provider + strong_memoize(:cdn_provider) do + provider = cdn_options['provider']&.downcase + + next unless provider + next GoogleCDN.new(cdn_options) if provider == 'google' + + raise "Unknown CDN provider: #{provider}" + end + end + + def cdn_options + return {} unless options.object_store.key?('cdn') + + options.object_store.cdn + end + end + end +end + +# rubocop:enable Naming/FileName diff --git a/app/uploaders/object_storage/cdn/google_cdn.rb b/app/uploaders/object_storage/cdn/google_cdn.rb new file mode 100644 index 00000000000..ea7683f131c --- /dev/null +++ b/app/uploaders/object_storage/cdn/google_cdn.rb @@ -0,0 +1,71 @@ +# rubocop:disable Naming/FileName +# frozen_string_literal: true + +module ObjectStorage + module CDN + class GoogleCDN + include Gitlab::Utils::StrongMemoize + + attr_reader :options + + def initialize(options) + @options = HashWithIndifferentAccess.new(options.to_h) + + GoogleIpCache.async_refresh unless GoogleIpCache.ready? + end + + def use_cdn?(request_ip) + return false unless config_valid? + + ip = IPAddr.new(request_ip) + + return false if ip.private? + + !GoogleIpCache.google_ip?(request_ip) + end + + def signed_url(path, expiry: 10.minutes) + expiration = (Time.current + expiry).utc.to_i + + uri = Addressable::URI.parse(cdn_url) + uri.path = path + uri.query = "Expires=#{expiration}&KeyName=#{key_name}" + + signature = OpenSSL::HMAC.digest('SHA1', decoded_key, uri.to_s) + encoded_signature = Base64.urlsafe_encode64(signature) + + uri.query += "&Signature=#{encoded_signature}" + uri.to_s + end + + private + + def config_valid? + [key_name, decoded_key, cdn_url].all?(&:present?) + end + + def key_name + strong_memoize(:key_name) do + options['key_name'] + end + end + + def decoded_key + strong_memoize(:decoded_key) do + Base64.urlsafe_decode64(options['key']) if options['key'] + rescue ArgumentError + Gitlab::ErrorTracking.log_exception(ArgumentError.new("Google CDN key is not base64-encoded")) + nil + end + end + + def cdn_url + strong_memoize(:cdn_url) do + options['url'] + end + end + end + end +end + +# rubocop:enable Naming/FileName diff --git a/app/uploaders/object_storage/cdn/google_ip_cache.rb b/app/uploaders/object_storage/cdn/google_ip_cache.rb new file mode 100644 index 00000000000..35ec7ce0c6e --- /dev/null +++ b/app/uploaders/object_storage/cdn/google_ip_cache.rb @@ -0,0 +1,60 @@ +# rubocop:disable Naming/FileName +# frozen_string_literal: true + +module ObjectStorage + module CDN + class GoogleIpCache + GOOGLE_CDN_LIST_KEY = 'google_cdn_ip_list' + CACHE_EXPIRATION_TIME = 1.day + + class << self + def update!(subnets) + caches.each { |cache| cache.write(GOOGLE_CDN_LIST_KEY, subnets) } + end + + def ready? + caches.any? { |cache| cache.exist?(GOOGLE_CDN_LIST_KEY) } + end + + def google_ip?(request_ip) + google_ip_ranges = cached_value(GOOGLE_CDN_LIST_KEY) + + return false unless google_ip_ranges + + google_ip_ranges.any? { |range| range.include?(request_ip) } + end + + def async_refresh + ::GoogleCloud::FetchGoogleIpListWorker.perform_async + end + + private + + def caches + [l1_cache, l2_cache] + end + + def l1_cache + Gitlab::ProcessMemoryCache.cache_backend + end + + def l2_cache + Rails.cache + end + + def cached_value(key) + l1_cache.fetch(key) do + result = l2_cache.fetch(key) + + # Don't populate the L1 cache if we can't find the entry + break unless result + + result + end + end + end + end + end +end + +# rubocop:enable Naming/FileName diff --git a/app/uploaders/packages/package_file_uploader.rb b/app/uploaders/packages/package_file_uploader.rb index 4b6dbe5b358..9c0a88c9bf8 100644 --- a/app/uploaders/packages/package_file_uploader.rb +++ b/app/uploaders/packages/package_file_uploader.rb @@ -22,8 +22,6 @@ class Packages::PackageFileUploader < GitlabUploader def dynamic_segment raise ObjectNotReadyError, "Package model not ready" unless model.id - package_segment = model.package.debian? ? 'debian' : model.package.id - - Gitlab::HashedPath.new('packages', package_segment, 'files', model.id, root_hash: model.package.project_id) + Gitlab::HashedPath.new('packages', model.package_id, 'files', model.id, root_hash: model.package.project_id) end end |