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