imageproxy 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.mdown +6 -0
- data/VERSION +1 -1
- data/config.ru +1 -1
- data/imageproxy.gemspec +1 -1
- data/lib/command.rb +17 -16
- data/lib/compare.rb +9 -7
- data/lib/convert.rb +59 -57
- data/lib/identify.rb +8 -6
- data/lib/options.rb +56 -54
- data/lib/selftest.rb +40 -38
- data/lib/server.rb +82 -80
- data/lib/signature.rb +19 -17
- data/spec/command_spec.rb +3 -3
- data/spec/convert_spec.rb +3 -3
- data/spec/options_spec.rb +16 -16
- data/spec/server_spec.rb +4 -4
- data/spec/signature_spec.rb +15 -15
- metadata +2 -2
data/README.mdown
CHANGED
@@ -50,6 +50,12 @@ imageproxy doesn't do any sort of caching. That kind of thing is better left up
|
|
50
50
|
Also, imageproxy itself isn't nearly as fast as it could be. It's written in an interpreted language, and it shells out to curl and ImageMagick to do its work. Presumably, it would be way faster written in C as an Apache module, but this implementation was quite a bit easier :)
|
51
51
|
|
52
52
|
|
53
|
+
INSTALLING
|
54
|
+
----------
|
55
|
+
|
56
|
+
gem install imageproxy
|
57
|
+
|
58
|
+
|
53
59
|
API
|
54
60
|
---
|
55
61
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.1
|
data/config.ru
CHANGED
data/imageproxy.gemspec
CHANGED
data/lib/command.rb
CHANGED
@@ -1,22 +1,23 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Imageproxy
|
2
|
+
class Command
|
3
|
+
protected
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
def execute_command(command_line)
|
6
|
+
stdin, stdout, stderr = Open3.popen3(command_line)
|
7
|
+
[output_to_string(stdout), output_to_string(stderr)].join("")
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
def curl(url, options={})
|
11
|
+
user_agent = options[:user_agent] || "imageproxy"
|
12
|
+
%|curl -s -A "#{user_agent}" "#{url}"|
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def to_path(obj)
|
16
|
+
obj.respond_to?(:path) ? obj.path : obj.to_s
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
19
|
+
def output_to_string(output)
|
20
|
+
output.readlines.join("").chomp
|
21
|
+
end
|
20
22
|
end
|
21
|
-
|
22
23
|
end
|
data/lib/compare.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
require File.join(File.expand_path(File.dirname(__FILE__)), "command")
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module Imageproxy
|
4
|
+
class Compare < Imageproxy::Command
|
5
|
+
def initialize(a, b)
|
6
|
+
@path_a = to_path(a)
|
7
|
+
@path_b = to_path(b)
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
10
|
+
def execute
|
11
|
+
execute_command %'compare -metric AE -fuzz 10% "#{@path_a}" "#{@path_b}" "#{Tempfile.new("compare").path}"'
|
12
|
+
end
|
11
13
|
end
|
12
14
|
end
|
data/lib/convert.rb
CHANGED
@@ -1,72 +1,74 @@
|
|
1
1
|
require File.join(File.expand_path(File.dirname(__FILE__)), "command")
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
def initialize(options)
|
7
|
-
@options = options
|
8
|
-
if (!(options.resize || options.thumbnail || options.rotate || options.flip || options.format || options.quality))
|
9
|
-
raise "Missing action or illegal parameter value"
|
10
|
-
end
|
11
|
-
end
|
3
|
+
module Imageproxy
|
4
|
+
class Convert < Imageproxy::Command
|
5
|
+
attr_reader :options
|
12
6
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
7
|
+
def initialize(options)
|
8
|
+
@options = options
|
9
|
+
if (!(options.resize || options.thumbnail || options.rotate || options.flip || options.format || options.quality))
|
10
|
+
raise "Missing action or illegal parameter value"
|
11
|
+
end
|
12
|
+
end
|
17
13
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
convert_options << "-flop" if options.flip == "horizontal"
|
23
|
-
convert_options << "-flip" if options.flip == "vertical"
|
24
|
-
convert_options << rotate_options if options.rotate
|
25
|
-
convert_options << "-colors 256" if options.format == "png8"
|
26
|
-
convert_options << "-quality #{options.quality}" if options.quality
|
27
|
-
convert_options << interlace_options if options.progressive
|
28
|
-
convert_options.join " "
|
29
|
-
end
|
14
|
+
def execute(user_agent=nil)
|
15
|
+
execute_command %'#{curl options.source, :user_agent => user_agent} | convert - #{convert_options} #{new_format}#{file.path}'
|
16
|
+
file
|
17
|
+
end
|
30
18
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
19
|
+
def convert_options
|
20
|
+
convert_options = []
|
21
|
+
convert_options << "-resize #{resize_thumbnail_options(options.resize)}" if options.resize
|
22
|
+
convert_options << "-thumbnail #{resize_thumbnail_options(options.thumbnail)}" if options.thumbnail
|
23
|
+
convert_options << "-flop" if options.flip == "horizontal"
|
24
|
+
convert_options << "-flip" if options.flip == "vertical"
|
25
|
+
convert_options << rotate_options if options.rotate
|
26
|
+
convert_options << "-colors 256" if options.format == "png8"
|
27
|
+
convert_options << "-quality #{options.quality}" if options.quality
|
28
|
+
convert_options << interlace_options if options.progressive
|
29
|
+
convert_options.join " "
|
42
30
|
end
|
43
|
-
end
|
44
31
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
32
|
+
def resize_thumbnail_options(size)
|
33
|
+
case options.shape
|
34
|
+
when "cut"
|
35
|
+
"#{size}^ -gravity center -extent #{size}"
|
36
|
+
when "preserve"
|
37
|
+
size
|
38
|
+
when "pad"
|
39
|
+
background = options.background ? %|"#{options.background}"| : %|none -matte|
|
40
|
+
"#{size} -background #{background} -gravity center -extent #{size}"
|
41
|
+
else
|
42
|
+
size
|
43
|
+
end
|
51
44
|
end
|
52
|
-
end
|
53
45
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
"-interlace JPEG"
|
58
|
-
when "false"
|
59
|
-
"-interlace none"
|
46
|
+
def rotate_options
|
47
|
+
if options.rotate.to_f % 90 == 0
|
48
|
+
"-rotate #{options.rotate}"
|
60
49
|
else
|
61
|
-
""
|
50
|
+
background = options.background ? %|"#{options.background}"| : %|none|
|
51
|
+
"-background #{background} -matte -rotate #{options.rotate}"
|
52
|
+
end
|
62
53
|
end
|
63
|
-
end
|
64
54
|
|
65
|
-
|
66
|
-
|
67
|
-
|
55
|
+
def interlace_options
|
56
|
+
case options.progressive
|
57
|
+
when "true"
|
58
|
+
"-interlace JPEG"
|
59
|
+
when "false"
|
60
|
+
"-interlace none"
|
61
|
+
else
|
62
|
+
""
|
63
|
+
end
|
64
|
+
end
|
68
65
|
|
69
|
-
|
70
|
-
|
66
|
+
def new_format
|
67
|
+
options.format ? "#{options.format}:" : ""
|
68
|
+
end
|
69
|
+
|
70
|
+
def file
|
71
|
+
@tempfile ||= Tempfile.new("imageproxy").tap(&:close)
|
72
|
+
end
|
71
73
|
end
|
72
74
|
end
|
data/lib/identify.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
require File.join(File.expand_path(File.dirname(__FILE__)), "command")
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
module Imageproxy
|
4
|
+
class Identify < Imageproxy::Command
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
def execute(user_agent=nil)
|
10
|
+
execute_command %'#{curl @options.source, :user_agent => user_agent} | identify -verbose -'
|
11
|
+
end
|
10
12
|
end
|
11
13
|
end
|
data/lib/options.rb
CHANGED
@@ -2,72 +2,74 @@ require 'uri'
|
|
2
2
|
require 'cgi'
|
3
3
|
require 'mime/types'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
module Imageproxy
|
6
|
+
class Options
|
7
|
+
def initialize(path, query_params)
|
8
|
+
params_from_path = path.split('/').reject { |s| s.nil? || s.empty? }
|
9
|
+
command = params_from_path.shift
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
unescape_source
|
17
|
-
unescape_signature
|
18
|
-
check_parameters
|
19
|
-
end
|
11
|
+
@hash = Hash[*params_from_path]
|
12
|
+
@hash['command'] = command
|
13
|
+
@hash.merge! query_params
|
14
|
+
merge_obfuscated
|
15
|
+
@hash["source"] = @hash.delete("src") if @hash.has_key?("src")
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
17
|
+
unescape_source
|
18
|
+
unescape_signature
|
19
|
+
check_parameters
|
20
|
+
end
|
21
|
+
|
22
|
+
def check_parameters
|
23
|
+
check_param('resize', /^[0-9]{1,5}(x[0-9]{1,5})?$/)
|
24
|
+
check_param('thumbnail', /^[0-9]{1,5}(x[0-9]{1,5})?$/)
|
25
|
+
check_param('rotate', /^(-)?[0-9]{1,3}(\.[0-9]+)?$/)
|
26
|
+
check_param('format', /^[0-9a-zA-Z]{2,6}$/)
|
27
|
+
check_param('progressive', /^true|false$/i)
|
28
|
+
check_param('background', /^#[0-9a-f]{3}([0-9a-f]{3})?|rgba\([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-1](.[0-9]+)?\)$/)
|
29
|
+
check_param('shape', /^preserve|pad|cut$/i)
|
30
|
+
@hash['quality'] = [[@hash['quality'].to_i, 100].min, 0].max.to_s if @hash.has_key?('quality')
|
31
|
+
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
def check_param(param, rega)
|
34
|
+
if @hash.has_key? param
|
35
|
+
if (!rega.match(@hash[param]))
|
36
|
+
@hash.delete(param)
|
37
|
+
end
|
36
38
|
end
|
37
39
|
end
|
38
|
-
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
def method_missing(symbol)
|
42
|
+
@hash[symbol.to_s] || @hash[symbol]
|
43
|
+
end
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
45
|
+
def content_type
|
46
|
+
MIME::Types.of(@hash['source']).first.content_type
|
47
|
+
end
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
def unescape_source
|
51
|
-
@hash['source'] &&= CGI.unescape(CGI.unescape(@hash['source']))
|
52
|
-
end
|
49
|
+
private
|
53
50
|
|
54
|
-
|
55
|
-
|
56
|
-
|
51
|
+
def unescape_source
|
52
|
+
@hash['source'] &&= CGI.unescape(CGI.unescape(@hash['source']))
|
53
|
+
end
|
57
54
|
|
58
|
-
|
59
|
-
|
60
|
-
decoded = Base64.decode64(CGI.unescape(@hash["_"]))
|
61
|
-
decoded_hash = CGI.parse(decoded)
|
62
|
-
@hash.delete "_"
|
63
|
-
decoded_hash.map { |k, v| @hash[k] = (v.class == Array) ? v.first : v }
|
55
|
+
def unescape_signature
|
56
|
+
@hash['signature'] &&= URI.unescape(@hash['signature'])
|
64
57
|
end
|
65
58
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
59
|
+
def merge_obfuscated
|
60
|
+
if @hash["_"]
|
61
|
+
decoded = Base64.decode64(CGI.unescape(@hash["_"]))
|
62
|
+
decoded_hash = CGI.parse(decoded)
|
63
|
+
@hash.delete "_"
|
64
|
+
decoded_hash.map { |k, v| @hash[k] = (v.class == Array) ? v.first : v }
|
65
|
+
end
|
66
|
+
|
67
|
+
if @hash["-"]
|
68
|
+
decoded = Base64.decode64(CGI.unescape(@hash["-"]))
|
69
|
+
decoded_hash = Hash[*decoded.split('/').reject { |s| s.nil? || s.empty? }]
|
70
|
+
@hash.delete "-"
|
71
|
+
decoded_hash.map { |k, v| @hash[k] = (v.class == Array) ? v.first : v }
|
72
|
+
end
|
71
73
|
end
|
72
74
|
end
|
73
75
|
end
|
data/lib/selftest.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
html
|
1
|
+
module Imageproxy
|
2
|
+
class Selftest
|
3
|
+
def self.html(request, signature_required, signature_secret)
|
4
|
+
html = <<-HTML
|
4
5
|
<html>
|
5
6
|
<head>
|
6
7
|
<title>imageproxy selftest</title>
|
@@ -12,61 +13,62 @@ class Selftest
|
|
12
13
|
</style>
|
13
14
|
</head>
|
14
15
|
<body>
|
15
|
-
|
16
|
+
HTML
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
url_prefix = "#{request.scheme}://#{request.host_with_port}"
|
19
|
+
raw_source = "http://eahanson.s3.amazonaws.com/imageproxy/sample.png"
|
20
|
+
source = CGI.escape(URI.escape(URI.escape(raw_source)))
|
20
21
|
|
21
|
-
|
22
|
+
html += <<-HTML
|
22
23
|
<h3>Original Image</h3>
|
23
24
|
<a href="#{raw_source}">#{raw_source}</a>
|
24
25
|
<img src="#{raw_source}">
|
25
|
-
|
26
|
+
HTML
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
examples = [
|
29
|
+
["Resize (regular query-string URL format)", "/convert?resize=100x100&source=#{source}"],
|
30
|
+
["Resize (CloudFront-compatible URL format)", "/convert/resize/100x100/source/#{source}"],
|
30
31
|
|
31
|
-
|
32
|
-
|
32
|
+
["Resize with padding", "/convert?resize=100x100&shape=pad&source=#{source}"],
|
33
|
+
["Resize with padding & background color", "/convert?resize=100x100&shape=pad&background=%23ff00ff&source=#{source}"],
|
33
34
|
|
34
|
-
|
35
|
+
["Resize with cutting", "/convert?resize=100x100&shape=cut&source=#{source}"],
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
["Flipping horizontally", "/convert?flip=horizontal&source=#{source}"],
|
38
|
+
["Flipping vertically", "/convert?flip=vertical&source=#{source}"],
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
["Rotating to a 90-degree increment", "/convert?rotate=90&source=#{source}"],
|
41
|
+
["Rotating to a non-90-degree increment", "/convert?rotate=120&source=#{source}"],
|
42
|
+
["Rotating to a non-90-degree increment with a background color", "/convert?rotate=120&background=%23ff00ff&source=#{source}"],
|
42
43
|
|
43
|
-
|
44
|
-
|
44
|
+
["Combo", "/convert?resize=100x100&shape=cut&rotate=45&background=%23ff00ff&source=#{source}"]
|
45
|
+
]
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
47
|
+
examples.each do |example|
|
48
|
+
path = example[1]
|
49
|
+
if (signature_required)
|
50
|
+
signature = CGI.escape(Signature.create(path, signature_secret))
|
51
|
+
if path.include?("&")
|
52
|
+
path += "&signature=#{signature}"
|
53
|
+
else
|
54
|
+
path += "/signature/#{signature}"
|
55
|
+
end
|
54
56
|
end
|
55
|
-
|
56
|
-
|
57
|
-
html += <<-HTML
|
57
|
+
example_url = url_prefix + path
|
58
|
+
html += <<-HTML
|
58
59
|
<h3>#{example[0]}</h3>
|
59
60
|
<a href="#{example_url}">#{example_url}</a>
|
60
61
|
<img src="#{example_url}">
|
61
|
-
|
62
|
-
|
62
|
+
HTML
|
63
|
+
end
|
63
64
|
|
64
|
-
|
65
|
+
html += <<-HTML
|
65
66
|
<div class="footer"><a href="https://github.com/eahanson/imageproxy">imageproxy</a> selftest</div>
|
66
67
|
</body>
|
67
68
|
</html>
|
68
|
-
|
69
|
+
HTML
|
69
70
|
|
70
|
-
|
71
|
+
html
|
72
|
+
end
|
71
73
|
end
|
72
74
|
end
|
data/lib/server.rb
CHANGED
@@ -5,101 +5,103 @@ require File.join(File.expand_path(File.dirname(__FILE__)), "selftest")
|
|
5
5
|
require File.join(File.expand_path(File.dirname(__FILE__)), "signature")
|
6
6
|
require 'uri'
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
module Imageproxy
|
9
|
+
class Server
|
10
|
+
def initialize
|
11
|
+
@file_server = Rack::File.new(File.join(File.expand_path(File.dirname(__FILE__)), "..", "public"))
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
14
|
+
def call(env)
|
15
|
+
request = Rack::Request.new(env)
|
16
|
+
options = Options.new(request.path_info, request.params)
|
17
|
+
user_agent = request.env["HTTP_USER_AGENT"]
|
18
|
+
cachetime = config(:cache_time) ? config(:cache_time) : 86400
|
19
|
+
|
20
|
+
case options.command
|
21
|
+
when "convert", "process", nil
|
22
|
+
check_signature request, options
|
23
|
+
check_domain options
|
24
|
+
check_size options
|
25
|
+
|
26
|
+
file = Convert.new(options).execute(user_agent)
|
27
|
+
class << file
|
28
|
+
alias to_path path
|
29
|
+
end
|
30
|
+
|
31
|
+
file.open
|
32
|
+
[200, {"Content-Type" => options.content_type, "Cache-Control" => "max-age=#{cachetime}, must-revalidate"}, file]
|
33
|
+
when "identify"
|
34
|
+
check_signature request, options
|
35
|
+
check_domain options
|
36
|
+
|
37
|
+
[200, {"Content-Type" => "text/plain"}, [Identify.new(options).execute(user_agent)]]
|
38
|
+
when "selftest"
|
39
|
+
[200, {"Content-Type" => "text/html"}, [Selftest.html(request, config?(:signature_required), config(:signature_secret))]]
|
40
|
+
else
|
41
|
+
@file_server.call(env)
|
42
|
+
end
|
43
|
+
rescue
|
44
|
+
STDERR.puts $!
|
45
|
+
[500, {"Content-Type" => "text/plain"}, ["Error (#{$!})"]]
|
41
46
|
end
|
42
|
-
rescue
|
43
|
-
STDERR.puts $!
|
44
|
-
[500, {"Content-Type" => "text/plain"}, ["Error (#{$!})"]]
|
45
|
-
end
|
46
47
|
|
47
|
-
|
48
|
+
private
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
50
|
+
def config(symbol)
|
51
|
+
ENV["IMAGEPROXY_#{symbol.to_s.upcase}"]
|
52
|
+
end
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
54
|
+
def config?(symbol)
|
55
|
+
config(symbol) && config(symbol).casecmp("TRUE") == 0
|
56
|
+
end
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
def check_signature(request, options)
|
59
|
+
if config?(:signature_required)
|
60
|
+
raise "Missing siganture" if options.signature.nil?
|
60
61
|
|
61
|
-
|
62
|
-
|
62
|
+
valid_signature = Signature.correct?(options.signature, request.fullpath, config(:signature_secret))
|
63
|
+
raise "Invalid signature #{options.signature} for #{request.url}" unless valid_signature
|
64
|
+
end
|
63
65
|
end
|
64
|
-
end
|
65
66
|
|
66
|
-
|
67
|
-
|
68
|
-
|
67
|
+
def check_domain(options)
|
68
|
+
raise "Invalid domain" unless domain_allowed? options.source
|
69
|
+
end
|
69
70
|
|
70
|
-
|
71
|
-
|
72
|
-
|
71
|
+
def check_size(options)
|
72
|
+
raise "Image size too large" if exceeds_max_size(options.resize, options.thumbnail)
|
73
|
+
end
|
73
74
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
75
|
+
def domain_allowed?(url)
|
76
|
+
return true unless allowed_domains
|
77
|
+
allowed_domains.include?(url_to_domain url)
|
78
|
+
end
|
78
79
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
80
|
+
def url_to_domain(url)
|
81
|
+
URI::parse(url).host.split(".")[-2, 2].join(".")
|
82
|
+
rescue
|
83
|
+
""
|
84
|
+
end
|
84
85
|
|
85
|
-
|
86
|
-
|
87
|
-
|
86
|
+
def allowed_domains
|
87
|
+
config(:allowed_domains) && config(:allowed_domains).split(",").map(&:strip)
|
88
|
+
end
|
88
89
|
|
89
|
-
|
90
|
-
|
91
|
-
|
90
|
+
def exceeds_max_size(*sizes)
|
91
|
+
max_size && sizes.any? { |size| size && requested_size(size) > max_size }
|
92
|
+
end
|
92
93
|
|
93
|
-
|
94
|
-
|
95
|
-
|
94
|
+
def max_size
|
95
|
+
config(:max_size) && config(:max_size).to_i
|
96
|
+
end
|
96
97
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
98
|
+
def requested_size(req_size)
|
99
|
+
sizes = req_size.scan(/\d*/)
|
100
|
+
if sizes[2] && (sizes[2].to_i > sizes[0].to_i)
|
101
|
+
sizes[2].to_i
|
102
|
+
else
|
103
|
+
sizes[0].to_i
|
104
|
+
end
|
103
105
|
end
|
104
106
|
end
|
105
|
-
end
|
107
|
+
end
|
data/lib/signature.rb
CHANGED
@@ -1,24 +1,26 @@
|
|
1
1
|
require 'base64'
|
2
2
|
require 'openssl'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
module Imageproxy
|
5
|
+
class Signature
|
6
|
+
def self.create(path, secret)
|
7
|
+
Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), secret, remove_signature_from(path))).strip.tr('+/', '-_')
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
10
|
+
def self.remove_signature_from(path)
|
11
|
+
#TODO: do this in fewer passes
|
12
|
+
path.
|
13
|
+
sub(%r{&signature(=[^&]*)?(?=&|$)}, "").
|
14
|
+
sub(%r{\?signature(=[^&]*)?&}, "?").
|
15
|
+
sub(%r{\?signature(=[^&]*)?$}, "").
|
16
|
+
sub(%r{/signature/[^\?/]+/}, "/").
|
17
|
+
sub(%r{/signature/[^\?/]+\?}, "?").
|
18
|
+
sub(%r{/signature/[^\?/]+}, "")
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
def self.correct?(signature, path, secret)
|
22
|
+
created = create(path, secret)
|
23
|
+
signature != nil && path != nil && secret != nil && (created == signature || created == signature.tr('+/', '-_'))
|
24
|
+
end
|
23
25
|
end
|
24
26
|
end
|
data/spec/command_spec.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Command do
|
3
|
+
describe Imageproxy::Command do
|
4
4
|
describe "#curl" do
|
5
5
|
context "when a user agent is supplied" do
|
6
6
|
it "should send that user agent" do
|
7
|
-
Command.new.send(:curl, "http://example.com/dog.jpg", :user_agent => "some user agent").should ==
|
7
|
+
Imageproxy::Command.new.send(:curl, "http://example.com/dog.jpg", :user_agent => "some user agent").should ==
|
8
8
|
%|curl -s -A "some user agent" "http://example.com/dog.jpg"|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
12
|
context "when no user agent is supplied" do
|
13
13
|
it "should send a default user agent" do
|
14
|
-
Command.new.send(:curl, "http://example.com/dog.jpg").should ==
|
14
|
+
Imageproxy::Command.new.send(:curl, "http://example.com/dog.jpg").should ==
|
15
15
|
%|curl -s -A "imageproxy" "http://example.com/dog.jpg"|
|
16
16
|
end
|
17
17
|
end
|
data/spec/convert_spec.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Convert do
|
3
|
+
describe Imageproxy::Convert do
|
4
4
|
before do
|
5
5
|
@mock_file = mock("file")
|
6
6
|
@mock_file.stub!(:path).and_return("/mock/file/path")
|
7
7
|
end
|
8
8
|
|
9
9
|
def command(options)
|
10
|
-
command = Convert.new(Options.new("", {:source => "http%3A%2F%2Fexample.com%2Fdog.jpg"}.merge(options)))
|
10
|
+
command = Imageproxy::Convert.new(Imageproxy::Options.new("", {:source => "http%3A%2F%2Fexample.com%2Fdog.jpg"}.merge(options)))
|
11
11
|
command.stub!(:file).and_return(@mock_file)
|
12
12
|
command.stub!(:system)
|
13
13
|
command
|
@@ -15,7 +15,7 @@ describe Convert do
|
|
15
15
|
|
16
16
|
context "general" do
|
17
17
|
before do
|
18
|
-
@command = Convert.new(Options.new("/convert/format/png/resize/10x20/source/http%3A%2F%2Fexample.com%2Fdog.jpg", {}))
|
18
|
+
@command = Imageproxy::Convert.new(Imageproxy::Options.new("/convert/format/png/resize/10x20/source/http%3A%2F%2Fexample.com%2Fdog.jpg", {}))
|
19
19
|
@command.stub!(:file).and_return(@mock_file)
|
20
20
|
@command.stub!(:system)
|
21
21
|
end
|
data/spec/options_spec.rb
CHANGED
@@ -2,10 +2,10 @@ require 'base64'
|
|
2
2
|
require "#{File.dirname(__FILE__)}/../imageproxy"
|
3
3
|
require "#{File.dirname(__FILE__)}/../lib/options"
|
4
4
|
|
5
|
-
describe Options do
|
5
|
+
describe Imageproxy::Options do
|
6
6
|
describe "parsing path" do
|
7
7
|
context "a simple URL" do
|
8
|
-
subject { Options.new "/process/color/blue/size/medium", {} }
|
8
|
+
subject { Imageproxy::Options.new "/process/color/blue/size/medium", {} }
|
9
9
|
its(:command) { should == "process" }
|
10
10
|
its(:color) { should == "blue" }
|
11
11
|
its(:size) { should == "medium" }
|
@@ -13,59 +13,59 @@ describe Options do
|
|
13
13
|
|
14
14
|
context "source" do
|
15
15
|
context "when double-escaped" do
|
16
|
-
subject { Options.new "/process/source/http%253A%252F%252Fexample.com%252Fdog.jpg", {} }
|
16
|
+
subject { Imageproxy::Options.new "/process/source/http%253A%252F%252Fexample.com%252Fdog.jpg", {} }
|
17
17
|
it("should unescape") { subject.source.should == "http://example.com/dog.jpg" }
|
18
18
|
end
|
19
19
|
|
20
20
|
context "when escaped" do
|
21
|
-
subject { Options.new "/process/source/http%3A%2F%2Fexample.com%2Fdog.jpg", {} }
|
21
|
+
subject { Imageproxy::Options.new "/process/source/http%3A%2F%2Fexample.com%2Fdog.jpg", {} }
|
22
22
|
it("should unescape") { subject.source.should == "http://example.com/dog.jpg" }
|
23
23
|
end
|
24
24
|
|
25
25
|
context "when not escaped" do
|
26
|
-
subject { Options.new "/process/source/foo", {} }
|
26
|
+
subject { Imageproxy::Options.new "/process/source/foo", {} }
|
27
27
|
it("should not unescape") { subject.source.should == "foo" }
|
28
28
|
end
|
29
29
|
|
30
30
|
context "when parameter is named 'src'" do
|
31
|
-
subject { Options.new "/process/src/foo", {} }
|
31
|
+
subject { Imageproxy::Options.new "/process/src/foo", {} }
|
32
32
|
it("should rename to 'source'") { subject.source.should == "foo" }
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
36
|
context "signature" do
|
37
37
|
context "when escaped with + signs" do
|
38
|
-
subject { Options.new "/process/source/foo/signature/foo+bar", {} }
|
38
|
+
subject { Imageproxy::Options.new "/process/source/foo/signature/foo+bar", {} }
|
39
39
|
it("should keep the + sign") { subject.signature.should == "foo+bar" }
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
44
|
describe "adding query params" do
|
45
|
-
subject { Options.new "/convert/source/foo", { "resize" => "20x20" } }
|
45
|
+
subject { Imageproxy::Options.new "/convert/source/foo", { "resize" => "20x20" } }
|
46
46
|
it("should add query params") { subject.resize.should == "20x20" }
|
47
47
|
it("should keep params from path") { subject.source.should == "foo" }
|
48
48
|
end
|
49
49
|
|
50
50
|
describe "content type" do
|
51
51
|
context "when guessing based on source filename" do
|
52
|
-
it("should understand .png") { Options.new("/convert", "source" => "foo.png").content_type.should == "image/png" }
|
53
|
-
it("should understand .jpg") { Options.new("/convert", "source" => "foo.jpg").content_type.should == "image/jpeg" }
|
54
|
-
it("should understand .JPEG") { Options.new("/convert", "source" => "foo.JPEG").content_type.should == "image/jpeg" }
|
52
|
+
it("should understand .png") { Imageproxy::Options.new("/convert", "source" => "foo.png").content_type.should == "image/png" }
|
53
|
+
it("should understand .jpg") { Imageproxy::Options.new("/convert", "source" => "foo.jpg").content_type.should == "image/jpeg" }
|
54
|
+
it("should understand .JPEG") { Imageproxy::Options.new("/convert", "source" => "foo.JPEG").content_type.should == "image/jpeg" }
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
58
|
describe "obfuscation" do
|
59
59
|
it "should allow the query string to be encoded in base64" do
|
60
60
|
encoded = CGI.escape(Base64.encode64("resize=20x20&source=http://example.com/dog.jpg"))
|
61
|
-
options = Options.new "/convert", "_" => encoded
|
61
|
+
options = Imageproxy::Options.new "/convert", "_" => encoded
|
62
62
|
options.resize.should == "20x20"
|
63
63
|
options.source.should == "http://example.com/dog.jpg"
|
64
64
|
end
|
65
65
|
|
66
66
|
it "should allow the path to be encoded in base64" do
|
67
67
|
encoded = CGI.escape(Base64.encode64("resize/20x20/source/http%3A%2F%2Fexample.com%2Fdog.jpg"))
|
68
|
-
options = Options.new "/convert/-/#{encoded}", {}
|
68
|
+
options = Imageproxy::Options.new "/convert/-/#{encoded}", {}
|
69
69
|
options.resize.should == "20x20"
|
70
70
|
options.source.should == "http://example.com/dog.jpg"
|
71
71
|
end
|
@@ -73,15 +73,15 @@ describe Options do
|
|
73
73
|
|
74
74
|
describe "quality" do
|
75
75
|
it "should be set to 0 if it's less than 0" do
|
76
|
-
Options.new("/convert", "quality" => "-39").quality.should == "0"
|
76
|
+
Imageproxy::Options.new("/convert", "quality" => "-39").quality.should == "0"
|
77
77
|
end
|
78
78
|
|
79
79
|
it "should be set to 100 if it's > 100" do
|
80
|
-
Options.new("/convert", "quality" => "293").quality.should == "100"
|
80
|
+
Imageproxy::Options.new("/convert", "quality" => "293").quality.should == "100"
|
81
81
|
end
|
82
82
|
|
83
83
|
it "should not change if it's >= 0 <= 100" do
|
84
|
-
Options.new("/convert", "quality" => "59").quality.should == "59"
|
84
|
+
Imageproxy::Options.new("/convert", "quality" => "59").quality.should == "59"
|
85
85
|
end
|
86
86
|
end
|
87
87
|
end
|
data/spec/server_spec.rb
CHANGED
@@ -16,14 +16,14 @@ describe "Server" do
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def app
|
19
|
-
@app ||= Server.new
|
19
|
+
@app ||= Imageproxy::Server.new
|
20
20
|
end
|
21
21
|
|
22
22
|
context "when converting" do
|
23
23
|
it "should send back the right result" do
|
24
24
|
app.stub!(:config) { |sym| nil }
|
25
25
|
get("/convert/resize/10x20/source/#{escaped_test_image_url}").should succeed
|
26
|
-
Compare.new(response_body_as_file, test_image_path("10x20")).execute.should == "0"
|
26
|
+
Imageproxy::Compare.new(response_body_as_file, test_image_path("10x20")).execute.should == "0"
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
@@ -53,13 +53,13 @@ describe "Server" do
|
|
53
53
|
|
54
54
|
it "should work if the signature is correct" do
|
55
55
|
url = "/convert/resize/10x20/source/#{escaped_test_image_url}"
|
56
|
-
signature = Signature.create(url, @secret)
|
56
|
+
signature = Imageproxy::Signature.create(url, @secret)
|
57
57
|
get("#{url}?signature=#{CGI.escape(signature)}").should succeed
|
58
58
|
end
|
59
59
|
|
60
60
|
it "should work if the signature is part of the path" do
|
61
61
|
url = "/convert/resize/10x20/source/#{escaped_test_image_url}"
|
62
|
-
signature = Signature.create(url, @secret)
|
62
|
+
signature = Imageproxy::Signature.create(url, @secret)
|
63
63
|
get("#{url}/signature/#{URI.escape(signature)}").should succeed
|
64
64
|
end
|
65
65
|
end
|
data/spec/signature_spec.rb
CHANGED
@@ -1,72 +1,72 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Signature do
|
3
|
+
describe Imageproxy::Signature do
|
4
4
|
describe "#create" do
|
5
5
|
it "should create a signature from a query string" do
|
6
|
-
Signature.create("/convert?src=http://www.example.com/dog.jpg&resize=400x400&signature=AAA&key=BBB", "SECRET").should_not be_nil
|
6
|
+
Imageproxy::Signature.create("/convert?src=http://www.example.com/dog.jpg&resize=400x400&signature=AAA&key=BBB", "SECRET").should_not be_nil
|
7
7
|
end
|
8
8
|
|
9
9
|
it "should ignore the signature param" do
|
10
|
-
Signature.create("/convert?src=SRC&resize=10x10&signature=SIG&key=KEY", "SECRET").should == Signature.create("/convert?src=SRC&resize=10x10&key=KEY", "SECRET")
|
10
|
+
Imageproxy::Signature.create("/convert?src=SRC&resize=10x10&signature=SIG&key=KEY", "SECRET").should == Imageproxy::Signature.create("/convert?src=SRC&resize=10x10&key=KEY", "SECRET")
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
14
|
describe "#correct?" do
|
15
15
|
it "should validate a signature" do
|
16
|
-
Signature.correct?(Signature.create("/convert?src=SRC&resize=10x10&key=KEY", "SECRET"), "/convert?src=SRC&resize=10x10&key=KEY", "SECRET").should be_true
|
16
|
+
Imageproxy::Signature.correct?(Imageproxy::Signature.create("/convert?src=SRC&resize=10x10&key=KEY", "SECRET"), "/convert?src=SRC&resize=10x10&key=KEY", "SECRET").should be_true
|
17
17
|
end
|
18
18
|
|
19
19
|
it "should return false if signature is nil" do
|
20
|
-
Signature.correct?(nil, "/convert?src=SRC&resize=10x10&key=KEY", "SECRET").should be_false
|
20
|
+
Imageproxy::Signature.correct?(nil, "/convert?src=SRC&resize=10x10&key=KEY", "SECRET").should be_false
|
21
21
|
end
|
22
22
|
|
23
23
|
it "should handle URL-safe signatures" do
|
24
|
-
Signature.correct?("_v70E0zfdcRR4cJehS2mhvqJ-8s=", "YLANEBHFSJGCAWKDNCKWEKJRXKPMYU", "SECRET").should be_true
|
24
|
+
Imageproxy::Signature.correct?("_v70E0zfdcRR4cJehS2mhvqJ-8s=", "YLANEBHFSJGCAWKDNCKWEKJRXKPMYU", "SECRET").should be_true
|
25
25
|
end
|
26
26
|
|
27
27
|
it "should handle non-URL-safe signatures" do
|
28
|
-
Signature.correct?("k7DMQ/G8YAsbSovX+mDFjlHHMjo=", "YPMMYCRRECCCIPSXPDDFIJFSINOIRC", "SECRET").should be_true
|
28
|
+
Imageproxy::Signature.correct?("k7DMQ/G8YAsbSovX+mDFjlHHMjo=", "YPMMYCRRECCCIPSXPDDFIJFSINOIRC", "SECRET").should be_true
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
describe "#remove_signature_from" do
|
33
33
|
it "should remove the signature when it's the only query param" do
|
34
|
-
Signature.remove_signature_from("/convert/a/apple/b/banana?signature=SIG").should ==
|
34
|
+
Imageproxy::Signature.remove_signature_from("/convert/a/apple/b/banana?signature=SIG").should ==
|
35
35
|
"/convert/a/apple/b/banana"
|
36
36
|
end
|
37
37
|
|
38
38
|
it "should remove the signature from the beginning of the query string" do
|
39
|
-
Signature.remove_signature_from("/convert/a/apple/b/banana?signature=SIG&c=cherry&d=donut").should ==
|
39
|
+
Imageproxy::Signature.remove_signature_from("/convert/a/apple/b/banana?signature=SIG&c=cherry&d=donut").should ==
|
40
40
|
"/convert/a/apple/b/banana?c=cherry&d=donut"
|
41
41
|
end
|
42
42
|
|
43
43
|
it "should remove the signature from the middle of the query string" do
|
44
|
-
Signature.remove_signature_from("/convert/a/apple/b/banana?c=cherry&signature=SIG&d=donut").should ==
|
44
|
+
Imageproxy::Signature.remove_signature_from("/convert/a/apple/b/banana?c=cherry&signature=SIG&d=donut").should ==
|
45
45
|
"/convert/a/apple/b/banana?c=cherry&d=donut"
|
46
46
|
end
|
47
47
|
|
48
48
|
it "should remove the signature from the end of the query string" do
|
49
|
-
Signature.remove_signature_from("/convert/a/apple/b/banana?c=cherry&d=donut&signature=SIG").should ==
|
49
|
+
Imageproxy::Signature.remove_signature_from("/convert/a/apple/b/banana?c=cherry&d=donut&signature=SIG").should ==
|
50
50
|
"/convert/a/apple/b/banana?c=cherry&d=donut"
|
51
51
|
end
|
52
52
|
|
53
53
|
it "should remove the signature from the beginning of the path" do
|
54
|
-
Signature.remove_signature_from("/convert/signature/SIG/a/apple/b/banana?c=cherry&d=donut").should ==
|
54
|
+
Imageproxy::Signature.remove_signature_from("/convert/signature/SIG/a/apple/b/banana?c=cherry&d=donut").should ==
|
55
55
|
"/convert/a/apple/b/banana?c=cherry&d=donut"
|
56
56
|
end
|
57
57
|
|
58
58
|
it "should remove the signature from the middle of the path" do
|
59
|
-
Signature.remove_signature_from("/convert/a/apple/signature/SIG/b/banana?c=cherry&d=donut").should ==
|
59
|
+
Imageproxy::Signature.remove_signature_from("/convert/a/apple/signature/SIG/b/banana?c=cherry&d=donut").should ==
|
60
60
|
"/convert/a/apple/b/banana?c=cherry&d=donut"
|
61
61
|
end
|
62
62
|
|
63
63
|
it "should remove the signature from the end of the path" do
|
64
|
-
Signature.remove_signature_from("/convert/a/apple/b/banana/signature/SIG?c=cherry&d=donut").should ==
|
64
|
+
Imageproxy::Signature.remove_signature_from("/convert/a/apple/b/banana/signature/SIG?c=cherry&d=donut").should ==
|
65
65
|
"/convert/a/apple/b/banana?c=cherry&d=donut"
|
66
66
|
end
|
67
67
|
|
68
68
|
it "should remove the signature from the end of the path when there's no query string" do
|
69
|
-
Signature.remove_signature_from("/convert/a/apple/b/banana/signature/SIG").should ==
|
69
|
+
Imageproxy::Signature.remove_signature_from("/convert/a/apple/b/banana/signature/SIG").should ==
|
70
70
|
"/convert/a/apple/b/banana"
|
71
71
|
end
|
72
72
|
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: imageproxy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.1.
|
5
|
+
version: 0.1.1
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Erik Hanson
|
@@ -163,7 +163,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
163
163
|
requirements:
|
164
164
|
- - ">="
|
165
165
|
- !ruby/object:Gem::Version
|
166
|
-
hash: -
|
166
|
+
hash: -2872923147310214835
|
167
167
|
segments:
|
168
168
|
- 0
|
169
169
|
version: "0"
|