image_vise 0.0.24 → 0.0.25
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/examples/error_handline_appsignal.rb +23 -0
- data/examples/error_handling_sentry.rb +25 -0
- data/image_vise.gemspec +16 -12
- data/lib/image_vise/fetchers/fetcher_file.rb +27 -0
- data/lib/image_vise/fetchers/fetcher_http.rb +43 -0
- data/lib/image_vise/file_response.rb +1 -2
- data/lib/image_vise/image_request.rb +5 -22
- data/lib/image_vise/pipeline.rb +1 -1
- data/lib/image_vise/render_engine.rb +25 -72
- data/lib/image_vise.rb +24 -4
- data/spec/image_vise/image_request_spec.rb +7 -30
- data/spec/image_vise/render_engine_spec.rb +3 -3
- data/spec/image_vise_spec.rb +14 -1
- metadata +15 -11
- /data/lib/image_vise/{auto_orient.rb → operators/auto_orient.rb} +0 -0
- /data/lib/image_vise/{crop.rb → operators/crop.rb} +0 -0
- /data/lib/image_vise/{ellipse_stencil.rb → operators/ellipse_stencil.rb} +0 -0
- /data/lib/image_vise/{fit_crop.rb → operators/fit_crop.rb} +0 -0
- /data/lib/image_vise/{geom.rb → operators/geom.rb} +0 -0
- /data/lib/image_vise/{sRGB_v4_ICC_preference_displayclass.icc → operators/sRGB_v4_ICC_preference_displayclass.icc} +0 -0
- /data/lib/image_vise/{sharpen.rb → operators/sharpen.rb} +0 -0
- /data/lib/image_vise/{srgb.rb → operators/srgb.rb} +0 -0
- /data/lib/image_vise/{strip_metadata.rb → operators/strip_metadata.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4505547a1ef87774694ee0a4e0adda3354e9f676
|
4
|
+
data.tar.gz: c403f4147f907b10b2f13ca6ea2564f73bc21303
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 736ed9bedaf46db49558b3fcec83d992368ccd670df57083be3e8ec66dbfc7bc31420a0debbd085b9237fdb29891b8c495bfbc27d0b6fac7e902b2f3938ef3f5
|
7
|
+
data.tar.gz: fe83b1a5f073e2b01075f6ba8f7e4451985727650e69ff390a1abc3350113639c8459e3e1341fcb45738390cf1750671694593edbcfd71bfa0db9802b66c49c7
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Anywhere in your app code
|
2
|
+
module ImageViseAppsignal
|
3
|
+
ImageVise::RenderEngine.prepend self
|
4
|
+
|
5
|
+
def setup_error_handling(rack_env)
|
6
|
+
txn = Appsignal::Transaction.current
|
7
|
+
txn.set_action('%s#%s' % [self.class, 'call'])
|
8
|
+
end
|
9
|
+
|
10
|
+
def handle_request_error(err)
|
11
|
+
Appsignal.add_exception(err)
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_generic_error(err)
|
15
|
+
Appsignal.add_exception(err)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# In config.ru
|
20
|
+
map '/thumbnails' do
|
21
|
+
use Appsignal::Rack::GenericInstrumentation
|
22
|
+
run ImageVise
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Anywhere in your app code
|
2
|
+
module ImageViseSentrySupport
|
3
|
+
ImageVise::RenderEngine.prepend self
|
4
|
+
|
5
|
+
def setup_error_handling(rack_env)
|
6
|
+
@env = rack_env
|
7
|
+
end
|
8
|
+
|
9
|
+
def handle_request_error(err)
|
10
|
+
@env['rack.exception'] = err
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle_generic_error(err)
|
14
|
+
@env['rack.exception'] = err
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# In config.ru
|
19
|
+
Raven.configure do |config|
|
20
|
+
config.dsn = 'https://secretoken@app.getsentry.com/1234567'
|
21
|
+
end
|
22
|
+
use Raven::Rack
|
23
|
+
map '/thumbnails' do
|
24
|
+
run ImageVise
|
25
|
+
end
|
data/image_vise.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
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.25 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.25"
|
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"]
|
13
13
|
s.authors = ["Julik Tarkhanov"]
|
14
|
-
s.date = "2016-10-
|
14
|
+
s.date = "2016-10-18"
|
15
15
|
s.description = "Image processing via URLs"
|
16
16
|
s.email = "me@julik.nl"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -24,21 +24,25 @@ Gem::Specification.new do |s|
|
|
24
24
|
"README.md",
|
25
25
|
"Rakefile",
|
26
26
|
"examples/config.ru",
|
27
|
+
"examples/error_handline_appsignal.rb",
|
28
|
+
"examples/error_handling_sentry.rb",
|
27
29
|
"image_vise.gemspec",
|
28
30
|
"lib/image_vise.rb",
|
29
|
-
"lib/image_vise/
|
30
|
-
"lib/image_vise/
|
31
|
-
"lib/image_vise/ellipse_stencil.rb",
|
31
|
+
"lib/image_vise/fetchers/fetcher_file.rb",
|
32
|
+
"lib/image_vise/fetchers/fetcher_http.rb",
|
32
33
|
"lib/image_vise/file_response.rb",
|
33
|
-
"lib/image_vise/fit_crop.rb",
|
34
|
-
"lib/image_vise/geom.rb",
|
35
34
|
"lib/image_vise/image_request.rb",
|
35
|
+
"lib/image_vise/operators/auto_orient.rb",
|
36
|
+
"lib/image_vise/operators/crop.rb",
|
37
|
+
"lib/image_vise/operators/ellipse_stencil.rb",
|
38
|
+
"lib/image_vise/operators/fit_crop.rb",
|
39
|
+
"lib/image_vise/operators/geom.rb",
|
40
|
+
"lib/image_vise/operators/sRGB_v4_ICC_preference_displayclass.icc",
|
41
|
+
"lib/image_vise/operators/sharpen.rb",
|
42
|
+
"lib/image_vise/operators/srgb.rb",
|
43
|
+
"lib/image_vise/operators/strip_metadata.rb",
|
36
44
|
"lib/image_vise/pipeline.rb",
|
37
45
|
"lib/image_vise/render_engine.rb",
|
38
|
-
"lib/image_vise/sRGB_v4_ICC_preference_displayclass.icc",
|
39
|
-
"lib/image_vise/sharpen.rb",
|
40
|
-
"lib/image_vise/srgb.rb",
|
41
|
-
"lib/image_vise/strip_metadata.rb",
|
42
46
|
"spec/image_vise/auto_orient_spec.rb",
|
43
47
|
"spec/image_vise/crop_spec.rb",
|
44
48
|
"spec/image_vise/ellipse_stencil_spec.rb",
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ImageVise::FetcherFile
|
2
|
+
class AccessError < StandardError
|
3
|
+
def http_status; 403; end
|
4
|
+
end
|
5
|
+
def self.fetch_uri_to_tempfile(uri)
|
6
|
+
tf = Tempfile.new 'imagevise-localfs-copy'
|
7
|
+
real_path_on_filesystem = File.expand_path(URI.decode(uri.path))
|
8
|
+
verify_filesystem_access! real_path_on_filesystem
|
9
|
+
# Do the checks
|
10
|
+
File.open(real_path_on_filesystem, 'rb') do |f|
|
11
|
+
IO.copy_stream(f, tf)
|
12
|
+
end
|
13
|
+
tf.rewind; tf
|
14
|
+
rescue Exception => e
|
15
|
+
ImageVise.close_and_unlink(tf)
|
16
|
+
raise e
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.verify_filesystem_access!(path_on_filesystem)
|
20
|
+
patterns = ImageVise.allowed_filesystem_sources
|
21
|
+
matches = patterns.any? { |glob_pattern| File.fnmatch?(glob_pattern, path_on_filesystem) }
|
22
|
+
raise AccessError, "filesystem access is disabled" unless patterns.any?
|
23
|
+
raise AccessError, "#{src_url} is not on the path whitelist" unless matches
|
24
|
+
end
|
25
|
+
|
26
|
+
ImageVise.register_fetcher 'file', self
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class ImageVise::FetcherHTTP
|
2
|
+
EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS = 5
|
3
|
+
|
4
|
+
class AccessError < StandardError; end
|
5
|
+
|
6
|
+
class UpstreamError < StandardError
|
7
|
+
attr_accessor :http_status
|
8
|
+
def initialize(http_status, message)
|
9
|
+
super(message)
|
10
|
+
@http_status = http_status
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.fetch_uri_to_tempfile(uri)
|
15
|
+
tf = Tempfile.new 'imagevise-http-download'
|
16
|
+
verify_uri_access!(uri)
|
17
|
+
s = Patron::Session.new
|
18
|
+
s.automatic_content_encoding = true
|
19
|
+
s.timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
20
|
+
s.connect_timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
21
|
+
|
22
|
+
response = s.get_file(uri.to_s, tf.path)
|
23
|
+
|
24
|
+
if response.status != 200
|
25
|
+
raise UpstreamError.new(response.status, "Unfortunate upstream response #{response.status}")
|
26
|
+
end
|
27
|
+
|
28
|
+
tf
|
29
|
+
rescue Exception => e
|
30
|
+
ImageVise.close_and_unlink(tf)
|
31
|
+
raise e
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.verify_uri_access!(uri)
|
35
|
+
host = uri.host
|
36
|
+
unless ImageVise.allowed_hosts.include?(uri.host)
|
37
|
+
raise AccessError, "#{uri} is not permitted as source"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
ImageVise.register_fetcher 'http', self
|
42
|
+
ImageVise.register_fetcher 'https', self
|
43
|
+
end
|
@@ -6,7 +6,7 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
|
6
6
|
|
7
7
|
# Initializes a new ParamsChecker from given HTTP server framework
|
8
8
|
# params. The params can be symbol- or string-keyed, does not matter.
|
9
|
-
def self.to_request(qs_params:, secrets
|
9
|
+
def self.to_request(qs_params:, secrets:)
|
10
10
|
base64_encoded_params = qs_params.fetch(:q) rescue qs_params.fetch('q')
|
11
11
|
given_signature = qs_params.fetch(:sig) rescue qs_params.fetch('sig')
|
12
12
|
|
@@ -20,22 +20,10 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
|
20
20
|
params = JSON.parse(decoded_json, symbolize_names: true)
|
21
21
|
|
22
22
|
# Pick up the URL and validate it
|
23
|
-
|
24
|
-
raise URLError, "the :src_url parameter must be non-empty" if
|
25
|
-
|
26
|
-
src_url = URI.parse(src_url)
|
27
|
-
if src_url.scheme == 'file'
|
28
|
-
file_path = URI.decode(src_url.path)
|
29
|
-
raise URLError, "#{src_url} not permitted since filesystem access is disabled" if allowed_filesystem_patterns.empty?
|
30
|
-
raise URLError, "#{src_url} is not on the path whitelist" unless allowed_path?(allowed_filesystem_patterns, file_path)
|
31
|
-
elsif src_url.scheme != 'file'
|
32
|
-
raise URLError, "#{src_url} is not permitted as source" unless permitted_source_hosts.include?(src_url.host)
|
33
|
-
end
|
34
|
-
|
35
|
-
# Build out the processing pipeline
|
23
|
+
source_url_str = params.fetch(:src_url).to_s
|
24
|
+
raise URLError, "the :src_url parameter must be non-empty" if source_url_str.empty?
|
36
25
|
pipeline_definition = params.fetch(:pipeline)
|
37
|
-
|
38
|
-
new(src_url: src_url.to_s, pipeline: ImageVise::Pipeline.from_param(pipeline_definition))
|
26
|
+
new(src_url: URI(source_url_str), pipeline: ImageVise::Pipeline.from_param(pipeline_definition))
|
39
27
|
rescue KeyError => e
|
40
28
|
raise InvalidRequest.new(e.message)
|
41
29
|
end
|
@@ -46,7 +34,7 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
|
46
34
|
end
|
47
35
|
|
48
36
|
def to_h
|
49
|
-
{pipeline: pipeline.to_params, src_url: src_url}
|
37
|
+
{pipeline: pipeline.to_params, src_url: src_url.to_s}
|
50
38
|
end
|
51
39
|
|
52
40
|
def cache_etag
|
@@ -55,11 +43,6 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
|
55
43
|
|
56
44
|
private
|
57
45
|
|
58
|
-
def self.allowed_path?(filesystem_glob_patterns, path_to_check)
|
59
|
-
expanded_path = File.expand_path(path_to_check)
|
60
|
-
filesystem_glob_patterns.any? {|pattern| File.fnmatch?(pattern, expanded_path) }
|
61
|
-
end
|
62
|
-
|
63
46
|
def self.valid_signature?(for_payload, given_signature, secrets)
|
64
47
|
# Check the signature against every key that we have,
|
65
48
|
# since different apps might be using different keys
|
data/lib/image_vise/pipeline.rb
CHANGED
@@ -6,7 +6,7 @@ class ImageVise::Pipeline
|
|
6
6
|
def self.from_param(array_of_operator_names_to_operator_params)
|
7
7
|
operators = array_of_operator_names_to_operator_params.map do |(operator_name, operator_params)|
|
8
8
|
operator_class = operator_by_name(operator_name)
|
9
|
-
if operator_params.any? && operator_class.method(:new).arity.nonzero?
|
9
|
+
if operator_params && operator_params.any? && operator_class.method(:new).arity.nonzero?
|
10
10
|
operator_class.new(**operator_params)
|
11
11
|
else
|
12
12
|
operator_class.new
|
@@ -2,9 +2,6 @@ class ImageVise::RenderEngine
|
|
2
2
|
class UnsupportedInputFormat < StandardError; end
|
3
3
|
class EmptyRender < StandardError; end
|
4
4
|
|
5
|
-
# Codes that have to be sent through to the requester
|
6
|
-
PASSTHROUGH_STATUS_CODES = [404, 403, 503, 504, 500]
|
7
|
-
|
8
5
|
DEFAULT_HEADERS = {
|
9
6
|
'Allow' => "GET"
|
10
7
|
}.freeze
|
@@ -15,6 +12,14 @@ class ImageVise::RenderEngine
|
|
15
12
|
'Cache-Control' => 'private, max-age=0, no-cache'
|
16
13
|
}).freeze
|
17
14
|
|
15
|
+
# "public" of course. Add max-age so that there is _some_
|
16
|
+
# revalidation after a time (otherwise some proxies treat it
|
17
|
+
# as "must-revalidate" always), and "no-transform" so that
|
18
|
+
# various deflate schemes are not applied to it (does happen
|
19
|
+
# with Rack::Cache and leads Chrome to throw up on content
|
20
|
+
# decoding for example).
|
21
|
+
IMAGE_CACHE_CONTROL = 'public, no-transform, max-age=2592000'
|
22
|
+
|
18
23
|
# How long is a render (the ImageMagick/write part) is allowed to
|
19
24
|
# take before we kill it
|
20
25
|
RENDER_TIMEOUT_SECONDS = 10
|
@@ -30,20 +35,7 @@ class ImageVise::RenderEngine
|
|
30
35
|
EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS = 4
|
31
36
|
|
32
37
|
# The default file type for images with alpha
|
33
|
-
PNG_FILE_TYPE =
|
34
|
-
def self.mime; 'image/png'; end
|
35
|
-
def self.ext; 'png'; end
|
36
|
-
end
|
37
|
-
|
38
|
-
# Fetch the given URL into a Tempfile and return the File object
|
39
|
-
def fetch_url_into_tempfile(source_image_uri)
|
40
|
-
parsed = URI.parse(source_image_uri)
|
41
|
-
if parsed.scheme == 'file'
|
42
|
-
copy_path_into_tempfile(URI.decode(parsed.path))
|
43
|
-
else
|
44
|
-
fetch_url(source_image_uri)
|
45
|
-
end
|
46
|
-
end
|
38
|
+
PNG_FILE_TYPE = MagicBytes::FileType.new('png','image/png').freeze
|
47
39
|
|
48
40
|
def bail(status, *errors_array)
|
49
41
|
h = JSON_ERROR_HEADERS.dup # Needed because some upstream middleware migh be modifying headers
|
@@ -69,8 +61,8 @@ class ImageVise::RenderEngine
|
|
69
61
|
req = Rack::Request.new(env)
|
70
62
|
bail(405, 'Only GET supported') unless req.get?
|
71
63
|
|
72
|
-
#
|
73
|
-
image_request = ImageVise::ImageRequest.to_request(qs_params: req.params,
|
64
|
+
# Parse and reinstate the URL and pipeline
|
65
|
+
image_request = ImageVise::ImageRequest.to_request(qs_params: req.params, secrets: ImageVise.secret_keys)
|
74
66
|
|
75
67
|
# Recover the source image URL and the pipeline instructions (all the image ops)
|
76
68
|
source_image_uri, pipeline = image_request.src_url, image_request.pipeline
|
@@ -80,8 +72,9 @@ class ImageVise::RenderEngine
|
|
80
72
|
# Assume the image URL contents does _never_ change.
|
81
73
|
etag = image_request.cache_etag
|
82
74
|
|
83
|
-
# Download the original into a Tempfile
|
84
|
-
|
75
|
+
# Download/copy the original into a Tempfile
|
76
|
+
fetcher = ImageVise.fetcher_for(source_image_uri.scheme)
|
77
|
+
source_file = fetcher.fetch_uri_to_tempfile(source_image_uri)
|
85
78
|
|
86
79
|
# Make sure we do not try to process something...questionable
|
87
80
|
source_file_type = detect_file_type(source_file)
|
@@ -106,7 +99,7 @@ class ImageVise::RenderEngine
|
|
106
99
|
response_headers = DEFAULT_HEADERS.merge({
|
107
100
|
'Content-Type' => render_file_type.mime,
|
108
101
|
'Content-Length' => '%d' % render_destination_file.size,
|
109
|
-
'Cache-Control' =>
|
102
|
+
'Cache-Control' => IMAGE_CACHE_CONTROL,
|
110
103
|
'ETag' => etag
|
111
104
|
})
|
112
105
|
|
@@ -115,12 +108,18 @@ class ImageVise::RenderEngine
|
|
115
108
|
[200, response_headers, ImageVise::FileResponse.new(render_destination_file)]
|
116
109
|
rescue *permanent_failures => e
|
117
110
|
handle_request_error(e)
|
118
|
-
|
111
|
+
http_status_code = e.respond_to?(:http_status) ? e.http_status : 422
|
112
|
+
raise_exception_or_error_response(e, http_status_code)
|
119
113
|
rescue Exception => e
|
120
|
-
|
121
|
-
|
114
|
+
if http_status_code = (e.respond_to?(:http_status) && e.http_status)
|
115
|
+
handle_request_error(e)
|
116
|
+
raise_exception_or_error_response(e, http_status_code)
|
117
|
+
else
|
118
|
+
handle_generic_error(e)
|
119
|
+
raise_exception_or_error_response(e, 500)
|
120
|
+
end
|
122
121
|
ensure
|
123
|
-
close_and_unlink(source_file)
|
122
|
+
ImageVise.close_and_unlink(source_file)
|
124
123
|
end
|
125
124
|
|
126
125
|
def raise_exception_or_error_response(exception, status_code)
|
@@ -131,12 +130,6 @@ class ImageVise::RenderEngine
|
|
131
130
|
end
|
132
131
|
end
|
133
132
|
|
134
|
-
def close_and_unlink(f)
|
135
|
-
return unless f
|
136
|
-
f.close unless f.closed?
|
137
|
-
f.unlink
|
138
|
-
end
|
139
|
-
|
140
133
|
def binary_tempfile
|
141
134
|
Tempfile.new('imagevise-tmp').tap{|f| f.binmode }
|
142
135
|
end
|
@@ -207,44 +200,4 @@ class ImageVise::RenderEngine
|
|
207
200
|
ImageVise.destroy(magick_image)
|
208
201
|
end
|
209
202
|
|
210
|
-
def image_request_options
|
211
|
-
{
|
212
|
-
secrets: ImageVise.secret_keys,
|
213
|
-
permitted_source_hosts: ImageVise.allowed_hosts,
|
214
|
-
allowed_filesystem_patterns: ImageVise.allowed_filesystem_sources,
|
215
|
-
}
|
216
|
-
end
|
217
|
-
|
218
|
-
def fetch_url(source_image_uri)
|
219
|
-
tf = binary_tempfile
|
220
|
-
s = Patron::Session.new
|
221
|
-
s.automatic_content_encoding = true
|
222
|
-
s.timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
223
|
-
s.connect_timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
|
224
|
-
response = s.get_file(source_image_uri, tf.path)
|
225
|
-
if PASSTHROUGH_STATUS_CODES.include?(response.status)
|
226
|
-
tf.close; tf.unlink;
|
227
|
-
bail response.status, "Unfortunate upstream response: #{response.status}"
|
228
|
-
end
|
229
|
-
tf
|
230
|
-
rescue Exception => e
|
231
|
-
tf.close; tf.unlink;
|
232
|
-
raise e
|
233
|
-
end
|
234
|
-
|
235
|
-
def copy_path_into_tempfile(path_on_filesystem)
|
236
|
-
tf = binary_tempfile
|
237
|
-
real_path_on_filesystem = File.expand_path(path_on_filesystem)
|
238
|
-
File.open(real_path_on_filesystem, 'rb') do |f|
|
239
|
-
IO.copy_stream(f, tf)
|
240
|
-
end
|
241
|
-
tf.rewind; tf
|
242
|
-
rescue Errno::ENOENT
|
243
|
-
tf.close; tf.unlink;
|
244
|
-
bail 404, "Image file not found"
|
245
|
-
rescue Exception => e
|
246
|
-
tf.close; tf.unlink;
|
247
|
-
raise e
|
248
|
-
end
|
249
|
-
|
250
203
|
end
|
data/lib/image_vise.rb
CHANGED
@@ -8,7 +8,7 @@ require 'base64'
|
|
8
8
|
require 'rack'
|
9
9
|
|
10
10
|
class ImageVise
|
11
|
-
VERSION = '0.0.
|
11
|
+
VERSION = '0.0.25'
|
12
12
|
S_MUTEX = Mutex.new
|
13
13
|
private_constant :S_MUTEX
|
14
14
|
|
@@ -16,7 +16,8 @@ class ImageVise
|
|
16
16
|
@keys = Set.new
|
17
17
|
@operators = {}
|
18
18
|
@allowed_glob_patterns = Set.new
|
19
|
-
|
19
|
+
@fetchers = {}
|
20
|
+
|
20
21
|
class << self
|
21
22
|
# Resets all allowed hosts
|
22
23
|
def reset_allowed_hosts!
|
@@ -80,7 +81,7 @@ class ImageVise
|
|
80
81
|
p = Pipeline.new
|
81
82
|
yield(p)
|
82
83
|
raise ArgumentError, "Image pipeline has no steps defined" if p.empty?
|
83
|
-
ImageRequest.new(src_url: src_url, pipeline: p).to_query_string_params(secret)
|
84
|
+
ImageRequest.new(src_url: URI(src_url), pipeline: p).to_query_string_params(secret)
|
84
85
|
end
|
85
86
|
|
86
87
|
# Adds an operator
|
@@ -97,8 +98,18 @@ class ImageVise
|
|
97
98
|
@operators.keys
|
98
99
|
end
|
99
100
|
|
101
|
+
def register_fetcher(scheme, fetcher)
|
102
|
+
S_MUTEX.synchronize { @fetchers[scheme.to_s] = fetcher }
|
103
|
+
end
|
104
|
+
|
105
|
+
def fetcher_for(scheme)
|
106
|
+
S_MUTEX.synchronize { @fetchers[scheme.to_s] or raise "No fetcher registered for #{scheme}" }
|
107
|
+
end
|
108
|
+
|
100
109
|
def operator_name_for(operator)
|
101
|
-
|
110
|
+
S_MUTEX.synchronize do
|
111
|
+
@operators.key(operator.class) or raise "Operator #{operator.inspect} not registered using ImageVise.add_operator"
|
112
|
+
end
|
102
113
|
end
|
103
114
|
end
|
104
115
|
|
@@ -131,6 +142,15 @@ class ImageVise
|
|
131
142
|
return if maybe_image.destroyed?
|
132
143
|
maybe_image.destroy!
|
133
144
|
end
|
145
|
+
|
146
|
+
# Used as a shorthand to force-dealloc Tempfiles in an ensure() blocks. Since
|
147
|
+
# ensure blocks sometimes deal with variables in inconsistent states (variable
|
148
|
+
# in scope but not yet set to an image) we take the possibility of nils into account.
|
149
|
+
def self.close_and_unlink(maybe_tempfile)
|
150
|
+
return unless maybe_tempfile
|
151
|
+
maybe_tempfile.close unless maybe_tempfile.closed?
|
152
|
+
maybe_tempfile.unlink
|
153
|
+
end
|
134
154
|
end
|
135
155
|
|
136
156
|
Dir.glob(__dir__ + '/**/*.rb').sort.each do |f|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe ImageVise::ImageRequest do
|
4
|
-
it 'accepts a set of params
|
4
|
+
it 'accepts a set of params and secrets, and returns a Pipeline' do
|
5
5
|
img_params = {src_url: 'http://bucket.s3.aws.com/image.jpg', pipeline: [[:crop, {width: 10, height: 10, gravity: 's'}]]}
|
6
6
|
img_params_json = JSON.dump(img_params)
|
7
7
|
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', img_params_json)
|
@@ -10,31 +10,14 @@ 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']
|
14
|
-
permitted_source_hosts: ['bucket.s3.aws.com'], allowed_filesystem_patterns: [])
|
13
|
+
image_request = described_class.to_request(qs_params: params, secrets: ['this is a secret'])
|
15
14
|
request_qs_params = image_request.to_query_string_params('this is a secret')
|
16
15
|
expect(request_qs_params).to be_kind_of(Hash)
|
17
16
|
|
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: [])
|
17
|
+
image_request_roundtrip = described_class.to_request(qs_params: request_qs_params, secrets: ['this is a secret'])
|
20
18
|
end
|
21
19
|
|
22
|
-
it '
|
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
|
20
|
+
it 'converts a file:// URL into a URI objectlist' do
|
38
21
|
img_params = {src_url: 'file:///etc/passwd', pipeline: [[:auto_orient, {}]]}
|
39
22
|
img_params_json = JSON.dump(img_params)
|
40
23
|
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', img_params_json)
|
@@ -42,11 +25,8 @@ describe ImageVise::ImageRequest do
|
|
42
25
|
q: Base64.encode64(img_params_json),
|
43
26
|
sig: signature
|
44
27
|
}
|
45
|
-
|
46
|
-
image_request
|
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)
|
28
|
+
image_request = described_class.to_request(qs_params: params, secrets: ['this is a secret'])
|
29
|
+
expect(image_request.src_url).to be_kind_of(URI)
|
50
30
|
end
|
51
31
|
|
52
32
|
describe 'fails with an invalid pipeline' do
|
@@ -59,8 +39,6 @@ describe ImageVise::ImageRequest do
|
|
59
39
|
describe 'fails with an invalid URL' do
|
60
40
|
it 'when the URL param is missing'
|
61
41
|
it 'when the URL param is empty'
|
62
|
-
it 'when the URL is referencing a non-permitted host'
|
63
|
-
it 'when the URL refers to a non-HTTP(S) scheme'
|
64
42
|
end
|
65
43
|
|
66
44
|
describe 'fails with an invalid signature' do
|
@@ -77,8 +55,7 @@ describe ImageVise::ImageRequest do
|
|
77
55
|
}
|
78
56
|
|
79
57
|
expect {
|
80
|
-
described_class.to_request(qs_params: params,
|
81
|
-
secrets: ['b'], permitted_source_hosts: ['bucket.s3.aws.com'], allowed_filesystem_patterns: [])
|
58
|
+
described_class.to_request(qs_params: params, secrets: ['b'])
|
82
59
|
}.to raise_error(/Invalid or missing signature/)
|
83
60
|
end
|
84
61
|
end
|
@@ -72,7 +72,7 @@ describe ImageVise::RenderEngine do
|
|
72
72
|
params = image_request.to_query_string_params('l33tness')
|
73
73
|
|
74
74
|
get '/', params
|
75
|
-
expect(last_response.status).to eq(
|
75
|
+
expect(last_response.status).to eq(403)
|
76
76
|
expect(last_response.body).to include('filesystem access is disabled')
|
77
77
|
end
|
78
78
|
|
@@ -90,7 +90,7 @@ describe ImageVise::RenderEngine do
|
|
90
90
|
expect(last_response.status).to eq(403)
|
91
91
|
expect(last_response.headers['Content-Type']).to eq('application/json')
|
92
92
|
parsed = JSON.load(last_response.body)
|
93
|
-
expect(parsed['errors']).to include("Unfortunate upstream response
|
93
|
+
expect(parsed['errors'].to_s).to include("Unfortunate upstream response")
|
94
94
|
end
|
95
95
|
|
96
96
|
it 'replays upstream error response codes that are selected to be replayed to the requester' do
|
@@ -112,7 +112,7 @@ describe ImageVise::RenderEngine do
|
|
112
112
|
|
113
113
|
expect(last_response.headers['Content-Type']).to eq('application/json')
|
114
114
|
parsed = JSON.load(last_response.body)
|
115
|
-
expect(parsed['errors']).to include("Unfortunate upstream response
|
115
|
+
expect(parsed['errors'].to_s).to include("Unfortunate upstream response")
|
116
116
|
end
|
117
117
|
end
|
118
118
|
|
data/spec/image_vise_spec.rb
CHANGED
@@ -63,7 +63,20 @@ describe ImageVise do
|
|
63
63
|
expect(params[:sig]).not_to be_empty
|
64
64
|
end
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
|
+
describe 'methods dealing with fetchers' do
|
68
|
+
it 'returns the fetchers for the default schemes' do
|
69
|
+
http = ImageVise.fetcher_for('http')
|
70
|
+
expect(http).to respond_to(:fetch_uri_to_tempfile)
|
71
|
+
file = ImageVise.fetcher_for('file')
|
72
|
+
expect(http).to respond_to(:fetch_uri_to_tempfile)
|
73
|
+
|
74
|
+
expect {
|
75
|
+
ImageVise.fetcher_for('undernet')
|
76
|
+
}.to raise_error(/No fetcher registered/)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
67
80
|
describe 'methods dealing with the operator list' do
|
68
81
|
it 'have the basic operators already set up' do
|
69
82
|
oplist = ImageVise.defined_operator_names
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: image_vise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.25
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-10-
|
11
|
+
date: 2016-10-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -253,21 +253,25 @@ files:
|
|
253
253
|
- README.md
|
254
254
|
- Rakefile
|
255
255
|
- examples/config.ru
|
256
|
+
- examples/error_handline_appsignal.rb
|
257
|
+
- examples/error_handling_sentry.rb
|
256
258
|
- image_vise.gemspec
|
257
259
|
- lib/image_vise.rb
|
258
|
-
- lib/image_vise/
|
259
|
-
- lib/image_vise/
|
260
|
-
- lib/image_vise/ellipse_stencil.rb
|
260
|
+
- lib/image_vise/fetchers/fetcher_file.rb
|
261
|
+
- lib/image_vise/fetchers/fetcher_http.rb
|
261
262
|
- lib/image_vise/file_response.rb
|
262
|
-
- lib/image_vise/fit_crop.rb
|
263
|
-
- lib/image_vise/geom.rb
|
264
263
|
- lib/image_vise/image_request.rb
|
264
|
+
- lib/image_vise/operators/auto_orient.rb
|
265
|
+
- lib/image_vise/operators/crop.rb
|
266
|
+
- lib/image_vise/operators/ellipse_stencil.rb
|
267
|
+
- lib/image_vise/operators/fit_crop.rb
|
268
|
+
- lib/image_vise/operators/geom.rb
|
269
|
+
- lib/image_vise/operators/sRGB_v4_ICC_preference_displayclass.icc
|
270
|
+
- lib/image_vise/operators/sharpen.rb
|
271
|
+
- lib/image_vise/operators/srgb.rb
|
272
|
+
- lib/image_vise/operators/strip_metadata.rb
|
265
273
|
- lib/image_vise/pipeline.rb
|
266
274
|
- lib/image_vise/render_engine.rb
|
267
|
-
- lib/image_vise/sRGB_v4_ICC_preference_displayclass.icc
|
268
|
-
- lib/image_vise/sharpen.rb
|
269
|
-
- lib/image_vise/srgb.rb
|
270
|
-
- lib/image_vise/strip_metadata.rb
|
271
275
|
- spec/image_vise/auto_orient_spec.rb
|
272
276
|
- spec/image_vise/crop_spec.rb
|
273
277
|
- spec/image_vise/ellipse_stencil_spec.rb
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|