Compare commits

2 Commits

Author SHA1 Message Date
a8637a6982 Remove purdytest, update Minitest class name 2023-10-26 16:14:46 +02:00
2cd13729e5 Add ENV vars to config file, switch to ERB template
Also change S3 endpoint config to not require a trailing slash.
2023-10-26 16:14:35 +02:00
10 changed files with 95 additions and 109 deletions

View File

@@ -1,16 +0,0 @@
kind: pipeline
name: default
steps:
- name: specs
image: ruby
environment:
REDIS_HOST: redis
commands:
- cp config.yml.erb.example config.yml.erb
- bundle install --jobs=3 --retry=3
- bundle exec rake test
services:
- name: redis
image: redis

33
.github/workflows/ruby.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.7', '3.0', '3.1']
redis-version: [6, 7]
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Start Redis
uses: supercharge/redis-github-action@1.4.0
with:
redis-version: ${{ matrix.redis-version }}
- name: Configure
run: cp config.yml.erb.example config.yml
- name: Run tests
run: bundle exec rake test

View File

@@ -1,13 +0,0 @@
FROM ruby:3.1.4
WORKDIR /liquorcabinet
ENV RACK_ENV=production
COPY Gemfile Gemfile.lock /liquorcabinet/
RUN bundle install
COPY . /liquorcabinet
COPY ./config.yml.erb.example /liquorcabinet/config.yml.erb
EXPOSE 4567
CMD ["bundle", "exec", "rainbows", "--listen", "0.0.0.0:4567"]

View File

@@ -3,11 +3,11 @@ 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 "redis", "~> 4.6.0"
gem "rest-client", "~> 2.1.0" gem "rest-client", "~> 2.1.0"
gem "aws-sigv4", "~> 1.0.0" gem "redis", "~> 4.6.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" gem "mime-types"
gem "rainbows"
group :test do group :test do
gem 'rake' gem 'rake'
@@ -19,5 +19,6 @@ group :test do
end end
group :staging, :production do group :staging, :production do
gem "rainbows"
gem "sentry-raven", require: false gem "sentry-raven", require: false
end end

View File

@@ -9,7 +9,6 @@ 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)
@@ -94,7 +93,6 @@ 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

