cors 1.0.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.
- data/.gitignore +2 -0
- data/.rspec +4 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +24 -0
- data/README.md +67 -0
- data/Rakefile +33 -0
- data/cors.gemspec +35 -0
- data/lib/cors.rb +32 -0
- data/lib/cors/policy.rb +109 -0
- data/lib/cors/policy/s3.rb +47 -0
- data/lib/cors/rules.rb +121 -0
- data/lib/cors/version.rb +4 -0
- data/spec/cors_spec.rb +60 -0
- data/spec/policies/s3_spec.rb +59 -0
- data/spec/rules_spec.rb +89 -0
- data/spec/spec_helper.rb +1 -0
- metadata +105 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.3)
|
5
|
+
rake (0.9.2.2)
|
6
|
+
redcarpet (2.1.1)
|
7
|
+
rspec (2.11.0)
|
8
|
+
rspec-core (~> 2.11.0)
|
9
|
+
rspec-expectations (~> 2.11.0)
|
10
|
+
rspec-mocks (~> 2.11.0)
|
11
|
+
rspec-core (2.11.1)
|
12
|
+
rspec-expectations (2.11.3)
|
13
|
+
diff-lcs (~> 1.1.3)
|
14
|
+
rspec-mocks (2.11.3)
|
15
|
+
yard (0.8.2.1)
|
16
|
+
|
17
|
+
PLATFORMS
|
18
|
+
ruby
|
19
|
+
|
20
|
+
DEPENDENCIES
|
21
|
+
rake
|
22
|
+
redcarpet
|
23
|
+
rspec
|
24
|
+
yard
|
data/README.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# CORS policy validation- and signing library
|
2
|
+
|
3
|
+
[](http://travis-ci.org/elabs/cors)
|
4
|
+
|
5
|
+
Cross-origin resource sharing (CORS) is great; it allows your visitors to asynchronously upload files to
|
6
|
+
e.g. Filepicker or Amazon S3, without the files having to round-trip through your web server. Unfortunately,
|
7
|
+
giving your users complete write access to your online storage also exposes you to malicious intent.
|
8
|
+
|
9
|
+
To combat harmful usage, good upload services that allow client-side upload, support a mechanism that allows
|
10
|
+
you to validate and sign all upload requests to your online storage. By validating every request, you can
|
11
|
+
give your visitors a nice upload experience, while keeping the bad visitors at bay.
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
UploadManifest = CORS::Policy::S3.create do |policy|
|
17
|
+
policy.required "method", "PUT"
|
18
|
+
policy.optional "md5" do |value|
|
19
|
+
Base64.strict_decode64(value)
|
20
|
+
end
|
21
|
+
policy.required "content-type", %r|image/|
|
22
|
+
policy.required "x-amz-date" do |date|
|
23
|
+
"2012-10-22T16:10:47+02:00" == date
|
24
|
+
end
|
25
|
+
policy.required "filename", %r|uploads/|
|
26
|
+
end
|
27
|
+
|
28
|
+
manifest = UploadManifest.new(params)
|
29
|
+
|
30
|
+
response = if manifest.valid?
|
31
|
+
{ success: manifest.sign(access_key, secret_access_key) }
|
32
|
+
else
|
33
|
+
{ error: manifest.errors }
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
## Supported services
|
38
|
+
|
39
|
+
Out-of-the box, the CORS library comes with support for the Amazon S3 REST API. Support
|
40
|
+
for Filepicker is planned.
|
41
|
+
|
42
|
+
- [Amazon S3 REST API](http://docs.amazonwebservices.com/AmazonS4/latest/dev/RESTAuthentication.html)
|
43
|
+
|
44
|
+
## License
|
45
|
+
|
46
|
+
Copyright (c) 2012 Kim Burgestrand
|
47
|
+
|
48
|
+
MIT License
|
49
|
+
|
50
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
51
|
+
a copy of this software and associated documentation files (the
|
52
|
+
"Software"), to deal in the Software without restriction, including
|
53
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
54
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
55
|
+
permit persons to whom the Software is furnished to do so, subject to
|
56
|
+
the following conditions:
|
57
|
+
|
58
|
+
The above copyright notice and this permission notice shall be
|
59
|
+
included in all copies or substantial portions of the Software.
|
60
|
+
|
61
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
62
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
63
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
64
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
65
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
66
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
67
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
begin
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'yard'
|
8
|
+
YARD::Rake::YardocTask.new('yard:doc') do |task|
|
9
|
+
task.options = ['--no-stats']
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Run documentation statistics"
|
13
|
+
task 'yard:stats' do
|
14
|
+
YARD::CLI::Stats.run('--list-undoc')
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Generate documentation and run documentation statistics"
|
18
|
+
task :yard => ['yard:doc', 'yard:stats']
|
19
|
+
rescue LoadError
|
20
|
+
puts "WARN: YARD not available. You may install documentation dependencies via bundler."
|
21
|
+
end
|
22
|
+
|
23
|
+
require "rspec/core/rake_task"
|
24
|
+
RSpec::Core::RakeTask.new do |spec|
|
25
|
+
spec.ruby_opts = "-W"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Launch a console with the library loaded"
|
29
|
+
task :console do
|
30
|
+
exec "irb", "-Ilib", "-rcors"
|
31
|
+
end
|
32
|
+
|
33
|
+
task :default => :spec
|
data/cors.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cors/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "cors"
|
8
|
+
gem.version = CORS::VERSION
|
9
|
+
gem.authors = ["Kim Burgestrand"]
|
10
|
+
gem.email = ["kim@burgestrand.se"]
|
11
|
+
gem.homepage = "http://github.com/elabs/cors"
|
12
|
+
gem.summary = "CORS policy validation- and signing library for Amazon S3 REST API."
|
13
|
+
gem.description = <<-DESCRIPTION.gsub(/ +/, "")
|
14
|
+
Cross-origin resource sharing (CORS) is great; it allows your visitors to
|
15
|
+
asynchronously upload files to e.g. Filepicker or Amazon S3, without the
|
16
|
+
files having to round-trip through your web server. Unfortunately, giving
|
17
|
+
your users complete write access to your online storage also exposes you to
|
18
|
+
malicious intent.
|
19
|
+
|
20
|
+
To combat harmful usage, good upload services that allow client-side
|
21
|
+
upload, support a mechanism that allows you to validate and sign all upload
|
22
|
+
requests to your online storage. By validating every request, you can give
|
23
|
+
your visitors a nice upload experience, while keeping the bad visitors at
|
24
|
+
bay.
|
25
|
+
|
26
|
+
The CORS gem comes with support for the Amazon S3 REST API.
|
27
|
+
DESCRIPTION
|
28
|
+
|
29
|
+
gem.files = `git ls-files`.split($/)
|
30
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
31
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
32
|
+
gem.require_paths = ["lib"]
|
33
|
+
|
34
|
+
gem.add_development_dependency "rspec", "~> 2.0"
|
35
|
+
end
|
data/lib/cors.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "cors/version"
|
3
|
+
require "cors/rules"
|
4
|
+
require "cors/policy"
|
5
|
+
require "cors/policy/s3"
|
6
|
+
|
7
|
+
# CORS policy validation and signature generation.
|
8
|
+
#
|
9
|
+
# @example usage for S3 REST API authorization header
|
10
|
+
# UploadManifest = CORS::Policy::S3.create do |policy|
|
11
|
+
# policy.required "method", "PUT"
|
12
|
+
# policy.optional "md5" do |value|
|
13
|
+
# Base64.strict_decode64(value)
|
14
|
+
# end
|
15
|
+
# policy.required "content-type", %r|image/|
|
16
|
+
# policy.required "x-amz-date" do |date|
|
17
|
+
# "2012-10-22T16:10:47+02:00" == date
|
18
|
+
# end
|
19
|
+
# policy.required "filename", %r|uploads/|
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# manifest = UploadManifest.new(params)
|
23
|
+
#
|
24
|
+
# response = if manifest.valid?
|
25
|
+
# { success: manifest.sign(access_key, secret_access_key) }
|
26
|
+
# else
|
27
|
+
# { error: manifest.errors }
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @see CORS::Policy
|
31
|
+
module CORS
|
32
|
+
end
|
data/lib/cors/policy.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
module CORS
|
2
|
+
# Mixin for declaring CORS Policies.
|
3
|
+
#
|
4
|
+
# Classes who include this mixin should define both #manifest and #sign.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# class S3
|
8
|
+
# include CORS::Policy
|
9
|
+
#
|
10
|
+
# def manifest
|
11
|
+
# # create the manifest
|
12
|
+
# [].tap do |manifest|
|
13
|
+
# manifest << attributes["method"].upcase
|
14
|
+
# end.join("\n")
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# def sign(access_key, secret_access_key)
|
18
|
+
# # sign the manifest
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# policy = S3.create do |rules|
|
23
|
+
# rules.required "method", %w[GET]
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @see CORS::Rules
|
27
|
+
module Policy
|
28
|
+
# Class methods added to includers of {CORS::Policy}.
|
29
|
+
#
|
30
|
+
# @see {CORS::Policy}
|
31
|
+
module ClassMethods
|
32
|
+
# @return [CORS::Rules]
|
33
|
+
attr_reader :rules
|
34
|
+
|
35
|
+
# Create an instance of this policy, declaring rules as well.
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# upload_policy = CORS::Policy::S3.create do |rules|
|
39
|
+
# rules.required "method", %w[GET]
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# @raise [ArgumentError] if no block is supplied
|
43
|
+
# @yield [rules] allows you to declare rules on the newly created policy
|
44
|
+
# @yieldparam [CORS::Rules] rules
|
45
|
+
def create(*, &block)
|
46
|
+
unless block_given?
|
47
|
+
raise ArgumentError, "manifest rules must be specified by a block, no block given"
|
48
|
+
end
|
49
|
+
|
50
|
+
Class.new(self) do
|
51
|
+
@rules = CORS::Rules.new(&block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class << self
|
57
|
+
# Extends the target with {ClassMethods}
|
58
|
+
#
|
59
|
+
# @param [#extend] other
|
60
|
+
def included(other)
|
61
|
+
other.extend(ClassMethods)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Initialize the policy with the given attributes and validate the attributes.
|
66
|
+
#
|
67
|
+
# @note attribute keys are converted to strings and downcased for validation
|
68
|
+
# @note validations are run instantly
|
69
|
+
#
|
70
|
+
# @param [Hash] attributes
|
71
|
+
# @see errors
|
72
|
+
# @see valid?
|
73
|
+
def initialize(attributes)
|
74
|
+
self.attributes = Hash[attributes.map { |k, v| [k.to_s.downcase, v] }]
|
75
|
+
self.errors = rules.validate(self.attributes)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Hash<String, Object>]
|
79
|
+
attr_accessor :attributes
|
80
|
+
protected :attributes=
|
81
|
+
|
82
|
+
# @return [Hash]
|
83
|
+
attr_accessor :errors
|
84
|
+
protected :errors=
|
85
|
+
|
86
|
+
# @raise [RuntimeError] raises if no rules have been defined
|
87
|
+
# @return [CORS::Rules] rules assigned to this policy
|
88
|
+
def rules
|
89
|
+
self.class.rules or raise "no rules defined for policy #{inspect}"
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Boolean] true if no errors was encountered during validation in {#initialize}
|
93
|
+
def valid?
|
94
|
+
errors.empty?
|
95
|
+
end
|
96
|
+
|
97
|
+
# @note must be overridden by includers!
|
98
|
+
# @return [String] the compiled manifest
|
99
|
+
def manifest
|
100
|
+
raise NotImplementedError, "#manifest has not been defined on #{inspect}"
|
101
|
+
end
|
102
|
+
|
103
|
+
# @note must be overridden by includers!
|
104
|
+
# @return [String] signature derived from the manifest
|
105
|
+
def sign(*)
|
106
|
+
raise NotImplementedError, "#sign has not been defined on #{inspect}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "base64"
|
3
|
+
|
4
|
+
module CORS::Policy
|
5
|
+
# CORS policy for Amazon S3. See {CORS} module documenation for an example.
|
6
|
+
#
|
7
|
+
# @see CORS
|
8
|
+
class S3
|
9
|
+
include CORS::Policy
|
10
|
+
|
11
|
+
# Compile the S3 authorization manifest from the parameters.
|
12
|
+
#
|
13
|
+
# @see http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html#ConstructingTheAuthenticationHeader
|
14
|
+
def manifest
|
15
|
+
[].tap do |manifest|
|
16
|
+
manifest << attributes["method"].upcase
|
17
|
+
manifest << attributes["md5"]
|
18
|
+
manifest << attributes["content-type"]
|
19
|
+
manifest << attributes["date"]
|
20
|
+
normalized_headers.each do |(header, *values)|
|
21
|
+
manifest << "#{header}:#{values.join(",")}"
|
22
|
+
end
|
23
|
+
manifest << attributes["filename"]
|
24
|
+
end.join("\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sign the {#manifest} with the AWS credentials.
|
28
|
+
#
|
29
|
+
# @param [String] access_key
|
30
|
+
# @param [String] secret_access_key
|
31
|
+
def sign(access_key, secret_access_key)
|
32
|
+
return if not valid?
|
33
|
+
digest = OpenSSL::HMAC.digest("sha1", secret_access_key, manifest)
|
34
|
+
signature = Base64.strict_encode64(digest)
|
35
|
+
"AWS #{access_key}:#{signature}"
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
# @return [Array] list of aws-specific headers properly sorted
|
41
|
+
def normalized_headers
|
42
|
+
attributes.select { |property, _| property =~ /x-amz-/ }
|
43
|
+
.map { |(header, values)| [header.downcase, values] }
|
44
|
+
.sort_by { |(header, _)| header }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/cors/rules.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
module CORS
|
2
|
+
# Internal class for handling rule definitions and validation.
|
3
|
+
#
|
4
|
+
# @private
|
5
|
+
class Rules
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# @example
|
9
|
+
# Rules.new do |rules|
|
10
|
+
# rules.required …
|
11
|
+
# rules.optional …
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @yield [self]
|
15
|
+
# @yieldparam [Rules] self
|
16
|
+
def initialize
|
17
|
+
@rules = []
|
18
|
+
yield self if block_given?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Yields each rule in order, or returns an Enumerator
|
22
|
+
# if no block was given.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# rules.each do |rule|
|
26
|
+
# …
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @example without block
|
30
|
+
# rules.each.with_index do |rule, index|
|
31
|
+
# …
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# @return [Hash<:name, :matcher, :required>, Enumerator]
|
35
|
+
def each
|
36
|
+
if block_given?
|
37
|
+
@rules.each { |rule| yield rule }
|
38
|
+
else
|
39
|
+
@rules.enum_for(__method__)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Declare a required rule; the value must be present, and it must
|
44
|
+
# match the given constraints or block matcher.
|
45
|
+
#
|
46
|
+
# @example with a regexp
|
47
|
+
# @required "content-type", %r|image/jpe?g|
|
48
|
+
#
|
49
|
+
# @example with a string
|
50
|
+
# required "content-type", "image/jpeg"
|
51
|
+
#
|
52
|
+
# @example with an array
|
53
|
+
# required "content-type", ["image/jpeg", "image/jpg"]
|
54
|
+
#
|
55
|
+
# @example with a block
|
56
|
+
# required "content-type" do |value|
|
57
|
+
# value =~ %r|image/jpe?g|
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# @param name can be any valid hash key of the parameters to be validated
|
61
|
+
# @param [Regexp, String, Array] constraints
|
62
|
+
# @yield [value]
|
63
|
+
# @yieldparam value of the key `name` in the parameters to be validated
|
64
|
+
# @return [Hash] the newly created rule
|
65
|
+
def required(name, constraints = nil, &block)
|
66
|
+
matcher = if block_given? then block
|
67
|
+
elsif constraints.is_a?(Regexp)
|
68
|
+
constraints.method(:===)
|
69
|
+
elsif constraints.is_a?(String)
|
70
|
+
constraints.method(:===)
|
71
|
+
elsif constraints.is_a?(Array)
|
72
|
+
constraints.method(:include?)
|
73
|
+
else
|
74
|
+
raise ArgumentError, "unknown matcher #{(constraints || block).inspect}"
|
75
|
+
end
|
76
|
+
|
77
|
+
{ name: name, matcher: matcher, required: true }.tap do |rule|
|
78
|
+
@rules << rule
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Same as {#required}, but the rule won’t run if the key is not present.
|
83
|
+
#
|
84
|
+
# @param (see required)
|
85
|
+
# @return (see required)
|
86
|
+
# @see required
|
87
|
+
def optional(*args, &block)
|
88
|
+
required(*args, &block).tap { |rule| rule[:required] = false }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Validate a set of attributes against the defined rules.
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# errors = rules.validate(params)
|
95
|
+
# if errors.empty?
|
96
|
+
# # valid
|
97
|
+
# else
|
98
|
+
# # not valid, errors is a hash of { name => [ reason, rule ] }
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# @see required
|
102
|
+
# @param [#has_key?, #[]] attributes
|
103
|
+
# @return [Hash<name: [reason, rule]>] list of errors, empty if attributes are valid
|
104
|
+
def validate(attributes)
|
105
|
+
each_with_object({}) do |rule, failures|
|
106
|
+
fail = lambda do |reason|
|
107
|
+
failures[rule[:name]] = [reason, rule]
|
108
|
+
end
|
109
|
+
|
110
|
+
unless attributes.has_key?(rule[:name])
|
111
|
+
fail[:required] if rule[:required]
|
112
|
+
next
|
113
|
+
end
|
114
|
+
|
115
|
+
unless rule[:matcher].call(attributes[rule[:name]])
|
116
|
+
fail[:match]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/cors/version.rb
ADDED
data/spec/cors_spec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
describe CORS::Policy do
|
2
|
+
let(:policy) do
|
3
|
+
Class.new { include CORS::Policy }
|
4
|
+
end
|
5
|
+
|
6
|
+
let(:valid_attributes) do
|
7
|
+
{
|
8
|
+
"anything" => "Yay!"
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:manifest) do
|
13
|
+
policy.create do |rules|
|
14
|
+
rules.required "anything", //
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe ".initialize" do
|
19
|
+
it "requires a block" do
|
20
|
+
expect { CORS::Policy::S3.create }.to raise_error(ArgumentError, /no block given/)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#initialize" do
|
25
|
+
it "requires attributes" do
|
26
|
+
expect { manifest.new }.to raise_error(ArgumentError, /wrong number of arguments/)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "normalizes the attribute keys" do
|
30
|
+
manifest.new(cOOl: :Yo).attributes.should eq({ "cool" => :Yo })
|
31
|
+
end
|
32
|
+
|
33
|
+
it "populates the hash of errors" do
|
34
|
+
manifest.new({}).errors.should_not be_empty
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#rules" do
|
39
|
+
it "raises an error if no rules have been defined" do
|
40
|
+
expect { policy.new({}) }.to raise_error(/no rules defined/)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "returns the raw rules" do
|
44
|
+
manifest.rules.should be_a CORS::Rules
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#valid?" do
|
49
|
+
it "returns true if validation succeeds" do
|
50
|
+
manifest.new(valid_attributes).tap do |manifest|
|
51
|
+
manifest.should be_valid
|
52
|
+
manifest.errors.should eq({})
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it "returns false if validation fails" do
|
57
|
+
manifest.new({}).should_not be_valid
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
describe CORS::Policy::S3 do
|
2
|
+
let(:valid_attributes) do
|
3
|
+
{
|
4
|
+
"method" => "PUT",
|
5
|
+
"md5" => "CCummMp6o4ZgypU7ePh7QA==",
|
6
|
+
"content-type" => "image/jpeg",
|
7
|
+
"x-amz-meta-filename" => "roflcopter.gif",
|
8
|
+
"x-amz-date" => "2012-10-22T16:10:47+02:00",
|
9
|
+
"x-amz-meta-ROFLCOPTER" => ["yes", "no", "maybe"],
|
10
|
+
"x-not-amz-header" => "I am ignored",
|
11
|
+
"filename" => "uploads/roflcopter.gif"
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:rules) do
|
16
|
+
lambda do |manifest|
|
17
|
+
manifest.required "method", "PUT"
|
18
|
+
manifest.optional "md5" do |value|
|
19
|
+
Base64.strict_decode64(value)
|
20
|
+
end
|
21
|
+
manifest.required "content-type", %r|image/|
|
22
|
+
manifest.required "x-amz-date" do |date|
|
23
|
+
"2012-10-22T16:10:47+02:00" == date
|
24
|
+
end
|
25
|
+
manifest.required "filename", %r|uploads/|
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
let(:manifest) { CORS::Policy::S3.create(&rules) }
|
30
|
+
|
31
|
+
describe "#manifest" do
|
32
|
+
it "is built according to specifications" do
|
33
|
+
manifest = CORS::Policy::S3.create(&rules).new(valid_attributes)
|
34
|
+
manifest.manifest.should eq <<-MANIFEST.gsub(/^ +/, "").rstrip
|
35
|
+
PUT
|
36
|
+
CCummMp6o4ZgypU7ePh7QA==
|
37
|
+
image/jpeg
|
38
|
+
|
39
|
+
x-amz-date:2012-10-22T16:10:47+02:00
|
40
|
+
x-amz-meta-filename:roflcopter.gif
|
41
|
+
x-amz-meta-roflcopter:yes,no,maybe
|
42
|
+
uploads/roflcopter.gif
|
43
|
+
MANIFEST
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#sign" do
|
48
|
+
it "signs the manifest if it is valid" do
|
49
|
+
manifest = CORS::Policy::S3.create(&rules).new(valid_attributes)
|
50
|
+
manifest.sign("LAWL", "HELLO").should eq "AWS LAWL:WZGsk2VzLz85B6oU19a5+fvzxXM="
|
51
|
+
end
|
52
|
+
|
53
|
+
it "does not sign if the manifest is invalid" do
|
54
|
+
manifest = CORS::Policy::S3.create(&rules).new(valid_attributes)
|
55
|
+
manifest.should_receive(:valid?).and_return(false)
|
56
|
+
manifest.sign("LAWL", "HELLO").should be_nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/spec/rules_spec.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
describe CORS::Rules do
|
2
|
+
describe "#each" do
|
3
|
+
let(:list) { [] }
|
4
|
+
let(:rules) do
|
5
|
+
CORS::Rules.new do |r|
|
6
|
+
list << r.required("yay", //)
|
7
|
+
list << r.optional("boo", //)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
it "is enumerable" do
|
12
|
+
rules.each_with_object([]) { |rule, result| result << rule }.should eq(list)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "returns an enumerator without a block" do
|
16
|
+
rules.each.with_object([]) { |rule, result| result << rule }.should eq(list)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#required" do
|
21
|
+
it "does not accept arbitrary constraints" do
|
22
|
+
expect { CORS::Rules.new { |r| r.required "method", false } }.to raise_error(ArgumentError, /unknown matcher/)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "results in an error when the value is missing" do
|
26
|
+
rules = CORS::Rules.new { |r| r.required "method", // }
|
27
|
+
errors = rules.validate({})
|
28
|
+
|
29
|
+
errors.should eq({ "method" => [:required, rules.first] })
|
30
|
+
end
|
31
|
+
|
32
|
+
it "results in an error when the value does not match" do
|
33
|
+
rules = CORS::Rules.new { |r| r.required "content-type", %r|image/jpe?g| }
|
34
|
+
errors = rules.validate({ "content-type" => "image/png" })
|
35
|
+
|
36
|
+
errors.should eq({ "content-type" => [:match, rules.first] })
|
37
|
+
end
|
38
|
+
|
39
|
+
it "can match a regexp" do
|
40
|
+
rules = CORS::Rules.new { |r| r.required "content-type", %r|image/jpe?g| }
|
41
|
+
|
42
|
+
rules.validate({ "content-type" => "image/jpeg" }).should be_empty
|
43
|
+
rules.validate({ "content-type" => "image/jpg" }).should be_empty
|
44
|
+
rules.validate({ "content-type" => "image/png" }).should_not be_empty
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can match a literal string" do
|
48
|
+
rules = CORS::Rules.new { |r| r.required "content-type", "image/jpeg" }
|
49
|
+
|
50
|
+
rules.validate({ "content-type" => "image/jpeg" }).should be_empty
|
51
|
+
rules.validate({ "content-type" => "image/jpg" }).should_not be_empty
|
52
|
+
end
|
53
|
+
|
54
|
+
it "can match an array" do
|
55
|
+
rules = CORS::Rules.new { |r| r.required "content-type", ["image/jpeg", "image/png"] }
|
56
|
+
|
57
|
+
rules.validate({ "content-type" => "image/jpeg" }).should be_empty
|
58
|
+
rules.validate({ "content-type" => "image/png" }).should be_empty
|
59
|
+
rules.validate({ "content-type" => "image/jpg" }).should_not be_empty
|
60
|
+
end
|
61
|
+
|
62
|
+
it "can match a block" do
|
63
|
+
rules = CORS::Rules.new do |r|
|
64
|
+
r.required "content-type" do |type|
|
65
|
+
"image/jpeg" == type
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
rules.validate({ "content-type" => "image/jpeg" }).should be_empty
|
70
|
+
rules.validate({ "content-type" => "image/png" }).should_not be_empty
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#optional" do
|
75
|
+
it "results in no error when the value is missing" do
|
76
|
+
rules = CORS::Rules.new { |r| r.optional "method", // }
|
77
|
+
errors = rules.validate({})
|
78
|
+
|
79
|
+
errors.should be_empty
|
80
|
+
end
|
81
|
+
|
82
|
+
it "results in an error when the value is present but does not match" do
|
83
|
+
rules = CORS::Rules.new { |r| r.optional "content-type", %r|image/jpe?g| }
|
84
|
+
errors = rules.validate({ "content-type" => "image/png" })
|
85
|
+
|
86
|
+
errors.should eq({ "content-type" => [:match, rules.first] })
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "cors"
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cors
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kim Burgestrand
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.0'
|
30
|
+
description: ! 'Cross-originresourcesharing(CORS)isgreat;itallowsyourvisitorsto
|
31
|
+
|
32
|
+
asynchronouslyuploadfilestoe.g.FilepickerorAmazonS3,withoutthe
|
33
|
+
|
34
|
+
fileshavingtoround-tripthroughyourwebserver.Unfortunately,giving
|
35
|
+
|
36
|
+
youruserscompletewriteaccesstoyouronlinestoragealsoexposesyouto
|
37
|
+
|
38
|
+
maliciousintent.
|
39
|
+
|
40
|
+
|
41
|
+
Tocombatharmfulusage,gooduploadservicesthatallowclient-side
|
42
|
+
|
43
|
+
upload,supportamechanismthatallowsyoutovalidateandsignallupload
|
44
|
+
|
45
|
+
requeststoyouronlinestorage.Byvalidatingeveryrequest,youcangive
|
46
|
+
|
47
|
+
yourvisitorsaniceuploadexperience,whilekeepingthebadvisitorsat
|
48
|
+
|
49
|
+
bay.
|
50
|
+
|
51
|
+
|
52
|
+
TheCORSgemcomeswithsupportfortheAmazonS3RESTAPI.
|
53
|
+
|
54
|
+
'
|
55
|
+
email:
|
56
|
+
- kim@burgestrand.se
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- .gitignore
|
62
|
+
- .rspec
|
63
|
+
- Gemfile
|
64
|
+
- Gemfile.lock
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- cors.gemspec
|
68
|
+
- lib/cors.rb
|
69
|
+
- lib/cors/policy.rb
|
70
|
+
- lib/cors/policy/s3.rb
|
71
|
+
- lib/cors/rules.rb
|
72
|
+
- lib/cors/version.rb
|
73
|
+
- spec/cors_spec.rb
|
74
|
+
- spec/policies/s3_spec.rb
|
75
|
+
- spec/rules_spec.rb
|
76
|
+
- spec/spec_helper.rb
|
77
|
+
homepage: http://github.com/elabs/cors
|
78
|
+
licenses: []
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 1.8.24
|
98
|
+
signing_key:
|
99
|
+
specification_version: 3
|
100
|
+
summary: CORS policy validation- and signing library for Amazon S3 REST API.
|
101
|
+
test_files:
|
102
|
+
- spec/cors_spec.rb
|
103
|
+
- spec/policies/s3_spec.rb
|
104
|
+
- spec/rules_spec.rb
|
105
|
+
- spec/spec_helper.rb
|