Compare commits

..

34 Commits

Author SHA1 Message Date
ce5f938938
Merge branch 'master' into stable 2022-05-13 19:30:43 +02:00
Greg Karékinian
2434c6e0b2 Merge branch 'master' into stable 2020-04-16 12:50:09 +02:00
Greg Karékinian
92e9e048a2 Merge branch 'master' into stable 2019-08-01 11:39:53 +02:00
Greg Karékinian
62f7d8790a Merge branch 'master' into stable 2019-02-07 11:02:05 +01:00
Greg Karékinian
acecb370c6 Merge branch 'master' into stable 2018-11-26 12:30:47 +01:00
Greg Karékinian
fbd8e994fb Merge branch 'master' into stable 2018-07-19 15:48:32 +02:00
Greg Karékinian
fa8a829ed9 Merge branch 'master' into stable 2018-05-23 11:14:48 +02:00
Greg Karékinian
eca4f1e1d7 Merge branch 'master' into stable 2018-01-05 13:46:28 +01:00
Greg Karékinian
eb7f39374c Merge branch 'master' into stable 2018-01-03 22:13:27 +01:00
Greg Karékinian
29c0d58c2a Merge branch 'master' into stable 2017-07-05 23:59:32 +02:00
Greg Karékinian
1b116cf65e Merge branch 'master' into stable 2016-12-15 18:19:48 +01:00
Greg Karékinian
28a6d1700b Merge branch 'master' into stable 2016-09-16 11:25:50 +02:00
Garret Alfert
4111cbdddf Merge branch 'master' into stable 2016-09-08 15:04:26 +02:00
Garret Alfert
04197a517e Merge branch 'master' into stable 2016-08-08 15:54:07 +02:00
Greg Karékinian
6a3181482d Merge branch 'master' into stable 2016-07-23 14:35:11 +02:00
Greg Karékinian
7110f8c315 Merge branch 'master' into stable 2016-07-21 15:13:22 +02:00
Garret Alfert
3fafac0d2d Merge branch 'master' into stable 2016-06-03 16:26:50 +02:00
Garret Alfert
51c1d0937a Merge branch 'master' into stable 2016-03-07 14:37:50 +01:00
Garret Alfert
c3d3e79bd7 Merge branch 'master' into stable 2016-03-04 23:13:24 +01:00
Garret Alfert
396a102755 Merge branch 'master' into stable 2016-03-03 12:00:19 +01:00
Greg Karékinian
6347433184 Fix .travis.yml file 2016-02-11 17:22:14 +01:00
Greg Karékinian
962a908a7a Force legacy Blue Box build for now on Travis 2016-02-11 17:22:02 +01:00
Greg Karékinian
d4b4b619e2 Change the way to load the Redis config to allow clusters 2016-02-11 16:49:03 +01:00
Greg Karékinian
1ce856919e Merge branch 'master' into stable 2015-11-24 15:38:15 +01:00
Greg Karékinian
64e6dc7992 Merge branch 'master' into stable 2015-09-30 16:22:20 +02:00
Greg Karékinian
031ce9453c Merge branch 'master' into stable 2015-09-29 15:04:48 +02:00
Greg Karékinian
9c1e19deb3 Merge branch 'master' into stable 2015-09-07 10:45:12 +02:00
Greg Karékinian
c40bb0ebf5 Merge branch 'master' into stable 2015-07-03 17:51:22 +02:00
Greg Karékinian
ca80b64111 Fix a regression with ETag and If-None-Match 2015-06-30 15:28:16 +02:00
Greg Karékinian
2a4d148cbb Really fix the directory listing for a user's root 2015-06-30 15:28:15 +02:00
Greg Karékinian
6430a4b605 Generate an ETag for a container's root 2015-06-30 15:28:15 +02:00
Greg Karékinian
9dda344b26 Fix root directory listing
It was flattened by accident

