• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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