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-contrib", "~> 2.2.0"
gem "activesupport", "~> 6.1.0"
gem "redis", "~> 4.6.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 "rainbows"
group :test do
gem 'rake'
@@ -19,5 +19,6 @@ group :test do
end
group :staging, :production do
gem "rainbows"
gem "sentry-raven", require: false
end

View File

@@ -9,7 +9,6 @@ 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)
@@ -94,7 +93,6 @@ PLATFORMS
DEPENDENCIES
activesupport (~> 6.1.0)
aws-sigv4 (~> 1.0.0)
m
mime-types
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 is where Frank stores all his stuff. It's a
[remoteStorage](https://remotestorage.io) HTTP API, based on Sinatra. The
metadata and OAuth tokens are stored in Redis, and
documents/files can be stored in anything that supports
the S3 object storage API.
metadata and OAuth tokens are stored in Redis, and documents can be stored in
anything that supports the storage API of either Openstack Swift or Amazon S3.
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
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.
There is an [open-source accounts management
app](https://gitea.kosmos.org/kosmos/akkounts/) by the Kosmos project, which
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
If you have any questions about this program, drop by #remotestorage on
Freenode, or [post to the RS
forums](https://community.remotestorage.io/c/server-development), and we'll
gladly 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_
happily answer them.
## Contributing
@@ -56,5 +23,5 @@ We love pull requests. If you want to submit a patch:
* Fork the project.
* 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.

View File

@@ -2,11 +2,10 @@ development: &defaults
maintenance: false
redis:
host: <%= ENV["REDIS_HOST"] || "localhost" %>
port: <%= ENV["REDIS_PORT"] || 6379 %>
db: <%= ENV["REDIS_DB"] || 1 %>
port: <%= ENV["REDIS_PORT"] || "6379" %>
s3: &s3_defaults
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" %>
secret_key_id: <%= ENV["S3_SECRET_KEY"] || "minioadmin" %>
bucket: <%= ENV["S3_BUCKET"] || "rs-development" %>

View File

@@ -431,9 +431,9 @@ module RemoteStorage
end
end
def escape(str)
def escape(url)
# 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
def redis

View File

@@ -10,29 +10,18 @@ module RemoteStorage
private
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,
uri_escape_path: false
)
end
# S3 already wraps the ETag with quotes
# S3 already wraps the ETag around quotes
def format_etag(etag)
etag
end
def do_put_request(url, data, content_type)
deal_with_unauthorized_requests do
headers = { "Content-Type" => content_type }
auth_headers = auth_headers_for("PUT", url, headers, data)
# TODO check if put was successful, e.g. it's returning a 413 directly
# if the back-end does, too (missing CORS headers in that case)
res = RestClient.put(url, data, headers.merge(auth_headers))
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)
return [
res.headers[:etag].delete('"'),
@@ -43,24 +32,24 @@ module RemoteStorage
def do_get_request(url, &block)
deal_with_unauthorized_requests do
headers = {}
headers = { }
headers["Range"] = server.env["HTTP_RANGE"] if server.env["HTTP_RANGE"]
auth_headers = auth_headers_for("GET", url, headers)
RestClient.get(url, headers.merge(auth_headers), &block)
authorization_headers = authorization_headers_for("GET", url)
RestClient.get(url, authorization_headers.merge(headers), &block)
end
end
def do_head_request(url, &block)
deal_with_unauthorized_requests do
auth_headers = auth_headers_for("HEAD", url)
RestClient.head(url, auth_headers, &block)
authorization_headers = authorization_headers_for("HEAD", url)
RestClient.head(url, authorization_headers, &block)
end
end
def do_delete_request(url)
deal_with_unauthorized_requests do
auth_headers = auth_headers_for("DELETE", url)
RestClient.delete(url, auth_headers)
authorization_headers = authorization_headers_for("DELETE", url)
RestClient.delete(url, authorization_headers)
end
end
@@ -78,11 +67,38 @@ module RemoteStorage
return found
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
)
signature.headers
# 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 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
def base_url
@@ -93,4 +109,5 @@ module RemoteStorage
"#{base_url}/#{settings.s3["bucket"]}/#{user}"
end
end
end

View File

@@ -64,7 +64,7 @@ class LiquorCabinet < Sinatra::Base
headers 'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, PUT, DELETE',
'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'
headers['Access-Control-Allow-Origin'] = env["HTTP_ORIGIN"] if env["HTTP_ORIGIN"]
headers['Cache-Control'] = 'no-cache'