8 # RemoteFetcher handles the details of fetching gems and gem information from
11 class Gem::RemoteFetcher
13 include Gem::UserInteraction
16 # A FetchError exception wraps up the various possible IO and HTTP failures
17 # that could happen while downloading from the internet.
19 class FetchError < Gem::Exception
22 # The URI which was being accessed when the exception happened.
26 def initialize(message, uri)
40 # Cached RemoteFetcher instance.
43 @fetcher ||= self.new Gem.configuration[:http_proxy]
47 # Initialize a remote fetcher using the source URI and possible proxy
51 # * [String]: explicit specification of proxy; overrides any environment
53 # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER,
55 # * <tt>:no_proxy</tt>: ignore environment variables and _don't_ use a proxy
58 Socket.do_not_reverse_lookup = true
61 @requests = Hash.new 0
64 when :no_proxy then nil
65 when nil then get_proxy_from_env
66 when URI::HTTP then proxy
72 # Moves the gem +spec+ from +source_uri+ to the cache dir unless it is
73 # already there. If the source_uri is local the gem cache dir copy is
76 def download(spec, source_uri, install_dir = Gem.dir)
77 cache_dir = File.join install_dir, 'cache'
78 gem_file_name = "#{spec.full_name}.gem"
79 local_gem_path = File.join cache_dir, gem_file_name
81 FileUtils.mkdir_p cache_dir rescue nil unless File.exist? cache_dir
83 source_uri = URI.parse source_uri unless URI::Generic === source_uri
84 scheme = source_uri.scheme
86 # URI.parse gets confused by MS Windows paths with forward slashes.
87 scheme = nil if scheme =~ /^[a-z]$/i
91 unless File.exist? local_gem_path then
93 say "Downloading gem #{gem_file_name}" if
94 Gem.configuration.really_verbose
96 remote_gem_path = source_uri + "gems/#{gem_file_name}"
98 gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path
99 rescue Gem::RemoteFetcher::FetchError
100 raise if spec.original_platform == spec.platform
102 alternate_name = "#{spec.original_name}.gem"
104 say "Failed, downloading gem #{alternate_name}" if
105 Gem.configuration.really_verbose
107 remote_gem_path = source_uri + "gems/#{alternate_name}"
109 gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path
112 File.open local_gem_path, 'wb' do |fp|
116 when nil, 'file' then # TODO test for local overriding cache
118 FileUtils.cp source_uri.to_s, local_gem_path
120 local_gem_path = source_uri.to_s
123 say "Using local gem #{local_gem_path}" if
124 Gem.configuration.really_verbose
126 raise Gem::InstallError, "unsupported URI scheme #{source_uri.scheme}"
133 # Downloads +uri+ and returns it as a String.
136 open_uri_or_path(uri) do |input|
141 rescue Timeout::Error
142 raise FetchError.new('timed out', uri)
143 rescue IOError, SocketError, SystemCallError => e
144 raise FetchError.new("#{e.class}: #{e}", uri)
146 raise FetchError.new("#{e.class}: #{e}", uri)
150 # Returns the size of +uri+ in bytes.
153 return File.size(get_file_uri_path(uri)) if file_uri? uri
155 uri = URI.parse uri unless URI::Generic === uri
157 raise ArgumentError, 'uri is not an HTTP URI' unless URI::HTTP === uri
159 response = request uri, Net::HTTP::Head
161 if response.code !~ /^2/ then
162 raise FetchError.new("bad response #{response.message} #{response.code}", uri)
165 if response['content-length'] then
166 return response['content-length'].to_i
168 response = http.get uri.request_uri
169 return response.body.size
172 rescue SocketError, SystemCallError, Timeout::Error => e
173 raise FetchError.new("#{e.message} (#{e.class})\n\tfetching size", uri)
189 # Returns an HTTP proxy URI if one is set in the environment variables.
191 def get_proxy_from_env
192 env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
194 return nil if env_proxy.nil? or env_proxy.empty?
196 uri = URI.parse env_proxy
198 if uri and uri.user.nil? and uri.password.nil? then
199 # Probably we have http_proxy_* variables?
200 uri.user = escape(ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER'])
201 uri.password = escape(ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS'])
208 # Normalize the URI by adding "http://" if it is missing.
210 def normalize_uri(uri)
211 (uri =~ /^(https?|ftp|file):/) ? uri : "http://#{uri}"
215 # Creates or an HTTP connection based on +uri+, or retrieves an existing
216 # connection, using a proxy if needed.
218 def connection_for(uri)
219 net_http_args = [uri.host, uri.port]
230 connection_id = net_http_args.join ':'
231 @connections[connection_id] ||= Net::HTTP.new(*net_http_args)
232 connection = @connections[connection_id]
234 if uri.scheme == 'https' and not connection.started? then
235 http_obj.use_ssl = true
236 http_obj.verify_mode = OpenSSL::SSL::VERIFY_NONE
239 connection.start unless connection.started?
245 # Read the data from the (source based) URI, but if it is a file:// URI,
246 # read from the filesystem instead.
248 def open_uri_or_path(uri, depth = 0, &block)
250 open(get_file_uri_path(uri), &block)
252 uri = URI.parse uri unless URI::Generic === uri
254 response = request uri
257 when Net::HTTPOK then
258 block.call(StringIO.new(response.body)) if block
259 when Net::HTTPRedirection then
260 raise FetchError.new('too many redirects', uri) if depth > 10
262 open_uri_or_path(response['Location'], depth + 1, &block)
264 raise FetchError.new("bad response #{response.message} #{response.code}", uri)
270 # Performs a Net::HTTP request of type +request_class+ on +uri+ returning
271 # a Net::HTTP response object. request maintains a table of persistent
272 # connections to reduce connect overhead.
274 def request(uri, request_class = Net::HTTP::Get)
275 request = request_class.new uri.request_uri
277 unless uri.nil? || uri.user.nil? || uri.user.empty? then
278 request.basic_auth uri.user, uri.password
281 ua = "RubyGems/#{Gem::RubyGemsVersion} #{Gem::Platform.local}"
282 ua << " Ruby/#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}"
283 ua << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL
286 request.add_field 'User-Agent', ua
287 request.add_field 'Connection', 'keep-alive'
288 request.add_field 'Keep-Alive', '30'
290 connection = connection_for uri
294 # HACK work around EOFError bug in Net::HTTP
295 # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible
298 @requests[connection.object_id] += 1
299 response = connection.request request
300 say "#{request.method} #{response.code} #{response.message}: #{uri}" if
301 Gem.configuration.really_verbose
302 rescue EOFError, Errno::ECONNABORTED, Errno::ECONNRESET
303 requests = @requests[connection.object_id]
304 say "connection reset after #{requests} requests, retrying" if
305 Gem.configuration.really_verbose
307 raise FetchError.new('too many connection resets', uri) if retried
309 @requests.delete connection.object_id
321 # Checks if the provided string is a file:// URI.
328 # Given a file:// URI, returns its local path.
330 def get_file_uri_path(uri)
331 uri.sub(%r{\Afile://}, '')