Initial implementation of HTTP.
[arvados.git] / lib / google / api_client / transport / http_transport.rb
1 # Copyright 2010 Google Inc.
2 #
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
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
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.
14
15 require 'net/http'
16 require 'net/https'
17 require 'addressable/uri'
18
19 module Google #:nodoc:
20   class APIClient #:nodoc:
21
22     ##
23     # Provides a consistent interface by which to make HTTP requests using the
24     # Net::HTTP class.
25     class HTTPTransport
26       ALLOWED_SCHEMES = ["http", "https"]
27       METHOD_MAPPING = {
28         # RFC 2616
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
44       }
45
46       ##
47       #
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]
53         else
54           @cert_store = OpenSSL::X509::Store.new
55           @cert_store.set_default_paths
56         end
57       end
58       
59       attr_reader :connection_pool
60       attr_reader :cert_store
61
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}"
68         end
69         headers = {
70           "Accept" => "application/json;q=1.0, */*;q=0.5"
71         }.merge(options[:headers] || {})
72
73         # TODO(bobaman) More stuff here to handle optional parameters like
74         # form data.
75
76         body = options[:body] || ""
77         if body != ""
78           entity_body_defaults = {
79             "Content-Length" => body.size.to_s,
80             "Content-Type" => "application/json"
81           }
82           headers = entity_body_defaults.merge(headers)
83         end
84         return [method.to_s.upcase, uri.to_s, headers, [body]]
85       end
86
87       def send_request(request)
88         retried = false
89         begin
90           method, uri, headers, body_wrapper = request
91           body = ""
92           body_wrapper.each do |chunk|
93             body += chunk
94           end
95
96           uri = Addressable::URI.parse(uri).normalize
97           connection = self.connect_to(uri)
98
99           # Translate to Net::HTTP request
100           request_class = METHOD_MAPPING[method.to_s.downcase.to_sym]
101           if !request_class
102             raise ArgumentError,
103               "Unsupported HTTP method: #{method.to_s.downcase.to_sym}"
104           end          
105           net_http_request = request_class.new(uri.request_uri)
106           for key, value in headers
107             net_http_request[key] = value
108           end
109           net_http_request.body = body
110           response = connection.request(net_http_request)
111
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
120           end
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?
126             retried = true
127             connection.finish
128             connection.start
129             retry
130           else
131             raise e
132           end
133         end
134       end
135
136       ##
137       # Builds a connection to the authority given in the URI using the
138       # appropriate protocol.
139       #
140       # @param [Addressable::URI, #to_str] uri The URI to connect to.
141       def connect_to(uri)
142         uri = Addressable::URI.parse(uri).normalize
143         if !ALLOWED_SCHEMES.include?(uri.scheme)
144           raise ArgumentError, "Unsupported protocol: #{uri.scheme}"
145         end
146         connection = @connection_pool[uri.site]
147         unless connection
148           connection = Net::HTTP.new(uri.host, uri.inferred_port)
149         end
150         retried = false
151         begin
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
157             end
158             connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
159             connection.cert_store = @cert_store
160           end
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.
164             connection.start
165           end
166         rescue Errno::EPIPE, IOError, EOFError => e
167           # If there's a problem with the connection, finish and restart
168           if !retried && connection.started?
169             retried = true
170             connection.finish
171             connection.start
172             retry
173           else
174             raise e
175           end
176         end
177         # Keep a reference to the connection around
178         @connection_pool[uri.site] = connection
179         return connection
180       end
181       protected :connect_to
182     end
183   end
184 end