image_vise 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.travis.yml +13 -0
  4. data/DEVELOPMENT.md +111 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +29 -0
  7. data/README.md +213 -0
  8. data/Rakefile +6 -0
  9. data/SECURITY.md +57 -0
  10. data/examples/config.ru +17 -0
  11. data/examples/custom_image_operator.rb +27 -0
  12. data/examples/error_handline_appsignal.rb +23 -0
  13. data/examples/error_handling_sentry.rb +25 -0
  14. data/image_vise.gemspec +43 -0
  15. data/lib/image_vise/fetchers/fetcher_file.rb +27 -0
  16. data/lib/image_vise/fetchers/fetcher_http.rb +42 -0
  17. data/lib/image_vise/file_response.rb +22 -0
  18. data/lib/image_vise/image_request.rb +70 -0
  19. data/lib/image_vise/operators/auto_orient.rb +10 -0
  20. data/lib/image_vise/operators/background_fill.rb +18 -0
  21. data/lib/image_vise/operators/crop.rb +32 -0
  22. data/lib/image_vise/operators/ellipse_stencil.rb +70 -0
  23. data/lib/image_vise/operators/fit_crop.rb +33 -0
  24. data/lib/image_vise/operators/force_jpg_out.rb +17 -0
  25. data/lib/image_vise/operators/geom.rb +16 -0
  26. data/lib/image_vise/operators/sRGB_v4_ICC_preference_displayclass.icc +0 -0
  27. data/lib/image_vise/operators/sharpen.rb +21 -0
  28. data/lib/image_vise/operators/srgb.rb +30 -0
  29. data/lib/image_vise/operators/strip_metadata.rb +10 -0
  30. data/lib/image_vise/pipeline.rb +64 -0
  31. data/lib/image_vise/render_engine.rb +298 -0
  32. data/lib/image_vise/version.rb +3 -0
  33. data/lib/image_vise/writers/auto_writer.rb +23 -0
  34. data/lib/image_vise/writers/jpeg_writer.rb +9 -0
  35. data/lib/image_vise.rb +177 -0
  36. data/spec/image_vise/auto_orient_spec.rb +10 -0
  37. data/spec/image_vise/background_fill_spec.rb +39 -0
  38. data/spec/image_vise/crop_spec.rb +20 -0
  39. data/spec/image_vise/ellipse_stencil_spec.rb +18 -0
  40. data/spec/image_vise/fetcher_file_spec.rb +48 -0
  41. data/spec/image_vise/fetcher_http_spec.rb +44 -0
  42. data/spec/image_vise/file_response_spec.rb +45 -0
  43. data/spec/image_vise/fit_crop_spec.rb +20 -0
  44. data/spec/image_vise/force_jpg_out_spec.rb +36 -0
  45. data/spec/image_vise/geom_spec.rb +33 -0
  46. data/spec/image_vise/image_request_spec.rb +62 -0
  47. data/spec/image_vise/pipeline_spec.rb +72 -0
  48. data/spec/image_vise/render_engine_spec.rb +336 -0
  49. data/spec/image_vise/sharpen_spec.rb +17 -0
  50. data/spec/image_vise/srgb_spec.rb +23 -0
  51. data/spec/image_vise/strip_metadata_spec.rb +14 -0
  52. data/spec/image_vise/writers/auto_writer_spec.rb +25 -0
  53. data/spec/image_vise/writers/jpeg_writer_spec.rb +32 -0
  54. data/spec/image_vise_spec.rb +110 -0
  55. data/spec/layers-with-blending.psd +0 -0
  56. data/spec/spec_helper.rb +112 -0
  57. data/spec/test_server.rb +61 -0
  58. data/spec/waterside_magic_hour.jpg +0 -0
  59. data/spec/waterside_magic_hour.psd +0 -0
  60. data/spec/waterside_magic_hour_adobergb.jpg +0 -0
  61. data/spec/waterside_magic_hour_gray.tif +0 -0
  62. data/spec/waterside_magic_hour_transp.png +0 -0
  63. metadata +63 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e98d1a278c1306d99e84e78721179a9b27e32b36
