1 # Copyright 2010 Google Inc.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
17 require 'addressable/uri'
19 module Google #:nodoc:
20 class APIClient #:nodoc:
23 # Provides a consistent interface by which to make HTTP requests using the
26 ALLOWED_SCHEMES = ["http", "https"]
29 :options => Net::HTTP::Options,
30 :get => Net::HTTP::Get,
31 :head => Net::HTTP::Head,
32 :post => Net::HTTP::Post,
33 :put => Net::HTTP::Put,
34 :delete => Net::HTTP::Delete,
35 :trace => Net::HTTP::Trace,
36 # Other standards supported by Net::HTTP
37 :copy => Net::HTTP::Copy,
38 :lock => Net::HTTP::Lock,
39 :mkcol => Net::HTTP::Mkcol,
40 :move => Net::HTTP::Move,
41 :propfind => Net::HTTP::Propfind,
42 :proppatch => Net::HTTP::Proppatch,
43 :unlock => Net::HTTP::Unlock
48 def initialize(options={})
49 # A mapping from authorities to Net::HTTP objects.
50 @connection_pool = options[:connection_pool] || {}
51 if options[:cert_store]
52 @cert_store = options[:cert_store]
54 @cert_store = OpenSSL::X509::Store.new
55 @cert_store.set_default_paths
59 attr_reader :connection_pool
60 attr_reader :cert_store
62 def build_request(method, uri, options={})
63 # No type-checking here, but OK because we check against a whitelist
64 method = method.to_s.downcase.to_sym
65 uri = Addressable::URI.parse(uri).normalize
66 if !METHOD_MAPPING.keys.include?(method)
67 raise ArgumentError, "Unsupported HTTP method: #{method}"
70 "Accept" => "application/json;q=1.0, */*;q=0.5"
71 }.merge(options[:headers] || {})
73 # TODO(bobaman) More stuff here to handle optional parameters like
76 body = options[:body] || ""
78 entity_body_defaults = {
79 "Content-Length" => body.size.to_s,
80 "Content-Type" => "application/json"
82 headers = entity_body_defaults.merge(headers)
84 return [method.to_s.upcase, uri.to_s, headers, [body]]
87 def send_request(request)
90 method, uri, headers, body_wrapper = request
92 body_wrapper.each do |chunk|
96 uri = Addressable::URI.parse(uri).normalize
97 connection = self.connect_to(uri)
99 # Translate to Net::HTTP request
100 request_class = METHOD_MAPPING[method.to_s.downcase.to_sym]
103 "Unsupported HTTP method: #{method.to_s.downcase.to_sym}"
105 net_http_request = request_class.new(uri.request_uri)
106 for key, value in headers
107 net_http_request[key] = value
109 net_http_request.body = body
110 response = connection.request(net_http_request)
112 response_headers = {}
113 # We want the canonical header name.
114 # Note that Net::HTTP is lossy in that it downcases header names and
115 # then capitalizes them afterwards.
116 # This results in less-than-ideal behavior for headers like 'ETag'.
117 # Not much we can do about it.
118 response.canonical_each do |header, value|
119 response_headers[header] = value
121 # We use the Rack spec to trivially abstract the response format
122 return [response.code.to_i, response_headers, [response.body]]
123 rescue Errno::EPIPE, IOError, EOFError => e
124 # If there's a problem with the connection, finish and restart
125 if !retried && connection.started?
137 # Builds a connection to the authority given in the URI using the
138 # appropriate protocol.
140 # @param [Addressable::URI, #to_str] uri The URI to connect to.
142 uri = Addressable::URI.parse(uri).normalize
143 if !ALLOWED_SCHEMES.include?(uri.scheme)
144 raise ArgumentError, "Unsupported protocol: #{uri.scheme}"
146 connection = @connection_pool[uri.site]
148 connection = Net::HTTP.new(uri.host, uri.inferred_port)
152 if uri.scheme == 'https' && !connection.started?
153 connection.use_ssl = true
154 if connection.respond_to?(:enable_post_connection_check=)
155 # Deals with a security vulnerability
156 connection.enable_post_connection_check = true
158 connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
159 connection.cert_store = @cert_store
161 unless connection.started?
162 # Since we allow a connection pool to be passed in, we don't
163 # actually know this connection has been started yet.
166 rescue Errno::EPIPE, IOError, EOFError => e
167 # If there's a problem with the connection, finish and restart
168 if !retried && connection.started?
177 # Keep a reference to the connection around
178 @connection_pool[uri.site] = connection
181 protected :connect_to