diff --git a/lib/fog/aws/parsers/storage/cors_configuration.rb b/lib/fog/aws/parsers/storage/cors_configuration.rb new file mode 100644 index 000000000..654f34f91 --- /dev/null +++ b/lib/fog/aws/parsers/storage/cors_configuration.rb @@ -0,0 +1,41 @@ +module Fog + module Parsers + module Storage + module AWS + + class CorsConfiguration < Fog::Parsers::Base + def reset + @in_cors_configuration_list = false + @cors_rule = {} + @response = { 'CORSConfiguration' => [] } + end + + def start_element(name, attrs = []) + super + if name == 'CORSConfiguration' + @in_cors_configuration_list = true + end + end + + def end_element(name) + case name + when 'CORSConfiguration' + @in_cors_configuration_list = false + when 'CORSRule' + @response['CORSConfiguration'] << @cors_rule + @cors_rule = {} + when 'MaxAgeSeconds' + @cors_rule[name] = value.to_i + when 'ID' + @cors_rule[name] = value + when 'AllowedOrigin', 'AllowedMethod', 'AllowedHeader', 'ExposeHeader' + (@cors_rule[name] ||= []) << value + end + end + + end + + end + end + end +end diff --git a/lib/fog/aws/requests/storage/cors_utils.rb b/lib/fog/aws/requests/storage/cors_utils.rb new file mode 100644 index 000000000..44ecc243a --- /dev/null +++ b/lib/fog/aws/requests/storage/cors_utils.rb @@ -0,0 +1,41 @@ +module Fog + module Storage + class AWS + + require 'fog/aws/parsers/storage/cors_configuration' + + private + + def self.hash_to_cors(cors) + data = "\n" + + [cors['CORSConfiguration']].flatten.compact.each do |rule| + data << " \n" + + ['ID', 'MaxAgeSeconds'].each do |key| + data << " <#{key}>#{rule[key]}\n" if rule[key] + end + + ['AllowedOrigin', 'AllowedMethod', 'AllowedHeader', 'ExposeHeader'].each do |key| + [rule[key]].flatten.compact.each do |value| + data << " <#{key}>#{value}\n" + end + end + + data << " \n" + end + + data << "" + + data + end + + def self.cors_to_hash(cors_xml) + parser = Fog::Parsers::Storage::AWS::CorsConfiguration.new + Nokogiri::XML::SAX::Parser.new(parser).parse(cors_xml) + parser.response + end + + end + end +end diff --git a/lib/fog/aws/requests/storage/delete_bucket_cors.rb b/lib/fog/aws/requests/storage/delete_bucket_cors.rb new file mode 100644 index 000000000..e26c5129f --- /dev/null +++ b/lib/fog/aws/requests/storage/delete_bucket_cors.rb @@ -0,0 +1,32 @@ +module Fog + module Storage + class AWS + class Real + + # Deletes the cors configuration information set for the bucket. + # + # ==== Parameters + # * bucket_name<~String> - name of bucket to delete cors rules from + # + # ==== Returns + # * response<~Excon::Response>: + # * status<~Integer> - 204 + # + # ==== See Also + # http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketDELETEcors.html + + def delete_bucket_cors(bucket_name) + request({ + :expects => 204, + :headers => {}, + :host => "#{bucket_name}.#{@host}", + :method => 'DELETE', + :query => {'cors' => nil} + }) + end + + end + + end + end +end diff --git a/lib/fog/aws/requests/storage/get_bucket_cors.rb b/lib/fog/aws/requests/storage/get_bucket_cors.rb new file mode 100644 index 000000000..e4dd0517e --- /dev/null +++ b/lib/fog/aws/requests/storage/get_bucket_cors.rb @@ -0,0 +1,68 @@ +module Fog + module Storage + class AWS + class Real + + require 'fog/aws/parsers/storage/cors_configuration' + + # Gets the CORS configuration for an S3 bucket + # + # ==== Parameters + # * bucket_name<~String> - name of bucket to get access control list for + # + # ==== Returns + # * response<~Excon::Response>: + # * body<~Hash>: + # * 'CORSConfiguration'<~Array>: + # * 'CORSRule'<~Hash>: + # * 'AllowedHeader'<~String> - Which headers are allowed in a pre-flight OPTIONS request through the Access-Control-Request-Headers header. + # * 'AllowedMethod'<~String> - Identifies an HTTP method that the domain/origin specified in the rule is allowed to execute. + # * 'AllowedOrigin'<~String> - One or more response headers that you want customers to be able to access from their applications (for example, from a JavaScript XMLHttpRequest object). + # * 'ExposeHeader'<~String> - One or more headers in the response that you want customers to be able to access from their applications (for example, from a JavaScript XMLHttpRequest object). + # * 'ID'<~String> - An optional unique identifier for the rule. The ID value can be up to 255 characters long. The IDs help you find a rule in the configuration. + # * 'MaxAgeSeconds'<~Integer> - The time in seconds that your browser is to cache the preflight response for the specified resource. + # + # ==== See Also + # http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketGETcors.html + + def get_bucket_cors(bucket_name) + unless bucket_name + raise ArgumentError.new('bucket_name is required') + end + request({ + :expects => 200, + :headers => {}, + :host => "#{bucket_name}.#{@host}", + :idempotent => true, + :method => 'GET', + :parser => Fog::Parsers::Storage::AWS::CorsConfiguration.new, + :query => {'cors' => nil} + }) + end + + end + + class Mock # :nodoc:all + + require 'fog/aws/requests/storage/cors_utils' + + def get_bucket_cors(bucket_name) + response = Excon::Response.new + if cors = self.data[:cors][:bucket][bucket_name] + response.status = 200 + if cors.is_a?(String) + response.body = Fog::Storage::AWS.cors_to_hash(cors) + else + response.body = cors + end + else + response.status = 404 + raise(Excon::Errors.status_error({:expects => 200}, response)) + end + response + end + + end + end + end +end diff --git a/lib/fog/aws/requests/storage/put_bucket_cors.rb b/lib/fog/aws/requests/storage/put_bucket_cors.rb new file mode 100644 index 000000000..bbad7ac1e --- /dev/null +++ b/lib/fog/aws/requests/storage/put_bucket_cors.rb @@ -0,0 +1,51 @@ +module Fog + module Storage + class AWS + class Real + + require 'fog/aws/requests/storage/cors_utils' + + # Sets the cors configuration for your bucket. If the configuration exists, Amazon S3 replaces it. + # + # ==== Parameters + # * bucket_name<~String> - name of bucket to modify + # * cors<~Hash>: + # * CORSConfiguration<~Array>: + # * ID<~String>: A unique identifier for the rule. + # * AllowedMethod<~String>: An HTTP method that you want to allow the origin to execute. + # * AllowedOrigin<~String>: An origin that you want to allow cross-domain requests from. + # * AllowedHeader<~String>: Specifies which headers are allowed in a pre-flight OPTIONS request via the Access-Control-Request-Headers header. + # * MaxAgeSeconds<~String>: The time in seconds that your browser is to cache the preflight response for the specified resource. + # * ExposeHeader<~String>: One or more headers in the response that you want customers to be able to access from their applications. + # + # ==== See Also + # http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketPUTcors.html + + def put_bucket_cors(bucket_name, cors) + data = Fog::Storage::AWS.hash_to_cors(cors) + + headers = {} + headers['Content-MD5'] = Base64.encode64(Digest::MD5.digest(data)).strip + headers['Content-Type'] = 'application/json' + headers['Date'] = Fog::Time.now.to_date_header + + request({ + :body => data, + :expects => 200, + :headers => headers, + :host => "#{bucket_name}.#{@host}", + :method => 'PUT', + :query => {'cors' => nil} + }) + end + end + + class Mock + def put_bucket_cors(bucket_name, cors) + self.data[:cors][:bucket][bucket_name] = Fog::Storage::AWS.hash_to_cors(cors) + end + end + + end + end +end diff --git a/lib/fog/aws/storage.rb b/lib/fog/aws/storage.rb index 42e3d3583..4a5a2d70f 100644 --- a/lib/fog/aws/storage.rb +++ b/lib/fog/aws/storage.rb @@ -22,6 +22,7 @@ module Fog request :complete_multipart_upload request :copy_object request :delete_bucket + request :delete_bucket_cors request :delete_bucket_lifecycle request :delete_bucket_policy request :delete_bucket_website @@ -29,6 +30,7 @@ module Fog request :delete_multiple_objects request :get_bucket request :get_bucket_acl + request :get_bucket_cors request :get_bucket_lifecycle request :get_bucket_location request :get_bucket_logging @@ -51,6 +53,7 @@ module Fog request :post_object_hidden_fields request :put_bucket request :put_bucket_acl + request :put_bucket_cors request :put_bucket_lifecycle request :put_bucket_logging request :put_bucket_policy @@ -338,6 +341,7 @@ DATA for key in (params[:query] || {}).keys.sort if %w{ acl + cors delete lifecycle location diff --git a/tests/aws/requests/storage/cors_utils_tests.rb b/tests/aws/requests/storage/cors_utils_tests.rb new file mode 100644 index 000000000..c8c8847c2 --- /dev/null +++ b/tests/aws/requests/storage/cors_utils_tests.rb @@ -0,0 +1,108 @@ +require 'fog/aws/requests/storage/cors_utils' + +Shindo.tests('Fog::Storage::AWS | CORS utils', ["aws"]) do + tests(".hash_to_cors") do + tests(".hash_to_cors({}) at xpath //CORSConfiguration").returns("", "has an empty CORSConfiguration") do + xml = Fog::Storage::AWS.hash_to_cors({}) + Nokogiri::XML(xml).xpath("//CORSConfiguration").first.content.chomp + end + + tests(".hash_to_cors({}) at xpath //CORSConfiguration/CORSRule").returns(nil, "has no CORSRules") do + xml = Fog::Storage::AWS.hash_to_cors({}) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule").first + end + + cors = { + 'CORSConfiguration' => [ + { + 'AllowedOrigin' => ['origin_123', 'origin_456'], + 'AllowedMethod' => ['GET', 'POST'], + 'AllowedHeader' => ['Accept', 'Content-Type'], + 'ID' => 'blah-888', + 'MaxAgeSeconds' => 2500, + 'ExposeHeader' => ['x-some-header', 'x-other-header'] + } + ] + } + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/AllowedOrigin").returns("origin_123", "returns the CORSRule AllowedOrigin") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/AllowedOrigin")[0].content + end + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/AllowedOrigin").returns("origin_456", "returns the CORSRule AllowedOrigin") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/AllowedOrigin")[1].content + end + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/AllowedMethod").returns("GET", "returns the CORSRule AllowedMethod") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/AllowedMethod")[0].content + end + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/AllowedMethod").returns("POST", "returns the CORSRule AllowedMethod") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/AllowedMethod")[1].content + end + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/AllowedHeader").returns("Accept", "returns the CORSRule AllowedHeader") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/AllowedHeader")[0].content + end + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/AllowedHeader").returns("Content-Type", "returns the CORSRule AllowedHeader") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/AllowedHeader")[1].content + end + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/ID").returns("blah-888", "returns the CORSRule ID") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/ID")[0].content + end + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/MaxAgeSeconds").returns("2500", "returns the CORSRule MaxAgeSeconds") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/MaxAgeSeconds")[0].content + end + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/ExposeHeader").returns("x-some-header", "returns the CORSRule ExposeHeader") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/ExposeHeader")[0].content + end + + tests(".hash_to_cors(#{cors.inspect}) at xpath //CORSConfiguration/CORSRule/ExposeHeader").returns("x-other-header", "returns the CORSRule ExposeHeader") do + xml = Fog::Storage::AWS.hash_to_cors(cors) + Nokogiri::XML(xml).xpath("//CORSConfiguration/CORSRule/ExposeHeader")[1].content + end + end + + tests(".cors_to_hash") do + cors_xml = <<-XML + + + http://www.example.com + http://www.example2.com + Content-Length + X-Foobar + PUT + GET + 3000 + x-amz-server-side-encryption + x-amz-balls + + +XML + + tests(".cors_to_hash(#{cors_xml.inspect})").returns({ + "CORSConfiguration" => [{ + "AllowedOrigin" => ["http://www.example.com", "http://www.example2.com"], + "AllowedHeader" => ["Content-Length", "X-Foobar"], + "AllowedMethod" => ["PUT", "GET"], + "MaxAgeSeconds" => 3000, + "ExposeHeader" => ["x-amz-server-side-encryption", "x-amz-balls"] + }] + }, 'returns hash of CORS XML') do + Fog::Storage::AWS.cors_to_hash(cors_xml) + end + end +end