1// Copyright Joyent, Inc. and other Node contributors. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a 4// copy of this software and associated documentation files (the 5// "Software"), to deal in the Software without restriction, including 6// without limitation the rights to use, copy, modify, merge, publish, 7// distribute, sublicense, and/or sell copies of the Software, and to permit 8// persons to whom the Software is furnished to do so, subject to the 9// following conditions: 10// 11// The above copyright notice and this permission notice shall be included 12// in all copies or substantial portions of the Software. 13// 14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20// USE OR OTHER DEALINGS IN THE SOFTWARE. 21 22'use strict'; 23 24const { 25 ObjectDefineProperty, 26 ObjectSetPrototypeOf, 27 StringPrototypeCharCodeAt, 28 StringPrototypeSlice, 29 StringPrototypeToLowerCase, 30 Symbol, 31} = primordials; 32 33const { Readable, finished } = require('stream'); 34 35const kHeaders = Symbol('kHeaders'); 36const kHeadersDistinct = Symbol('kHeadersDistinct'); 37const kHeadersCount = Symbol('kHeadersCount'); 38const kTrailers = Symbol('kTrailers'); 39const kTrailersDistinct = Symbol('kTrailersDistinct'); 40const kTrailersCount = Symbol('kTrailersCount'); 41 42function readStart(socket) { 43 if (socket && !socket._paused && socket.readable) 44 socket.resume(); 45} 46 47function readStop(socket) { 48 if (socket) 49 socket.pause(); 50} 51 52/* Abstract base class for ServerRequest and ClientResponse. */ 53function IncomingMessage(socket) { 54 let streamOptions; 55 56 if (socket) { 57 streamOptions = { 58 highWaterMark: socket.readableHighWaterMark, 59 }; 60 } 61 62 Readable.call(this, streamOptions); 63 64 this._readableState.readingMore = true; 65 66 this.socket = socket; 67 68 this.httpVersionMajor = null; 69 this.httpVersionMinor = null; 70 this.httpVersion = null; 71 this.complete = false; 72 this[kHeaders] = null; 73 this[kHeadersCount] = 0; 74 this.rawHeaders = []; 75 this[kTrailers] = null; 76 this[kTrailersCount] = 0; 77 this.rawTrailers = []; 78 this.joinDuplicateHeaders = false; 79 this.aborted = false; 80 81 this.upgrade = null; 82 83 // request (server) only 84 this.url = ''; 85 this.method = null; 86 87 // response (client) only 88 this.statusCode = null; 89 this.statusMessage = null; 90 this.client = socket; 91 92 this._consuming = false; 93 // Flag for when we decide that this message cannot possibly be 94 // read by the user, so there's no point continuing to handle it. 95 this._dumped = false; 96} 97ObjectSetPrototypeOf(IncomingMessage.prototype, Readable.prototype); 98ObjectSetPrototypeOf(IncomingMessage, Readable); 99 100ObjectDefineProperty(IncomingMessage.prototype, 'connection', { 101 __proto__: null, 102 get: function() { 103 return this.socket; 104 }, 105 set: function(val) { 106 this.socket = val; 107 }, 108}); 109 110ObjectDefineProperty(IncomingMessage.prototype, 'headers', { 111 __proto__: null, 112 get: function() { 113 if (!this[kHeaders]) { 114 this[kHeaders] = {}; 115 116 const src = this.rawHeaders; 117 const dst = this[kHeaders]; 118 119 for (let n = 0; n < this[kHeadersCount]; n += 2) { 120 this._addHeaderLine(src[n + 0], src[n + 1], dst); 121 } 122 } 123 return this[kHeaders]; 124 }, 125 set: function(val) { 126 this[kHeaders] = val; 127 }, 128}); 129 130ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', { 131 __proto__: null, 132 get: function() { 133 if (!this[kHeadersDistinct]) { 134 this[kHeadersDistinct] = {}; 135 136 const src = this.rawHeaders; 137 const dst = this[kHeadersDistinct]; 138 139 for (let n = 0; n < this[kHeadersCount]; n += 2) { 140 this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst); 141 } 142 } 143 return this[kHeadersDistinct]; 144 }, 145 set: function(val) { 146 this[kHeadersDistinct] = val; 147 }, 148}); 149 150ObjectDefineProperty(IncomingMessage.prototype, 'trailers', { 151 __proto__: null, 152 get: function() { 153 if (!this[kTrailers]) { 154 this[kTrailers] = {}; 155 156 const src = this.rawTrailers; 157 const dst = this[kTrailers]; 158 159 for (let n = 0; n < this[kTrailersCount]; n += 2) { 160 this._addHeaderLine(src[n + 0], src[n + 1], dst); 161 } 162 } 163 return this[kTrailers]; 164 }, 165 set: function(val) { 166 this[kTrailers] = val; 167 }, 168}); 169 170ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', { 171 __proto__: null, 172 get: function() { 173 if (!this[kTrailersDistinct]) { 174 this[kTrailersDistinct] = {}; 175 176 const src = this.rawTrailers; 177 const dst = this[kTrailersDistinct]; 178 179 for (let n = 0; n < this[kTrailersCount]; n += 2) { 180 this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst); 181 } 182 } 183 return this[kTrailersDistinct]; 184 }, 185 set: function(val) { 186 this[kTrailersDistinct] = val; 187 }, 188}); 189 190IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { 191 if (callback) 192 this.on('timeout', callback); 193 this.socket.setTimeout(msecs); 194 return this; 195}; 196 197// Argument n cannot be factored out due to the overhead of 198// argument adaptor frame creation inside V8 in case that number of actual 199// arguments is different from expected arguments. 200// Ref: https://bugs.chromium.org/p/v8/issues/detail?id=10201 201// NOTE: Argument adapt frame issue might be solved in V8 engine v8.9. 202// Refactoring `n` out might be possible when V8 is upgraded to that 203// version. 204// Ref: https://v8.dev/blog/v8-release-89 205IncomingMessage.prototype._read = function _read(n) { 206 if (!this._consuming) { 207 this._readableState.readingMore = false; 208 this._consuming = true; 209 } 210 211 // We actually do almost nothing here, because the parserOnBody 212 // function fills up our internal buffer directly. However, we 213 // do need to unpause the underlying socket so that it flows. 214 if (this.socket.readable) 215 readStart(this.socket); 216}; 217 218// It's possible that the socket will be destroyed, and removed from 219// any messages, before ever calling this. In that case, just skip 220// it, since something else is destroying this connection anyway. 221IncomingMessage.prototype._destroy = function _destroy(err, cb) { 222 if (!this.readableEnded || !this.complete) { 223 this.aborted = true; 224 this.emit('aborted'); 225 } 226 227 // If aborted and the underlying socket is not already destroyed, 228 // destroy it. 229 // We have to check if the socket is already destroyed because finished 230 // does not call the callback when this method is invoked from `_http_client` 231 // in `test/parallel/test-http-client-spurious-aborted.js` 232 if (this.socket && !this.socket.destroyed && this.aborted) { 233 this.socket.destroy(err); 234 const cleanup = finished(this.socket, (e) => { 235 if (e?.code === 'ERR_STREAM_PREMATURE_CLOSE') { 236 e = null; 237 } 238 cleanup(); 239 process.nextTick(onError, this, e || err, cb); 240 }); 241 } else { 242 process.nextTick(onError, this, err, cb); 243 } 244}; 245 246IncomingMessage.prototype._addHeaderLines = _addHeaderLines; 247function _addHeaderLines(headers, n) { 248 if (headers && headers.length) { 249 let dest; 250 if (this.complete) { 251 this.rawTrailers = headers; 252 this[kTrailersCount] = n; 253 dest = this[kTrailers]; 254 } else { 255 this.rawHeaders = headers; 256 this[kHeadersCount] = n; 257 dest = this[kHeaders]; 258 } 259 260 if (dest) { 261 for (let i = 0; i < n; i += 2) { 262 this._addHeaderLine(headers[i], headers[i + 1], dest); 263 } 264 } 265 } 266} 267 268 269// This function is used to help avoid the lowercasing of a field name if it 270// matches a 'traditional cased' version of a field name. It then returns the 271// lowercased name to both avoid calling toLowerCase() a second time and to 272// indicate whether the field was a 'no duplicates' field. If a field is not a 273// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception 274// to this is the Set-Cookie header which is indicated by a `1` byte flag, since 275// it is an 'array' field and thus is treated differently in _addHeaderLines(). 276// TODO: perhaps http_parser could be returning both raw and lowercased versions 277// of known header names to avoid us having to call toLowerCase() for those 278// headers. 279function matchKnownFields(field, lowercased) { 280 switch (field.length) { 281 case 3: 282 if (field === 'Age' || field === 'age') return 'age'; 283 break; 284 case 4: 285 if (field === 'Host' || field === 'host') return 'host'; 286 if (field === 'From' || field === 'from') return 'from'; 287 if (field === 'ETag' || field === 'etag') return 'etag'; 288 if (field === 'Date' || field === 'date') return '\u0000date'; 289 if (field === 'Vary' || field === 'vary') return '\u0000vary'; 290 break; 291 case 6: 292 if (field === 'Server' || field === 'server') return 'server'; 293 if (field === 'Cookie' || field === 'cookie') return '\u0002cookie'; 294 if (field === 'Origin' || field === 'origin') return '\u0000origin'; 295 if (field === 'Expect' || field === 'expect') return '\u0000expect'; 296 if (field === 'Accept' || field === 'accept') return '\u0000accept'; 297 break; 298 case 7: 299 if (field === 'Referer' || field === 'referer') return 'referer'; 300 if (field === 'Expires' || field === 'expires') return 'expires'; 301 if (field === 'Upgrade' || field === 'upgrade') return '\u0000upgrade'; 302 break; 303 case 8: 304 if (field === 'Location' || field === 'location') 305 return 'location'; 306 if (field === 'If-Match' || field === 'if-match') 307 return '\u0000if-match'; 308 break; 309 case 10: 310 if (field === 'User-Agent' || field === 'user-agent') 311 return 'user-agent'; 312 if (field === 'Set-Cookie' || field === 'set-cookie') 313 return '\u0001'; 314 if (field === 'Connection' || field === 'connection') 315 return '\u0000connection'; 316 break; 317 case 11: 318 if (field === 'Retry-After' || field === 'retry-after') 319 return 'retry-after'; 320 break; 321 case 12: 322 if (field === 'Content-Type' || field === 'content-type') 323 return 'content-type'; 324 if (field === 'Max-Forwards' || field === 'max-forwards') 325 return 'max-forwards'; 326 break; 327 case 13: 328 if (field === 'Authorization' || field === 'authorization') 329 return 'authorization'; 330 if (field === 'Last-Modified' || field === 'last-modified') 331 return 'last-modified'; 332 if (field === 'Cache-Control' || field === 'cache-control') 333 return '\u0000cache-control'; 334 if (field === 'If-None-Match' || field === 'if-none-match') 335 return '\u0000if-none-match'; 336 break; 337 case 14: 338 if (field === 'Content-Length' || field === 'content-length') 339 return 'content-length'; 340 break; 341 case 15: 342 if (field === 'Accept-Encoding' || field === 'accept-encoding') 343 return '\u0000accept-encoding'; 344 if (field === 'Accept-Language' || field === 'accept-language') 345 return '\u0000accept-language'; 346 if (field === 'X-Forwarded-For' || field === 'x-forwarded-for') 347 return '\u0000x-forwarded-for'; 348 break; 349 case 16: 350 if (field === 'Content-Encoding' || field === 'content-encoding') 351 return '\u0000content-encoding'; 352 if (field === 'X-Forwarded-Host' || field === 'x-forwarded-host') 353 return '\u0000x-forwarded-host'; 354 break; 355 case 17: 356 if (field === 'If-Modified-Since' || field === 'if-modified-since') 357 return 'if-modified-since'; 358 if (field === 'Transfer-Encoding' || field === 'transfer-encoding') 359 return '\u0000transfer-encoding'; 360 if (field === 'X-Forwarded-Proto' || field === 'x-forwarded-proto') 361 return '\u0000x-forwarded-proto'; 362 break; 363 case 19: 364 if (field === 'Proxy-Authorization' || field === 'proxy-authorization') 365 return 'proxy-authorization'; 366 if (field === 'If-Unmodified-Since' || field === 'if-unmodified-since') 367 return 'if-unmodified-since'; 368 break; 369 } 370 if (lowercased) { 371 return '\u0000' + field; 372 } 373 return matchKnownFields(StringPrototypeToLowerCase(field), true); 374} 375// Add the given (field, value) pair to the message 376// 377// Per RFC2616, section 4.2 it is acceptable to join multiple instances of the 378// same header with a ', ' if the header in question supports specification of 379// multiple values this way. The one exception to this is the Cookie header, 380// which has multiple values joined with a '; ' instead. If a header's values 381// cannot be joined in either of these ways, we declare the first instance the 382// winner and drop the second. Extended header fields (those beginning with 383// 'x-') are always joined. 384IncomingMessage.prototype._addHeaderLine = _addHeaderLine; 385function _addHeaderLine(field, value, dest) { 386 field = matchKnownFields(field); 387 const flag = StringPrototypeCharCodeAt(field, 0); 388 if (flag === 0 || flag === 2) { 389 field = StringPrototypeSlice(field, 1); 390 // Make a delimited list 391 if (typeof dest[field] === 'string') { 392 dest[field] += (flag === 0 ? ', ' : '; ') + value; 393 } else { 394 dest[field] = value; 395 } 396 } else if (flag === 1) { 397 // Array header -- only Set-Cookie at the moment 398 if (dest['set-cookie'] !== undefined) { 399 dest['set-cookie'].push(value); 400 } else { 401 dest['set-cookie'] = [value]; 402 } 403 } else if (this.joinDuplicateHeaders) { 404 // RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2 405 // https://github.com/nodejs/node/issues/45699 406 // allow authorization multiple fields 407 // Make a delimited list 408 if (dest[field] === undefined) { 409 dest[field] = value; 410 } else { 411 dest[field] += ', ' + value; 412 } 413 } else if (dest[field] === undefined) { 414 // Drop duplicates 415 dest[field] = value; 416 } 417} 418 419IncomingMessage.prototype._addHeaderLineDistinct = _addHeaderLineDistinct; 420function _addHeaderLineDistinct(field, value, dest) { 421 field = StringPrototypeToLowerCase(field); 422 if (!dest[field]) { 423 dest[field] = [value]; 424 } else { 425 dest[field].push(value); 426 } 427} 428 429 430// Call this instead of resume() if we want to just 431// dump all the data to /dev/null 432IncomingMessage.prototype._dump = function _dump() { 433 if (!this._dumped) { 434 this._dumped = true; 435 // If there is buffered data, it may trigger 'data' events. 436 // Remove 'data' event listeners explicitly. 437 this.removeAllListeners('data'); 438 this.resume(); 439 } 440}; 441 442function onError(self, error, cb) { 443 // This is to keep backward compatible behavior. 444 // An error is emitted only if there are listeners attached to the event. 445 if (self.listenerCount('error') === 0) { 446 cb(); 447 } else { 448 cb(error); 449 } 450} 451 452module.exports = { 453 IncomingMessage, 454 readStart, 455 readStop, 456}; 457