Refs #62
2015-06-30 15:28:07 +02:00
Greg Karékinian
207bb0efde Revert "Revert "Fix listing of a user's root directory""
This reverts commit 3ce3efd0fc20ad6a3816256bc2e786a00fa6e6f0.
2015-06-30 15:27:29 +02:00
Greg Karékinian
3ce3efd0fc Revert "Fix listing of a user's root directory"
This reverts commit 1cf99377dc7ecf26fd2d5b43c03f615d82876d13.
2015-06-29 21:42:21 +02:00
14 changed files with 189 additions and 192 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

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

@ -0,0 +1,35 @@
name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.5', '2.6', '2.7']
redis-version: [4, 5, 6]
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.example.s3 config.yml
- name: Run tests
run: bundle exec rake test

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
config.yml
config.yml.erb
cs_credentials.json
pids
.bundle

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"]

11
Gemfile
View File

@ -2,22 +2,23 @@ source "https://rubygems.org"
gem "sinatra", "~> 2.2.0"
gem "sinatra-contrib", "~> 2.2.0"
gem "activesupport", "~> 6.1.0"
gem "activesupport", "~> 6.0.5"
gem "rest-client", "~> 2.1.0" # Fixes a memory leak in Ruby 2.4
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"
gem "rainbows"
group :test do
gem 'rake'
gem 'rack-test'
gem 'purdytest', :require => false
gem 'm'
gem 'minitest'
gem 'minitest-stub_any_instance'
gem 'webmock'
end
group :staging, :production do
gem "rainbows"
gem "sentry-raven", require: false
end

View File

@ -1,104 +1,102 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (6.1.7.6)
activesupport (6.0.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
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)
rexml
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
concurrent-ruby (1.1.10)
crack (0.4.3)
safe_yaml (~> 1.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
faraday (2.7.11)
base64
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-net_http (3.0.2)
hashdiff (1.0.1)
faraday (0.17.0)
multipart-post (>= 1.2, < 3)
hashdiff (1.0.0)
http-accept (1.7.0)
http-cookie (1.0.5)
http-cookie (1.0.3)
domain_name (~> 0.5)
i18n (1.14.1)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
kgio (2.11.4)
m (1.6.2)
kgio (2.11.2)
m (1.5.1)
method_source (>= 0.6.7)
rake (>= 0.9.2.2)
method_source (1.0.0)
mime-types (3.5.1)
method_source (0.9.2)
mime-types (3.3)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.1003)
minitest (5.20.0)
minitest-stub_any_instance (1.0.3)
mime-types-data (3.2019.1009)
minitest (5.13.0)
minitest-stub_any_instance (1.0.2)
multi_json (1.15.0)
mustermann (2.0.2)
multipart-post (2.1.1)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
netrc (0.11.0)
public_suffix (5.0.3)
rack (2.2.8)
rack-protection (2.2.4)
public_suffix (4.0.1)
purdytest (2.0.0)
minitest (~> 5.5)
rack (2.2.3)
rack-protection (2.2.0)
rack
rack-test (2.1.0)
rack (>= 1.3)
rainbows (5.2.1)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rainbows (5.2.0)
kgio (~> 2.5)
rack (>= 1.1, < 3.0)
unicorn (~> 5.1)
raindrops (0.20.1)
rake (13.0.6)
raindrops (0.19.0)
rake (13.0.1)
redis (4.6.0)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rexml (3.2.6)
ruby2_keywords (0.0.5)
sentry-raven (3.1.2)
faraday (>= 1.0)
sinatra (2.2.4)
mustermann (~> 2.0)
safe_yaml (1.0.5)
sentry-raven (2.12.3)
faraday (>= 0.7.6, < 1.0)
sinatra (2.2.0)
mustermann (~> 1.0)
rack (~> 2.2)
rack-protection (= 2.2.4)
rack-protection (= 2.2.0)
tilt (~> 2.0)
sinatra-contrib (2.2.4)
sinatra-contrib (2.2.0)
multi_json
mustermann (~> 2.0)
rack-protection (= 2.2.4)
sinatra (= 2.2.4)
mustermann (~> 1.0)
rack-protection (= 2.2.0)
sinatra (= 2.2.0)
tilt (~> 2.0)
tilt (2.3.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
thread_safe (0.3.6)
tilt (2.0.10)
tzinfo (1.2.9)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicorn (5.8.0)
unf_ext (0.0.7.6)
unicorn (5.5.1)
kgio (~> 2.6)
raindrops (~> 0.7)
webmock (3.19.1)
addressable (>= 2.8.0)
webmock (3.7.6)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
zeitwerk (2.6.12)
zeitwerk (2.5.4)
PLATFORMS
ruby
DEPENDENCIES
activesupport (~> 6.1.0)
aws-sigv4 (~> 1.0.0)
activesupport (~> 6.0.5)
m
mime-types
minitest
minitest-stub_any_instance
purdytest
rack-test
rainbows
rake
@ -110,4 +108,4 @@ DEPENDENCIES
webmock
BUNDLED WITH
2.3.7
1.16.0

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

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

26
config.yml.example.s3 Normal file
View File

@ -0,0 +1,26 @@
development: &defaults
maintenance: false
s3:
endpoint: "https://some-endpoint"
region: "region"
access_key_id: ""
secret_key_id: ""
bucket: "test-bucket"
redis:
host: localhost
port: 6379
test:
<<: *defaults
s3:
endpoint: "https://some-endpoint"
region: "region"
access_key_id: ""
secret_key_id: ""
bucket: "test-bucket"
staging:
<<: *defaults
production:
<<: *defaults

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

@ -1,8 +1,8 @@
require "remote_storage/rest_provider"
require "digest"
require "base64"
require "cgi"
require "openssl"
require "webrick/httputils"
module RemoteStorage
class S3
@ -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)
WEBrick::HTTPUtils.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
@ -90,7 +106,8 @@ module RemoteStorage
end
def container_url_for(user)
"#{base_url}/#{settings.s3["bucket"]}/#{user}"
"#{base_url}#{settings.s3["bucket"]}/#{user}"
end
end
end

