You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					236 lines
				
				6.3 KiB
			
		
		
			
		
	
	
					236 lines
				
				6.3 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | /* eslint no-prototype-builtins: 0 */ | ||
|  | 
 | ||
|  | const { Readable } = require('stream') | ||
|  | const util = require('util') | ||
|  | const cookie = require('cookie') | ||
|  | const assert = require('assert') | ||
|  | const warning = require('process-warning')() | ||
|  | 
 | ||
|  | const parseURL = require('./parseURL') | ||
|  | const { EventEmitter } = require('events') | ||
|  | 
 | ||
|  | // request.connectin deprecation https://nodejs.org/api/http.html#http_request_connection
 | ||
|  | warning.create('FastifyDeprecationLightMyRequest', 'FST_LIGHTMYREQUEST_DEP01', 'You are accessing "request.connection", use "request.socket" instead.') | ||
|  | 
 | ||
|  | /** | ||
|  |  * Get hostname:port | ||
|  |  * | ||
|  |  * @param {URL} parsedURL | ||
|  |  * @return {String} | ||
|  |  */ | ||
|  | function hostHeaderFromURL (parsedURL) { | ||
|  |   return parsedURL.port | ||
|  |     ? parsedURL.host | ||
|  |     : parsedURL.hostname + (parsedURL.protocol === 'https:' ? ':443' : ':80') | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Mock socket object used to fake access to a socket for a request | ||
|  |  * | ||
|  |  * @constructor | ||
|  |  * @param {String} remoteAddress the fake address to show consumers of the socket | ||
|  |  */ | ||
|  | class MockSocket extends EventEmitter { | ||
|  |   constructor (remoteAddress) { | ||
|  |     super() | ||
|  |     this.remoteAddress = remoteAddress | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * CustomRequest | ||
|  |  * | ||
|  |  * @constructor | ||
|  |  * @param {Object} options | ||
|  |  * @param {(Object|String)} options.url || options.path | ||
|  |  * @param {String} [options.method='GET'] | ||
|  |  * @param {String} [options.remoteAddress] | ||
|  |  * @param {Object} [options.cookies] | ||
|  |  * @param {Object} [options.headers] | ||
|  |  * @param {Object} [options.query] | ||
|  |  * @param {Object} [options.Request] | ||
|  |  * @param {any} [options.payload] | ||
|  |  */ | ||
|  | function CustomRequest (options) { | ||
|  |   return new _CustomLMRRequest(this) | ||
|  | 
 | ||
|  |   function _CustomLMRRequest (obj) { | ||
|  |     Request.call(obj, { | ||
|  |       ...options, | ||
|  |       Request: undefined | ||
|  |     }) | ||
|  |     Object.assign(this, obj) | ||
|  | 
 | ||
|  |     for (const fn of Object.keys(Request.prototype)) { | ||
|  |       this.constructor.prototype[fn] = Request.prototype[fn] | ||
|  |     } | ||
|  | 
 | ||
|  |     util.inherits(this.constructor, options.Request) | ||
|  |     return this | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Request | ||
|  |  * | ||
|  |  * @constructor | ||
|  |  * @param {Object} options | ||
|  |  * @param {(Object|String)} options.url || options.path | ||
|  |  * @param {String} [options.method='GET'] | ||
|  |  * @param {String} [options.remoteAddress] | ||
|  |  * @param {Object} [options.cookies] | ||
|  |  * @param {Object} [options.headers] | ||
|  |  * @param {Object} [options.query] | ||
|  |  * @param {any} [options.payload] | ||
|  |  */ | ||
|  | function Request (options) { | ||
|  |   Readable.call(this, { | ||
|  |     autoDestroy: false | ||
|  |   }) | ||
|  | 
 | ||
|  |   const parsedURL = parseURL(options.url || options.path, options.query) | ||
|  | 
 | ||
|  |   this.url = parsedURL.pathname + parsedURL.search | ||
|  | 
 | ||
|  |   this.httpVersion = '1.1' | ||
|  |   this.method = options.method ? options.method.toUpperCase() : 'GET' | ||
|  | 
 | ||
|  |   this.headers = {} | ||
|  |   this.rawHeaders = [] | ||
|  |   const headers = options.headers || {} | ||
|  | 
 | ||
|  |   for (const field in headers) { | ||
|  |     const value = headers[field] | ||
|  |     assert(value !== undefined, 'invalid value "undefined" for header ' + field) | ||
|  |     this.headers[field.toLowerCase()] = '' + value | ||
|  |   } | ||
|  | 
 | ||
|  |   this.headers['user-agent'] = this.headers['user-agent'] || 'lightMyRequest' | ||
|  |   this.headers.host = this.headers.host || options.authority || hostHeaderFromURL(parsedURL) | ||
|  | 
 | ||
|  |   if (options.cookies) { | ||
|  |     const { cookies } = options | ||
|  |     const cookieValues = Object.keys(cookies).map(key => cookie.serialize(key, cookies[key])) | ||
|  |     if (this.headers.cookie) { | ||
|  |       cookieValues.unshift(this.headers.cookie) | ||
|  |     } | ||
|  |     this.headers.cookie = cookieValues.join('; ') | ||
|  |   } | ||
|  | 
 | ||
|  |   this.socket = new MockSocket(options.remoteAddress || '127.0.0.1') | ||
|  | 
 | ||
|  |   Object.defineProperty(this, 'connection', { | ||
|  |     get () { | ||
|  |       warning.emit('FST_LIGHTMYREQUEST_DEP01') | ||
|  |       return this.socket | ||
|  |     }, | ||
|  |     configurable: true | ||
|  |   }) | ||
|  | 
 | ||
|  |   // we keep both payload and body for compatibility reasons
 | ||
|  |   let payload = options.payload || options.body || null | ||
|  |   const payloadResume = payload && typeof payload.resume === 'function' | ||
|  | 
 | ||
|  |   if (payload && typeof payload !== 'string' && !payloadResume && !Buffer.isBuffer(payload)) { | ||
|  |     payload = JSON.stringify(payload) | ||
|  |     this.headers['content-type'] = this.headers['content-type'] || 'application/json' | ||
|  |   } | ||
|  | 
 | ||
|  |   // Set the content-length for the corresponding payload if none set
 | ||
|  |   if (payload && !payloadResume && !this.headers.hasOwnProperty('content-length')) { | ||
|  |     this.headers['content-length'] = (Buffer.isBuffer(payload) ? payload.length : Buffer.byteLength(payload)).toString() | ||
|  |   } | ||
|  | 
 | ||
|  |   for (const header of Object.keys(this.headers)) { | ||
|  |     this.rawHeaders.push(header, this.headers[header]) | ||
|  |   } | ||
|  | 
 | ||
|  |   // Use _lightMyRequest namespace to avoid collision with Node
 | ||
|  |   this._lightMyRequest = { | ||
|  |     payload, | ||
|  |     isDone: false, | ||
|  |     simulate: options.simulate || {} | ||
|  |   } | ||
|  | 
 | ||
|  |   return this | ||
|  | } | ||
|  | 
 | ||
|  | util.inherits(Request, Readable) | ||
|  | util.inherits(CustomRequest, Request) | ||
|  | 
 | ||
|  | Request.prototype.prepare = function (next) { | ||
|  |   const payload = this._lightMyRequest.payload | ||
|  |   if (!payload || typeof payload.resume !== 'function') { // does not quack like a stream
 | ||
|  |     return next() | ||
|  |   } | ||
|  | 
 | ||
|  |   const chunks = [] | ||
|  | 
 | ||
|  |   payload.on('data', (chunk) => chunks.push(Buffer.from(chunk))) | ||
|  | 
 | ||
|  |   payload.on('end', () => { | ||
|  |     const payload = Buffer.concat(chunks) | ||
|  |     this.headers['content-length'] = this.headers['content-length'] || ('' + payload.length) | ||
|  |     this._lightMyRequest.payload = payload | ||
|  |     return next() | ||
|  |   }) | ||
|  | 
 | ||
|  |   // Force to resume the stream. Needed for Stream 1
 | ||
|  |   payload.resume() | ||
|  | } | ||
|  | 
 | ||
|  | Request.prototype._read = function (size) { | ||
|  |   setImmediate(() => { | ||
|  |     if (this._lightMyRequest.isDone) { | ||
|  |       // 'end' defaults to true
 | ||
|  |       if (this._lightMyRequest.simulate.end !== false) { | ||
|  |         this.push(null) | ||
|  |       } | ||
|  | 
 | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     this._lightMyRequest.isDone = true | ||
|  | 
 | ||
|  |     if (this._lightMyRequest.payload) { | ||
|  |       if (this._lightMyRequest.simulate.split) { | ||
|  |         this.push(this._lightMyRequest.payload.slice(0, 1)) | ||
|  |         this.push(this._lightMyRequest.payload.slice(1)) | ||
|  |       } else { | ||
|  |         this.push(this._lightMyRequest.payload) | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (this._lightMyRequest.simulate.error) { | ||
|  |       this.emit('error', new Error('Simulated')) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (this._lightMyRequest.simulate.close) { | ||
|  |       this.emit('close') | ||
|  |     } | ||
|  | 
 | ||
|  |     // 'end' defaults to true
 | ||
|  |     if (this._lightMyRequest.simulate.end !== false) { | ||
|  |       this.push(null) | ||
|  |     } | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | Request.prototype.destroy = function (error) { | ||
|  |   if (this.destroyed) return | ||
|  |   this.destroyed = true | ||
|  | 
 | ||
|  |   if (error) { | ||
|  |     this._error = true | ||
|  |     process.nextTick(() => this.emit('error', error)) | ||
|  |   } | ||
|  | 
 | ||
|  |   process.nextTick(() => this.emit('close')) | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = Request | ||
|  | module.exports.Request = Request | ||
|  | module.exports.CustomRequest = CustomRequest |