Compare commits

...

10 Commits

Author SHA1 Message Date
8b6f201a0b Merge pull request 'Switch to AWS v4 signatures' (#5) from feature/aws_v4_signatures into master
Some checks reported warnings
Tests / test (6, 2.7) (push) Has been cancelled
Tests / test (6, 3.0) (push) Has been cancelled
Tests / test (6, 3.1) (push) Has been cancelled
Tests / test (7, 2.7) (push) Has been cancelled
Tests / test (7, 3.0) (push) Has been cancelled
Tests / test (7, 3.1) (push) Has been cancelled
Reviewed-on: #5
2023-11-21 14:19:38 +00:00
209fcca5ea
Make headers argument optional 2023-11-21 15:18:13 +01:00
589dcf7fa2
Update example config 2023-11-16 12:39:46 +01:00
91cadbf228
Switch Docker image back to Ruby 3.1 2023-11-16 11:45:07 +01:00
d09c6e7a39
Use production env for Docker image 2023-11-16 11:41:57 +01:00
024f516a9d
Expose Content-Type header 2023-11-16 11:39:33 +01:00
96417e3884
Use development env and Webrick in dev run script 2023-11-16 11:27:15 +01:00
b7ff3e7d42
Remove debug output, rescue 2023-11-16 10:27:58 +01:00
02820203f0
Switch to AWS V4 signatures 2023-11-15 19:05:21 +01:00
189c304a08
Add example script for running server in dev 2023-11-15 19:03:33 +01:00
7 changed files with 47 additions and 50 deletions

View File

@ -1,8 +1,7 @@
# FROM ruby:3.1.4 FROM ruby:3.1.4
FROM ruby:2.7.8
WORKDIR /liquorcabinet WORKDIR /liquorcabinet
ENV RACK_ENV=staging ENV RACK_ENV=production
COPY Gemfile Gemfile.lock /liquorcabinet/ COPY Gemfile Gemfile.lock /liquorcabinet/
RUN bundle install RUN bundle install

View File

@ -3,8 +3,9 @@ source "https://rubygems.org"
gem "sinatra", "~> 2.2.0" gem "sinatra", "~> 2.2.0"
gem "sinatra-contrib", "~> 2.2.0" gem "sinatra-contrib", "~> 2.2.0"
gem "activesupport", "~> 6.1.0" gem "activesupport", "~> 6.1.0"
gem "rest-client", "~> 2.1.0"
gem "redis", "~> 4.6.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 # Remove require when we can update to 3.0, which sets the new storage
# format to columnar by default. Increases performance # format to columnar by default. Increases performance
gem "mime-types" gem "mime-types"

View File

@ -9,6 +9,7 @@ GEM
zeitwerk (~> 2.3) zeitwerk (~> 2.3)
addressable (2.8.5) addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
aws-sigv4 (1.0.3)
base64 (0.1.1) base64 (0.1.1)
concurrent-ruby (1.2.2) concurrent-ruby (1.2.2)
crack (0.4.5) crack (0.4.5)
@ -93,6 +94,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
activesupport (~> 6.1.0) activesupport (~> 6.1.0)
aws-sigv4 (~> 1.0.0)
m m
mime-types mime-types
minitest minitest

View File

@ -2,11 +2,11 @@ development: &defaults
maintenance: false maintenance: false
redis: redis:
host: <%= ENV["REDIS_HOST"] || "localhost" %> host: <%= ENV["REDIS_HOST"] || "localhost" %>
port: <%= ENV["REDIS_PORT"] || "6379" %> port: <%= ENV["REDIS_PORT"] || 6379 %>
db: <%= ENV["REDIS_DB"] || 0 %> db: <%= ENV["REDIS_DB"] || 1 %>
s3: &s3_defaults s3: &s3_defaults
endpoint: <%= ENV["S3_ENDPOINT"] || "http://127.0.0.1:9000" %> endpoint: <%= ENV["S3_ENDPOINT"] || "http://127.0.0.1:9000" %>
region: <%= ENV["S3_REGION"] %> region: <%= ENV["S3_REGION"] || "us-east-1" %>
access_key_id: <%= ENV["S3_ACCESS_KEY"] || "minioadmin" %> access_key_id: <%= ENV["S3_ACCESS_KEY"] || "minioadmin" %>
secret_key_id: <%= ENV["S3_SECRET_KEY"] || "minioadmin" %> secret_key_id: <%= ENV["S3_SECRET_KEY"] || "minioadmin" %>
bucket: <%= ENV["S3_BUCKET"] || "rs-development" %> bucket: <%= ENV["S3_BUCKET"] || "rs-development" %>

View File

@ -10,18 +10,26 @@ module RemoteStorage
private 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) def format_etag(etag)
etag etag
end end
def do_put_request(url, data, content_type) def do_put_request(url, data, content_type)
deal_with_unauthorized_requests do deal_with_unauthorized_requests do
md5 = Digest::MD5.base64digest(data) headers = { "Content-Type" => content_type }
authorization_headers = authorization_headers_for( auth_headers = auth_headers_for("PUT", url, headers, data)
"PUT", url, md5, content_type
).merge({ "Content-Type" => content_type, "Content-Md5" => md5 }) res = RestClient.put(url, data, headers.merge(auth_headers))
res = RestClient.put(url, data, authorization_headers)
return [ return [
res.headers[:etag].delete('"'), res.headers[:etag].delete('"'),
@ -32,24 +40,24 @@ module RemoteStorage
def do_get_request(url, &block) def do_get_request(url, &block)
deal_with_unauthorized_requests do deal_with_unauthorized_requests do
headers = { } headers = {}
headers["Range"] = server.env["HTTP_RANGE"] if server.env["HTTP_RANGE"] headers["Range"] = server.env["HTTP_RANGE"] if server.env["HTTP_RANGE"]
authorization_headers = authorization_headers_for("GET", url) auth_headers = auth_headers_for("GET", url, headers)
RestClient.get(url, authorization_headers.merge(headers), &block) RestClient.get(url, headers.merge(auth_headers), &block)
end end
end end
def do_head_request(url, &block) def do_head_request(url, &block)
deal_with_unauthorized_requests do deal_with_unauthorized_requests do
authorization_headers = authorization_headers_for("HEAD", url) auth_headers = auth_headers_for("HEAD", url)
RestClient.head(url, authorization_headers, &block) RestClient.head(url, auth_headers, &block)
end end
end end
def do_delete_request(url) def do_delete_request(url)
deal_with_unauthorized_requests do deal_with_unauthorized_requests do
authorization_headers = authorization_headers_for("DELETE", url) auth_headers = auth_headers_for("DELETE", url)
RestClient.delete(url, authorization_headers) RestClient.delete(url, auth_headers)
end end
end end
@ -67,40 +75,17 @@ module RemoteStorage
return found return found
end end
# This is using the S3 authorizations, not the newer AW V4 Signatures def auth_headers_for(http_method, url, headers = {}, data = nil)
# (https://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html) signature = s3_signer.sign_request(
def authorization_headers_for(http_verb, url, md5 = nil, content_type = nil) http_method: http_method, url: url, headers: headers, body: data
url = File.join("/", url.gsub(base_url, "")) )
date = Time.now.httpdate signature.headers
signed_data = generate_s3_signature(http_verb, md5, content_type, date, url)
{
"Authorization" => "AWS #{credentials[:access_key_id]}:#{signed_data}",
"Date" => date
}
end
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)
end end
def uri_escape(s) def uri_escape(s)
CGI.escape(s).gsub('%5B', '[').gsub('%5D', ']') CGI.escape(s).gsub('%5B', '[').gsub('%5D', ']')
end 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 def base_url
@base_url ||= settings.s3["endpoint"] @base_url ||= settings.s3["endpoint"]
end end
@ -109,5 +94,4 @@ module RemoteStorage
"#{base_url}/#{settings.s3["bucket"]}/#{user}" "#{base_url}/#{settings.s3["bucket"]}/#{user}"
end end
end end
end end

