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