# frozen_string_literal: true
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require_relative 'header/split'
require_relative 'header/multiple'
require_relative 'header/cookie'
require_relative 'header/connection'
require_relative 'header/cache_control'
require_relative 'header/etag'
require_relative 'header/etags'
require_relative 'header/vary'
require_relative 'header/authorization'
module Protocol
module HTTP
# Headers are an array of key-value pairs. Some header keys represent multiple values.
class Headers
Split = Header::Split
Multiple = Header::Multiple
TRAILERS = 'trailers'
# Construct an instance from a headers Array or Hash. No-op if already an instance of `Headers`. If the underlying array is frozen, it will be duped.
# @return [Headers] an instance of headers.
def self.[] headers
if headers.nil?
return self.new
end
if headers.is_a?(self)
if headers.frozen?
return headers.dup
else
return headers
end
end
fields = headers.to_a
if fields.frozen?
fields = fields.dup
end
return self.new(fields)
end
def initialize(fields = [], indexed = nil)
@fields = fields
@indexed = indexed
# Marks where trailers start in the @fields array.
@tail = nil
end
def initialize_dup(other)
super
@fields = @fields.dup
@indexed = @indexed.dup
end
def clear
@fields.clear
@indexed = nil
@tail = nil
end
# Flatten trailers into the headers.
def flatten!
if @tail
self.delete(TRAILERS)
@tail = nil
end
return self
end
def flatten
self.dup.flatten!
end
# An array of `[key, value]` pairs.
attr :fields
# @return the trailers if there are any.
def trailers?
@tail != nil
end
# Record the current headers, and prepare to receive trailers.
def trailers!(&block)
return nil unless self.include?(TRAILERS)
@tail ||= @fields.size
return to_enum(:trailers!) unless block_given?
if @tail
@fields.drop(@tail).each(&block)
end
end
# Enumerate all trailers, if there are any.
def trailers(&block)
return to_enum(:trailers) unless block_given?
if @tail
@fields.drop(@tail).each(&block)
end
end
def freeze
return if frozen?
# Ensure @indexed is generated:
self.to_h
@fields.freeze
@indexed.freeze
super
end
def empty?
@fields.empty?
end
def each(&block)
@fields.each(&block)
end
def include? key
self[key] != nil
end
def keys
self.to_h.keys
end
def extract(keys)
deleted, @fields = @fields.partition do |field|
keys.include?(field.first.downcase)
end
if @indexed
keys.each do |key|
@indexed.delete(key)
end
end
return deleted
end
# Add the specified header key value pair.
#
# @param key [String] the header key.
# @param value [String] the header value to assign.
def add(key, value)
self[key] = value
end
# Set the specified header key to the specified value, replacing any existing header keys with the same name.
# @param key [String] the header key to replace.
# @param value [String] the header value to assign.
def set(key, value)
# TODO This could be a bit more efficient:
self.delete(key)
self.add(key, value)
end
def merge!(headers)
headers.each do |key, value|
self[key] = value
end
return self
end
def merge(headers)
self.dup.merge!(headers)
end
# Append the value to the given key. Some values can be appended multiple times, others can only be set once.
# @param key [String] The header key.
# @param value The header value.
def []= key, value
if @indexed
merge_into(@indexed, key.downcase, value)
end
@fields << [key, value]
end
POLICY = {
# Headers which may only be specified once.
'content-type' => false,
'content-disposition' => false,
'content-length' => false,
'user-agent' => false,
'referer' => false,
'host' => false,
'if-modified-since' => false,
'if-unmodified-since' => false,
'from' => false,
'location' => false,
'max-forwards' => false,
# Custom headers:
'connection' => Header::Connection,
'cache-control' => Header::CacheControl,
'vary' => Header::Vary,
# Headers specifically for proxies:
'via' => Split,
'x-forwarded-for' => Split,
# Authorization headers:
'authorization' => Header::Authorization,
'proxy-authorization' => Header::Authorization,
# Cache validations:
'etag' => Header::ETag,
'if-match' => Header::ETags,
'if-none-match' => Header::ETags,
# Headers which may be specified multiple times, but which can't be concatenated:
'www-authenticate' => Multiple,
'proxy-authenticate' => Multiple,
# Custom headers:
'set-cookie' => Header::SetCookie,
'cookie' => Header::Cookie,
}.tap{|hash| hash.default = Split}
# Delete all headers with the given key, and return the merged value.
def delete(key)
deleted, @fields = @fields.partition do |field|
field.first.downcase == key
end
if deleted.empty?
return nil
end
if @indexed
return @indexed.delete(key)
elsif policy = POLICY[key]
(key, value), *tail = deleted
merged = policy.new(value)
tail.each{|k,v| merged << v}
return merged
else
key, value = deleted.last
return value
end
end
protected def merge_into(hash, key, value)
if policy = POLICY[key]
if current_value = hash[key]
current_value << value
else
hash[key] = policy.new(value)
end
else
# We can't merge these, we only expose the last one set.
hash[key] = value
end
end
def [] key
to_h[key]
end
# A hash table of `{key, policy[key].map(values)}`
def to_h
@indexed ||= @fields.inject({}) do |hash, (key, value)|
merge_into(hash, key.downcase, value)
hash
end
end
def inspect
"#<#{self.class} #{@fields.inspect}>"
end
def == other
case other
when Hash
to_h == other
when Headers
@fields == other.fields
else
@fields == other
end
end
# Used for merging objects into a sequential list of headers. Normalizes header keys and values.
class Merged
include Enumerable
def initialize(*all)
@all = all
end
def clear
@all.clear
end
def << headers
@all << headers
return self
end
# @yield [String, String] header key (lower case) and value (as string).
def each(&block)
@all.each do |headers|
headers.each do |key, value|
yield key.downcase, value.to_s
end
end
end
end
end
end
end