View File

@ -64,7 +64,7 @@ class LiquorCabinet < Sinatra::Base
headers 'Access-Control-Allow-Origin' => '*', headers 'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, PUT, DELETE', 'Access-Control-Allow-Methods' => 'GET, PUT, DELETE',
'Access-Control-Allow-Headers' => 'Authorization, Content-Type, Origin, If-Match, If-None-Match, Range', 'Access-Control-Allow-Headers' => 'Authorization, Content-Type, Origin, If-Match, If-None-Match, Range',
'Access-Control-Expose-Headers' => 'ETag, Content-Length, Content-Range', 'Access-Control-Expose-Headers' => 'ETag, Content-Length, Content-Range, Content-Type',
'Accept-Ranges' => 'bytes' 'Accept-Ranges' => 'bytes'
headers['Access-Control-Allow-Origin'] = env["HTTP_ORIGIN"] if env["HTTP_ORIGIN"] headers['Access-Control-Allow-Origin'] = env["HTTP_ORIGIN"] if env["HTTP_ORIGIN"]
headers['Cache-Control'] = 'no-cache' headers['Cache-Control'] = 'no-cache'

11
run-dev.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
RACK_ENV=development \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_DB=1 \
S3_ENDPOINT='http://localhost:9000' \
S3_ACCESS_KEY='dev-key' \
S3_SECRET_KEY='123456789' \
S3_BUCKET=remotestorage \
bundle exec rackup -p 4567