image_vise 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +17 -10
- data/image_vise.gemspec +2 -2
- data/lib/image_vise.rb +27 -9
- data/lib/image_vise/image_request.rb +16 -8
- data/lib/image_vise/render_engine.rb +45 -17
- data/spec/image_vise/image_request_spec.rb +35 -3
- data/spec/image_vise/render_engine_spec.rb +73 -27
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5fd7e2b6f4a49ab832e600a46708d2974b74ed17
|
4
|
+
data.tar.gz: 1b13d15d9e99308a001ee62aeed21bf3e45c7429
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04fcaa4b8fba508845ba29634636edf9f67758d21687754d9d2117d82eefb22282cb22a678de26f4f0e31980a509beb9f6e62b229e08ebfe6dc1b2d5718f83b6
|
7
|
+
data.tar.gz: 6ac6c1bda81dbfa6b2ce27106277d016ff725ac80a51a116cba447a51fc878cb89393110e53f4c06befc65d617f041d6fc865a824103c1b9d8803ec776408989
|
data/README.md
CHANGED
@@ -57,7 +57,6 @@ You might want to define a helper method for generating signed URLs as well, whi
|
|
57
57
|
|
58
58
|
def thumb_url(source_image_url)
|
59
59
|
qs_params = ImageVise.image_params(src_url: source_image_url, secret: ENV.fetch('IMAGE_VISE_SECRET')) do |pipe|
|
60
|
-
# For example, you can also yield `pipeline` to the caller
|
61
60
|
pipe.fit_crop width: 256, height: 256, gravity: 'c'
|
62
61
|
pipe.sharpen sigma: 0.5, radius: 2
|
63
62
|
pipe.ellipse_stencil
|
@@ -66,6 +65,15 @@ You might want to define a helper method for generating signed URLs as well, whi
|
|
66
65
|
'/images?' + Rack::Utils.build_query(image_request)
|
67
66
|
end
|
68
67
|
|
68
|
+
|
69
|
+
## Processing files on the local filesystem instead of remote ones
|
70
|
+
|
71
|
+
If you want to grab a local file, compose a `file://` URL (mind the endcoding!)
|
72
|
+
|
73
|
+
src_url = 'file://' + URI.encode(File.expand_path(my_pic))
|
74
|
+
|
75
|
+
Note that you need to permit certain glob patterns as sources before this will work, see below.
|
76
|
+
|
69
77
|
## Operators and pipelining
|
70
78
|
|
71
79
|
ImageVise processes an image using _operators_. Each operator is just like an adjustment layer in Photoshop, except
|
@@ -160,26 +168,25 @@ multiple applications all using different keys for their signatures. Every reque
|
|
160
168
|
each key and if at least one key generates the same signature for the same given parameters, it is going to be
|
161
169
|
accepted and the request will be allowed to go through.
|
162
170
|
|
163
|
-
## Hostname validation
|
171
|
+
## Hostname and filesystem validation
|
164
172
|
|
165
173
|
By default, `ImageVise` will refuse to process images from URLs on "unknown" hosts. To mark a host as "known"
|
166
174
|
tell `ImageVise` to
|
167
175
|
|
168
176
|
ImageVise.add_allowed_host!('my-image-store.ourcompany.co.uk')
|
169
177
|
|
178
|
+
If you want to permit images from the local server filesystem to be accessed, add the glob pattern
|
179
|
+
to the set of allowed filesystem patterns:
|
180
|
+
|
181
|
+
ImageVise.allow_filesystem_source!(Rails.root + '/public/*.jpg')
|
182
|
+
|
183
|
+
Note that these are _glob_ patterns. The image path will be checked against them using `File.fnmatch`.
|
184
|
+
|
170
185
|
## State
|
171
186
|
|
172
187
|
Except for the HTTP cache for redirects et.al no state is stored (`ImageVise` does not care whether you store
|
173
188
|
your images using Dragonfly, CarrierWave or some custom handling code). All the app needs is the full URL.
|
174
189
|
|
175
|
-
## FAQ
|
176
|
-
|
177
|
-
* _Yo dawg, I thought you like URLs so I have put encoded URL in your URL so you can..._ - well, the only alternative
|
178
|
-
is also managing image storage, and this something we want to avoid to keep `ImageVise` stateless
|
179
|
-
* _But the URLs can be exploited_ - this is highly unlikely if you pick strong keys for the HMAC signatures
|
180
|
-
* _I can load any image into the thumbnailer_ - in fact, no. First you have the URL checks, and then - all the URLs
|
181
|
-
are supposed to be coming from the sources you trust since they are signed.
|
182
|
-
|
183
190
|
## Running the tests, versioning, contributing
|
184
191
|
|
185
192
|
By default, `bundle exec rake` will run RSpec and will also open the generated images using the `$ open` command available
|
data/image_vise.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: image_vise 0.0.
|
5
|
+
# stub: image_vise 0.0.19 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "image_vise"
|
9
|
-
s.version = "0.0.
|
9
|
+
s.version = "0.0.19"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
data/lib/image_vise.rb
CHANGED
@@ -3,45 +3,63 @@ require 'json'
|
|
3
3
|
require 'patron'
|
4
4
|
require 'rmagick'
|
5
5
|
require 'magic_bytes'
|
6
|
+
require 'thread'
|
6
7
|
|
7
8
|
class ImageVise
|
8
|
-
VERSION = '0.0.
|
9
|
-
|
9
|
+
VERSION = '0.0.19'
|
10
|
+
S_MUTEX = Mutex.new
|
11
|
+
private_constant :S_MUTEX
|
12
|
+
|
10
13
|
@allowed_hosts = Set.new
|
11
14
|
@keys = Set.new
|
12
15
|
@operators = {}
|
13
|
-
|
16
|
+
@allowed_glob_patterns = Set.new
|
17
|
+
|
14
18
|
class << self
|
15
19
|
# Resets all allowed hosts
|
16
20
|
def reset_allowed_hosts!
|
17
|
-
@allowed_hosts.clear
|
21
|
+
S_MUTEX.synchronize { @allowed_hosts.clear }
|
18
22
|
end
|
19
23
|
|
20
24
|
# Add an allowed host
|
21
25
|
def add_allowed_host!(hostname)
|
22
|
-
@allowed_hosts << hostname
|
26
|
+
S_MUTEX.synchronize { @allowed_hosts << hostname }
|
23
27
|
end
|
24
28
|
|
25
29
|
# Returns both the allowed hosts added at runtime and the ones set in the constant
|
26
30
|
def allowed_hosts
|
27
|
-
@allowed_hosts.to_a
|
31
|
+
S_MUTEX.synchronize { @allowed_hosts.to_a }
|
28
32
|
end
|
29
33
|
|
30
34
|
# Removes all set keys
|
31
35
|
def reset_secret_keys!
|
32
|
-
@keys.clear
|
36
|
+
S_MUTEX.synchronize { @keys.clear }
|
33
37
|
end
|
34
38
|
|
39
|
+
def allow_filesystem_source!(glob_pattern)
|
40
|
+
S_MUTEX.synchronize { @allowed_glob_patterns << glob_pattern }
|
41
|
+
end
|
42
|
+
|
43
|
+
def allowed_filesystem_sources
|
44
|
+
S_MUTEX.synchronize { @allowed_glob_patterns.to_a }
|
45
|
+
end
|
46
|
+
|
47
|
+
def deny_filesystem_sources!
|
48
|
+
S_MUTEX.synchronize { @allowed_glob_patterns.clear }
|
49
|
+
end
|
50
|
+
|
35
51
|
# Adds a key against which the parameters are going to be verified.
|
36
52
|
# Multiple applications may have their own different keys,
|
37
53
|
# so we need to have multiple keys.
|
38
54
|
def add_secret_key!(key)
|
39
|
-
@keys << key
|
55
|
+
S_MUTEX.synchronize { @keys << key }
|
56
|
+
self
|
40
57
|
end
|
41
58
|
|
42
59
|
# Returns the array of defined keys or raises an exception if no keys have been set yet
|
43
60
|
def secret_keys
|
44
|
-
|
61
|
+
keys = S_MUTEX.synchronize { @keys.any? && @keys.to_a }
|
62
|
+
keys or raise "No keys set, add a key using `ImageVise.add_secret_key!(key)'"
|
45
63
|
end
|
46
64
|
|
47
65
|
# Generate a set of querystring params for a resized image. Yields a Pipeline object that
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'base64'
|
2
|
+
require 'rack'
|
2
3
|
|
3
4
|
class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
4
5
|
class InvalidRequest < ArgumentError; end
|
@@ -8,7 +9,7 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
|
8
9
|
|
9
10
|
# Initializes a new ParamsChecker from given HTTP server framework
|
10
11
|
# params. The params can be symbol- or string-keyed, does not matter.
|
11
|
-
def self.to_request(qs_params:, secrets:, permitted_source_hosts:)
|
12
|
+
def self.to_request(qs_params:, secrets:, permitted_source_hosts:, allowed_filesystem_patterns:)
|
12
13
|
base64_encoded_params = qs_params.fetch(:q) rescue qs_params.fetch('q')
|
13
14
|
given_signature = qs_params.fetch(:sig) rescue qs_params.fetch('sig')
|
14
15
|
|
@@ -24,12 +25,19 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
|
24
25
|
# Pick up the URL and validate it
|
25
26
|
src_url = params.fetch(:src_url).to_s
|
26
27
|
raise URLError, "the :src_url parameter must be non-empty" if src_url.empty?
|
27
|
-
raise URLError, "#{src_url} is not permitted as source" unless valid_host?(src_url, permitted_source_hosts)
|
28
28
|
|
29
|
+
src_url = URI.parse(src_url)
|
30
|
+
if src_url.scheme == 'file'
|
31
|
+
raise URLError, "#{src_url} not permitted since filesystem access is disabled" if allowed_filesystem_patterns.empty?
|
32
|
+
raise URLError, "#{src_url} is not on the path whitelist" unless allowed_path?(allowed_filesystem_patterns, src_url.path)
|
33
|
+
elsif src_url.scheme != 'file'
|
34
|
+
raise URLError, "#{src_url} is not permitted as source" unless permitted_source_hosts.include?(src_url.host)
|
35
|
+
end
|
36
|
+
|
29
37
|
# Build out the processing pipeline
|
30
38
|
pipeline_definition = params.fetch(:pipeline)
|
31
39
|
|
32
|
-
new(src_url: src_url, pipeline: ImageVise::Pipeline.from_param(pipeline_definition))
|
40
|
+
new(src_url: src_url.to_s, pipeline: ImageVise::Pipeline.from_param(pipeline_definition))
|
33
41
|
rescue KeyError => e
|
34
42
|
raise InvalidRequest.new(e.message)
|
35
43
|
end
|
@@ -49,6 +57,11 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
|
49
57
|
|
50
58
|
private
|
51
59
|
|
60
|
+
def self.allowed_path?(filesystem_glob_patterns, path_to_check)
|
61
|
+
expanded_path = File.expand_path(path_to_check)
|
62
|
+
filesystem_glob_patterns.any? {|pattern| File.fnmatch?(pattern, expanded_path) }
|
63
|
+
end
|
64
|
+
|
52
65
|
def self.valid_signature?(for_payload, given_signature, secrets)
|
53
66
|
# Check the signature against every key that we have,
|
54
67
|
# since different apps might be using different keys
|
@@ -60,9 +73,4 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
|
60
73
|
end
|
61
74
|
seen_valid_signature
|
62
75
|
end
|
63
|
-
|
64
|
-
def self.valid_host?(src_url, permitted_hosts)
|
65
|
-
parsed_url = URI.parse(src_url)
|
66
|
-
permitted_hosts.include?(parsed_url.host)
|
67
|
-
end
|
68
76
|
end
|
@@ -35,20 +35,12 @@ class ImageVise::RenderEngine
|
|
35
35
|
|
36
36
|
# Fetch the given URL into a Tempfile and return the File object
|
37
37
|
def fetch_url_into_tempfile(source_image_uri)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
response = s.get_file(source_image_uri, tf.path)
|
44
|
-
if PASSTHROUGH_STATUS_CODES.include?(response.status)
|
45
|
-
tf.close; tf.unlink;
|
46
|
-
bail response.status, "Unfortunate upstream response: #{response.status}"
|
38
|
+
parsed = URI.parse(source_image_uri)
|
39
|
+
if parsed.scheme == 'file'
|
40
|
+
copy_path_into_tempfile(URI.decode(parsed.path))
|
41
|
+
else
|
42
|
+
fetch_url(source_image_uri)
|
47
43
|
end
|
48
|
-
tf
|
49
|
-
rescue Exception => e
|
50
|
-
tf.close; tf.unlink;
|
51
|
-
raise e
|
52
44
|
end
|
53
45
|
|
54
46
|
def bail(status, *errors_array)
|
@@ -76,9 +68,7 @@ class ImageVise::RenderEngine
|
|
76
68
|
bail(405, 'Only GET supported') unless req.get?
|
77
69
|
|
78
70
|
# Validate the inputs
|
79
|
-
image_request = ImageVise::ImageRequest.to_request(qs_params: req.params,
|
80
|
-
secrets: ImageVise.secret_keys,
|
81
|
-
permitted_source_hosts: ImageVise.allowed_hosts)
|
71
|
+
image_request = ImageVise::ImageRequest.to_request(qs_params: req.params, **image_request_options)
|
82
72
|
|
83
73
|
# Recover the source image URL and the pipeline instructions (all the image ops)
|
84
74
|
source_image_uri, pipeline = image_request.src_url, image_request.pipeline
|
@@ -206,5 +196,43 @@ class ImageVise::RenderEngine
|
|
206
196
|
ensure
|
207
197
|
ImageVise.destroy(magick_image)
|
208
198
|
end
|
209
|
-
|
199
|
+
|
200
|
+
def image_request_options
|
201
|
+
{
|
202
|
+
secrets: ImageVise.secret_keys,
|
203
|
+
permitted_source_hosts: ImageVise.allowed_hosts,
|
204
|
+
allowed_filesystem_patterns: ImageVise.allowed_filesystem_sources,
|
205
|
+
}
|
206
|
+
end
|
207
|
+
|
208
|
+
def fetch_url(source_image_uri)
|
209
|
+
tf = binary_tempfile
|
210
|
+
s = Patron::Session.new
|
211
|
+
s.automatic_content_encoding = true
|
212
|
+
s.timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
213
|
+
s.connect_timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
214
|
+
response = s.get_file(source_image_uri, tf.path)
|
215
|
+
if PASSTHROUGH_STATUS_CODES.include?(response.status)
|
216
|
+
tf.close; tf.unlink;
|
217
|
+
bail response.status, "Unfortunate upstream response: #{response.status}"
|
218
|
+
end
|
219
|
+
tf
|
220
|
+
rescue Exception => e
|
221
|
+
tf.close; tf.unlink;
|
222
|
+
raise e
|
223
|
+
end
|
224
|
+
|
225
|
+
def copy_path_into_tempfile(path_on_filesystem)
|
226
|
+
tf = binary_tempfile
|
227
|
+
File.open(path_on_filesystem, 'rb') do |f|
|
228
|
+
IO.copy_stream(f, tf)
|
229
|
+
end
|
230
|
+
tf.rewind; tf
|
231
|
+
rescue Errno::ENOENT
|
232
|
+
bail 404, "Image file not found"
|
233
|
+
rescue Exception => e
|
234
|
+
tf.close; tf.unlink;
|
235
|
+
raise e
|
236
|
+
end
|
237
|
+
|
210
238
|
end
|
@@ -10,11 +10,43 @@ describe ImageVise::ImageRequest do
|
|
10
10
|
sig: signature
|
11
11
|
}
|
12
12
|
|
13
|
-
image_request = described_class.to_request(qs_params: params, secrets: ['this is a secret'],
|
13
|
+
image_request = described_class.to_request(qs_params: params, secrets: ['this is a secret'],
|
14
|
+
permitted_source_hosts: ['bucket.s3.aws.com'], allowed_filesystem_patterns: [])
|
14
15
|
request_qs_params = image_request.to_query_string_params('this is a secret')
|
15
16
|
expect(request_qs_params).to be_kind_of(Hash)
|
16
17
|
|
17
|
-
image_request_roundtrip = described_class.to_request(qs_params: request_qs_params,
|
18
|
+
image_request_roundtrip = described_class.to_request(qs_params: request_qs_params,
|
19
|
+
secrets: ['this is a secret'], permitted_source_hosts: ['bucket.s3.aws.com'], allowed_filesystem_patterns: [])
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'forbids a file:// URL if the flag is not enabled' do
|
23
|
+
img_params = {src_url: 'file:///etc/passwd', pipeline: [[:auto_orient]]}
|
24
|
+
img_params_json = JSON.dump(img_params)
|
25
|
+
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', img_params_json)
|
26
|
+
params = {
|
27
|
+
q: Base64.encode64(img_params_json),
|
28
|
+
sig: signature
|
29
|
+
}
|
30
|
+
|
31
|
+
expect {
|
32
|
+
described_class.to_request(qs_params: params, secrets: ['this is a secret'],
|
33
|
+
permitted_source_hosts: ['bucket.s3.aws.com'], allowed_filesystem_patterns: [])
|
34
|
+
}.to raise_error(/filesystem access is disabled/)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'allows a file:// URL if its path is within the permit list' do
|
38
|
+
img_params = {src_url: 'file:///etc/passwd', pipeline: [[:auto_orient, {}]]}
|
39
|
+
img_params_json = JSON.dump(img_params)
|
40
|
+
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', img_params_json)
|
41
|
+
params = {
|
42
|
+
q: Base64.encode64(img_params_json),
|
43
|
+
sig: signature
|
44
|
+
}
|
45
|
+
|
46
|
+
image_request = described_class.to_request(qs_params: params, secrets: ['this is a secret'],
|
47
|
+
permitted_source_hosts: ['bucket.s3.aws.com'], allowed_filesystem_patterns: %w( /etc/* ))
|
48
|
+
request_qs_params = image_request.to_query_string_params('this is a secret')
|
49
|
+
expect(request_qs_params).to be_kind_of(Hash)
|
18
50
|
end
|
19
51
|
|
20
52
|
describe 'fails with an invalid pipeline' do
|
@@ -46,7 +78,7 @@ describe ImageVise::ImageRequest do
|
|
46
78
|
|
47
79
|
expect {
|
48
80
|
described_class.to_request(qs_params: params,
|
49
|
-
secrets: ['b'], permitted_source_hosts: ['bucket.s3.aws.com'])
|
81
|
+
secrets: ['b'], permitted_source_hosts: ['bucket.s3.aws.com'], allowed_filesystem_patterns: [])
|
50
82
|
}.to raise_error(/Invalid or missing signature/)
|
51
83
|
end
|
52
84
|
end
|
@@ -30,38 +30,50 @@ describe ImageVise::RenderEngine do
|
|
30
30
|
end
|
31
31
|
|
32
32
|
context 'when requesting an image' do
|
33
|
+
before :each do
|
34
|
+
parsed_url = Addressable::URI.parse(public_url)
|
35
|
+
ImageVise.add_allowed_host!(parsed_url.host)
|
36
|
+
end
|
37
|
+
|
33
38
|
after :each do
|
34
39
|
ImageVise.reset_allowed_hosts!
|
35
40
|
ImageVise.reset_secret_keys!
|
36
41
|
end
|
37
42
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
it 'halts with 422 when the requested image cannot be opened by ImageMagick' do
|
44
|
+
uri = Addressable::URI.parse(public_url)
|
45
|
+
ImageVise.add_allowed_host!(uri.host)
|
46
|
+
ImageVise.add_secret_key!('l33tness')
|
47
|
+
uri.path = '/___nonexistent_image.jpg'
|
48
|
+
|
49
|
+
p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
|
50
|
+
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
51
|
+
params = image_request.to_query_string_params('l33tness')
|
52
|
+
|
53
|
+
expect_any_instance_of(Patron::Session).to receive(:get_file) {|_self, url, path|
|
54
|
+
File.open(path, 'wb') {|f| f << 'totally not an image' }
|
55
|
+
double(status: 200)
|
56
|
+
}
|
57
|
+
expect(app).to receive(:handle_request_error).and_call_original
|
58
|
+
|
59
|
+
get '/', params
|
60
|
+
expect(last_response.status).to eq(422)
|
61
|
+
expect(last_response['Cache-Control']).to eq("private, max-age=0, no-cache")
|
62
|
+
expect(last_response.body).to include('Unsupported/unknown')
|
63
|
+
end
|
43
64
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
double(status: 200)
|
57
|
-
}
|
58
|
-
expect(app).to receive(:handle_request_error).and_call_original
|
59
|
-
|
60
|
-
get '/', params
|
61
|
-
expect(last_response.status).to eq(422)
|
62
|
-
expect(last_response['Cache-Control']).to eq("private, max-age=0, no-cache")
|
63
|
-
expect(last_response.body).to include('Unsupported/unknown')
|
64
|
-
end
|
65
|
+
it 'halts with 422 when a file:// URL is given and filesystem access is not enabled' do
|
66
|
+
uri = 'file://' + test_image_path
|
67
|
+
ImageVise.deny_filesystem_sources!
|
68
|
+
ImageVise.add_secret_key!('l33tness')
|
69
|
+
|
70
|
+
p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
|
71
|
+
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
72
|
+
params = image_request.to_query_string_params('l33tness')
|
73
|
+
|
74
|
+
get '/', params
|
75
|
+
expect(last_response.status).to eq(422)
|
76
|
+
expect(last_response.body).to include('filesystem access is disabled')
|
65
77
|
end
|
66
78
|
|
67
79
|
it 'responds with 403 when upstream returns it' do
|
@@ -145,7 +157,41 @@ describe ImageVise::RenderEngine do
|
|
145
157
|
parsed_image = Magick::Image.from_blob(last_response.body)[0]
|
146
158
|
expect(parsed_image.columns).to eq(10)
|
147
159
|
end
|
148
|
-
|
160
|
+
|
161
|
+
it 'picks the image from the filesystem if that is permitted' do
|
162
|
+
uri = 'file://' + test_image_path
|
163
|
+
ImageVise.allow_filesystem_source!(File.dirname(test_image_path) + '/*.*')
|
164
|
+
ImageVise.add_secret_key!('l33tness')
|
165
|
+
|
166
|
+
p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
|
167
|
+
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
168
|
+
params = image_request.to_query_string_params('l33tness')
|
169
|
+
|
170
|
+
get '/', params
|
171
|
+
expect(last_response.status).to eq(200)
|
172
|
+
expect(last_response.headers['Content-Type']).to eq('image/jpeg')
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'expands and forbids a path outside of the permitted sources'
|
176
|
+
|
177
|
+
it 'URI-decodes the path in a file:// URL for a file with a Unicode path' do
|
178
|
+
utf8_file_path = File.dirname(test_image_path) + '/картинка.jpg'
|
179
|
+
FileUtils.cp_r(test_image_path, utf8_file_path)
|
180
|
+
uri = 'file://' + URI.encode(utf8_file_path)
|
181
|
+
|
182
|
+
ImageVise.allow_filesystem_source!(File.dirname(test_image_path) + '/*.*')
|
183
|
+
ImageVise.add_secret_key!('l33tness')
|
184
|
+
|
185
|
+
p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
|
186
|
+
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
187
|
+
params = image_request.to_query_string_params('l33tness')
|
188
|
+
|
189
|
+
get '/', params
|
190
|
+
File.unlink(utf8_file_path)
|
191
|
+
expect(last_response.status).to eq(200)
|
192
|
+
expect(last_response.headers['Content-Type']).to eq('image/jpeg')
|
193
|
+
end
|
194
|
+
|
149
195
|
it 'returns the processed JPEG image as a PNG if it had to get an alpha channel during processing' do
|
150
196
|
uri = Addressable::URI.parse(public_url)
|
151
197
|
ImageVise.add_allowed_host!(uri.host)
|