aboutsummaryrefslogtreecommitdiffstats
path: root/lib/rubygems/remote_fetcher.rb
blob: f49ee2f4a10a18c87b245697b096e92c980f166a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
require 'net/http'
require 'uri'

require 'rubygems'

##
# RemoteFetcher handles the details of fetching gems and gem information from
# a remote source.

class Gem::RemoteFetcher

  include Gem::UserInteraction

  class FetchError < Gem::Exception; end

  @fetcher = nil

  # Cached RemoteFetcher instance.
  def self.fetcher
    @fetcher ||= self.new Gem.configuration[:http_proxy]
  end

  # Initialize a remote fetcher using the source URI and possible proxy
  # information.
  #
  # +proxy+
  # * [String]: explicit specification of proxy; overrides any environment
  #             variable setting
  # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER,
  #        HTTP_PROXY_PASS)
  # * <tt>:no_proxy</tt>: ignore environment variables and _don't_ use a proxy
  def initialize(proxy)
    Socket.do_not_reverse_lookup = true

    @connections = {}
    @requests = Hash.new 0
    @proxy_uri =
      case proxy
      when :no_proxy then nil
      when nil then get_proxy_from_env
      when URI::HTTP then proxy
      else URI.parse(proxy)
      end
  end

  ##
  # Moves the gem +spec+ from +source_uri+ to the cache dir unless it is
  # already there.  If the source_uri is local the gem cache dir copy is
  # always replaced.
  def download(spec, source_uri, install_dir = Gem.dir)
    gem_file_name = "#{spec.full_name}.gem"
    local_gem_path = File.join install_dir, 'cache', gem_file_name

    Gem.ensure_gem_subdirectories install_dir

    source_uri = URI.parse source_uri unless URI::Generic === source_uri
    scheme = source_uri.scheme

    # URI.parse gets confused by MS Windows paths with forward slashes.
    scheme = nil if scheme =~ /^[a-z]$/i

    case scheme
    when 'http' then
      unless File.exist? local_gem_path then
        begin
          say "Downloading gem #{gem_file_name}" if
            Gem.configuration.really_verbose

          remote_gem_path = source_uri + "gems/#{gem_file_name}"

          gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path
        rescue Gem::RemoteFetcher::FetchError
          raise if spec.original_platform == spec.platform

          alternate_name = "#{spec.original_name}.gem"

          say "Failed, downloading gem #{alternate_name}" if
            Gem.configuration.really_verbose

          remote_gem_path = source_uri + "gems/#{alternate_name}"

          gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path
        end

        File.open local_gem_path, 'wb' do |fp|
          fp.write gem
        end
      end
    when nil, 'file' then # TODO test for local overriding cache
      begin
        FileUtils.cp source_uri.to_s, local_gem_path
      rescue Errno::EACCES
        local_gem_path = source_uri.to_s
      end

      say "Using local gem #{local_gem_path}" if
        Gem.configuration.really_verbose
    else
      raise Gem::InstallError, "unsupported URI scheme #{source_uri.scheme}"
    end

    local_gem_path
  end

  # Downloads +uri+.
  def fetch_path(uri)
    open_uri_or_path(uri) do |input|
      input.read
    end
  rescue Timeout::Error
    raise FetchError, "timed out fetching #{uri}"
  rescue IOError, SocketError, SystemCallError => e
    raise FetchError, "#{e.class}: #{e} reading #{uri}"
  rescue => e
    message = "#{e.class}: #{e} reading #{uri}"
    raise FetchError, message
  end

  # Returns the size of +uri+ in bytes.
  def fetch_size(uri)
    return File.size(get_file_uri_path(uri)) if file_uri? uri

    uri = URI.parse uri unless URI::Generic === uri

    raise ArgumentError, 'uri is not an HTTP URI' unless URI::HTTP === uri

    http = connect_to uri.host, uri.port

    request = Net::HTTP::Head.new uri.request_uri

    request.basic_auth unescape(uri.user), unescape(uri.password) unless
      uri.user.nil? or uri.user.empty?

    resp = http.request request

    if resp.code !~ /^2/ then
      raise Gem::RemoteSourceException,
            "HTTP Response #{resp.code} fetching #{uri}"
    end

    if resp['content-length'] then
      return resp['content-length'].to_i
    else
      resp = http.get uri.request_uri
      return resp.body.size
    end

  rescue SocketError, SystemCallError, Timeout::Error => e
    raise Gem::RemoteFetcher::FetchError,
          "#{e.message} (#{e.class})\n\tgetting size of #{uri}"
  end

  private

  def escape(str)
    return unless str
    URI.escape(str)
  end

  def unescape(str)
    return unless str
    URI.unescape(str)
  end

  # Returns an HTTP proxy URI if one is set in the environment variables.
  def get_proxy_from_env
    env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']

    return nil if env_proxy.nil? or env_proxy.empty?

    uri = URI.parse env_proxy

    if uri and uri.user.nil? and uri.password.nil? then
      # Probably we have http_proxy_* variables?
      uri.user = escape(ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER'])
      uri.password = escape(ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS'])
    end

    uri
  end

  # Normalize the URI by adding "http://" if it is missing.
  def normalize_uri(uri)
    (uri =~ /^(https?|ftp|file):/) ? uri : "http://#{uri}"
  end

  # Connect to the source host/port, using a proxy if needed.
  def connect_to(host, port)
    if @proxy_uri
      Net::HTTP::Proxy(@proxy_uri.host, @proxy_uri.port, unescape(@proxy_uri.user), unescape(@proxy_uri.password)).new(host, port)
    else
      Net::HTTP.new(host, port)
    end
  end

  # Read the data from the (source based) URI, but if it is a file:// URI,
  # read from the filesystem instead.
  def open_uri_or_path(uri, depth = 0, &block)
    if file_uri?(uri)
      open(get_file_uri_path(uri), &block)
    else
      uri = URI.parse uri unless URI::Generic === uri
      net_http_args = [uri.host, uri.port]

      if @proxy_uri then
        net_http_args += [  @proxy_uri.host,
                            @proxy_uri.port,
                            @proxy_uri.user,
                            @proxy_uri.password
        ]
      end

      connection_id = net_http_args.join ':'
      @connections[connection_id] ||= Net::HTTP.new(*net_http_args)
      connection = @connections[connection_id]

      if uri.scheme == 'https' && ! connection.started?
        http_obj.use_ssl = true
        http_obj.verify_mode = OpenSSL::SSL::VERIFY_NONE
      end

      connection.start unless connection.started?

      request = Net::HTTP::Get.new(uri.request_uri)
      unless uri.nil? || uri.user.nil? || uri.user.empty? then
        request.basic_auth(uri.user, uri.password)
      end

      ua = "RubyGems/#{Gem::RubyGemsVersion} #{Gem::Platform.local}"
      ua << " Ruby/#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}"
      ua << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL
      ua << ")"

      request.add_field 'User-Agent', ua
      request.add_field 'Connection', 'keep-alive'
      request.add_field 'Keep-Alive', '30'

      # HACK work around EOFError bug in Net::HTTP
      retried = false
      begin
        @requests[connection_id] += 1
        response = connection.request(request)
      rescue EOFError
        requests = @requests[connection_id]
        say "connection reset after #{requests} requests, retrying" if
          Gem.configuration.really_verbose

        raise Gem::RemoteFetcher::FetchError, 'too many connection resets' if
          retried

        @requests[connection_id] = 0

        connection.finish
        connection.start
        retried = true
        retry
      end

      case response
      when Net::HTTPOK then
        block.call(StringIO.new(response.body)) if block
      when Net::HTTPRedirection then
        raise Gem::RemoteFetcher::FetchError, "too many redirects" if depth > 10
        open_uri_or_path(response['Location'], depth + 1, &block)
      else
        raise Gem::RemoteFetcher::FetchError,
              "bad response #{response.message} #{response.code}"
      end
    end
  end

  # Checks if the provided string is a file:// URI.
  def file_uri?(uri)
    uri =~ %r{\Afile://}
  end

  # Given a file:// URI, returns its local path.
  def get_file_uri_path(uri)
    uri.sub(%r{\Afile://}, '')
  end

end