Permalink
Cannot retrieve contributors at this time
355 lines (292 sloc)
11.1 KB
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
codeql-action/node_modules/nock/lib/intercepted_request_router.js
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
const debug = require('debug')('nock.request_overrider') | |
const { | |
IncomingMessage, | |
ClientRequest, | |
request: originalHttpRequest, | |
} = require('http') | |
const { request: originalHttpsRequest } = require('https') | |
const propagate = require('propagate') | |
const common = require('./common') | |
const globalEmitter = require('./global_emitter') | |
const Socket = require('./socket') | |
const { playbackInterceptor } = require('./playback_interceptor') | |
function socketOnClose(req) { | |
debug('socket close') | |
if (!req.res && !req.socket._hadError) { | |
// If we don't have a response then we know that the socket | |
// ended prematurely and we need to emit an error on the request. | |
req.socket._hadError = true | |
const err = new Error('socket hang up') | |
err.code = 'ECONNRESET' | |
req.emit('error', err) | |
} | |
req.emit('close') | |
} | |
/** | |
* Given a group of interceptors, appropriately route an outgoing request. | |
* Identify which interceptor ought to respond, if any, then delegate to | |
* `playbackInterceptor()` to consume the request itself. | |
*/ | |
class InterceptedRequestRouter { | |
constructor({ req, options, interceptors }) { | |
this.req = req | |
this.options = { | |
// We may be changing the options object and we don't want those changes | |
// affecting the user so we use a clone of the object. | |
...options, | |
// We use lower-case header field names throughout Nock. | |
headers: common.headersFieldNamesToLowerCase( | |
options.headers || {}, | |
false | |
), | |
} | |
this.interceptors = interceptors | |
this.socket = new Socket(options) | |
// support setting `timeout` using request `options` | |
// https://nodejs.org/docs/latest-v12.x/api/http.html#http_http_request_url_options_callback | |
// any timeout in the request options override any timeout in the agent options. | |
// per https://github.com/nodejs/node/pull/21204 | |
const timeout = | |
options.timeout || | |
(options.agent && options.agent.options && options.agent.options.timeout) | |
if (timeout) { | |
this.socket.setTimeout(timeout) | |
} | |
this.response = new IncomingMessage(this.socket) | |
this.requestBodyBuffers = [] | |
this.playbackStarted = false | |
// For parity with Node, it's important the socket event is emitted before we begin playback. | |
// This flag is set when playback is triggered if we haven't yet gotten the | |
// socket event to indicate that playback should start as soon as it comes in. | |
this.readyToStartPlaybackOnSocketEvent = false | |
this.attachToReq() | |
// Emit a fake socket event on the next tick to mimic what would happen on a real request. | |
// Some clients listen for a 'socket' event to be emitted before calling end(), | |
// which causes Nock to hang. | |
process.nextTick(() => this.connectSocket()) | |
} | |
attachToReq() { | |
const { req, options } = this | |
for (const [name, val] of Object.entries(options.headers)) { | |
req.setHeader(name.toLowerCase(), val) | |
} | |
if (options.auth && !options.headers.authorization) { | |
req.setHeader( | |
// We use lower-case header field names throughout Nock. | |
'authorization', | |
`Basic ${Buffer.from(options.auth).toString('base64')}` | |
) | |
} | |
req.path = options.path | |
req.method = options.method | |
req.write = (...args) => this.handleWrite(...args) | |
req.end = (...args) => this.handleEnd(...args) | |
req.flushHeaders = (...args) => this.handleFlushHeaders(...args) | |
// https://github.com/nock/nock/issues/256 | |
if (options.headers.expect === '100-continue') { | |
common.setImmediate(() => { | |
debug('continue') | |
req.emit('continue') | |
}) | |
} | |
} | |
connectSocket() { | |
const { req, socket } = this | |
if (common.isRequestDestroyed(req)) { | |
return | |
} | |
// ClientRequest.connection is an alias for ClientRequest.socket | |
// https://nodejs.org/api/http.html#http_request_socket | |
// https://github.com/nodejs/node/blob/b0f75818f39ed4e6bd80eb7c4010c1daf5823ef7/lib/_http_client.js#L640-L641 | |
// The same Socket is shared between the request and response to mimic native behavior. | |
req.socket = req.connection = socket | |
propagate(['error', 'timeout'], socket, req) | |
socket.on('close', () => socketOnClose(req)) | |
socket.connecting = false | |
req.emit('socket', socket) | |
// https://nodejs.org/api/net.html#net_event_connect | |
socket.emit('connect') | |
// https://nodejs.org/api/tls.html#tls_event_secureconnect | |
if (socket.authorized) { | |
socket.emit('secureConnect') | |
} | |
if (this.readyToStartPlaybackOnSocketEvent) { | |
this.maybeStartPlayback() | |
} | |
} | |
// from docs: When write function is called with empty string or buffer, it does nothing and waits for more input. | |
// However, actually implementation checks the state of finished and aborted before checking if the first arg is empty. | |
handleWrite(...args) { | |
debug('request write') | |
let [buffer, encoding] = args | |
const { req } = this | |
if (req.finished) { | |
const err = new Error('write after end') | |
err.code = 'ERR_STREAM_WRITE_AFTER_END' | |
process.nextTick(() => req.emit('error', err)) | |
// It seems odd to return `true` here, not sure why you'd want to have | |
// the stream potentially written to more, but it's what Node does. | |
// https://github.com/nodejs/node/blob/a9270dcbeba4316b1e179b77ecb6c46af5aa8c20/lib/_http_outgoing.js#L662-L665 | |
return true | |
} | |
if (req.socket && req.socket.destroyed) { | |
return false | |
} | |
if (!buffer) { | |
return true | |
} | |
if (!Buffer.isBuffer(buffer)) { | |
buffer = Buffer.from(buffer, encoding) | |
} | |
this.requestBodyBuffers.push(buffer) | |
// writable.write encoding param is optional | |
// so if callback is present it's the last argument | |
const callback = args.length > 1 ? args[args.length - 1] : undefined | |
// can't use instanceof Function because some test runners | |
// run tests in vm.runInNewContext where Function is not same | |
// as that in the current context | |
// https://github.com/nock/nock/pull/1754#issuecomment-571531407 | |
if (typeof callback === 'function') { | |
callback() | |
} | |
common.setImmediate(function () { | |
req.emit('drain') | |
}) | |
return false | |
} | |
handleEnd(chunk, encoding, callback) { | |
debug('request end') | |
const { req } = this | |
// handle the different overloaded arg signatures | |
if (typeof chunk === 'function') { | |
callback = chunk | |
chunk = null | |
} else if (typeof encoding === 'function') { | |
callback = encoding | |
encoding = null | |
} | |
if (typeof callback === 'function') { | |
req.once('finish', callback) | |
} | |
if (chunk) { | |
req.write(chunk, encoding) | |
} | |
req.finished = true | |
this.maybeStartPlayback() | |
return req | |
} | |
handleFlushHeaders() { | |
debug('request flushHeaders') | |
this.maybeStartPlayback() | |
} | |
/** | |
* Set request headers of the given request. This is needed both during the | |
* routing phase, in case header filters were specified, and during the | |
* interceptor-playback phase, to correctly pass mocked request headers. | |
* TODO There are some problems with this; see https://github.com/nock/nock/issues/1718 | |
*/ | |
setHostHeaderUsingInterceptor(interceptor) { | |
const { req, options } = this | |
// If a filtered scope is being used we have to use scope's host in the | |
// header, otherwise 'host' header won't match. | |
// NOTE: We use lower-case header field names throughout Nock. | |
const HOST_HEADER = 'host' | |
if (interceptor.__nock_filteredScope && interceptor.__nock_scopeHost) { | |
options.headers[HOST_HEADER] = interceptor.__nock_scopeHost | |
req.setHeader(HOST_HEADER, interceptor.__nock_scopeHost) | |
} else { | |
// For all other cases, we always add host header equal to the requested | |
// host unless it was already defined. | |
if (options.host && !req.getHeader(HOST_HEADER)) { | |
let hostHeader = options.host | |
if (options.port === 80 || options.port === 443) { | |
hostHeader = hostHeader.split(':')[0] | |
} | |
req.setHeader(HOST_HEADER, hostHeader) | |
} | |
} | |
} | |
maybeStartPlayback() { | |
const { req, socket, playbackStarted } = this | |
// In order to get the events in the right order we need to delay playback | |
// if we get here before the `socket` event is emitted. | |
if (socket.connecting) { | |
this.readyToStartPlaybackOnSocketEvent = true | |
return | |
} | |
if (!common.isRequestDestroyed(req) && !playbackStarted) { | |
this.startPlayback() | |
} | |
} | |
startPlayback() { | |
debug('ending') | |
this.playbackStarted = true | |
const { req, response, socket, options, interceptors } = this | |
Object.assign(options, { | |
// Re-update `options` with the current value of `req.path` because badly | |
// behaving agents like superagent like to change `req.path` mid-flight. | |
path: req.path, | |
// Similarly, node-http-proxy will modify headers in flight, so we have | |
// to put the headers back into options. | |
// https://github.com/nock/nock/pull/1484 | |
headers: req.getHeaders(), | |
// Fixes https://github.com/nock/nock/issues/976 | |
protocol: `${options.proto}:`, | |
}) | |
interceptors.forEach(interceptor => { | |
this.setHostHeaderUsingInterceptor(interceptor) | |
}) | |
const requestBodyBuffer = Buffer.concat(this.requestBodyBuffers) | |
// When request body is a binary buffer we internally use in its hexadecimal | |
// representation. | |
const requestBodyIsUtf8Representable = | |
common.isUtf8Representable(requestBodyBuffer) | |
const requestBodyString = requestBodyBuffer.toString( | |
requestBodyIsUtf8Representable ? 'utf8' : 'hex' | |
) | |
const matchedInterceptor = interceptors.find(i => | |
i.match(req, options, requestBodyString) | |
) | |
if (matchedInterceptor) { | |
matchedInterceptor.scope.logger( | |
'interceptor identified, starting mocking' | |
) | |
matchedInterceptor.markConsumed() | |
// wait to emit the finish event until we know for sure an Interceptor is going to playback. | |
// otherwise an unmocked request might emit finish twice. | |
req.emit('finish') | |
playbackInterceptor({ | |
req, | |
socket, | |
options, | |
requestBodyString, | |
requestBodyIsUtf8Representable, | |
response, | |
interceptor: matchedInterceptor, | |
}) | |
} else { | |
globalEmitter.emit('no match', req, options, requestBodyString) | |
// Try to find a hostname match that allows unmocked. | |
const allowUnmocked = interceptors.some( | |
i => i.matchHostName(options) && i.options.allowUnmocked | |
) | |
if (allowUnmocked && req instanceof ClientRequest) { | |
const newReq = | |
options.proto === 'https' | |
? originalHttpsRequest(options) | |
: originalHttpRequest(options) | |
propagate(newReq, req) | |
// We send the raw buffer as we received it, not as we interpreted it. | |
newReq.end(requestBodyBuffer) | |
} else { | |
const reqStr = common.stringifyRequest(options, requestBodyString) | |
const err = new Error(`Nock: No match for request ${reqStr}`) | |
err.code = 'ERR_NOCK_NO_MATCH' | |
err.statusCode = err.status = 404 | |
req.destroy(err) | |
} | |
} | |
} | |
} | |
module.exports = { InterceptedRequestRouter } |