google-api-ruby-client/lib/google/api_client/transport/http_transport.rb

185 lines
6.2 KiB
Ruby

# Copyright 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'net/http'
require 'net/https'
require 'addressable/uri'
module Google #:nodoc:
class APIClient #:nodoc:
##
# Provides a consistent interface by which to make HTTP requests using the
# Net::HTTP class.
class HTTPTransport
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
}
##
#
def initialize(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
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
##
# Builds a connection to the authority given in the URI using the
# appropriate protocol.
#
# @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