1 | 'use strict'
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | const { InterceptedRequestRouter } = require('./intercepted_request_router')
|
8 | const common = require('./common')
|
9 | const { inherits } = require('util')
|
10 | const http = require('http')
|
11 | const debug = require('debug')('nock.intercept')
|
12 | const globalEmitter = require('./global_emitter')
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | function NetConnectNotAllowedError(host, path) {
|
25 | Error.call(this)
|
26 |
|
27 | this.name = 'NetConnectNotAllowedError'
|
28 | this.code = 'ENETUNREACH'
|
29 | this.message = `Nock: Disallowed net connect for "${host}${path}"`
|
30 |
|
31 | Error.captureStackTrace(this, this.constructor)
|
32 | }
|
33 |
|
34 | inherits(NetConnectNotAllowedError, Error)
|
35 |
|
36 | let allInterceptors = {}
|
37 | let allowNetConnect
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | function enableNetConnect(matcher) {
|
57 | if (typeof matcher === 'string') {
|
58 | allowNetConnect = new RegExp(matcher)
|
59 | } else if (matcher instanceof RegExp) {
|
60 | allowNetConnect = matcher
|
61 | } else if (typeof matcher === 'function') {
|
62 | allowNetConnect = { test: matcher }
|
63 | } else {
|
64 | allowNetConnect = /.*/
|
65 | }
|
66 | }
|
67 |
|
68 | function isEnabledForNetConnect(options) {
|
69 | common.normalizeRequestOptions(options)
|
70 |
|
71 | const enabled = allowNetConnect && allowNetConnect.test(options.host)
|
72 | debug('Net connect', enabled ? '' : 'not', 'enabled for', options.host)
|
73 | return enabled
|
74 | }
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 | function disableNetConnect() {
|
83 | allowNetConnect = undefined
|
84 | }
|
85 |
|
86 | function isOn() {
|
87 | return !isOff()
|
88 | }
|
89 |
|
90 | function isOff() {
|
91 | return process.env.NOCK_OFF === 'true'
|
92 | }
|
93 |
|
94 | function addInterceptor(key, interceptor, scope, scopeOptions, host) {
|
95 | if (!(key in allInterceptors)) {
|
96 | allInterceptors[key] = { key, interceptors: [] }
|
97 | }
|
98 | interceptor.__nock_scope = scope
|
99 |
|
100 |
|
101 | interceptor.__nock_scopeKey = key
|
102 | interceptor.__nock_scopeOptions = scopeOptions
|
103 |
|
104 | interceptor.__nock_scopeHost = host
|
105 | interceptor.interceptionCounter = 0
|
106 |
|
107 | if (scopeOptions.allowUnmocked) allInterceptors[key].allowUnmocked = true
|
108 |
|
109 | allInterceptors[key].interceptors.push(interceptor)
|
110 | }
|
111 |
|
112 | function remove(interceptor) {
|
113 | if (interceptor.__nock_scope.shouldPersist() || --interceptor.counter > 0) {
|
114 | return
|
115 | }
|
116 |
|
117 | const { basePath } = interceptor
|
118 | const interceptors =
|
119 | (allInterceptors[basePath] && allInterceptors[basePath].interceptors) || []
|
120 |
|
121 |
|
122 |
|
123 |
|
124 | interceptors.some(function (thisInterceptor, i) {
|
125 | return thisInterceptor === interceptor ? interceptors.splice(i, 1) : false
|
126 | })
|
127 | }
|
128 |
|
129 | function removeAll() {
|
130 | Object.keys(allInterceptors).forEach(function (key) {
|
131 | allInterceptors[key].interceptors.forEach(function (interceptor) {
|
132 | interceptor.scope.keyedInterceptors = {}
|
133 | })
|
134 | })
|
135 | allInterceptors = {}
|
136 | }
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | function interceptorsFor(options) {
|
144 | common.normalizeRequestOptions(options)
|
145 |
|
146 | debug('interceptors for %j', options.host)
|
147 |
|
148 | const basePath = `${options.proto}://${options.host}`
|
149 |
|
150 | debug('filtering interceptors for basepath', basePath)
|
151 |
|
152 |
|
153 | for (const { key, interceptors, allowUnmocked } of Object.values(
|
154 | allInterceptors
|
155 | )) {
|
156 | for (const interceptor of interceptors) {
|
157 | const { filteringScope } = interceptor.__nock_scopeOptions
|
158 |
|
159 |
|
160 |
|
161 | if (filteringScope && filteringScope(basePath)) {
|
162 | interceptor.scope.logger('found matching scope interceptor')
|
163 |
|
164 |
|
165 |
|
166 | interceptors.forEach(ic => {
|
167 | ic.__nock_filteredScope = ic.__nock_scopeKey
|
168 | })
|
169 | return interceptors
|
170 | }
|
171 | }
|
172 |
|
173 | if (common.matchStringOrRegexp(basePath, key)) {
|
174 | if (allowUnmocked && interceptors.length === 0) {
|
175 | debug('matched base path with allowUnmocked (no matching interceptors)')
|
176 | return [
|
177 | {
|
178 | options: { allowUnmocked: true },
|
179 | matchOrigin() {
|
180 | return false
|
181 | },
|
182 | },
|
183 | ]
|
184 | } else {
|
185 | debug(
|
186 | `matched base path (${interceptors.length} interceptor${
|
187 | interceptors.length > 1 ? 's' : ''
|
188 | })`
|
189 | )
|
190 | return interceptors
|
191 | }
|
192 | }
|
193 | }
|
194 |
|
195 | return undefined
|
196 | }
|
197 |
|
198 | function removeInterceptor(options) {
|
199 |
|
200 | const Interceptor = require('./interceptor')
|
201 |
|
202 | let baseUrl, key, method, proto
|
203 | if (options instanceof Interceptor) {
|
204 | baseUrl = options.basePath
|
205 | key = options._key
|
206 | } else {
|
207 | proto = options.proto ? options.proto : 'http'
|
208 |
|
209 | common.normalizeRequestOptions(options)
|
210 | baseUrl = `${proto}://${options.host}`
|
211 | method = (options.method && options.method.toUpperCase()) || 'GET'
|
212 | key = `${method} ${baseUrl}${options.path || '/'}`
|
213 | }
|
214 |
|
215 | if (
|
216 | allInterceptors[baseUrl] &&
|
217 | allInterceptors[baseUrl].interceptors.length > 0
|
218 | ) {
|
219 | for (let i = 0; i < allInterceptors[baseUrl].interceptors.length; i++) {
|
220 | const interceptor = allInterceptors[baseUrl].interceptors[i]
|
221 | if (interceptor._key === key) {
|
222 | allInterceptors[baseUrl].interceptors.splice(i, 1)
|
223 | interceptor.scope.remove(key, interceptor)
|
224 | break
|
225 | }
|
226 | }
|
227 |
|
228 | return true
|
229 | }
|
230 |
|
231 | return false
|
232 | }
|
233 |
|
234 |
|
235 | let originalClientRequest
|
236 |
|
237 | function ErroringClientRequest(error) {
|
238 | http.OutgoingMessage.call(this)
|
239 | process.nextTick(
|
240 | function () {
|
241 | this.emit('error', error)
|
242 | }.bind(this)
|
243 | )
|
244 | }
|
245 |
|
246 | inherits(ErroringClientRequest, http.ClientRequest)
|
247 |
|
248 | function overrideClientRequest() {
|
249 |
|
250 |
|
251 |
|
252 |
|
253 | debug('Overriding ClientRequest')
|
254 |
|
255 |
|
256 |
|
257 |
|
258 | function OverriddenClientRequest(...args) {
|
259 | const { options, callback } = common.normalizeClientRequestArgs(...args)
|
260 |
|
261 | if (Object.keys(options).length === 0) {
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 | throw Error(
|
271 | 'Creating a ClientRequest with empty `options` is not supported in Nock'
|
272 | )
|
273 | }
|
274 |
|
275 | http.OutgoingMessage.call(this)
|
276 |
|
277 |
|
278 | const interceptors = interceptorsFor(options)
|
279 |
|
280 | if (isOn() && interceptors) {
|
281 | debug('using', interceptors.length, 'interceptors')
|
282 |
|
283 |
|
284 |
|
285 |
|
286 | const overrider = new InterceptedRequestRouter({
|
287 | req: this,
|
288 | options,
|
289 | interceptors,
|
290 | })
|
291 | Object.assign(this, overrider)
|
292 |
|
293 | if (callback) {
|
294 | this.once('response', callback)
|
295 | }
|
296 | } else {
|
297 | debug('falling back to original ClientRequest')
|
298 |
|
299 |
|
300 | if (isOff() || isEnabledForNetConnect(options)) {
|
301 | originalClientRequest.apply(this, arguments)
|
302 | } else {
|
303 | common.setImmediate(
|
304 | function () {
|
305 | const error = new NetConnectNotAllowedError(
|
306 | options.host,
|
307 | options.path
|
308 | )
|
309 | this.emit('error', error)
|
310 | }.bind(this)
|
311 | )
|
312 | }
|
313 | }
|
314 | }
|
315 | inherits(OverriddenClientRequest, http.ClientRequest)
|
316 |
|
317 |
|
318 |
|
319 | originalClientRequest = http.ClientRequest
|
320 | http.ClientRequest = OverriddenClientRequest
|
321 |
|
322 | debug('ClientRequest overridden')
|
323 | }
|
324 |
|
325 | function restoreOverriddenClientRequest() {
|
326 | debug('restoring overridden ClientRequest')
|
327 |
|
328 |
|
329 | if (!originalClientRequest) {
|
330 | debug('- ClientRequest was not overridden')
|
331 | } else {
|
332 | http.ClientRequest = originalClientRequest
|
333 | originalClientRequest = undefined
|
334 |
|
335 | debug('- ClientRequest restored')
|
336 | }
|
337 | }
|
338 |
|
339 | function isActive() {
|
340 |
|
341 |
|
342 | return originalClientRequest !== undefined
|
343 | }
|
344 |
|
345 | function interceptorScopes() {
|
346 | const nestedInterceptors = Object.values(allInterceptors).map(
|
347 | i => i.interceptors
|
348 | )
|
349 | return [].concat(...nestedInterceptors).map(i => i.scope)
|
350 | }
|
351 |
|
352 | function isDone() {
|
353 | return interceptorScopes().every(scope => scope.isDone())
|
354 | }
|
355 |
|
356 | function pendingMocks() {
|
357 | return [].concat(...interceptorScopes().map(scope => scope.pendingMocks()))
|
358 | }
|
359 |
|
360 | function activeMocks() {
|
361 | return [].concat(...interceptorScopes().map(scope => scope.activeMocks()))
|
362 | }
|
363 |
|
364 | function activate() {
|
365 | if (originalClientRequest) {
|
366 | throw new Error('Nock already active')
|
367 | }
|
368 |
|
369 | overrideClientRequest()
|
370 |
|
371 |
|
372 |
|
373 | common.overrideRequests(function (proto, overriddenRequest, args) {
|
374 |
|
375 |
|
376 | const { options, callback } = common.normalizeClientRequestArgs(...args)
|
377 |
|
378 | if (Object.keys(options).length === 0) {
|
379 |
|
380 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 | throw Error(
|
388 | 'Making a request with empty `options` is not supported in Nock'
|
389 | )
|
390 | }
|
391 |
|
392 |
|
393 |
|
394 |
|
395 | options.proto = proto
|
396 |
|
397 | const interceptors = interceptorsFor(options)
|
398 |
|
399 | if (isOn() && interceptors) {
|
400 | const matches = interceptors.some(interceptor =>
|
401 | interceptor.matchOrigin(options)
|
402 | )
|
403 | const allowUnmocked = interceptors.some(
|
404 | interceptor => interceptor.options.allowUnmocked
|
405 | )
|
406 |
|
407 | if (!matches && allowUnmocked) {
|
408 | let req
|
409 | if (proto === 'https') {
|
410 | const { ClientRequest } = http
|
411 | http.ClientRequest = originalClientRequest
|
412 | req = overriddenRequest(options, callback)
|
413 | http.ClientRequest = ClientRequest
|
414 | } else {
|
415 | req = overriddenRequest(options, callback)
|
416 | }
|
417 | globalEmitter.emit('no match', req)
|
418 | return req
|
419 | }
|
420 |
|
421 |
|
422 |
|
423 | return new http.ClientRequest(options, callback)
|
424 | } else {
|
425 | globalEmitter.emit('no match', options)
|
426 | if (isOff() || isEnabledForNetConnect(options)) {
|
427 | return overriddenRequest(options, callback)
|
428 | } else {
|
429 | const error = new NetConnectNotAllowedError(options.host, options.path)
|
430 | return new ErroringClientRequest(error)
|
431 | }
|
432 | }
|
433 | })
|
434 | }
|
435 |
|
436 | module.exports = {
|
437 | addInterceptor,
|
438 | remove,
|
439 | removeAll,
|
440 | removeInterceptor,
|
441 | isOn,
|
442 | activate,
|
443 | isActive,
|
444 | isDone,
|
445 | pendingMocks,
|
446 | activeMocks,
|
447 | enableNetConnect,
|
448 | disableNetConnect,
|
449 | restoreOverriddenClientRequest,
|
450 | abortPendingRequests: common.removeAllTimers,
|
451 | }
|