# A `Hash`-like object that holds HTTP headers. # # Two headers are considered the same if their downcase representation is the same # (in which `_` is the downcase version of `-`). # # NOTE: To use `Headers`, you must explicitly import it with `require "http/headers"` struct HTTP::Headers include Enumerable({String, Array(String)}) # :nodoc: record Key, name : String do def hash(hasher) name.each_byte do |c| hasher = normalize_byte(c).hash(hasher) end hasher end def ==(key2) key1 = name key2 = key2.name return false if key1.bytesize != key2.bytesize cstr1 = key1.to_unsafe cstr2 = key2.to_unsafe key1.bytesize.times do |i| next if cstr1[i] == cstr2[i] # Optimize the common case byte1 = normalize_byte(cstr1[i]) byte2 = normalize_byte(cstr2[i]) return false if byte1 != byte2 end true end private def normalize_byte(byte) char = byte.unsafe_chr return byte if char.ascii_lowercase? || char == '-' # Optimize the common case return byte + 32 if char.ascii_uppercase? return '-'.ord if char == '_' byte end end def initialize # We keep a Hash with String | Array(String) values because # the most common case is a single value and so we avoid allocating # memory for arrays. @hash = Hash(Key, String | Array(String)).new end def []=(key : String, value : String) : String check_invalid_header_content(value) @hash[wrap(key)] = value end def []=(key : String, value : Array(String)) : Array(String) value.each { |val| check_invalid_header_content val } @hash[wrap(key)] = value end def [](key : String) : String values = @hash[wrap(key)] concat values end def []?(key : String) : String? fetch(key, nil) end # Returns if among the headers for *key* there is some that contains *word* as a value. # The *word* is expected to match between word boundaries (i.e. non-alphanumeric chars). # # ``` # require "http/headers" # # headers = HTTP::Headers{"Connection" => "keep-alive, Upgrade"} # headers.includes_word?("Connection", "Upgrade") # => true # ``` def includes_word?(key : String, word : String) : Bool return false if word.empty? values = @hash[wrap(key)]? case values when Nil false when String includes_word_in_header_value?(word.downcase, values.downcase) else word = word.downcase values.any? do |value| includes_word_in_header_value?(word, value.downcase) end end end private def includes_word_in_header_value?(word, value) offset = 0 while true start = value.index(word, offset) return false unless start offset = start + word.size # check if the match is not surrounded by alphanumeric chars next if start > 0 && value[start - 1].ascii_alphanumeric? next if start + word.size < value.size && value[start + word.size].ascii_alphanumeric? return true end false end # Adds a header with *key* and *value* to the header set. If a header with # *key* already exists in the set, *value* is appended to the existing header. # # ``` # require "http/headers" # # headers = HTTP::Headers.new # headers.add("Connection", "keep-alive") # headers["Connection"] # => "keep-alive" # headers.add("Connection", "Upgrade") # headers["Connection"] # => "keep-alive,Upgrade" # ``` def add(key : String, value : String) : self check_invalid_header_content value unsafe_add(key, value) self end def add(key : String, value : Array(String)) : self value.each { |val| check_invalid_header_content val } unsafe_add(key, value) self end def add?(key : String, value : String) : Bool return false unless valid_value?(value) unsafe_add(key, value) true end def add?(key : String, value : Array(String)) : Bool value.each { |val| return false unless valid_value?(val) } unsafe_add(key, value) true end def fetch(key : String, default : String?) : String? fetch(wrap(key)) { default } end def fetch(key, &) values = @hash[wrap(key)]? values ? concat(values) : yield key end def has_key?(key : String) : Bool @hash.has_key? wrap(key) end def empty? : Bool @hash.empty? end def delete(key : String) : String? values = @hash.delete wrap(key) values ? concat(values) : nil end def merge!(other) : self other.each do |key, value| self[key] = value end self end # Equality operator. # # Returns `true` if *other* is equal to `self`. # # Keys are matched case-insensitive. # String values are treated equal to an array values with the same string as # single element. # # ``` # HTTP::Headers{"Foo" => "bar"} == HTTP::Headers{"Foo" => "bar"} # => true # HTTP::Headers{"Foo" => "bar"} == HTTP::Headers{"foo" => "bar"} # => true # HTTP::Headers{"Foo" => "bar"} == HTTP::Headers{"Foo" => ["bar"]} # => true # HTTP::Headers{"Foo" => "bar"} == HTTP::Headers{"Foo" => "baz"} # => false # ``` def ==(other : self) : Bool # Adapts `Hash#==` to treat string values equal to a single element array. return false unless @hash.size == other.@hash.size other.@hash.each do |key, value| this_value = @hash.fetch(key) { return false } case {value, this_value} in {String, String}, {Array, Array} return false unless this_value == value in {String, Array} return false unless this_value.size == 1 && this_value.unsafe_fetch(0) == value in {Array, String} return false unless value.size == 1 && value.unsafe_fetch(0) == this_value end end true end # See `Object#hash(hasher)` def hash(hasher) # Adapts `Hash#hash` to ensure consistency with equality operator. # The hash value must be the same regardless of the # order of the keys. result = hasher.result @hash.each do |key, value| copy = hasher copy = key.hash(copy) if value.is_a?(Array) copy = value.hash(copy) else copy = 1.hash(copy) copy = value.hash(copy) end result &+= copy.result end result.hash(hasher) end def each(&) @hash.each do |key, value| yield({key.name, cast(value)}) end end def get(key : String) : Array(String) cast @hash[wrap(key)] end # :nodoc: def get(key : HTTP::Headers::Key) : Array(String) cast @hash[key] end def get?(key : String) : Array(String)? @hash[wrap(key)]?.try { |value| cast(value) } end def dup dup = HTTP::Headers.new @hash.each do |key, value| dup.@hash[key] = value end dup end def clone : HTTP::Headers dup end def same?(other : HTTP::Headers) : Bool object_id == other.object_id end def to_s(io : IO) : Nil io << "HTTP::Headers{" @hash.each_with_index do |(key, values), index| io << ", " if index > 0 key.name.inspect(io) io << " => " case values when Array if values.size == 1 values.first.inspect(io) else values.inspect(io) end else values.inspect(io) end end io << '}' end def inspect(io : IO) : Nil to_s(io) end def pretty_print(pp) pp.list("HTTP::Headers{", @hash.keys.sort_by!(&.name), "}") do |key| pp.group do key.name.pretty_print(pp) pp.text " =>" pp.nest do pp.breakable values = get(key) if values.size == 1 values.first.pretty_print(pp) else values.pretty_print(pp) end end end end end # Serializes headers according to the HTTP protocol. # # Prints a list of HTTP header fields in the format described in [RFC 7230 ยง3.2](https://www.rfc-editor.org/rfc/rfc7230#section-3.2), # with each field terminated by a CRLF sequence (`"\r\n"`). # # The serialization does *not* include a double CRLF sequence at the end. # # ``` # headers = HTTP::Headers{"foo" => "bar", "baz" => %w[qux qox]} # headers.serialize # => "foo: bar\r\nbaz: qux\r\nbaz: qox\r\n" # ``` def serialize : String String.build do |io| serialize(io) end end # :ditto: def serialize(io : IO) : Nil each do |name, values| values.each do |value| io << name << ": " << value << "\r\n" end end end def valid_value?(value : String) : Bool invalid_value_char(value).nil? end def clear @hash.clear end def object_id @hash.object_id end private def unsafe_add(key, value : String) key = wrap(key) existing = @hash[key]? if existing if existing.is_a?(Array) existing << value else @hash[key] = [existing, value] end else @hash[key] = value end end private def unsafe_add(key, value : Array(String)) key = wrap(key) existing = @hash[key]? if existing if existing.is_a?(Array) existing.concat value else new_value = [existing] new_value.concat(value) @hash[key] = new_value end else @hash[key] = value end end private def wrap(key) key.is_a?(Key) ? key : Key.new(key) end private def cast(value : String) [value] end private def cast(value : Array(String)) value end private def concat(values : String) values end private def concat(values : Array(String)) case values.size when 0 "" when 1 values.first else values.join "," end end private def check_invalid_header_content(value) if char = invalid_value_char(value) raise ArgumentError.new("Header content contains invalid character #{char.inspect}") end end private def valid_char?(char) # According to RFC 7230, characters accepted as HTTP header # are '\t', ' ', all US-ASCII printable characters and # range from '\x80' to '\xff' (but the last is obsoleted.) return true if char == '\t' if char < ' ' || char > '\u{ff}' || char == '\u{7f}' return false end true end private def invalid_value_char(value) value.each_byte do |byte| unless valid_char?(char = byte.unsafe_chr) return char end end end end