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} = primordials; 28 29const Stream = require('stream'); 30 31function readStart(socket) { 32 if (socket && !socket._paused && socket.readable) 33 socket.resume(); 34} 35 36function readStop(socket) { 37 if (socket) 38 socket.pause(); 39} 40 41/* Abstract base class for ServerRequest and ClientResponse. */ 42function IncomingMessage(socket) { 43 let streamOptions; 44 45 if (socket) { 46 streamOptions = { 47 highWaterMark: socket.readableHighWaterMark 48 }; 49 } 50 51 Stream.Readable.call(this, { autoDestroy: false, ...streamOptions }); 52 53 this._readableState.readingMore = true; 54 55 this.socket = socket; 56 57 this.httpVersionMajor = null; 58 this.httpVersionMinor = null; 59 this.httpVersion = null; 60 this.complete = false; 61 this.headers = {}; 62 this.rawHeaders = []; 63 this.trailers = {}; 64 this.rawTrailers = []; 65 66 this.aborted = false; 67 68 this.upgrade = null; 69 70 // request (server) only 71 this.url = ''; 72 this.method = null; 73 74 // response (client) only 75 this.statusCode = null; 76 this.statusMessage = null; 77 this.client = socket; 78 79 this._consuming = false; 80 // Flag for when we decide that this message cannot possibly be 81 // read by the user, so there's no point continuing to handle it. 82 this._dumped = false; 83} 84ObjectSetPrototypeOf(IncomingMessage.prototype, Stream.Readable.prototype); 85ObjectSetPrototypeOf(IncomingMessage, Stream.Readable); 86 87ObjectDefineProperty(IncomingMessage.prototype, 'connection', { 88 get: function() { 89 return this.socket; 90 }, 91 set: function(val) { 92 this.socket = val; 93 } 94}); 95 96IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { 97 if (callback) 98 this.on('timeout', callback); 99 this.socket.setTimeout(msecs); 100 return this; 101}; 102 103// Argument n cannot be factored out due to the overhead of 104// argument adaptor frame creation inside V8 in case that number of actual 105// arguments is different from expected arguments. 106// Ref: https://bugs.chromium.org/p/v8/issues/detail?id=10201 107// NOTE: Argument adapt frame issue might be solved in V8 engine v8.9. 108// Refactoring `n` out might be possible when V8 is upgraded to that 109// version. 110// Ref: https://v8.dev/blog/v8-release-89 111IncomingMessage.prototype._read = function _read(n) { 112 if (!this._consuming) { 113 this._readableState.readingMore = false; 114 this._consuming = true; 115 } 116 117 // We actually do almost nothing here, because the parserOnBody 118 // function fills up our internal buffer directly. However, we 119 // do need to unpause the underlying socket so that it flows. 120 if (this.socket.readable) 121 readStart(this.socket); 122}; 123 124 125// It's possible that the socket will be destroyed, and removed from 126// any messages, before ever calling this. In that case, just skip 127// it, since something else is destroying this connection anyway. 128IncomingMessage.prototype.destroy = function destroy(error) { 129 if (this.socket) 130 this.socket.destroy(error); 131 return this; 132}; 133 134 135IncomingMessage.prototype._addHeaderLines = _addHeaderLines; 136function _addHeaderLines(headers, n) { 137 if (headers && headers.length) { 138 let dest; 139 if (this.complete) { 140 this.rawTrailers = headers; 141 dest = this.trailers; 142 } else { 143 this.rawHeaders = headers; 144 dest = this.headers; 145 } 146 147 for (let i = 0; i < n; i += 2) { 148 this._addHeaderLine(headers[i], headers[i + 1], dest); 149 } 150 } 151} 152 153 154// This function is used to help avoid the lowercasing of a field name if it 155// matches a 'traditional cased' version of a field name. It then returns the 156// lowercased name to both avoid calling toLowerCase() a second time and to 157// indicate whether the field was a 'no duplicates' field. If a field is not a 158// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception 159// to this is the Set-Cookie header which is indicated by a `1` byte flag, since 160// it is an 'array' field and thus is treated differently in _addHeaderLines(). 161// TODO: perhaps http_parser could be returning both raw and lowercased versions 162// of known header names to avoid us having to call toLowerCase() for those 163// headers. 164function matchKnownFields(field, lowercased) { 165 switch (field.length) { 166 case 3: 167 if (field === 'Age' || field === 'age') return 'age'; 168 break; 169 case 4: 170 if (field === 'Host' || field === 'host') return 'host'; 171 if (field === 'From' || field === 'from') return 'from'; 172 if (field === 'ETag' || field === 'etag') return 'etag'; 173 if (field === 'Date' || field === 'date') return '\u0000date'; 174 if (field === 'Vary' || field === 'vary') return '\u0000vary'; 175 break; 176 case 6: 177 if (field === 'Server' || field === 'server') return 'server'; 178 if (field === 'Cookie' || field === 'cookie') return '\u0002cookie'; 179 if (field === 'Origin' || field === 'origin') return '\u0000origin'; 180 if (field === 'Expect' || field === 'expect') return '\u0000expect'; 181 if (field === 'Accept' || field === 'accept') return '\u0000accept'; 182 break; 183 case 7: 184 if (field === 'Referer' || field === 'referer') return 'referer'; 185 if (field === 'Expires' || field === 'expires') return 'expires'; 186 if (field === 'Upgrade' || field === 'upgrade') return '\u0000upgrade'; 187 break; 188 case 8: 189 if (field === 'Location' || field === 'location') 190 return 'location'; 191 if (field === 'If-Match' || field === 'if-match') 192 return '\u0000if-match'; 193 break; 194 case 10: 195 if (field === 'User-Agent' || field === 'user-agent') 196 return 'user-agent'; 197 if (field === 'Set-Cookie' || field === 'set-cookie') 198 return '\u0001'; 199 if (field === 'Connection' || field === 'connection') 200 return '\u0000connection'; 201 break; 202 case 11: 203 if (field === 'Retry-After' || field === 'retry-after') 204 return 'retry-after'; 205 break; 206 case 12: 207 if (field === 'Content-Type' || field === 'content-type') 208 return 'content-type'; 209 if (field === 'Max-Forwards' || field === 'max-forwards') 210 return 'max-forwards'; 211 break; 212 case 13: 213 if (field === 'Authorization' || field === 'authorization') 214 return 'authorization'; 215 if (field === 'Last-Modified' || field === 'last-modified') 216 return 'last-modified'; 217 if (field === 'Cache-Control' || field === 'cache-control') 218 return '\u0000cache-control'; 219 if (field === 'If-None-Match' || field === 'if-none-match') 220 return '\u0000if-none-match'; 221 break; 222 case 14: 223 if (field === 'Content-Length' || field === 'content-length') 224 return 'content-length'; 225 break; 226 case 15: 227 if (field === 'Accept-Encoding' || field === 'accept-encoding') 228 return '\u0000accept-encoding'; 229 if (field === 'Accept-Language' || field === 'accept-language') 230 return '\u0000accept-language'; 231 if (field === 'X-Forwarded-For' || field === 'x-forwarded-for') 232 return '\u0000x-forwarded-for'; 233 break; 234 case 16: 235 if (field === 'Content-Encoding' || field === 'content-encoding') 236 return '\u0000content-encoding'; 237 if (field === 'X-Forwarded-Host' || field === 'x-forwarded-host') 238 return '\u0000x-forwarded-host'; 239 break; 240 case 17: 241 if (field === 'If-Modified-Since' || field === 'if-modified-since') 242 return 'if-modified-since'; 243 if (field === 'Transfer-Encoding' || field === 'transfer-encoding') 244 return '\u0000transfer-encoding'; 245 if (field === 'X-Forwarded-Proto' || field === 'x-forwarded-proto') 246 return '\u0000x-forwarded-proto'; 247 break; 248 case 19: 249 if (field === 'Proxy-Authorization' || field === 'proxy-authorization') 250 return 'proxy-authorization'; 251 if (field === 'If-Unmodified-Since' || field === 'if-unmodified-since') 252 return 'if-unmodified-since'; 253 break; 254 } 255 if (lowercased) { 256 return '\u0000' + field; 257 } 258 return matchKnownFields(field.toLowerCase(), true); 259} 260// Add the given (field, value) pair to the message 261// 262// Per RFC2616, section 4.2 it is acceptable to join multiple instances of the 263// same header with a ', ' if the header in question supports specification of 264// multiple values this way. The one exception to this is the Cookie header, 265// which has multiple values joined with a '; ' instead. If a header's values 266// cannot be joined in either of these ways, we declare the first instance the 267// winner and drop the second. Extended header fields (those beginning with 268// 'x-') are always joined. 269IncomingMessage.prototype._addHeaderLine = _addHeaderLine; 270function _addHeaderLine(field, value, dest) { 271 field = matchKnownFields(field); 272 const flag = field.charCodeAt(0); 273 if (flag === 0 || flag === 2) { 274 field = field.slice(1); 275 // Make a delimited list 276 if (typeof dest[field] === 'string') { 277 dest[field] += (flag === 0 ? ', ' : '; ') + value; 278 } else { 279 dest[field] = value; 280 } 281 } else if (flag === 1) { 282 // Array header -- only Set-Cookie at the moment 283 if (dest['set-cookie'] !== undefined) { 284 dest['set-cookie'].push(value); 285 } else { 286 dest['set-cookie'] = [value]; 287 } 288 } else if (dest[field] === undefined) { 289 // Drop duplicates 290 dest[field] = value; 291 } 292} 293 294 295// Call this instead of resume() if we want to just 296// dump all the data to /dev/null 297IncomingMessage.prototype._dump = function _dump() { 298 if (!this._dumped) { 299 this._dumped = true; 300 // If there is buffered data, it may trigger 'data' events. 301 // Remove 'data' event listeners explicitly. 302 this.removeAllListeners('data'); 303 this.resume(); 304 } 305}; 306 307module.exports = { 308 IncomingMessage, 309 readStart, 310 readStop 311}; 312