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