185 lines
6.2 KiB
Ruby
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
|