@@ -1,54 +1,21 @@
[![Build Status](https://drone.kosmos.org/api/badges/5apps/liquor-cabinet/status.svg)](https://drone.kosmos.org/5apps/liquor-cabinet) [![Build Status](https://github.com/5apps/liquor-cabinet/actions/workflows/ruby.yml/badge.svg)](https://github.com/5apps/liquor-cabinet/actions/workflows/ruby.yml)
# Liquor Cabinet # Liquor Cabinet
Liquor Cabinet is where Frank stores all his stuff. It's a Liquor Cabinet is where Frank stores all his stuff. It's a
[remoteStorage](https://remotestorage.io) HTTP API, based on Sinatra. The [remoteStorage](https://remotestorage.io) HTTP API, based on Sinatra. The
metadata and OAuth tokens are stored in Redis, and metadata and OAuth tokens are stored in Redis, and documents can be stored in
documents/files can be stored in anything that supports anything that supports the storage API of either Openstack Swift or Amazon S3.
the S3 object storage API.
Liquor Cabinet only implements the storage API part of the remoteStorage Liquor Cabinet only implements the storage API part of the remoteStorage
protocol, but does not include the Webfinger and OAuth parts. It is meant to be protocol, but does not include the Webfinger and OAuth parts. It is meant to be
added to existing systems and user accounts, so you will have to add your own added to existing systems and user accounts, so you will have to add your own
OAuth dialog for remoteStorage authorizations and persist the tokens in Redis. OAuth dialog for remoteStorage authorizations and persist the tokens in Redis.
There is an [open-source accounts management If you have any questions about this program, drop by #remotestorage on
app](https://gitea.kosmos.org/kosmos/akkounts/) by the Kosmos project, which Freenode, or [post to the RS
comes with a built-in remoteStorage dashboard and is compatible with Liquor
Cabinet.
If you have any questions about this program, please [post to the RS
forums](https://community.remotestorage.io/c/server-development), and we'll forums](https://community.remotestorage.io/c/server-development), and we'll
gladly answer them. happily answer them.
## System requirements
* [Ruby](https://www.ruby-lang.org/en/) and [Bundler](https://bundler.io/)
* [Redis](https://redis.io/)
* S3-compatible object storage (e.g. [Garage](https://garagehq.deuxfleurs.fr/)
or [MinIO](https://min.io/) for self-hosting)
## Setup
1. Check the `config.yml.erb.example` file. Either copy it to `config.yml.erb`
and use the enviroment variables it contains, or create/deploy your own
config YAML file with custom values.
2. Install dependencies: `bundle install`
## Development
Running the test suite:
bundle exec rake test
Running the app:
bundle exec rainbows
## Deployment
_TODO document options_
## Contributing ## Contributing
@@ -56,5 +23,5 @@ We love pull requests. If you want to submit a patch:
* Fork the project. * Fork the project.
* Make your feature addition or bug fix. * Make your feature addition or bug fix.
* Write specs for it. This is important so nobody breaks it in a future version. * Write specs for it. This is important so nobody breaks it in a future version unintentionally.
* Push to your fork and send a pull request. * Push to your fork and send a pull request.

View File

@@ -2,11 +2,10 @@ 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"] || 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"] || "us-east-1" %> region: <%= ENV["S3_REGION"] %>
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

@@ -431,9 +431,9 @@ module RemoteStorage
end end
end end
def escape(str) def escape(url)
# We want spaces to turn into %20 and slashes to stay slashes # We want spaces to turn into %20 and slashes to stay slashes
CGI::escape(str).gsub('+', '%20').gsub('%2F', '/') CGI::escape(url).gsub('+', '%20').gsub('%2F', '/')
end end
def redis def redis

View File

@@ -10,29 +10,18 @@ module RemoteStorage
private private
def s3_signer # S3 already wraps the ETag around quotes
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,
uri_escape_path: false
)
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
headers = { "Content-Type" => content_type } md5 = Digest::MD5.base64digest(data)
auth_headers = auth_headers_for("PUT", url, headers, data) authorization_headers = authorization_headers_for(
"PUT", url, md5, content_type
# TODO check if put was successful, e.g. it's returning a 413 directly ).merge({ "Content-Type" => content_type, "Content-Md5" => md5 })
# if the back-end does, too (missing CORS headers in that case) res = RestClient.put(url, data, authorization_headers)
res = RestClient.put(url, data, headers.merge(auth_headers))
return [ return [
res.headers[:etag].delete('"'), res.headers[:etag].delete('"'),
@@ -43,24 +32,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"]
auth_headers = auth_headers_for("GET", url, headers) authorization_headers = authorization_headers_for("GET", url)
RestClient.get(url, headers.merge(auth_headers), &block) RestClient.get(url, authorization_headers.merge(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
auth_headers = auth_headers_for("HEAD", url) authorization_headers = authorization_headers_for("HEAD", url)
RestClient.head(url, auth_headers, &block) RestClient.head(url, authorization_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
auth_headers = auth_headers_for("DELETE", url) authorization_headers = authorization_headers_for("DELETE", url)
RestClient.delete(url, auth_headers) RestClient.delete(url, authorization_headers)
end end
end end
@@ -78,11 +67,38 @@ module RemoteStorage
return found return found
end end
def auth_headers_for(http_method, url, headers = {}, data = nil) # This is using the S3 authorizations, not the newer AW V4 Signatures
signature = s3_signer.sign_request( # (https://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html)
http_method: http_method, url: url, headers: headers, body: data def authorization_headers_for(http_verb, url, md5 = nil, content_type = nil)
) url = File.join("/", url.gsub(base_url, ""))
signature.headers 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 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
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 end
def base_url def base_url
@@ -93,4 +109,5 @@ 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, Content-Type', 'Access-Control-Expose-Headers' => 'ETag, Content-Length, Content-Range',
'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'