# 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.
module Async
module HTTP
class Headers
class Split < Array
COMMA = /\s*,\s*/
def initialize(value)
super(value.split(COMMA))
end
def << value
super value.split(COMMA)
end
def to_s
join(", ")
end
end
class Multiple < Array
def initialize(value)
super()
self << value
end
def to_s
join("\n")
end
end
def self.[] hash
self.new(hash.to_a)
end
def initialize(fields = [])
@fields = fields
@indexed = to_h
end
attr :fields
def freeze
return if frozen?
@indexed = to_h
super
end
def empty?
@fields.empty?
end
def each(&block)
@fields.each(&block)
end
def include? key
self[key] != nil
end
# Delete all headers with the given key, and return the value of the last one, if any.
def delete(key)
values, @fields = @fields.partition do |field|
field.first.downcase == key
end
if @indexed
@indexed.delete(key)
end
if field = values.last
return field.last
end
end
def slice!(keys)
values, @fields = @fields.partition do |field|
keys.include?(field.first.downcase)
end
if @indexed
keys.each do |key|
@indexed.delete(key)
end
end
end
def add(key, value)
self[key] = value
end
def []= key, value
@fields << [key, value]
if @indexed
# It would be good to do some kind of validation here.
merge(@indexed, key.downcase, value)
end
end
MERGE_POLICY = {
# Headers which may only be specified once.
'content-type' => false,
'content-disposition' => false,
'content-length' => false,
'user-agent' => false,
'referer' => false,
'host' => false,
'authorization' => false,
'proxy-authorization' => false,
'if-modified-since' => false,
'if-unmodified-since' => false,
'from' => false,
'location' => false,
'max-forwards' => false,
'connection' => Split,
# Headers specifically for proxies:
'via' => Split,
'x-forwarded-for' => Split,
# Headers which may be specified multiple times, but which can't be concatenated.
'set-cookie' => Multiple,
'www-authenticate' => Multiple,
'proxy-authenticate' => Multiple
}.tap{|hash| hash.default = Split}
def merge(hash, key, value)
if policy = MERGE_POLICY[key]
if current_value = hash[key]
current_value << value
else
hash[key] = policy.new(value)
end
else
raise ArgumentError, "Header #{key} can only be set once!" if hash.include?(key)
# We can't merge these, we only expose the last one set.
hash[key] = value
end
end
def [] key
@indexed ||= to_h
@indexed[key]
end
def to_h
@fields.inject({}) do |hash, (key, value)|
merge(hash, key.downcase, value)
hash
end
end
def == other
if other.is_a? Hash
to_h == other
else
@fields == other.fields
end
end
class Merged
def initialize(*all)
@all = all
end
def each(&block)
@all.each do |headers|
headers.each do |key, value|
yield key, value.to_s
end
end
end
end
end
end
end