mirror of
https://github.com/github/codeql-action.git
synced 2025-12-28 02:00:12 +08:00
356 lines
11 KiB
JavaScript
356 lines
11 KiB
JavaScript
'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 }
|