require 'uri' require 'openssl' require 'net/http' require 'net/https' require 'stringio' base = File.dirname(__FILE__) require File.join(base, 's33r_exception') require File.join(base, 's33r_http') require File.join(base, 'utility') # Core functionality for managing HTTP requests to S3. module S33r module Networking include S3Exception include Net #-- These are specific to the mechanics of the HTTP request. # Net::HTTP instance. attr_accessor :client # Are HTTP connections persistent? attr_accessor :persistent # Default chunk size for connections. attr_accessor :chunk_size # Should requests be dumped? attr_accessor :dump_requests # The last response received by the client. attr_reader :last_response # Get default options to use on every response. def request_defaults @request_defaults || {} end # Set the defaults. def request_defaults=(options={}) @request_defaults = options end # Send a request over the wire. # # This method streams +data+ if it responds to the +stat+ method # (as file handles do). # # Keys for +headers+ should be strings (e.g. 'Content-Type'). # # +url_options+ is a standard set of options acceptable to s3_url; # if any options aren't set, they are set using the +request_defaults+ method # (options passed in here override defaults). # # You can also pass :authenticated => false if you want to visit a # public URL here; otherwise, an Authorization header is generated and sent. If the client # doesn't have an access key or secret access key specified, however, trying to create # an access key will result in an error being raised. # # Returns a Net::HTTPResponse instance. def do_request(method, url_options={}, data=nil, headers={}) # Use the default settings only if not specified in url_options. url_options = request_defaults.merge(url_options) # Get the URL. url = s3_url(url_options) uri = URI(url) # Bar any except the allowed methods. raise MethodNotAllowed, "The #{method} HTTP method is not supported" unless METHOD_VERBS.include?(method) # Get a requester. path = uri.path path += "?" + uri.query if uri.query req = eval("HTTP::" + method[0,1].upcase + method[1..-1].downcase + ".new('#{path}')") req.chunk_size = chunk_size || DEFAULT_CHUNK_SIZE # Add the S3 headers which are always required. headers.merge!(default_headers(headers)) # Headers for canned ACL headers.merge! canned_acl_header(url_options[:canned_acl]) if 'PUT' == method # Generate the S3 authorization header if the client has # the appropriate instance variable getters. unless (false == url_options[:authenticated]) headers['Authorization'] = generate_auth_header_value(method, path, headers, url_options[:access], url_options[:secret]) end # Insert the headers into the request object. headers.each do |key, value| req[key] = value end # Add data to the request as a stream. if req.request_body_permitted? # For streaming files; NB Content-Length will be set by Net::HTTP # for character-based data: this section of is only used # when reading directly from a file. if data.respond_to?(:stat) length = data.stat.size # Strings can be streamed too. elsif data.is_a?(String) length = data.length data = StringIO.new(data) else length = 0 end # Data can be streamed if it responds to the read method. if data.respond_to?(:read) req.body_stream = data req['Content-Length'] = length.to_s data = nil end else data = nil end puts req.to_s if @dump_requests # Set up the request. ### start shameless stealing from Marcel Molina request_runner = Proc.new do response = @client.request(req, data) # Add some nice messages to the response (like the S3 error # message if it occurred). response.conveniencify(method) @last_response = response return response end # Get a client instance. init_client(uri) # Run the request. if persistent @client.start unless @client.started? response = request_runner.call else response = @client.start(&request_runner) end response ### end shameless stealing from Marcel Molina end # Setup an HTTP client instance. # # Note that when you send your first request, the client is set # up using whichever parameters for host and port you passed the first # time. If you change the host or port, the client will be regenerated. # # +url+ is a URI instance generated from a full URL, including a host # name and scheme. def init_client(url) host = url.host || HOST port = url.port if @client.nil? or @client.port != port or @client.address != host @client = HTTP.new(host, port) @client.use_ssl = false # Check whether client needs to use SSL. if port == PORT # turn off SSL certificate verification @client.verify_mode = OpenSSL::SSL::VERIFY_NONE @client.use_ssl = true end end end # Perform a get request. def do_get(options={}, headers={}) do_request('GET', options, nil, headers) end # Perform a put request. def do_put(data, options={}, headers={}) do_request('PUT', options, data, headers) end # Perform a delete request. def do_delete(options={}, headers={}) do_request('DELETE', options, headers) end # Perform a head request. def do_head(options={}, headers={}) do_request('HEAD', options, headers) end end end