image_vise 0.0.28 → 0.1.0
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 +7 -6
- data/image_vise.gemspec +2 -2
- data/lib/image_vise/image_request.rb +4 -0
- data/lib/image_vise/render_engine.rb +20 -4
- data/lib/image_vise.rb +20 -1
- data/spec/image_vise/image_request_spec.rb +8 -0
- data/spec/image_vise/render_engine_spec.rb +21 -34
- data/spec/image_vise_spec.rb +9 -0
- 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: e9dbe4f26897009011c3a9f42b233e7e232d6634
|
4
|
+
data.tar.gz: 6910d9c75d0a2c2c13b9a3688d7302cd22758992
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bb4e6b545fc8d847ed9a6462c4cf5c2bc3b4cec035df034e0c5cc7da982d9cc8bc680fc438c467ea5e6e3b063a2eb26c31daf250bcb59324899488e7a14f58d
|
7
|
+
data.tar.gz: 01d4a26bf68dead0b3c58d5c6ecbf07759be957a8c5594e57ca3b9e792d92dbd061ad7ee46cf7301f113cc730eea473d87e8c626ec05b7dc4c12590858fc62b2
|
data/README.md
CHANGED
@@ -6,7 +6,8 @@ framework. The main uses are:
|
|
6
6
|
* Image resizing on request
|
7
7
|
* Applying image filters
|
8
8
|
|
9
|
-
It is implemented as a Rack application that responds to any URL and accepts the following
|
9
|
+
It is implemented as a Rack application that responds to any URL and accepts the following two _last_ path
|
10
|
+
compnents, internally named `q` and `sig`:
|
10
11
|
|
11
12
|
* `q` - Base64 encoded JSON object with `src_url` and `pipeline` properties
|
12
13
|
(the source URL of the image and processing steps to apply)
|
@@ -14,7 +15,7 @@ It is implemented as a Rack application that responds to any URL and accepts the
|
|
14
15
|
|
15
16
|
A request to `ImageVise` might look like this:
|
16
17
|
|
17
|
-
|
18
|
+
/acbhGyfhyYErghff/acfgheg123
|
18
19
|
|
19
20
|
The URL that gets generated is best composed with the included `ImageVise.image_params` method. This method will
|
20
21
|
take care of encoding the source URL and the commands in the right way, as well as signing.
|
@@ -38,11 +39,11 @@ You might want to define a helper method for generating signed URLs as well, whi
|
|
38
39
|
|
39
40
|
```ruby
|
40
41
|
def thumb_url(source_image_url)
|
41
|
-
|
42
|
+
path = ImageVise.image_path(src_url: source_image_url, secret: ENV.fetch('IMAGE_VISE_SECRET')) do |pipeline|
|
42
43
|
# For example, you can also yield `pipeline` to the caller
|
43
44
|
pipeline.fit_crop width: 128, height: 128, gravity: 'c'
|
44
45
|
end
|
45
|
-
'/images
|
46
|
+
'/images' + path
|
46
47
|
end
|
47
48
|
```
|
48
49
|
|
@@ -65,13 +66,13 @@ You might want to define a helper method for generating signed URLs as well, whi
|
|
65
66
|
|
66
67
|
```ruby
|
67
68
|
def thumb_url(source_image_url)
|
68
|
-
|
69
|
+
path_param = ImageVise.image_path(src_url: source_image_url, secret: ENV.fetch('IMAGE_VISE_SECRET')) do |pipe|
|
69
70
|
pipe.fit_crop width: 256, height: 256, gravity: 'c'
|
70
71
|
pipe.sharpen sigma: 0.5, radius: 2
|
71
72
|
pipe.ellipse_stencil
|
72
73
|
end
|
73
74
|
# Output a URL to the app
|
74
|
-
'/images
|
75
|
+
'/images' + path
|
75
76
|
end
|
76
77
|
```
|
77
78
|
|
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.1.0 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.1.0"
|
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"]
|
@@ -29,6 +29,10 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
|
29
29
|
raise InvalidRequest.new(e.message)
|
30
30
|
end
|
31
31
|
|
32
|
+
def to_path_params(signed_with_secret)
|
33
|
+
'/%{q}/%{sig}' % to_query_string_params(signed_with_secret)
|
34
|
+
end
|
35
|
+
|
32
36
|
def to_query_string_params(signed_with_secret)
|
33
37
|
payload = JSON.dump(to_h)
|
34
38
|
base64_enc = Base64.strict_encode64(payload).gsub(/\=+$/, '')
|
@@ -67,11 +67,9 @@ class ImageVise::RenderEngine
|
|
67
67
|
|
68
68
|
req = parse_env_into_request(env)
|
69
69
|
bail(405, 'Only GET supported') unless req.get?
|
70
|
+
params = extract_params_from_request(req)
|
70
71
|
|
71
|
-
|
72
|
-
bail(400, 'Too many params') if req.params.length > 2
|
73
|
-
|
74
|
-
image_request = ImageVise::ImageRequest.from_params(qs_params: req.params, secrets: ImageVise.secret_keys)
|
72
|
+
image_request = ImageVise::ImageRequest.from_params(qs_params: params, secrets: ImageVise.secret_keys)
|
75
73
|
render_destination_file, render_file_type, etag = process_image_request(image_request)
|
76
74
|
image_rack_response(render_destination_file, render_file_type, etag)
|
77
75
|
rescue *permanent_failures => e
|
@@ -98,6 +96,24 @@ class ImageVise::RenderEngine
|
|
98
96
|
Rack::Request.new(rack_env)
|
99
97
|
end
|
100
98
|
|
99
|
+
# Extracts the image params from the Rack::Request
|
100
|
+
#
|
101
|
+
# @param rack_request[#path_info] an object that has a path info
|
102
|
+
# @return [Hash] the params hash with `:q` and `:sig` keys
|
103
|
+
def extract_params_from_request(rack_request)
|
104
|
+
# Prevent cache bypass DOS attacks by only permitting :sig and :q
|
105
|
+
bail(400, 'Query strings are not supported') if rack_request.params.any?
|
106
|
+
|
107
|
+
# Extract the last two path components
|
108
|
+
*, q_from_path, sig_from_path = rack_request.path_info.split('/')
|
109
|
+
|
110
|
+
# Raise if any of them are empty or blank
|
111
|
+
nothing_recovered = [q_from_path, sig_from_path].all?{|v| v.nil? || v.empty? }
|
112
|
+
bail(400, 'Need 2 usable path components') if nothing_recovered
|
113
|
+
|
114
|
+
{q: q_from_path, sig: sig_from_path}
|
115
|
+
end
|
116
|
+
|
101
117
|
# Processes the ImageRequest object created from the request parameters,
|
102
118
|
# and returns a triplet of the File object containing the rendered image,
|
103
119
|
# the MagicBytes::FileType object of the render, and the cache ETag value
|
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.1.0'
|
12
12
|
S_MUTEX = Mutex.new
|
13
13
|
private_constant :S_MUTEX
|
14
14
|
|
@@ -84,6 +84,25 @@ class ImageVise
|
|
84
84
|
ImageRequest.new(src_url: URI(src_url), pipeline: p).to_query_string_params(secret)
|
85
85
|
end
|
86
86
|
|
87
|
+
# Generate a path for a resized image. Yields a Pipeline object that
|
88
|
+
# will receive method calls for adding image operations to a stack.
|
89
|
+
#
|
90
|
+
# ImageVise.image_path(src_url: image_url_on_s3, secret: '...') do |p|
|
91
|
+
# p.center_fit width: 128, height: 128
|
92
|
+
# p.elliptic_stencil
|
93
|
+
# end #=> "/abcdef/xyz123"
|
94
|
+
#
|
95
|
+
# The query string elements can be then passed on to RenderEngine for validation and execution.
|
96
|
+
#
|
97
|
+
# @yield {ImageVise::Pipeline}
|
98
|
+
# @return [String]
|
99
|
+
def image_path(src_url:, secret:)
|
100
|
+
p = Pipeline.new
|
101
|
+
yield(p)
|
102
|
+
raise ArgumentError, "Image pipeline has no steps defined" if p.empty?
|
103
|
+
ImageRequest.new(src_url: URI(src_url), pipeline: p).to_path_params(secret)
|
104
|
+
end
|
105
|
+
|
87
106
|
# Adds an operator
|
88
107
|
def add_operator(operator_name, object_responding_to_new)
|
89
108
|
@operators[operator_name.to_s] = object_responding_to_new
|
@@ -26,6 +26,14 @@ describe ImageVise::ImageRequest do
|
|
26
26
|
expect(image_request.src_url).to be_kind_of(URI)
|
27
27
|
end
|
28
28
|
|
29
|
+
it 'composes path parameters' do
|
30
|
+
parametrized = double(to_params: {foo: 'bar'})
|
31
|
+
uri = URI('http://example.com/image.psd')
|
32
|
+
image_request = described_class.new(src_url: uri, pipeline: parametrized)
|
33
|
+
path = image_request.to_path_params('password')
|
34
|
+
expect(path).to start_with('/eyJwaXB')
|
35
|
+
expect(path).to end_with('f207b')
|
36
|
+
end
|
29
37
|
|
30
38
|
it 'never apppends "="-padding to the Base64-encoded "q"' do
|
31
39
|
parametrized = double(to_params: {foo: 'bar'})
|
@@ -21,10 +21,9 @@ describe ImageVise::RenderEngine do
|
|
21
21
|
|
22
22
|
p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
|
23
23
|
image_request = ImageVise::ImageRequest.new(src_url: 'http://unknown.com/image.jpg', pipeline: p)
|
24
|
-
params = image_request.to_query_string_params('l33tness')
|
25
24
|
expect(app).to receive(:handle_generic_error).and_call_original
|
26
25
|
expect {
|
27
|
-
get '
|
26
|
+
get image_request.to_path_params('l33tness')
|
28
27
|
}.to raise_error(/No keys set/)
|
29
28
|
end
|
30
29
|
end
|
@@ -48,7 +47,6 @@ describe ImageVise::RenderEngine do
|
|
48
47
|
|
49
48
|
p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
|
50
49
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
51
|
-
params = image_request.to_query_string_params('l33tness')
|
52
50
|
|
53
51
|
expect_any_instance_of(Patron::Session).to receive(:get_file) {|_self, url, path|
|
54
52
|
File.open(path, 'wb') {|f| f << 'totally not an image' }
|
@@ -56,7 +54,7 @@ describe ImageVise::RenderEngine do
|
|
56
54
|
}
|
57
55
|
expect(app).to receive(:handle_request_error).and_call_original
|
58
56
|
|
59
|
-
get '
|
57
|
+
get image_request.to_path_params('l33tness')
|
60
58
|
expect(last_response.status).to eq(422)
|
61
59
|
expect(last_response['Cache-Control']).to eq("private, max-age=0, no-cache")
|
62
60
|
expect(last_response.body).to include('Unsupported/unknown')
|
@@ -69,9 +67,8 @@ describe ImageVise::RenderEngine do
|
|
69
67
|
|
70
68
|
p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
|
71
69
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
72
|
-
params = image_request.to_query_string_params('l33tness')
|
73
70
|
|
74
|
-
get '
|
71
|
+
get image_request.to_path_params('l33tness')
|
75
72
|
expect(last_response.status).to eq(403)
|
76
73
|
expect(last_response.body).to include('filesystem access is disabled')
|
77
74
|
end
|
@@ -84,9 +81,8 @@ describe ImageVise::RenderEngine do
|
|
84
81
|
|
85
82
|
p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
|
86
83
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
87
|
-
params = image_request.to_query_string_params('l33tness')
|
88
84
|
|
89
|
-
get '
|
85
|
+
get image_request.to_path_params('l33tness')
|
90
86
|
expect(last_response.status).to eq(403)
|
91
87
|
expect(last_response.headers['Content-Type']).to eq('application/json')
|
92
88
|
parsed = JSON.load(last_response.body)
|
@@ -104,9 +100,9 @@ describe ImageVise::RenderEngine do
|
|
104
100
|
|
105
101
|
p = ImageVise::Pipeline.new.crop(width: 10, height: 10, gravity: 'c')
|
106
102
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
107
|
-
params = image_request.to_query_string_params('l33tness')
|
108
103
|
|
109
|
-
get '
|
104
|
+
get image_request.to_path_params('l33tness')
|
105
|
+
|
110
106
|
expect(last_response.status).to eq(error_code)
|
111
107
|
expect(last_response.headers).to have_key('Cache-Control')
|
112
108
|
expect(last_response.headers['Cache-Control']).to eq("private, max-age=0, no-cache")
|
@@ -124,20 +120,20 @@ describe ImageVise::RenderEngine do
|
|
124
120
|
|
125
121
|
p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 35, gravity: 'c')
|
126
122
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
127
|
-
params = image_request.to_query_string_params('l33tness')
|
128
123
|
|
129
|
-
|
124
|
+
req_path = image_request.to_path_params('l33tness')
|
130
125
|
|
126
|
+
get req_path, {}
|
131
127
|
expect(last_response).to be_ok
|
132
128
|
expect(last_response['ETag']).not_to be_nil
|
133
129
|
expect(last_response['Cache-Control']).to match(/public/)
|
134
130
|
|
135
|
-
get
|
131
|
+
get req_path, {}, {'HTTP_IF_NONE_MATCH' => last_response['ETag']}
|
136
132
|
expect(last_response.status).to eq(304)
|
137
133
|
|
138
134
|
# Should consider _any_ ETag a request to rerender something
|
139
135
|
# that already exists in an upstream cache
|
140
|
-
get
|
136
|
+
get req_path, {}, {'HTTP_IF_NONE_MATCH' => SecureRandom.hex(4)}
|
141
137
|
expect(last_response.status).to eq(304)
|
142
138
|
end
|
143
139
|
|
@@ -148,9 +144,8 @@ describe ImageVise::RenderEngine do
|
|
148
144
|
|
149
145
|
p = ImageVise::Pipeline.new.geom(geometry_string: '512x335').fit_crop(width: 10, height: 10, gravity: 'c')
|
150
146
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
151
|
-
params = image_request.to_query_string_params('l33tness')
|
152
147
|
|
153
|
-
get '
|
148
|
+
get image_request.to_path_params('l33tness')
|
154
149
|
expect(last_response.status).to eq(200)
|
155
150
|
|
156
151
|
expect(last_response.headers['Content-Type']).to eq('image/jpeg')
|
@@ -175,7 +170,7 @@ describe ImageVise::RenderEngine do
|
|
175
170
|
expect(app).to receive(:output_file_type_permitted?).and_call_original
|
176
171
|
expect(app).to receive(:enable_forking?).and_call_original
|
177
172
|
|
178
|
-
get '
|
173
|
+
get image_request.to_path_params('l33tness')
|
179
174
|
expect(last_response.status).to eq(200)
|
180
175
|
end
|
181
176
|
|
@@ -186,9 +181,8 @@ describe ImageVise::RenderEngine do
|
|
186
181
|
|
187
182
|
p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
|
188
183
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
189
|
-
params = image_request.to_query_string_params('l33tness')
|
190
184
|
|
191
|
-
get '
|
185
|
+
get image_request.to_path_params('l33tness')
|
192
186
|
expect(last_response.status).to eq(200)
|
193
187
|
expect(last_response.headers['Content-Type']).to eq('image/jpeg')
|
194
188
|
end
|
@@ -205,9 +199,8 @@ describe ImageVise::RenderEngine do
|
|
205
199
|
|
206
200
|
p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
|
207
201
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
208
|
-
params = image_request.to_query_string_params('l33tness')
|
209
202
|
|
210
|
-
get '
|
203
|
+
get image_request.to_path_params('l33tness')
|
211
204
|
File.unlink(utf8_file_path)
|
212
205
|
expect(last_response.status).to eq(200)
|
213
206
|
expect(last_response.headers['Content-Type']).to eq('image/jpeg')
|
@@ -218,10 +211,8 @@ describe ImageVise::RenderEngine do
|
|
218
211
|
|
219
212
|
p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
|
220
213
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
221
|
-
params = image_request.to_query_string_params('l33tness')
|
222
214
|
|
223
|
-
|
224
|
-
get '/', params
|
215
|
+
get image_request.to_path_params('l33tness'), {'extra' => '123'}
|
225
216
|
|
226
217
|
expect(last_response.status).to eq(400)
|
227
218
|
end
|
@@ -233,9 +224,8 @@ describe ImageVise::RenderEngine do
|
|
233
224
|
|
234
225
|
p = ImageVise::Pipeline.new.geom(geometry_string: '220x220').ellipse_stencil
|
235
226
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
236
|
-
params = image_request.to_query_string_params('l33tness')
|
237
227
|
|
238
|
-
get '
|
228
|
+
get image_request.to_path_params('l33tness')
|
239
229
|
expect(last_response.status).to eq(200)
|
240
230
|
|
241
231
|
expect(last_response.headers['Content-Type']).to eq('image/png')
|
@@ -251,9 +241,8 @@ describe ImageVise::RenderEngine do
|
|
251
241
|
|
252
242
|
p = ImageVise::Pipeline.new.geom(geometry_string: '220x220').ellipse_stencil
|
253
243
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
254
|
-
params = image_request.to_query_string_params('l33tness')
|
255
244
|
|
256
|
-
get '
|
245
|
+
get image_request.to_path_params('l33tness')
|
257
246
|
expect(last_response.status).to eq(422)
|
258
247
|
expect(last_response.body).to include('unknown input file format .psd')
|
259
248
|
end
|
@@ -265,13 +254,12 @@ describe ImageVise::RenderEngine do
|
|
265
254
|
|
266
255
|
p = ImageVise::Pipeline.new.geom(geometry_string: '220x220')
|
267
256
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
268
|
-
params = image_request.to_query_string_params('l33tness')
|
269
257
|
|
270
258
|
class << app
|
271
259
|
def source_file_type_permitted?(type); true; end
|
272
260
|
end
|
273
261
|
|
274
|
-
get '
|
262
|
+
get image_request.to_path_params('l33tness')
|
275
263
|
expect(last_response.status).to eq(200)
|
276
264
|
expect(last_response.headers['Content-Type']).to eq('image/png')
|
277
265
|
end
|
@@ -283,13 +271,12 @@ describe ImageVise::RenderEngine do
|
|
283
271
|
|
284
272
|
p = ImageVise::Pipeline.new.geom(geometry_string: '220x220')
|
285
273
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
286
|
-
params = image_request.to_query_string_params('l33tness')
|
287
274
|
|
288
275
|
class << app
|
289
276
|
def source_file_type_permitted?(type); true; end
|
290
277
|
end
|
291
278
|
|
292
|
-
get '
|
279
|
+
get image_request.to_path_params('l33tness')
|
293
280
|
expect(last_response.status).to eq(200)
|
294
281
|
expect(last_response.headers['Content-Type']).to eq('image/png')
|
295
282
|
end
|
@@ -301,14 +288,14 @@ describe ImageVise::RenderEngine do
|
|
301
288
|
|
302
289
|
p = ImageVise::Pipeline.new.geom(geometry_string: '220x220')
|
303
290
|
image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
|
304
|
-
|
291
|
+
|
305
292
|
|
306
293
|
class << app
|
307
294
|
def source_file_type_permitted?(type); true; end
|
308
295
|
def output_file_type_permitted?(type); true; end
|
309
296
|
end
|
310
297
|
|
311
|
-
get '
|
298
|
+
get image_request.to_path_params('l33tness')
|
312
299
|
expect(last_response.status).to eq(200)
|
313
300
|
expect(last_response.headers['Content-Type']).to eq('image/tiff')
|
314
301
|
end
|
data/spec/image_vise_spec.rb
CHANGED
@@ -77,6 +77,15 @@ describe ImageVise do
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
+
describe '.image_path' do
|
81
|
+
it 'returns the path to the image within the application' do
|
82
|
+
path = ImageVise.image_path(src_url: 'file://tmp/img.jpg', secret: 'a') do |p|
|
83
|
+
p.ellipse_stencil
|
84
|
+
end
|
85
|
+
expect(path).to start_with('/')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
80
89
|
describe 'methods dealing with the operator list' do
|
81
90
|
it 'have the basic operators already set up' do
|
82
91
|
oplist = ImageVise.defined_operator_names
|