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

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