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