1# HTTPGenericRequest is the parent of the HTTPRequest class.
2# Do not use this directly; use a subclass of HTTPRequest.
3#
4# Mixes in the HTTPHeader module to provide easier access to HTTP headers.
5#
6class Net::HTTPGenericRequest
7
8  include Net::HTTPHeader
9
10  def initialize(m, reqbody, resbody, uri_or_path, initheader = nil)
11    @method = m
12    @request_has_body = reqbody
13    @response_has_body = resbody
14
15    if URI === uri_or_path then
16      @uri = uri_or_path.dup
17      host = @uri.hostname
18      host += ":#{@uri.port}" if @uri.port != @uri.class::DEFAULT_PORT
19      path = uri_or_path.request_uri
20    else
21      @uri = nil
22      host = nil
23      path = uri_or_path
24    end
25
26    raise ArgumentError, "no HTTP request path given" unless path
27    raise ArgumentError, "HTTP request path is empty" if path.empty?
28    @path = path
29
30    @decode_content = false
31
32    if @response_has_body and Net::HTTP::HAVE_ZLIB then
33      if !initheader ||
34         !initheader.keys.any? { |k|
35           %w[accept-encoding range].include? k.downcase
36         } then
37        @decode_content = true
38        initheader = initheader ? initheader.dup : {}
39        initheader["accept-encoding"] =
40          "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
41      end
42    end
43
44    initialize_http_header initheader
45    self['Accept'] ||= '*/*'
46    self['User-Agent'] ||= 'Ruby'
47    self['Host'] ||= host
48    @body = nil
49    @body_stream = nil
50    @body_data = nil
51  end
52
53  attr_reader :method
54  attr_reader :path
55  attr_reader :uri
56
57  # Automatically set to false if the user sets the Accept-Encoding header.
58  # This indicates they wish to handle Content-encoding in responses
59  # themselves.
60  attr_reader :decode_content
61
62  def inspect
63    "\#<#{self.class} #{@method}>"
64  end
65
66  ##
67  # Don't automatically decode response content-encoding if the user indicates
68  # they want to handle it.
69
70  def []=(key, val) # :nodoc:
71    @decode_content = false if key.downcase == 'accept-encoding'
72
73    super key, val
74  end
75
76  def request_body_permitted?
77    @request_has_body
78  end
79
80  def response_body_permitted?
81    @response_has_body
82  end
83
84  def body_exist?
85    warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?" if $VERBOSE
86    response_body_permitted?
87  end
88
89  attr_reader :body
90
91  def body=(str)
92    @body = str
93    @body_stream = nil
94    @body_data = nil
95    str
96  end
97
98  attr_reader :body_stream
99
100  def body_stream=(input)
101    @body = nil
102    @body_stream = input
103    @body_data = nil
104    input
105  end
106
107  def set_body_internal(str)   #:nodoc: internal use only
108    raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
109    self.body = str if str
110    if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted?
111      self.body = ''
112    end
113  end
114
115  #
116  # write
117  #
118
119  def exec(sock, ver, path)   #:nodoc: internal use only
120    if @uri
121      if @uri.port == @uri.default_port
122        # [Bug #7650] Amazon ECS API and GFE/1.3 disallow extra default port number
123        self['host'] = @uri.host
124      else
125        self['host'] = "#{@uri.host}:#{@uri.port}"
126      end
127    end
128
129    if @body
130      send_request_with_body sock, ver, path, @body
131    elsif @body_stream
132      send_request_with_body_stream sock, ver, path, @body_stream
133    elsif @body_data
134      send_request_with_body_data sock, ver, path, @body_data
135    else
136      write_header sock, ver, path
137    end
138  end
139
140  def update_uri(host, port, ssl) # :nodoc: internal use only
141    return unless @uri
142
143    @uri.host ||= host
144    @uri.port = port
145
146    scheme = ssl ? 'https' : 'http'
147
148    # convert the class of the URI
149    unless scheme == @uri.scheme then
150      new_uri = @uri.to_s.sub(/^https?/, scheme)
151      @uri = URI new_uri
152    end
153
154    @uri
155  end
156
157  private
158
159  class Chunker #:nodoc:
160    def initialize(sock)
161      @sock = sock
162      @prev = nil
163    end
164
165    def write(buf)
166      # avoid memcpy() of buf, buf can huge and eat memory bandwidth
167      @sock.write("#{buf.bytesize.to_s(16)}\r\n")
168      rv = @sock.write(buf)
169      @sock.write("\r\n")
170      rv
171    end
172
173    def finish
174      @sock.write("0\r\n\r\n")
175    end
176  end
177
178  def send_request_with_body(sock, ver, path, body)
179    self.content_length = body.bytesize
180    delete 'Transfer-Encoding'
181    supply_default_content_type
182    write_header sock, ver, path
183    wait_for_continue sock, ver if sock.continue_timeout
184    sock.write body
185  end
186
187  def send_request_with_body_stream(sock, ver, path, f)
188    unless content_length() or chunked?
189      raise ArgumentError,
190          "Content-Length not given and Transfer-Encoding is not `chunked'"
191    end
192    supply_default_content_type
193    write_header sock, ver, path
194    wait_for_continue sock, ver if sock.continue_timeout
195    if chunked?
196      chunker = Chunker.new(sock)
197      IO.copy_stream(f, chunker)
198      chunker.finish
199    else
200      # copy_stream can sendfile() to sock.io unless we use SSL.
201      # If sock.io is an SSLSocket, copy_stream will hit SSL_write()
202      IO.copy_stream(f, sock.io)
203    end
204  end
205
206  def send_request_with_body_data(sock, ver, path, params)
207    if /\Amultipart\/form-data\z/i !~ self.content_type
208      self.content_type = 'application/x-www-form-urlencoded'
209      return send_request_with_body(sock, ver, path, URI.encode_www_form(params))
210    end
211
212    opt = @form_option.dup
213    require 'securerandom' unless defined?(SecureRandom)
214    opt[:boundary] ||= SecureRandom.urlsafe_base64(40)
215    self.set_content_type(self.content_type, boundary: opt[:boundary])
216    if chunked?
217      write_header sock, ver, path
218      encode_multipart_form_data(sock, params, opt)
219    else
220      require 'tempfile'
221      file = Tempfile.new('multipart')
222      file.binmode
223      encode_multipart_form_data(file, params, opt)
224      file.rewind
225      self.content_length = file.size
226      write_header sock, ver, path
227      IO.copy_stream(file, sock)
228      file.close(true)
229    end
230  end
231
232  def encode_multipart_form_data(out, params, opt)
233    charset = opt[:charset]
234    boundary = opt[:boundary]
235    require 'securerandom' unless defined?(SecureRandom)
236    boundary ||= SecureRandom.urlsafe_base64(40)
237    chunked_p = chunked?
238
239    buf = ''
240    params.each do |key, value, h={}|
241      key = quote_string(key, charset)
242      filename =
243        h.key?(:filename) ? h[:filename] :
244        value.respond_to?(:to_path) ? File.basename(value.to_path) :
245        nil
246
247      buf << "--#{boundary}\r\n"
248      if filename
249        filename = quote_string(filename, charset)
250        type = h[:content_type] || 'application/octet-stream'
251        buf << "Content-Disposition: form-data; " \
252          "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
253          "Content-Type: #{type}\r\n\r\n"
254        if !out.respond_to?(:write) || !value.respond_to?(:read)
255          # if +out+ is not an IO or +value+ is not an IO
256          buf << (value.respond_to?(:read) ? value.read : value)
257        elsif value.respond_to?(:size) && chunked_p
258          # if +out+ is an IO and +value+ is a File, use IO.copy_stream
259          flush_buffer(out, buf, chunked_p)
260          out << "%x\r\n" % value.size if chunked_p
261          IO.copy_stream(value, out)
262          out << "\r\n" if chunked_p
263        else
264          # +out+ is an IO, and +value+ is not a File but an IO
265          flush_buffer(out, buf, chunked_p)
266          1 while flush_buffer(out, value.read(4096), chunked_p)
267        end
268      else
269        # non-file field:
270        #   HTML5 says, "The parts of the generated multipart/form-data
271        #   resource that correspond to non-file fields must not have a
272        #   Content-Type header specified."
273        buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
274        buf << (value.respond_to?(:read) ? value.read : value)
275      end
276      buf << "\r\n"
277    end
278    buf << "--#{boundary}--\r\n"
279    flush_buffer(out, buf, chunked_p)
280    out << "0\r\n\r\n" if chunked_p
281  end
282
283  def quote_string(str, charset)
284    str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
285    str = str.gsub(/[\\"]/, '\\\\\&')
286  end
287
288  def flush_buffer(out, buf, chunked_p)
289    return unless buf
290    out << "%x\r\n"%buf.bytesize if chunked_p
291    out << buf
292    out << "\r\n" if chunked_p
293    buf.clear
294  end
295
296  def supply_default_content_type
297    return if content_type()
298    warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE
299    set_content_type 'application/x-www-form-urlencoded'
300  end
301
302  ##
303  # Waits up to the continue timeout for a response from the server provided
304  # we're speaking HTTP 1.1 and are expecting a 100-continue response.
305
306  def wait_for_continue(sock, ver)
307    if ver >= '1.1' and @header['expect'] and
308        @header['expect'].include?('100-continue')
309      if IO.select([sock.io], nil, nil, sock.continue_timeout)
310        res = Net::HTTPResponse.read_new(sock)
311        unless res.kind_of?(Net::HTTPContinue)
312          res.decode_content = @decode_content
313          throw :response, res
314        end
315      end
316    end
317  end
318
319  def write_header(sock, ver, path)
320    buf = "#{@method} #{path} HTTP/#{ver}\r\n"
321    each_capitalized do |k,v|
322      buf << "#{k}: #{v}\r\n"
323    end
324    buf << "\r\n"
325    sock.write buf
326  end
327
328end
329
330