1# frozen_string_literal: false 2# The HTTPHeader module defines methods for reading and writing 3# HTTP headers. 4# 5# It is used as a mixin by other classes, to provide hash-like 6# access to HTTP header values. Unlike raw hash access, HTTPHeader 7# provides access via case-insensitive keys. It also provides 8# methods for accessing commonly-used HTTP header values in more 9# convenient formats. 10# 11module Net::HTTPHeader 12 13 def initialize_http_header(initheader) 14 @header = {} 15 return unless initheader 16 initheader.each do |key, value| 17 warn "net/http: duplicated HTTP header: #{key}", uplevel: 1 if key?(key) and $VERBOSE 18 if value.nil? 19 warn "net/http: nil HTTP header: #{key}", uplevel: 1 if $VERBOSE 20 else 21 value = value.strip # raise error for invalid byte sequences 22 if value.count("\r\n") > 0 23 raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF" 24 end 25 @header[key.downcase.to_s] = [value] 26 end 27 end 28 end 29 30 def size #:nodoc: obsolete 31 @header.size 32 end 33 34 alias length size #:nodoc: obsolete 35 36 # Returns the header field corresponding to the case-insensitive key. 37 # For example, a key of "Content-Type" might return "text/html" 38 def [](key) 39 a = @header[key.downcase.to_s] or return nil 40 a.join(', ') 41 end 42 43 # Sets the header field corresponding to the case-insensitive key. 44 def []=(key, val) 45 unless val 46 @header.delete key.downcase.to_s 47 return val 48 end 49 set_field(key, val) 50 end 51 52 # [Ruby 1.8.3] 53 # Adds a value to a named header field, instead of replacing its value. 54 # Second argument +val+ must be a String. 55 # See also #[]=, #[] and #get_fields. 56 # 57 # request.add_field 'X-My-Header', 'a' 58 # p request['X-My-Header'] #=> "a" 59 # p request.get_fields('X-My-Header') #=> ["a"] 60 # request.add_field 'X-My-Header', 'b' 61 # p request['X-My-Header'] #=> "a, b" 62 # p request.get_fields('X-My-Header') #=> ["a", "b"] 63 # request.add_field 'X-My-Header', 'c' 64 # p request['X-My-Header'] #=> "a, b, c" 65 # p request.get_fields('X-My-Header') #=> ["a", "b", "c"] 66 # 67 def add_field(key, val) 68 stringified_downcased_key = key.downcase.to_s 69 if @header.key?(stringified_downcased_key) 70 append_field_value(@header[stringified_downcased_key], val) 71 else 72 set_field(key, val) 73 end 74 end 75 76 private def set_field(key, val) 77 case val 78 when Enumerable 79 ary = [] 80 append_field_value(ary, val) 81 @header[key.downcase.to_s] = ary 82 else 83 val = val.to_s # for compatibility use to_s instead of to_str 84 if val.b.count("\r\n") > 0 85 raise ArgumentError, 'header field value cannot include CR/LF' 86 end 87 @header[key.downcase.to_s] = [val] 88 end 89 end 90 91 private def append_field_value(ary, val) 92 case val 93 when Enumerable 94 val.each{|x| append_field_value(ary, x)} 95 else 96 val = val.to_s 97 if /[\r\n]/n.match?(val.b) 98 raise ArgumentError, 'header field value cannot include CR/LF' 99 end 100 ary.push val 101 end 102 end 103 104 # [Ruby 1.8.3] 105 # Returns an array of header field strings corresponding to the 106 # case-insensitive +key+. This method allows you to get duplicated 107 # header fields without any processing. See also #[]. 108 # 109 # p response.get_fields('Set-Cookie') 110 # #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23", 111 # "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"] 112 # p response['Set-Cookie'] 113 # #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23" 114 # 115 def get_fields(key) 116 stringified_downcased_key = key.downcase.to_s 117 return nil unless @header[stringified_downcased_key] 118 @header[stringified_downcased_key].dup 119 end 120 121 # Returns the header field corresponding to the case-insensitive key. 122 # Returns the default value +args+, or the result of the block, or 123 # raises an IndexError if there's no header field named +key+ 124 # See Hash#fetch 125 def fetch(key, *args, &block) #:yield: +key+ 126 a = @header.fetch(key.downcase.to_s, *args, &block) 127 a.kind_of?(Array) ? a.join(', ') : a 128 end 129 130 # Iterates through the header names and values, passing in the name 131 # and value to the code block supplied. 132 # 133 # Returns an enumerator if no block is given. 134 # 135 # Example: 136 # 137 # response.header.each_header {|key,value| puts "#{key} = #{value}" } 138 # 139 def each_header #:yield: +key+, +value+ 140 block_given? or return enum_for(__method__) { @header.size } 141 @header.each do |k,va| 142 yield k, va.join(', ') 143 end 144 end 145 146 alias each each_header 147 148 # Iterates through the header names in the header, passing 149 # each header name to the code block. 150 # 151 # Returns an enumerator if no block is given. 152 def each_name(&block) #:yield: +key+ 153 block_given? or return enum_for(__method__) { @header.size } 154 @header.each_key(&block) 155 end 156 157 alias each_key each_name 158 159 # Iterates through the header names in the header, passing 160 # capitalized header names to the code block. 161 # 162 # Note that header names are capitalized systematically; 163 # capitalization may not match that used by the remote HTTP 164 # server in its response. 165 # 166 # Returns an enumerator if no block is given. 167 def each_capitalized_name #:yield: +key+ 168 block_given? or return enum_for(__method__) { @header.size } 169 @header.each_key do |k| 170 yield capitalize(k) 171 end 172 end 173 174 # Iterates through header values, passing each value to the 175 # code block. 176 # 177 # Returns an enumerator if no block is given. 178 def each_value #:yield: +value+ 179 block_given? or return enum_for(__method__) { @header.size } 180 @header.each_value do |va| 181 yield va.join(', ') 182 end 183 end 184 185 # Removes a header field, specified by case-insensitive key. 186 def delete(key) 187 @header.delete(key.downcase.to_s) 188 end 189 190 # true if +key+ header exists. 191 def key?(key) 192 @header.key?(key.downcase.to_s) 193 end 194 195 # Returns a Hash consisting of header names and array of values. 196 # e.g. 197 # {"cache-control" => ["private"], 198 # "content-type" => ["text/html"], 199 # "date" => ["Wed, 22 Jun 2005 22:11:50 GMT"]} 200 def to_hash 201 @header.dup 202 end 203 204 # As for #each_header, except the keys are provided in capitalized form. 205 # 206 # Note that header names are capitalized systematically; 207 # capitalization may not match that used by the remote HTTP 208 # server in its response. 209 # 210 # Returns an enumerator if no block is given. 211 def each_capitalized 212 block_given? or return enum_for(__method__) { @header.size } 213 @header.each do |k,v| 214 yield capitalize(k), v.join(', ') 215 end 216 end 217 218 alias canonical_each each_capitalized 219 220 def capitalize(name) 221 name.to_s.split(/-/).map {|s| s.capitalize }.join('-') 222 end 223 private :capitalize 224 225 # Returns an Array of Range objects which represent the Range: 226 # HTTP header field, or +nil+ if there is no such header. 227 def range 228 return nil unless @header['range'] 229 230 value = self['Range'] 231 # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec ) 232 # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] ) 233 # corrected collected ABNF 234 # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1 235 # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C 236 # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5 237 unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value 238 raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'" 239 end 240 241 byte_range_set = $1 242 result = byte_range_set.split(/,/).map {|spec| 243 m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or 244 raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'" 245 d1 = m[1].to_i 246 d2 = m[2].to_i 247 if m[1] and m[2] 248 if d1 > d2 249 raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'" 250 end 251 d1..d2 252 elsif m[1] 253 d1..-1 254 elsif m[2] 255 -d2..-1 256 else 257 raise Net::HTTPHeaderSyntaxError, 'range is not specified' 258 end 259 } 260 # if result.empty? 261 # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec 262 # but above regexp already denies it. 263 if result.size == 1 && result[0].begin == 0 && result[0].end == -1 264 raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length' 265 end 266 result 267 end 268 269 # Sets the HTTP Range: header. 270 # Accepts either a Range object as a single argument, 271 # or a beginning index and a length from that index. 272 # Example: 273 # 274 # req.range = (0..1023) 275 # req.set_range 0, 1023 276 # 277 def set_range(r, e = nil) 278 unless r 279 @header.delete 'range' 280 return r 281 end 282 r = (r...r+e) if e 283 case r 284 when Numeric 285 n = r.to_i 286 rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}") 287 when Range 288 first = r.first 289 last = r.end 290 last -= 1 if r.exclude_end? 291 if last == -1 292 rangestr = (first > 0 ? "#{first}-" : "-#{-first}") 293 else 294 raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 295 raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 296 raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last 297 rangestr = "#{first}-#{last}" 298 end 299 else 300 raise TypeError, 'Range/Integer is required' 301 end 302 @header['range'] = ["bytes=#{rangestr}"] 303 r 304 end 305 306 alias range= set_range 307 308 # Returns an Integer object which represents the HTTP Content-Length: 309 # header field, or +nil+ if that field was not provided. 310 def content_length 311 return nil unless key?('Content-Length') 312 len = self['Content-Length'].slice(/\d+/) or 313 raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format' 314 len.to_i 315 end 316 317 def content_length=(len) 318 unless len 319 @header.delete 'content-length' 320 return nil 321 end 322 @header['content-length'] = [len.to_i.to_s] 323 end 324 325 # Returns "true" if the "transfer-encoding" header is present and 326 # set to "chunked". This is an HTTP/1.1 feature, allowing the 327 # the content to be sent in "chunks" without at the outset 328 # stating the entire content length. 329 def chunked? 330 return false unless @header['transfer-encoding'] 331 field = self['Transfer-Encoding'] 332 (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false 333 end 334 335 # Returns a Range object which represents the value of the Content-Range: 336 # header field. 337 # For a partial entity body, this indicates where this fragment 338 # fits inside the full entity body, as range of byte offsets. 339 def content_range 340 return nil unless @header['content-range'] 341 m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or 342 raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format' 343 m[1].to_i .. m[2].to_i 344 end 345 346 # The length of the range represented in Content-Range: header. 347 def range_length 348 r = content_range() or return nil 349 r.end - r.begin + 1 350 end 351 352 # Returns a content type string such as "text/html". 353 # This method returns nil if Content-Type: header field does not exist. 354 def content_type 355 return nil unless main_type() 356 if sub_type() 357 then "#{main_type()}/#{sub_type()}" 358 else main_type() 359 end 360 end 361 362 # Returns a content type string such as "text". 363 # This method returns nil if Content-Type: header field does not exist. 364 def main_type 365 return nil unless @header['content-type'] 366 self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip 367 end 368 369 # Returns a content type string such as "html". 370 # This method returns nil if Content-Type: header field does not exist 371 # or sub-type is not given (e.g. "Content-Type: text"). 372 def sub_type 373 return nil unless @header['content-type'] 374 _, sub = *self['Content-Type'].split(';').first.to_s.split('/') 375 return nil unless sub 376 sub.strip 377 end 378 379 # Any parameters specified for the content type, returned as a Hash. 380 # For example, a header of Content-Type: text/html; charset=EUC-JP 381 # would result in type_params returning {'charset' => 'EUC-JP'} 382 def type_params 383 result = {} 384 list = self['Content-Type'].to_s.split(';') 385 list.shift 386 list.each do |param| 387 k, v = *param.split('=', 2) 388 result[k.strip] = v.strip 389 end 390 result 391 end 392 393 # Sets the content type in an HTTP header. 394 # The +type+ should be a full HTTP content type, e.g. "text/html". 395 # The +params+ are an optional Hash of parameters to add after the 396 # content type, e.g. {'charset' => 'iso-8859-1'} 397 def set_content_type(type, params = {}) 398 @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] 399 end 400 401 alias content_type= set_content_type 402 403 # Set header fields and a body from HTML form data. 404 # +params+ should be an Array of Arrays or 405 # a Hash containing HTML form data. 406 # Optional argument +sep+ means data record separator. 407 # 408 # Values are URL encoded as necessary and the content-type is set to 409 # application/x-www-form-urlencoded 410 # 411 # Example: 412 # http.form_data = {"q" => "ruby", "lang" => "en"} 413 # http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"} 414 # http.set_form_data({"q" => "ruby", "lang" => "en"}, ';') 415 # 416 def set_form_data(params, sep = '&') 417 query = URI.encode_www_form(params) 418 query.gsub!(/&/, sep) if sep != '&' 419 self.body = query 420 self.content_type = 'application/x-www-form-urlencoded' 421 end 422 423 alias form_data= set_form_data 424 425 # Set an HTML form data set. 426 # +params+ is the form data set; it is an Array of Arrays or a Hash 427 # +enctype is the type to encode the form data set. 428 # It is application/x-www-form-urlencoded or multipart/form-data. 429 # +formopt+ is an optional hash to specify the detail. 430 # 431 # boundary:: the boundary of the multipart message 432 # charset:: the charset of the message. All names and the values of 433 # non-file fields are encoded as the charset. 434 # 435 # Each item of params is an array and contains following items: 436 # +name+:: the name of the field 437 # +value+:: the value of the field, it should be a String or a File 438 # +opt+:: an optional hash to specify additional information 439 # 440 # Each item is a file field or a normal field. 441 # If +value+ is a File object or the +opt+ have a filename key, 442 # the item is treated as a file field. 443 # 444 # If Transfer-Encoding is set as chunked, this send the request in 445 # chunked encoding. Because chunked encoding is HTTP/1.1 feature, 446 # you must confirm the server to support HTTP/1.1 before sending it. 447 # 448 # Example: 449 # http.set_form([["q", "ruby"], ["lang", "en"]]) 450 # 451 # See also RFC 2388, RFC 2616, HTML 4.01, and HTML5 452 # 453 def set_form(params, enctype='application/x-www-form-urlencoded', formopt={}) 454 @body_data = params 455 @body = nil 456 @body_stream = nil 457 @form_option = formopt 458 case enctype 459 when /\Aapplication\/x-www-form-urlencoded\z/i, 460 /\Amultipart\/form-data\z/i 461 self.content_type = enctype 462 else 463 raise ArgumentError, "invalid enctype: #{enctype}" 464 end 465 end 466 467 # Set the Authorization: header for "Basic" authorization. 468 def basic_auth(account, password) 469 @header['authorization'] = [basic_encode(account, password)] 470 end 471 472 # Set Proxy-Authorization: header for "Basic" authorization. 473 def proxy_basic_auth(account, password) 474 @header['proxy-authorization'] = [basic_encode(account, password)] 475 end 476 477 def basic_encode(account, password) 478 'Basic ' + ["#{account}:#{password}"].pack('m0') 479 end 480 private :basic_encode 481 482 def connection_close? 483 token = /(?:\A|,)\s*close\s*(?:\z|,)/i 484 @header['connection']&.grep(token) {return true} 485 @header['proxy-connection']&.grep(token) {return true} 486 false 487 end 488 489 def connection_keep_alive? 490 token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i 491 @header['connection']&.grep(token) {return true} 492 @header['proxy-connection']&.grep(token) {return true} 493 false 494 end 495 496end 497