1'use strict' 2const invalidTokenRegex = /[^^_`a-zA-Z\-0-9!#$%&'*+.|~]/ 3const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/ 4 5const validateName = name => { 6 name = `${name}` 7 if (invalidTokenRegex.test(name) || name === '') { 8 throw new TypeError(`${name} is not a legal HTTP header name`) 9 } 10} 11 12const validateValue = value => { 13 value = `${value}` 14 if (invalidHeaderCharRegex.test(value)) { 15 throw new TypeError(`${value} is not a legal HTTP header value`) 16 } 17} 18 19const find = (map, name) => { 20 name = name.toLowerCase() 21 for (const key in map) { 22 if (key.toLowerCase() === name) { 23 return key 24 } 25 } 26 return undefined 27} 28 29const MAP = Symbol('map') 30class Headers { 31 constructor (init = undefined) { 32 this[MAP] = Object.create(null) 33 if (init instanceof Headers) { 34 const rawHeaders = init.raw() 35 const headerNames = Object.keys(rawHeaders) 36 for (const headerName of headerNames) { 37 for (const value of rawHeaders[headerName]) { 38 this.append(headerName, value) 39 } 40 } 41 return 42 } 43 44 // no-op 45 if (init === undefined || init === null) { 46 return 47 } 48 49 if (typeof init === 'object') { 50 const method = init[Symbol.iterator] 51 if (method !== null && method !== undefined) { 52 if (typeof method !== 'function') { 53 throw new TypeError('Header pairs must be iterable') 54 } 55 56 // sequence<sequence<ByteString>> 57 // Note: per spec we have to first exhaust the lists then process them 58 const pairs = [] 59 for (const pair of init) { 60 if (typeof pair !== 'object' || 61 typeof pair[Symbol.iterator] !== 'function') { 62 throw new TypeError('Each header pair must be iterable') 63 } 64 const arrPair = Array.from(pair) 65 if (arrPair.length !== 2) { 66 throw new TypeError('Each header pair must be a name/value tuple') 67 } 68 pairs.push(arrPair) 69 } 70 71 for (const pair of pairs) { 72 this.append(pair[0], pair[1]) 73 } 74 } else { 75 // record<ByteString, ByteString> 76 for (const key of Object.keys(init)) { 77 this.append(key, init[key]) 78 } 79 } 80 } else { 81 throw new TypeError('Provided initializer must be an object') 82 } 83 } 84 85 get (name) { 86 name = `${name}` 87 validateName(name) 88 const key = find(this[MAP], name) 89 if (key === undefined) { 90 return null 91 } 92 93 return this[MAP][key].join(', ') 94 } 95 96 forEach (callback, thisArg = undefined) { 97 let pairs = getHeaders(this) 98 for (let i = 0; i < pairs.length; i++) { 99 const [name, value] = pairs[i] 100 callback.call(thisArg, value, name, this) 101 // refresh in case the callback added more headers 102 pairs = getHeaders(this) 103 } 104 } 105 106 set (name, value) { 107 name = `${name}` 108 value = `${value}` 109 validateName(name) 110 validateValue(value) 111 const key = find(this[MAP], name) 112 this[MAP][key !== undefined ? key : name] = [value] 113 } 114 115 append (name, value) { 116 name = `${name}` 117 value = `${value}` 118 validateName(name) 119 validateValue(value) 120 const key = find(this[MAP], name) 121 if (key !== undefined) { 122 this[MAP][key].push(value) 123 } else { 124 this[MAP][name] = [value] 125 } 126 } 127 128 has (name) { 129 name = `${name}` 130 validateName(name) 131 return find(this[MAP], name) !== undefined 132 } 133 134 delete (name) { 135 name = `${name}` 136 validateName(name) 137 const key = find(this[MAP], name) 138 if (key !== undefined) { 139 delete this[MAP][key] 140 } 141 } 142 143 raw () { 144 return this[MAP] 145 } 146 147 keys () { 148 return new HeadersIterator(this, 'key') 149 } 150 151 values () { 152 return new HeadersIterator(this, 'value') 153 } 154 155 [Symbol.iterator] () { 156 return new HeadersIterator(this, 'key+value') 157 } 158 159 entries () { 160 return new HeadersIterator(this, 'key+value') 161 } 162 163 get [Symbol.toStringTag] () { 164 return 'Headers' 165 } 166 167 static exportNodeCompatibleHeaders (headers) { 168 const obj = Object.assign(Object.create(null), headers[MAP]) 169 170 // http.request() only supports string as Host header. This hack makes 171 // specifying custom Host header possible. 172 const hostHeaderKey = find(headers[MAP], 'Host') 173 if (hostHeaderKey !== undefined) { 174 obj[hostHeaderKey] = obj[hostHeaderKey][0] 175 } 176 177 return obj 178 } 179 180 static createHeadersLenient (obj) { 181 const headers = new Headers() 182 for (const name of Object.keys(obj)) { 183 if (invalidTokenRegex.test(name)) { 184 continue 185 } 186 187 if (Array.isArray(obj[name])) { 188 for (const val of obj[name]) { 189 if (invalidHeaderCharRegex.test(val)) { 190 continue 191 } 192 193 if (headers[MAP][name] === undefined) { 194 headers[MAP][name] = [val] 195 } else { 196 headers[MAP][name].push(val) 197 } 198 } 199 } else if (!invalidHeaderCharRegex.test(obj[name])) { 200 headers[MAP][name] = [obj[name]] 201 } 202 } 203 return headers 204 } 205} 206 207Object.defineProperties(Headers.prototype, { 208 get: { enumerable: true }, 209 forEach: { enumerable: true }, 210 set: { enumerable: true }, 211 append: { enumerable: true }, 212 has: { enumerable: true }, 213 delete: { enumerable: true }, 214 keys: { enumerable: true }, 215 values: { enumerable: true }, 216 entries: { enumerable: true }, 217}) 218 219const getHeaders = (headers, kind = 'key+value') => 220 Object.keys(headers[MAP]).sort().map( 221 kind === 'key' ? k => k.toLowerCase() 222 : kind === 'value' ? k => headers[MAP][k].join(', ') 223 : k => [k.toLowerCase(), headers[MAP][k].join(', ')] 224 ) 225 226const INTERNAL = Symbol('internal') 227 228class HeadersIterator { 229 constructor (target, kind) { 230 this[INTERNAL] = { 231 target, 232 kind, 233 index: 0, 234 } 235 } 236 237 get [Symbol.toStringTag] () { 238 return 'HeadersIterator' 239 } 240 241 next () { 242 /* istanbul ignore if: should be impossible */ 243 if (!this || Object.getPrototypeOf(this) !== HeadersIterator.prototype) { 244 throw new TypeError('Value of `this` is not a HeadersIterator') 245 } 246 247 const { target, kind, index } = this[INTERNAL] 248 const values = getHeaders(target, kind) 249 const len = values.length 250 if (index >= len) { 251 return { 252 value: undefined, 253 done: true, 254 } 255 } 256 257 this[INTERNAL].index++ 258 259 return { value: values[index], done: false } 260 } 261} 262 263// manually extend because 'extends' requires a ctor 264Object.setPrototypeOf(HeadersIterator.prototype, 265 Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))) 266 267module.exports = Headers 268