debian-mirror-gitlab/lib/gitlab/middleware/multipart.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

186 lines
6.7 KiB
Ruby
Raw Permalink Normal View History

2019-02-15 15:39:39 +05:30
# frozen_string_literal: true
2017-08-17 22:00:37 +05:30
# Gitlab::Middleware::Multipart - a Rack::Multipart replacement
#
# Rack::Multipart leaves behind tempfiles in /tmp and uses valuable Ruby
# process time to copy files around. This alternative solution uses
# gitlab-workhorse to clean up the tempfiles and puts the tempfiles in a
# location where copying should not be needed.
#
# When gitlab-workhorse finds files in a multipart MIME body it sends
# a signed message via a request header. This message lists the names of
# the multipart entries that gitlab-workhorse filtered out of the
# multipart structure and saved to tempfiles. Workhorse adds new entries
# in the multipart structure with paths to the tempfiles.
#
# The job of this Rack middleware is to detect and decode the message
# from workhorse. If present, it walks the Rack 'params' hash for the
# current request, opens the respective tempfiles, and inserts the open
# Ruby File objects in the params hash where Rack::Multipart would have
# put them. The goal is that application code deeper down can keep
# working the way it did with Rack::Multipart without changes.
#
# CAVEAT: the code that modifies the params hash is a bit complex. It is
# conceivable that certain Rack params structures will not be modified
# correctly. We are not aware of such bugs at this time though.
#
module Gitlab
module Middleware
class Multipart
2019-12-04 20:38:33 +05:30
RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
2020-11-24 15:15:51 +05:30
JWT_PARAM_SUFFIX = '.gitlab-workhorse-upload'
JWT_PARAM_FIXED_KEY = 'upload'
2021-04-29 21:17:54 +05:30
REWRITTEN_FIELD_NAME_MAX_LENGTH = 10000
2017-08-17 22:00:37 +05:30
class Handler
def initialize(env, message)
2019-12-04 20:38:33 +05:30
@request = Rack::Request.new(env)
2017-08-17 22:00:37 +05:30
@rewritten_fields = message['rewritten_fields']
@open_files = []
end
def with_open_files
2021-03-08 18:12:59 +05:30
@rewritten_fields.keys.each do |field|
2020-11-05 12:06:23 +05:30
raise "invalid field: #{field.inspect}" unless valid_field_name?(field)
2017-08-17 22:00:37 +05:30
parsed_field = Rack::Utils.parse_nested_query(field)
raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
key, value = parsed_field.first
2020-05-01 12:34:13 +05:30
if value.nil? # we have a top level param, eg. field = 'foo' and not 'foo[bar]'
raise "invalid field: #{field.inspect}" if field != key
2021-03-08 18:12:59 +05:30
value = open_file(extract_upload_params_from(@request.params, with_prefix: key))
2017-08-17 22:00:37 +05:30
@open_files << value
else
2021-03-08 18:12:59 +05:30
value = decorate_params_value(value, @request.params[key])
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
2019-12-04 20:38:33 +05:30
update_param(key, value)
2017-08-17 22:00:37 +05:30
end
yield
ensure
2020-11-24 15:15:51 +05:30
@open_files.compact
.each(&:close)
2017-08-17 22:00:37 +05:30
end
# This function calls itself recursively
2021-03-08 18:12:59 +05:30
def decorate_params_value(hash_path, value_hash)
unless hash_path.is_a?(Hash) && hash_path.count == 1
raise "invalid path: #{hash_path.inspect}"
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
2021-03-08 18:12:59 +05:30
path_key, path_value = hash_path.first
2017-08-17 22:00:37 +05:30
unless value_hash.is_a?(Hash) && value_hash[path_key]
raise "invalid value hash: #{value_hash.inspect}"
end
case path_value
when nil
2021-03-08 18:12:59 +05:30
value_hash[path_key] = open_file(extract_upload_params_from(value_hash[path_key]))
2017-08-17 22:00:37 +05:30
@open_files << value_hash[path_key]
value_hash
when Hash
2021-03-08 18:12:59 +05:30
decorate_params_value(path_value, value_hash[path_key])
2017-08-17 22:00:37 +05:30
value_hash
else
raise "unexpected path value: #{path_value.inspect}"
end
end
2021-03-08 18:12:59 +05:30
def open_file(params)
::UploadedFile.from_params(params, allowed_paths)
2017-08-17 22:00:37 +05:30
end
2019-12-04 20:38:33 +05:30
# update_params ensures that both rails controllers and rack middleware can find
# workhorse accelerate files in the request
def update_param(key, value)
# we make sure we have key in POST otherwise update_params will add it in GET
@request.POST[key] ||= value
# this will force Rack::Request to properly update env keys
@request.update_param(key, value)
# ActionDispatch::Request is based on Rack::Request but it caches params
# inside other env keys, here we ensure everything is updated correctly
ActionDispatch::Request.new(@request.env).update_param(key, value)
end
2020-03-28 13:19:24 +05:30
private
2021-03-08 18:12:59 +05:30
def extract_upload_params_from(params, with_prefix: '')
param_key = "#{with_prefix}#{JWT_PARAM_SUFFIX}"
jwt_token = params[param_key]
raise "Empty JWT param: #{param_key}" if jwt_token.blank?
2022-03-02 08:16:31 +05:30
payload = Gitlab::Workhorse.decode_jwt_with_issuer(jwt_token).first
2021-03-08 18:12:59 +05:30
raise "Invalid JWT payload: not a Hash" unless payload.is_a?(Hash)
upload_params = payload.fetch(JWT_PARAM_FIXED_KEY, {})
raise "Empty params for: #{param_key}" if upload_params.empty?
upload_params
end
2020-11-05 12:06:23 +05:30
def valid_field_name?(name)
# length validation
return false if name.size >= REWRITTEN_FIELD_NAME_MAX_LENGTH
# brackets validation
return false if name.include?('[]') || name.start_with?('[', ']')
return false unless ::Gitlab::Utils.valid_brackets?(name, allow_nested: false)
true
end
2020-07-28 23:09:34 +05:30
def package_allowed_paths
packages_config = ::Gitlab.config.packages
return [] unless allow_packages_storage_path?(packages_config)
[::Packages::PackageFileUploader.workhorse_upload_path]
end
def allow_packages_storage_path?(packages_config)
return false unless packages_config.enabled
return false unless packages_config['storage_path']
return false if packages_config.object_store.enabled && packages_config.object_store.direct_upload
true
end
2020-03-28 13:19:24 +05:30
def allowed_paths
[
2020-11-24 15:15:51 +05:30
Dir.tmpdir,
2020-03-28 13:19:24 +05:30
::FileUploader.root,
2020-11-24 15:15:51 +05:30
::Gitlab.config.uploads.storage_path,
::JobArtifactUploader.workhorse_upload_path,
::LfsObjectUploader.workhorse_upload_path,
2021-11-18 22:05:49 +05:30
::DependencyProxy::FileUploader.workhorse_upload_path,
2020-03-28 13:19:24 +05:30
File.join(Rails.root, 'public/uploads/tmp')
2020-07-28 23:09:34 +05:30
] + package_allowed_paths
2020-03-28 13:19:24 +05:30
end
2017-08-17 22:00:37 +05:30
end
def initialize(app)
@app = app
end
def call(env)
encoded_message = env.delete(RACK_ENV_KEY)
return @app.call(env) if encoded_message.blank?
2022-03-02 08:16:31 +05:30
message = ::Gitlab::Workhorse.decode_jwt_with_issuer(encoded_message)[0]
2017-08-17 22:00:37 +05:30
2021-03-08 18:12:59 +05:30
::Gitlab::Middleware::Multipart::Handler.new(env, message).with_open_files do
2017-08-17 22:00:37 +05:30
@app.call(env)
end
2021-09-04 01:27:46 +05:30
rescue UploadedFile::InvalidPathError, ApolloUploadServer::GraphQLDataBuilder::OutOfBounds => e
2021-10-27 15:23:28 +05:30
[400, { 'Content-Type' => 'text/plain' }, [e.message]]
2017-08-17 22:00:37 +05:30
end
end
end
end