Imported File#ftype spec from rubyspecs.
[rbx.git] / lib / rubygems / remote_fetcher.rb
blob93252fe83aa6e84192658ec82ba783fc7b5be507
1 require 'net/http'
2 require 'stringio'
3 require 'uri'
5 require 'rubygems'
7 ##
8 # RemoteFetcher handles the details of fetching gems and gem information from
9 # a remote source.
11 class Gem::RemoteFetcher
13   include Gem::UserInteraction
15   ##
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
21     ##
22     # The URI which was being accessed when the exception happened.
24     attr_accessor :uri
26     def initialize(message, uri)
27       super message
28       @uri = uri
29     end
31     def to_s # :nodoc:
32       "#{super} (#{uri})"
33     end
35   end
37   @fetcher = nil
39   ##
40   # Cached RemoteFetcher instance.
42   def self.fetcher
43     @fetcher ||= self.new Gem.configuration[:http_proxy]
44   end
46   ##
47   # Initialize a remote fetcher using the source URI and possible proxy
48   # information.
49   #
50   # +proxy+
51   # * [String]: explicit specification of proxy; overrides any environment
52   #             variable setting
53   # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER,
54   #        HTTP_PROXY_PASS)
55   # * <tt>:no_proxy</tt>: ignore environment variables and _don't_ use a proxy
57   def initialize(proxy)
58     Socket.do_not_reverse_lookup = true
60     @connections = {}
61     @requests = Hash.new 0
62     @proxy_uri =
63       case proxy
64       when :no_proxy then nil
65       when nil then get_proxy_from_env
66       when URI::HTTP then proxy
67       else URI.parse(proxy)
68       end
69   end
71   ##
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
74   # always replaced.
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
89     case scheme
90     when 'http' then
91       unless File.exist? local_gem_path then
92         begin
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
110         end
112         File.open local_gem_path, 'wb' do |fp|
113           fp.write gem
114         end
115       end
116     when nil, 'file' then # TODO test for local overriding cache
117       begin
118         FileUtils.cp source_uri.to_s, local_gem_path
119       rescue Errno::EACCES
120         local_gem_path = source_uri.to_s
121       end
123       say "Using local gem #{local_gem_path}" if
124         Gem.configuration.really_verbose
125     else
126       raise Gem::InstallError, "unsupported URI scheme #{source_uri.scheme}"
127     end
129     local_gem_path
130   end
132   ##
133   # Downloads +uri+ and returns it as a String.
135   def fetch_path(uri)
136     open_uri_or_path(uri) do |input|
137       input.read
138     end
139   rescue FetchError
140     raise
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)
145   rescue => e
146     raise FetchError.new("#{e.class}: #{e}", uri)
147   end
149   ##
150   # Returns the size of +uri+ in bytes.
152   def fetch_size(uri)
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)
163     end
165     if response['content-length'] then
166       return response['content-length'].to_i
167     else
168       response = http.get uri.request_uri
169       return response.body.size
170     end
172   rescue SocketError, SystemCallError, Timeout::Error => e
173     raise FetchError.new("#{e.message} (#{e.class})\n\tfetching size", uri)
174   end
176   private
178   def escape(str)
179     return unless str
180     URI.escape(str)
181   end
183   def unescape(str)
184     return unless str
185     URI.unescape(str)
186   end
188   ##
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'])
202     end
204     uri
205   end
207   ##
208   # Normalize the URI by adding "http://" if it is missing.
210   def normalize_uri(uri)
211     (uri =~ /^(https?|ftp|file):/) ? uri : "http://#{uri}"
212   end
214   ##
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]
221     if @proxy_uri then
222       net_http_args += [
223         @proxy_uri.host,
224         @proxy_uri.port,
225         @proxy_uri.user,
226         @proxy_uri.password
227       ]
228     end
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
237     end
239     connection.start unless connection.started?
241     connection
242   end
244   ##
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)
249     if file_uri?(uri)
250       open(get_file_uri_path(uri), &block)
251     else
252       uri = URI.parse uri unless URI::Generic === uri
254       response = request uri
256       case response
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)
263       else
264         raise FetchError.new("bad response #{response.message} #{response.code}", uri)
265       end
266     end
267   end
269   ##
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
279     end
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
284     ua << ")"
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
292     retried = false
294     # HACK work around EOFError bug in Net::HTTP
295     # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible
296     # to install gems.
297     begin
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
311       connection.finish
312       connection.start
313       retried = true
314       retry
315     end
317     response
318   end
320   ##
321   # Checks if the provided string is a file:// URI.
323   def file_uri?(uri)
324     uri =~ %r{\Afile://}
325   end
327   ##
328   # Given a file:// URI, returns its local path.
330   def get_file_uri_path(uri)
331     uri.sub(%r{\Afile://}, '')
332   end