image_vise 0.0.17 → 0.0.19
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|