View File

@ -18,7 +18,7 @@ class LiquorCabinet < Sinatra::Base
register Sinatra::ConfigFile
set :environments, %w{development test production staging}
config_file 'config.yml.erb'
config_file 'config.yml'
end
configure :development do
@ -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'

View File

@ -2,7 +2,7 @@ require_relative "../spec_helper"
describe "S3 provider" do
def container_url_for(user)
"#{app.settings.s3["endpoint"]}/#{app.settings.s3["bucket"]}/#{user}"
"#{app.settings.s3["endpoint"]}#{app.settings.s3["bucket"]}/#{user}"
end
def storage_class

View File

@ -8,6 +8,7 @@ require_relative '../liquor-cabinet'
require 'minitest/autorun'
require "minitest/stub_any_instance"
require 'rack/test'
require 'purdytest'
require "redis"
require "rest_client"
require "ostruct"
@ -35,22 +36,22 @@ if app.settings.respond_to? :redis
end
end
Minitest::Spec.class_eval do
MiniTest::Spec.class_eval do
def self.shared_examples
@shared_examples ||= {}
end
end
module Minitest::Spec::SharedExamples
module MiniTest::Spec::SharedExamples
def shared_examples_for(desc, &block)
Minitest::Spec.shared_examples[desc] = block
MiniTest::Spec.shared_examples[desc] = block
end
def it_behaves_like(desc)
self.instance_eval(&Minitest::Spec.shared_examples[desc])
self.instance_eval(&MiniTest::Spec.shared_examples[desc])
end
end
Object.class_eval { include(Minitest::Spec::SharedExamples) }
Object.class_eval { include(MiniTest::Spec::SharedExamples) }
require_relative 'shared_examples'