4
- data.tar.gz: 81b82d9c4a68142d346a36c44fc0d5716d3332fb
3
+ metadata.gz: cdf5ad5923491f14b204be61539b2e45869d4003
4
+ data.tar.gz: 4aeebb9df5fa9f232c8c6e7bbc305b3bbc49ab1b
5
5
  SHA512:
6
- metadata.gz: 6e7935983502915b40a4739ae9a22ec0dd344d80a4bcd5215d9a54c74845851a8a12dbb0d14599c7d85754636d33eb57fc299c750f14da0622f7b408dae3ad1b
7
- data.tar.gz: f7836e89a7cb3bc3544ca09029ab550d91281ad73fbecf51ed48563dbdc552b71f5cac2d82302be0bdeae1396a78c41719e619e599bef4d1a49a0260c04e6551
6
+ metadata.gz: 2f906ce580e67875972c699045df0bc1ed3826a078db16bcbc126a661b191091562644b07bcd7ec555fc05435b1d6f1570eca816850284656aae09fc4aa55206
7
+ data.tar.gz: ba77348924856dc11a5230cd8f430b06c93236337006c5c202eccf456a5dc8ed7e05a59b4d95c53a17a28317b267d45513f00e25748ff06ccfe31f211ad47d85
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ scratchpad
2
+ Gemfile.lock
3
+ .bundle
4
+ .env
5
+ *.sqlite3
6
+ *.log
7
+ pkg
8
+ coverage
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ rvm:
2
+ - 2.1
3
+ - 2.2
4
+ - 2.3.0
5
+ - 2.4.1
6
+ sudo: false
7
+ cache: bundler
8
+
9
+ env:
10
+ global:
11
+ - SKIP_INTERACTIVE=yes
12
+
13
+ script: bundle exec rspec
data/DEVELOPMENT.md ADDED
@@ -0,0 +1,111 @@
1
+ # NOTE: Until 1.0.0 ImageVise API is somewhat in flux
2
+
3
+ ## Defining image operators
4
+
5
+ To add an image operator, define a class roughly like so, and add it to the list of operators.
6
+
7
+ class MyBlur
8
+ # The constructor must accept keyword arguments if your operator accepts them
9
+ def initialize(radius:, sigma:)
10
+ @radius = radius.to_f
11
+ @sigma = sigma.to_f
12
+ end
13
+
14
+ # Needed for the operator to be serialized to parameters
15
+ def to_h
16
+ {radius: @radius, sigma: @sigma}
17
+ end
18
+
19
+ def apply!(magick_image) # The argument is a Magick::Image
20
+ # Apply the method to the function argument
21
+ blurred_image = magick_image.blur_image(@radius, @sigma)
22
+
23
+ # Some methods (without !) return a new Magick::Image.
24
+ # That image has to be composited back into the original.
25
+ magick_image.composite!(blurred_image, Magick::CenterGravity, Magick::CopyCompositeOp)
26
+ ensure
27
+ # Make sure to explicitly destroy() all the intermediate images
28
+ ImageVise.destroy(blurred_image)
29
+ end
30
+ end
31
+
32
+ # This will also make `ImageVise::Pipeline` respond to `blur(radius:.., sigma:..)`
33
+ ImageVise.add_operator 'my_blur', MyBlur
34
+
35
+ ## Defining fetchers
36
+
37
+ Fetchers are based on the scheme of the image URL. Default fetchers are already defined for `http`, `https`
38
+ and `file` schemes. If you need to grab data from a database, for instance, you can define a fetcher and register
39
+ it:
40
+
41
+ module DatabaseFetcher
42
+ def self.fetch_uri_to_tempfile(uri_object)
43
+ tf = Tempfile.new 'data-fetch'
44
+
45
+ object_id = uri_object.path[/\d+/]
46
+ data_model = ProfilePictures.find(object_id)
47
+ tf << data_model.body
48
+
49
+ tf.rewind; tf
50
+ rescue Exception => e
51
+ ImageVise.close_and_unlink(tf) # do not litter Tempfiles
52
+ raise e
53
+ end
54
+ end
55
+
56
+ ImageVise.register_fetcher 'profilepictures', self
57
+
58
+ Once done, you can use URLs like `profilepictures:/5674`. A very simple Fetcher would just
59
+ override the standard filesystem one (be mindful that the filesystem fetcher still checks
60
+ path access using the glob whitelist)
61
+
62
+ class PicFetcher < ImageVise::FetcherFile
63
+ def self.fetch_uri_to_tempfile(uri_object)
64
+ # Convert an internal "pic://sites/uploads/abcdef.jpg" to a full path URL
65
+ partial_path = URI.decode(uri_object.path)
66
+ full_path = File.join(Mappe::ROOT, 'sites', partial_path)
67
+ full_path_uri = URI('file://' + URI.encode(full_path))
68
+ super(full_path_uri)
69
+ end
70
+ ImageVise.register_fetcher 'pic', self
71
+ end
72
+
73
+ ## Overriding the render engine
74
+
75
+ By default, `ImageVise.call` delegates to `ImageVise::RenderEngine.new.call`. You can mount your own subclass
76
+ instead, and it will handle everything the same way:
77
+
78
+ class MyThumbnailer < ImageVise::RenderEngine
79
+ ...
80
+ end
81
+
82
+ map '/thumbs' do
83
+ run MyThumbnailer.new
84
+ end
85
+
86
+ Note that since the API is in flux the methods you can override in `RenderEngine` can change.
87
+ So far none of them are private.
88
+
89
+ ## Performance and memory
90
+
91
+ ImageVise uses ImageMagick and RMagick. It _does not_ shell out to `convert` or `mogrify`, because shelling out
92
+ is _expensive_ in terms of wall clock. It _does_ do it's best to deallocate (`#destroy!`) the image it works on,
93
+ but it is not 100% bullet proof.
94
+
95
+ Additionally, in contrast to `convert` and `mogrify` ImageVise supports _stackable_ operations, and these operations
96
+ might be repeated with different parameters. Unfortunately, `convert` is _not_ Shake, so we cannot pass a DAG of
97
+ image operators to it and just expect it to work. If we want to do processing of multiple steps that `convert` is
98
+ unable to execute in one call, we have to do
99
+
100
+ [fork+exec read + convert + write] -> [fork+exec read + convert + write] + ...
101
+
102
+ for each operator we want to apply in a consistent fashion. We cannot stuff all the operators into one `convert`
103
+ command because the order the operators get applied within `convert` is not clear, whereas we need a reproducible
104
+ deterministic order of operations (as set in the pipeline). A much better solution is - load the image into memory
105
+ **once**, do all the transformations, save. Additionally, if you use things like OpenCL with ImageMagick, the overhead
106
+ of loading the library and compiling the compute kernels will outweigh _any_ performance gains you might get when
107
+ actually using them. If you are using a library it is a one-time cost, with very fast processing afterwards.
108
+
109
+ Also note that image operators are not per definition Imagemagick-specific - it's completely possible to not only use
110
+ a different library for processing them, but even to use a different image processing server complying to the
111
+ same protocol (a signed JSON-encodded waybill of HTTP(S) source-URL + pipeline instructions).
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in image_vise.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,29 @@
1
+ Copyright (c) 2016 WeTransfer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ To anyone who acknowledges that the file "sRGB_v4_ICC_preference_displayclass" is
23
+ provided "AS IS" WITH NO EXPRESS OR IMPLIED WARRANTY, permission to use,
24
+ copy and distribute this file for any purpose is hereby granted without
25
+ fee, provided that the file is not changed including the ICC copyright
26
+ notice tag, and that the name of ICC shall not be used in advertising or
27
+ publicity pertaining to distribution of the software without specific,
28
+ written prior permission. ICC makes no representations about the
29
+ suitability of this software for any purpose.
data/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # A thumbnailing server
2
+
3
+ `ImageVise` is an image-from-url-as-a-service server for use either standalone or within a larger Rails/Rack
4
+ framework. The main uses are:
5
+
6
+ * Image resizing on request
7
+ * Applying image filters
8
+
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`:
11
+
12
+ * `q` - Base64 encoded JSON object with `src_url` and `pipeline` properties
13
+ (the source URL of the image and processing steps to apply)
14
+ * `sig` - the HMAC signature, computed over the JSON in `q` before it gets Base64-encoded
15
+
16
+ A request to `ImageVise` might look like this:
17
+
18
+ /acbhGyfhyYErghff/acfgheg123
19
+
20
+ The URL that gets generated is best composed with the included `ImageVise.image_params` method. This method will
21
+ take care of encoding the source URL and the commands in the right way, as well as signing.
22
+
23
+ ## Using ImageVise within a Rails application
24
+
25
+ Mount ImageVise in your `routes.rb`:
26
+
27
+ ```ruby
28
+ mount '/images' => ImageVise
29
+ ```
30
+
31
+ and add an initializer (like `config/initializers/image_vise_config.rb`) to set up the permitted hosts
32
+
33
+ ```ruby
34
+ ImageVise.add_allowed_host! your_application_hostname
35
+ ImageVise.add_secret_key! ENV.fetch('IMAGE_VISE_SECRET')
36
+ ```
37
+
38
+ You might want to define a helper method for generating signed URLs as well, which will look something like this:
39
+
40
+ ```ruby
41
+ def thumb_url(source_image_url)
42
+ path = ImageVise.image_path(src_url: source_image_url, secret: ENV.fetch('IMAGE_VISE_SECRET')) do |pipeline|
43
+ # For example, you can also yield `pipeline` to the caller
44
+ pipeline.fit_crop width: 128, height: 128, gravity: 'c'
45
+ end
46
+ '/images' + path
47
+ end
48
+ ```
49
+
50
+ To preserve your sanity, make the route to the ImageVise engine terminal and do _not_ perform rewrites
51
+ on it in your webserver configuration - for instance, Base64 permits slashes.
52
+
53
+ ## Using ImageVise within a Rack application
54
+
55
+ Mount ImageVise under a script name in your `config.ru`:
56
+
57
+ ```ruby
58
+ map '/images' do
59
+ run ImageVise
60
+ end
61
+ ```
62
+
63
+ and add the initialization code either to `config.ru` proper or to some file in your application:
64
+
65
+ ImageVise.add_allowed_host! your_application_hostname
66
+ ImageVise.add_secret_key! ENV.fetch('IMAGE_VISE_SECRET')
67
+
68
+ You might want to define a helper method for generating signed URLs as well, which will look something like this:
69
+
70
+ ```ruby
71
+ def thumb_url(source_image_url)
72
+ path_param = ImageVise.image_path(src_url: source_image_url, secret: ENV.fetch('IMAGE_VISE_SECRET')) do |pipe|
73
+ pipe.fit_crop width: 256, height: 256, gravity: 'c'
74
+ pipe.sharpen sigma: 0.5, radius: 2
75
+ pipe.ellipse_stencil
76
+ end
77
+ # Output a URL to the app
78
+ '/images' + path
79
+ end
80
+ ```
81
+ ## Path decoding and SCRIPT_NAME
82
+
83
+ `ImageVise::RenderEngine` _must_ be mounted under a `SCRIPT_NAME` (using either `mount` in Rails
84
+ or using `map` in Rack). That is so since we may have more than 1 path component that we have to
85
+ decode (when the Base64 payload contains slashes).
86
+
87
+ ## Processing files on the local filesystem instead of remote ones
88
+
89
+ If you want to grab a local file, compose a `file://` URL (mind the endcoding!)
90
+
91
+ src_url = 'file://' + URI.encode(File.expand_path(my_pic))
92
+
93
+ Note that you need to permit certain glob patterns as sources before this will work, see below.
94
+
95
+ ## Operators and pipelining
96
+
97
+ ImageVise processes an image using _operators_. Each operator is just like an adjustment layer in Photoshop, except
98
+ that it can also resize the canvas. If you are familiar with node-based compositing systems like Shake, Nuke or Fusion
99
+ the pipeline is a node DAG with only one connection arrow going all the way. The operations are always applied in a
100
+ destructive way, so that the additional intermediate versions don't have to be deallocated manually after processing.
101
+
102
+ Each Operator is described in the pipeline using a tuple (Array) of roughly this structure:
103
+
104
+ [<operator_name>, {"<operator_param1>": <operator_param1_value>}]
105
+
106
+ You can have an unlimited number of such Operators per thumbnail, and they all get encoded in the URL (well,
107
+ technically, you _are_ limited - by the URL length supported by your web server).
108
+
109
+ For example, you can use the pipeline to apply a sharpening operator _after_ resising an image (for the lack
110
+ of decent image filtering choices in ImageMagick proper).
111
+
112
+ Here is an example pipeline, JSON-encoded (this is what is passed in the URL):
113
+
114
+ ```json
115
+ [
116
+ ["auto_orient", {}],
117
+ ["geom", {"geometry_string": "512x512"}],
118
+ ["fit_crop", {"width": 32, "height": 32, "gravity": "se"}],
119
+ ["sharpen", {"radius": 0.75, "sigma": 0.5}],
120
+ ["ellipse_stencil", {}]
121
+ ]
122
+ ```
123
+
124
+ The same pipeline can be created using the `Pipeline` DSL:
125
+
126
+ ```ruby
127
+ pipe = Pipeline.new.
128
+ auto_orient.
129
+ geom(geometry_string: '512x512').
130
+ fit_crop(width: 32, height: 32, gravity: 'se').
131
+ sharpen(radius: 0.75, sigma: 0.5).
132
+ ellipse_stencil
133
+ ```
134
+ and can then be applied to a `Magick::Image` object:
135
+
136
+ ```ruby
137
+ image = Magick::Image.read(my_image_path)[0]
138
+ pipe.apply!(image)
139
+ ```
140
+
141
+
142
+ ## Caching
143
+
144
+ The app is _designed_ to be run behind a frontline HTTP cache. The easiest is to use `Rack::Cache`, but this might
145
+ be instance-local depending on the storage backend used. A much better idea is to run ImageVise behind a long-caching
146
+ CDN.
147
+
148
+ ## Shared HMAC keys for signed URLs
149
+
150
+ To allow `ImageVise` to recognize the signature when the signature is going to be received, add it to the list
151
+ of the shared keys on the `ImageVise` server:
152
+
153
+ ```ruby
154
+ ImageVise.add_secret_key!('ahoy! this is a secret!')
155
+ ```
156
+
157
+ A single `ImageVise` server can maintain multiple signature keys, so that you will be able to generate thumbnails from
158
+ multiple applications all using different keys for their signatures. Every request will be validated against
159
+ each key and if at least one key generates the same signature for the same given parameters, it is going to be
160
+ accepted and the request will be allowed to go through.
161
+
162
+ ## Hostname and filesystem validation
163
+
164
+ By default, `ImageVise` will refuse to process images from URLs on "unknown" hosts. To mark a host as "known"
165
+ tell `ImageVise` to
166
+
167
+ ```ruby
168
+ ImageVise.add_allowed_host!('my-image-store.ourcompany.co.uk')
169
+ ```
170
+
171
+ If you want to permit images from the local server filesystem to be accessed, add the glob pattern
172
+ to the set of allowed filesystem patterns:
173
+
174
+ ```ruby
175
+ ImageVise.allow_filesystem_source!(Rails.root + '/public/*.jpg')
176
+ ```
177
+
178
+ Note that these are _glob_ patterns. The image path will be checked against them using `File.fnmatch`.
179
+
180
+ ## Handling errors within the rendering Rack app
181
+
182
+ By default, the Rack app within ImageVise swallows all exceptions and returns the error message
183
+ within a machine-readable JSON payload. If that doesn't work for you, or you want to add error
184
+ handling using some error tracking provider, either subclass `ImageVise::RenderEngine` or prepend
185
+ a module into it that will intercept the errors. See error handling in `examples/` for more.
186
+
187
+ ## State
188
+
189
+ Except for the HTTP cache no state is stored (`ImageVise` does not care whether you store
190
+ your images using Dragonfly, CarrierWave or some custom handling code). All the app needs is the full URL.
191
+
192
+ ## Running the tests, versioning, contributing
193
+
194
+ By default, `bundle exec rake` will run RSpec and will also open the generated images using the `$ open` command available
195
+ on your CLI. If you want to skip viewing those images, set the `SKIP_INTERACTIVE` environment variable to any value.
196
+
197
+ The gem version is specified in `image_vise.rb`. When contributing, please follow:
198
+
199
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
200
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
201
+ * Fork the project.
202
+ * Start a feature/bugfix branch.
203
+ * Commit and push until you are happy with your contribution.
204
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
205
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
206
+
207
+ ### Copyright
208
+
209
+ Copyright (c) 2016 WeTransfer. See LICENSE.txt for further details.
210
+ The licensing terms also apply to the `waterside_magic_hour.jpg` test image.
211
+
212
+ The sRGB color profiles are [downloaded from the ICC](http://www.color.org/srgbprofiles.xalter) and it's
213
+ use is governed by the terms present in the LICENSE.txt
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/SECURITY.md ADDED
@@ -0,0 +1,57 @@
1
+ # Security implications for ImageVise
2
+
3
+ This lists out the implementation details of security-sensitive parts of ImageVise.
4
+
5
+ ## Protection of the URLs
6
+
7
+ URLs are passed as Base64-encoded JSON. The HMAC signature is computed over the Base-64 encoded string,
8
+ so altering the string (with the intention to bust the cache) will invalidate the signature.
9
+
10
+ For checking HMAC values `Rack::Utils.secure_compare` constant-time comparison is used.
11
+
12
+ ## Throttling still recommended
13
+
14
+ Throttling between the caching CDN/proxy is recommended.
15
+
16
+ ## Cache bypass protection for fuzzed paths
17
+
18
+ ImageVise accepts exactly 2 path components, and will return early if there are more
19
+
20
+ ## Cache bypass protection for randomized query string params
21
+
22
+ ImageVise defaults to using paths. If you have a way to forbid query strings on the fronting CDN
23
+ or proxy server we suggest you to do so, to prevent randomized URLs from filling up your cache
24
+ and extreme amounts of processing from happening.
25
+
26
+ * `/image/<pipeline>/<sig>?&random=123`
27
+ * `/image/<pipeline>/<sig>?&random=456`
28
+
29
+ These URLs would in fact resolve to the same source image and pipeline, but would not be stored in an upstream
30
+ CDN cache because the query string params contain extra data.
31
+
32
+ ## Protection for remote URLs from HTTP(s) origins
33
+
34
+ Only URLs referring to permitted hosts are going to be permitted for fetching. If there are no hosts added,
35
+ any remote URL is going to cause an exception. No special verification for whether the upstream must be HTTP
36
+ or HTTPS is performed at this time.
37
+
38
+ ## Protection for "file:/" URLs
39
+
40
+ The file URLs are going to be decoded, and the path component will be matched against permitted _glob patterns._
41
+ The matching takes links (hard and soft) into account, and uses Ruby's `File.fnmatch?` under the hood. The path
42
+ is always expanded first using `File.expand_path`. The data is not read into ImageMagick from the original location,
43
+ but gets copied into a tempfile first.
44
+
45
+ The path in to the file gets encoded in the image processing request and may be examined by the user, that will
46
+ disclose where the source image is stored on the server's filesystem. This might be an issue - if it is,
47
+ a customised version with a custom URL scheme should be used for the source URL.
48
+
49
+ ## ImageMagick memory constraints
50
+
51
+ ImageVise does not set RMagick limits by itself. You should
52
+ [set them according to the RMagick documentation.](https://rmagick.github.io/magick.html#limit_resource)
53
+
54
+ ## Processing time constraints
55
+
56
+ If you are using forking, there will be a timeout used for how long the forked child process may run,
57
+ which is the default timeout used in ExceptionalFork.
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/lib/image_vise'
2
+
3
+ # Add all the secret keys specified in the environment separated by a comma
4
+ if ENV['VISE_SECRET_KEYS']
5
+ ENV['VISE_SECRET_KEYS'].split(',').map do | key |
6
+ ImageVise.add_secret_key!(key.strip)
7
+ end
8
+ end
9
+
10
+ # Cover things with caching, even redirects (since we do not want to ask S3 too often)
11
+ use Rack::Cache, :metastore => 'file:tmp/cache/rack/meta', :entitystore => 'file:tmp/cache/rack/entity',
12
+ :verbose => true, :allow_reload => false, :allow_revalidate => false
13
+
14
+ # Serve runtime thumbnails, under a specific URL
15
+ map '/images'do
16
+ run ImageVise
17
+ end
@@ -0,0 +1,27 @@
1
+ class MyBlur
2
+ # The constructor must accept keyword arguments if your operator accepts them
3
+ def initialize(radius:, sigma:)
4
+ @radius = radius.to_f
5
+ @sigma = sigma.to_f
6
+ end
7
+
8
+ # Needed for the operator to be serialized to parameters
9
+ def to_h
10
+ {radius: @radius, sigma: @sigma}
11
+ end
12
+
13
+ def apply!(magick_image) # The argument is a Magick::Image
14
+ # Apply the method to the function argument
15
+ blurred_image = magick_image.blur_image(@radius, @sigma)
16
+
17
+ # Some methods (without !) return a new Magick::Image.
18
+ # That image has to be composited back into the original.
19
+ magick_image.composite!(blurred_image, Magick::CenterGravity, Magick::CopyCompositeOp)
20
+ ensure
21
+ # Make sure to explicitly destroy() all the intermediate images
22
+ ImageVise.destroy(blurred_image)
23
+ end
24
+ end
25
+
26
+ # This will also make `ImageVise::Pipeline` respond to `blur(radius:.., sigma:..)`
27
+ ImageVise.add_operator 'my_blur', MyBlur
@@ -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
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'image_vise/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "image_vise"
8
+ spec.version = ImageVise::VERSION
9
+ spec.authors = ["Julik Tarkhanov"]
10
+ spec.email = ["me@julik.nl"]
11
+
12
+ spec.summary = "Runtime thumbnailing proxy"
13
+ spec.description = "Image processing via URLs"
14
+ spec.homepage = "https://github.com/WeTransfer/image_vise"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0")
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency 'patron', '~> 0.6'
31
+ spec.add_dependency 'rmagick', '~> 2.15'
32
+ spec.add_dependency 'ks'
33
+ spec.add_dependency 'magic_bytes', '~> 1'
34
+ spec.add_dependency 'rack', '~> 1'
35
+
36
+ spec.add_development_dependency "bundler", "~> 1.7"
37
+ spec.add_development_dependency "rake", "~> 10.0"
38
+ spec.add_development_dependency "rack-test"
39
+ spec.add_development_dependency "rspec", "~> 3.0"
40
+ spec.add_development_dependency "addressable"
41
+ spec.add_development_dependency "strenv"
42
+ spec.add_development_dependency "simplecov"
43
+ end
@@ -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, "#{path_on_filesystem} is not on the path whitelist" unless matches
24
+ end
25
+
26
+ ImageVise.register_fetcher 'file', self
27
+ end
@@ -0,0 +1,42 @@
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} on #{uri}")
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
+ return if ImageVise.allowed_hosts.include?(uri.host)
37
+ raise AccessError, "#{uri} is not permitted as source"
38
+ end
39
+
40
+ ImageVise.register_fetcher 'http', self
41
+ ImageVise.register_fetcher 'https', self
42
+ end