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
							 |