From 02820203f0a4c2588e8d0cc97436d7add09ab8a9 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Wed, 15 Nov 2023 19:05:21 +0100 Subject: [PATCH] Switch to AWS V4 signatures --- Gemfile | 3 +- Gemfile.lock | 2 ++ lib/remote_storage/s3.rb | 72 +++++++++++++++++----------------------- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/Gemfile b/Gemfile index b65ae92..d17ab0e 100644 --- a/Gemfile +++ b/Gemfile @@ -3,8 +3,9 @@ source "https://rubygems.org" gem "sinatra", "~> 2.2.0" gem "sinatra-contrib", "~> 2.2.0" gem "activesupport", "~> 6.1.0" -gem "rest-client", "~> 2.1.0" gem "redis", "~> 4.6.0" +gem "rest-client", "~> 2.1.0" +gem "aws-sigv4", "~> 1.0.0" # Remove require when we can update to 3.0, which sets the new storage # format to columnar by default. Increases performance gem "mime-types" diff --git a/Gemfile.lock b/Gemfile.lock index c9676ac..5f987a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,6 +9,7 @@ GEM zeitwerk (~> 2.3) addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) + aws-sigv4 (1.0.3) base64 (0.1.1) concurrent-ruby (1.2.2) crack (0.4.5) @@ -93,6 +94,7 @@ PLATFORMS DEPENDENCIES activesupport (~> 6.1.0) + aws-sigv4 (~> 1.0.0) m mime-types minitest diff --git a/lib/remote_storage/s3.rb b/lib/remote_storage/s3.rb index e6c2a2a..2a35a82 100644 --- a/lib/remote_storage/s3.rb +++ b/lib/remote_storage/s3.rb @@ -10,46 +10,58 @@ module RemoteStorage private - # S3 already wraps the ETag around quotes + def s3_signer + signer ||= Aws::Sigv4::Signer.new( + service: 's3', + region: settings.s3["region"], + access_key_id: settings.s3["access_key_id"].to_s, + secret_access_key: settings.s3["secret_key_id"].to_s + ) + end + + # S3 already wraps the ETag with quotes def format_etag(etag) etag end def do_put_request(url, data, content_type) deal_with_unauthorized_requests do - md5 = Digest::MD5.base64digest(data) - authorization_headers = authorization_headers_for( - "PUT", url, md5, content_type - ).merge({ "Content-Type" => content_type, "Content-Md5" => md5 }) - res = RestClient.put(url, data, authorization_headers) + headers = { "Content-Type" => content_type } + auth_headers = auth_headers_for("PUT", url, headers, data) + + res = RestClient.put(url, data, headers.merge(auth_headers)) return [ res.headers[:etag].delete('"'), timestamp_for(res.headers[:date]) # S3 does not return a Last-Modified response header on PUTs ] end + + rescue RestClient::Forbidden => ex + puts ex.response.to_s + raise ex end def do_get_request(url, &block) deal_with_unauthorized_requests do - headers = { } + headers = {} headers["Range"] = server.env["HTTP_RANGE"] if server.env["HTTP_RANGE"] - authorization_headers = authorization_headers_for("GET", url) - RestClient.get(url, authorization_headers.merge(headers), &block) + auth_headers = auth_headers_for("GET", url, headers) + RestClient.get(url, headers.merge(auth_headers), &block) end end def do_head_request(url, &block) deal_with_unauthorized_requests do - authorization_headers = authorization_headers_for("HEAD", url) - RestClient.head(url, authorization_headers, &block) + auth_headers = auth_headers_for("HEAD", url, {}) + RestClient.head(url, auth_headers, &block) end end def do_delete_request(url) deal_with_unauthorized_requests do - authorization_headers = authorization_headers_for("DELETE", url) - RestClient.delete(url, authorization_headers) + auth_headers = auth_headers_for("DELETE", url, {}) + RestClient.delete(url, auth_headers) end end @@ -67,40 +79,19 @@ module RemoteStorage return found end - # This is using the S3 authorizations, not the newer AW V4 Signatures - # (https://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html) - def authorization_headers_for(http_verb, url, md5 = nil, content_type = nil) - url = File.join("/", url.gsub(base_url, "")) - date = Time.now.httpdate - signed_data = generate_s3_signature(http_verb, md5, content_type, date, url) - { - "Authorization" => "AWS #{credentials[:access_key_id]}:#{signed_data}", - "Date" => date - } - end + def auth_headers_for(http_method, url, headers, data = nil) + signature = s3_signer.sign_request( + http_method: http_method, url: url, headers: headers, body: data + ) - def credentials - @credentials ||= { access_key_id: settings.s3["access_key_id"], secret_key_id: settings.s3["secret_key_id"] } - end - - def digest(secret, string_to_sign) - Base64.encode64(hmac(secret, string_to_sign)).strip - end - - def hmac(key, value) - OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), key, value) + puts signature.headers.inspect + signature.headers end def uri_escape(s) CGI.escape(s).gsub('%5B', '[').gsub('%5D', ']') end - def generate_s3_signature(http_verb, md5, content_type, date, url) - string_to_sign = [http_verb, md5, content_type, date, url].join "\n" - signature = digest(credentials[:secret_key_id], string_to_sign) - uri_escape(signature) - end - def base_url @base_url ||= settings.s3["endpoint"] end @@ -109,5 +100,4 @@ module RemoteStorage "#{base_url}/#{settings.s3["bucket"]}/#{user}" end end - end