image_vise 0.0.24 → 0.0.25
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/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
|