Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for URL signatures #6

Merged
merged 1 commit into from
Jun 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@

# rspec failure tracking
.rspec_status

/.vscode/
5 changes: 5 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,11 @@ Style/RaiseArgs:
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#exception-class-messages"
Enabled: true

Style/RedundantFetchBlock:
Description: "This cop identifies places where fetch(key) { value } can be replaced by fetch(key, value)."
StyleGuide: "https://docs.rubocop.org/rubocop/0.86/cops_style.html#styleredundantfetchblock"
Enabled: true

Style/RedundantRegexpCharacterClass:
Description: "This cop checks for unnecessary single-element Regexp character classes."
StyleGuide: "https://docs.rubocop.org/rubocop/cops_style.html#styleredundantregexpcharacterclass"
Expand Down
16 changes: 8 additions & 8 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ GEM
specs:
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
ast (2.4.0)
ast (2.4.1)
coderay (1.1.3)
diff-lcs (1.3)
method_source (1.0.0)
parallel (1.19.1)
parser (2.7.1.3)
ast (~> 2.4.0)
parallel (1.19.2)
parser (2.7.1.4)
ast (~> 2.4.1)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (4.0.5)
rainbow (3.0.0)
rake (13.0.1)
regexp_parser (1.7.0)
regexp_parser (1.7.1)
rexml (3.2.4)
rspec (3.9.0)
rspec-core (~> 3.9.0)
Expand All @@ -37,16 +37,16 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-support (3.9.3)
rubocop (0.85.0)
rubocop (0.86.0)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
rexml
rubocop-ast (>= 0.0.3)
rubocop-ast (>= 0.0.3, < 1.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.0.3)
rubocop-ast (0.1.0)
parser (>= 2.7.0.1)
rubocop-performance (1.6.0)
rubocop (>= 0.71.0)
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Supports Ruby `2.4` and above, `JRuby`, and `TruffleRuby`.
- [Chainable helpers](#chainable-helpers)
- [Aliases](#aliases)
- [Custom helpers](#custom-helpers)
- [Security](#security)
- [Development](#development)
- [TODOs](#todos)
- [Contributing](#contributing)
Expand Down Expand Up @@ -50,6 +51,15 @@ object:
client = Cloudimage::Client.new(token: 'mysecrettoken')
```

Cloudimage client accepts the following options:

| Option | Required? | Additional info |
| ------------------ | --------- | --------------------------------------------------- |
| `token` | Yes | |
| `salt` | No | See [Security](#security). |
| `signature_length` | No | Integer value in the range `6..40`. Defaults to 18. |
| `api_version` | No | Defaults to the current stable version. |

Calling `path` on the client object returns an instance of `Cloudimage::URI`.
It accepts path to the image as a string and we we will use it to build
Cloudimage URLs.
Expand Down Expand Up @@ -109,6 +119,20 @@ need to accept arguments and will be translated into `param=1` in the final URL.
For a list of custom helpers available to you, please consult
[`Cloudimage::CustomHelpers`](lib/cloudimage/custom_helpers.rb) module.

### Security

If `salt` is defined, all URLs will be signed.

You can control the length of the generated signature by specifying `signature_length`
when initializing the client.

```ruby
client = Cloudimage::Client.new(token: 'mysecrettoken', salt: 'mysecretsalt', signature_length: 10)
uri = client.path('/assets/image.png')
uri.w(200).h(400).to_url
# => "https://mysecrettoken.cloudimg.io/v7/assets/image.png?h=400&w=200&ci_sign=79cfbc458b"
```

## Development

After checking out the repo, run `bin/setup` to install dependencies.
Expand All @@ -119,7 +143,7 @@ experiment.
### TODOs

- Implement the remaining supported Cloudimage params
- URL signature
- URL sealing
- Add support for aliases
- Add support for custom CNAMEs
- Add support for presets
Expand Down
30 changes: 22 additions & 8 deletions lib/cloudimage/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,44 @@ module Cloudimage
class InvalidConfig < StandardError; end

class Client
attr_reader :token
attr_reader :config

API_VERSION = 'v7'
DEFAULT_SIGNATURE_LENGTH = 18

def initialize(token: nil)
@token = token
def initialize(**options)
@config = {}
@config[:token] = options[:token]
@config[:salt] = options[:salt]
@config[:signature_length] =
options[:signature_length] || DEFAULT_SIGNATURE_LENGTH
@config[:api_version] = API_VERSION

ensure_valid_config
end

def path(path)
URI.new(base_url_for(token), path)
URI.new(path, **config)
end

private

def base_url_for(token)
"https://#{token}.cloudimg.io/#{API_VERSION}"
def ensure_valid_config
ensure_valid_token
ensure_valid_signature_length
end

def ensure_valid_config
return unless token.to_s.strip.empty?
def ensure_valid_token
return unless config[:token].nil?

raise InvalidConfig, 'Please specify your Cloudimage customer token.'
end

def ensure_valid_signature_length
return if config[:salt].nil?
return if (6..40).cover? config[:signature_length]

raise InvalidConfig, 'Signature length must be must be 6-40 characters.'
end
end
end
51 changes: 41 additions & 10 deletions lib/cloudimage/uri.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'digest'

require_relative 'params'
require_relative 'custom_helpers'

Expand All @@ -8,12 +10,12 @@ class URI
include Params
include CustomHelpers

attr_reader :uri, :params
attr_reader :uri, :params, :config

def initialize(base_url, path)
path = ensure_path_format(path)
@uri = Addressable::URI.parse(base_url + path)
def initialize(path, **config)
@config = config
@params = {}
@uri = build_uri_from(path)
end

PARAMS.each do |param|
Expand All @@ -32,16 +34,45 @@ def initialize(base_url, path)
alias_method from, to
end

def to_url(extra_params = {})
url_params = params.merge(extra_params)
uri.query_values = url_params if url_params.any?
uri.to_s
def to_url(**extra_params)
set_uri_params(**extra_params)
sign_url
end

private

def ensure_path_format(path)
path.start_with?('/') ? path : "/#{path}"
def base_url
"https://#{config[:token]}.cloudimg.io"
end

def base_url_with_api_version
"#{base_url}/#{config[:api_version]}"
end

def build_uri_from(path)
formatted_path = path.start_with?('/') ? path : "/#{path}"
Addressable::URI.parse(base_url_with_api_version + formatted_path)
end

def set_uri_params(**extra_params)
url_params = params.merge(**extra_params)
return unless url_params.any?

uri.query_values = url_params
end

def sign_url
url = uri.to_s

return url if config[:salt].nil?

url + "#{uri.query_values ? '&' : '?'}ci_sign=#{signature}"
end

def signature
path = uri.to_s.sub(base_url_with_api_version, '')
digest = Digest::SHA1.hexdigest(config[:salt] + path)
digest[0..(config[:signature_length] - 1)]
end
end
end
13 changes: 13 additions & 0 deletions spec/cloudimage/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,18 @@
expect { described_class.new }
.to raise_error Cloudimage::InvalidConfig, /Cloudimage customer token/
end

context 'URL signatures' do
it 'has a default signature length' do
client = described_class.new(token: 'mytoken')
expect((6..40)).to cover client.config[:signature_length]
end

it 'raises an error when given an invalid signature length' do
expect do
described_class.new(token: 'token', salt: 'slt', signature_length: 5)
end.to raise_error Cloudimage::InvalidConfig, /Signature length must be/
end
end
end
end
42 changes: 39 additions & 3 deletions spec/cloudimage/uri_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

describe Cloudimage::URI do
before do
token = 'token'
@client = Cloudimage::Client.new(token: token)
@base = "https://#{token}.cloudimg.io/v7"
@token = 'token'
@client = Cloudimage::Client.new(token: @token)
@base = "https://#{@token}.cloudimg.io/v7"
end

it 'does not catch missing methods' do
Expand Down Expand Up @@ -81,6 +81,42 @@
end
end

describe 'signature' do
context 'no params' do
it 'returns signed image URL' do
client = Cloudimage::Client.new(token: 'token', salt: 'salt')
expected = @base + '/assets/image.jpg?ci_sign=8e71efd440164f3cc8'
expect(client.path('/assets/image.jpg').to_url).to eq expected
end
end

context 'salt given' do
it 'returns signed image URL' do
client = Cloudimage::Client.new(token: @token, salt: 'salt')
expected = @base + '/assets/image.jpg?w=200&ci_sign=84c81ef20852013046'
expect(client.path('/assets/image.jpg').w(200).to_url).to eq expected
end

it 'returns trimmed signature if specified' do
client = Cloudimage::Client.new(
token: @token,
salt: 'salt',
signature_length: 8,
)
expected = @base + '/assets/image.jpg?w=200&ci_sign=84c81ef2'
expect(client.path('/assets/image.jpg').w(200).to_url).to eq expected
end

it 'handles a mix of helpers and to_url params' do
client = Cloudimage::Client.new(token: @token, salt: 'salt')
expected = @base +
'/assets/image.jpg?ci_info=1&h=100&w=200&ci_sign=40bd5a1d99455fe5cf'
expect(client.path('/assets/image.jpg').debug.to_url(w: 200, h: 100))
.to eq expected
end
end
end

context 'custom helpers' do
describe 'positionable_crop' do
it 'returns image URL with params encoded' do
Expand Down