Initial implementation of HTTP.

git-svn-id: https://google-api-ruby-client.googlecode.com/svn/trunk@22 c1d61fac-ed7f-fcc1-18f7-ff78120a04ef
This commit is contained in:
bobaman@google.com committed 2010-08-19 23:21:45 +00:00
1 parent a9523bca32
commit bc844db311
6 files changed
+395 -81

No files matched your search

+151 -38
View File
@@ -12,60 +12,173 @@
# See the License for the specific language governing permissions and
# limitations under the License.
require 'google/api_client/parser/json_parser'
require 'net/http'
require 'net/https'
require 'addressable/uri'
module Google #:nodoc:
class APIClient #:nodoc:
##
# Factory for HTTP backed client requests.
##
# Provides a consistent interface by which to make HTTP requests using the
# Net::HTTP class.
class HTTPTransport
##
# The default transport configuration values. These may be overridden
# simply by passing in the same key to the constructor.
DEFAULTS = {
:parser => :json_parser
ALLOWED_SCHEMES = ["http", "https"]
METHOD_MAPPING = {
# RFC 2616
:options => Net::HTTP::Options,
:get => Net::HTTP::Get,
:head => Net::HTTP::Head,
:post => Net::HTTP::Post,
:put => Net::HTTP::Put,
:delete => Net::HTTP::Delete,
:trace => Net::HTTP::Trace,
# Other standards supported by Net::HTTP
:copy => Net::HTTP::Copy,
:lock => Net::HTTP::Lock,
:mkcol => Net::HTTP::Mkcol,
:move => Net::HTTP::Move,
:propfind => Net::HTTP::Propfind,
:proppatch => Net::HTTP::Proppatch,
:unlock => Net::HTTP::Unlock
}
##
# The default implementations of various parsers. These may be overriden
# simply by passing the same key to the constructor.
PARSERS = {
:json_parser => JSONParser.new
}
##
# Creates a new HTTP request factory.
#
# @param [Hash] options
# @return [Google::APIClient::Discovery] The HTTP request factory.
def initialize(options={})
@options = DEFAULTS.clone
@options.merge!(options)
# A mapping from authorities to Net::HTTP objects.
@connection_pool = options[:connection_pool] || {}
if options[:cert_store]
@cert_store = options[:cert_store]
else
@cert_store = OpenSSL::X509::Store.new
@cert_store.set_default_paths
end
end
attr_reader :connection_pool
attr_reader :cert_store
# first check if user passed a parser then fallback on appropriate default
@parser = @options[@options[:parser]] || PARSERS[@options[:parser]]
unless @parser
raise ArgumentError,
'Invalid :parser configuration.'
def build_request(method, uri, options={})
# No type-checking here, but OK because we check against a whitelist
method = method.to_s.downcase.to_sym
uri = Addressable::URI.parse(uri).normalize
if !METHOD_MAPPING.keys.include?(method)
raise ArgumentError, "Unsupported HTTP method: #{method}"
end
headers = {
"Accept" => "application/json;q=1.0, */*;q=0.5"
}.merge(options[:headers] || {})
# TODO(bobaman) More stuff here to handle optional parameters like
# form data.
body = options[:body] || ""
if body != ""
entity_body_defaults = {
"Content-Length" => body.size.to_s,
"Content-Type" => "application/json"
}
headers = entity_body_defaults.merge(headers)
end
return [method.to_s.upcase, uri.to_s, headers, [body]]
end
def send_request(request)
retried = false
begin
method, uri, headers, body_wrapper = request
body = ""
body_wrapper.each do |chunk|
body += chunk
end
uri = Addressable::URI.parse(uri).normalize
connection = self.connect_to(uri)
# Translate to Net::HTTP request
request_class = METHOD_MAPPING[method.to_s.downcase.to_sym]
if !request_class
raise ArgumentError,
"Unsupported HTTP method: #{method.to_s.downcase.to_sym}"
end
net_http_request = request_class.new(uri.request_uri)
for key, value in headers
net_http_request[key] = value
end
net_http_request.body = body
response = connection.request(net_http_request)
response_headers = {}
# We want the canonical header name.
# Note that Net::HTTP is lossy in that it downcases header names and
# then capitalizes them afterwards.
# This results in less-than-ideal behavior for headers like 'ETag'.
# Not much we can do about it.
response.canonical_each do |header, value|
response_headers[header] = value
end
# We use the Rack spec to trivially abstract the response format
return [response.code.to_i, response_headers, [response.body]]
rescue Errno::EPIPE, IOError, EOFError => e
# If there's a problem with the connection, finish and restart
if !retried && connection.started?
retried = true
connection.finish
connection.start
retry
else
raise e
end
end
end
##
# Returns configuration of the transport.
# Builds a connection to the authority given in the URI using the
# appropriate protocol.
#
# @return [Hash] The configuration options.
def options
return @options
end
##
# Returns the parser used by the transport.
#
# @return The handle to the parser.
def parser
return @parser
# @param [Addressable::URI, #to_str] uri The URI to connect to.
def connect_to(uri)
uri = Addressable::URI.parse(uri).normalize
if !ALLOWED_SCHEMES.include?(uri.scheme)
raise ArgumentError, "Unsupported protocol: #{uri.scheme}"
end
connection = @connection_pool[uri.site]
unless connection
connection = Net::HTTP.new(uri.host, uri.inferred_port)
end
retried = false
begin
if uri.scheme == 'https' && !connection.started?
connection.use_ssl = true
if connection.respond_to?(:enable_post_connection_check=)
# Deals with a security vulnerability
connection.enable_post_connection_check = true
end
connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
connection.cert_store = @cert_store
end
unless connection.started?
# Since we allow a connection pool to be passed in, we don't
# actually know this connection has been started yet.
connection.start
end
rescue Errno::EPIPE, IOError, EOFError => e
# If there's a problem with the connection, finish and restart
if !retried && connection.started?
retried = true
connection.finish
connection.start
retry
else
raise e
end
end
# Keep a reference to the connection around
@connection_pool[uri.site] = connection
return connection
end
protected :connect_to
end
end
end
+7 -10
View File
@@ -12,17 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Used to prevent the class/module from being loaded more than once
unless defined? Google::APIClient::VERSION
module Google #:nodoc:
class APIClient #:nodoc:
module VERSION #:nodoc:
MAJOR = 0
MINOR = 1
TINY = 0
module Google #:nodoc:
class APIClient #:nodoc:
module VERSION #:nodoc:
MAJOR = 0
MINOR = 1
TINY = 0
STRING = [MAJOR, MINOR, TINY].join('.')
end
STRING = [MAJOR, MINOR, TINY].join('.')
end
end
end