Skip to content

Commit c8a4a82

Browse files
committed
Refactor nodejs module rewriting for further extension
We want to be able to also inject electron apps, so reorganising this a little will be useful.
1 parent f899400 commit c8a4a82

File tree

6 files changed

+177
-141
lines changed

6 files changed

+177
-141
lines changed

overrides/js/prepend-node.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* --require'd by node before loading any other modules.
3+
* This file sets up a global agent for the http & https modules,
4+
* plus tweaks various other HTTP clients that need nudges, so they
5+
* all correctly pick up the proxy from the environment.
6+
*
7+
* Tested against Node 6, 8, 10 and 12.
8+
*/
9+
10+
const wrapModule = require('./wrap-require');
11+
12+
let httpAlreadyIntercepted = false;
13+
14+
function interceptAllHttp() {
15+
if (httpAlreadyIntercepted) return;
16+
httpAlreadyIntercepted = true;
17+
18+
const MAJOR_NODEJS_VERSION = parseInt(process.version.slice(1).split('.')[0], 10);
19+
20+
if (MAJOR_NODEJS_VERSION >= 10) {
21+
// `global-agent` works with Node.js v10 and above.
22+
const globalAgent = require('global-agent');
23+
globalAgent.bootstrap();
24+
} else {
25+
// `global-tunnel-ng` works only with Node.js v10 and below.
26+
const globalTunnel = require('global-tunnel-ng');
27+
globalTunnel.initialize();
28+
}
29+
}
30+
31+
wrapModule('http', interceptAllHttp, true);
32+
wrapModule('https', interceptAllHttp, true);
33+
34+
wrapModule('axios', function wrapAxios (loadedModule) {
35+
// Global agent handles this automatically, if used (i.e. Node >= 10)
36+
if (global.GLOBAL_AGENT) return;
37+
38+
// Disable built-in proxy support, to let global-tunnel take precedence
39+
// Supported back to the very first release of Axios
40+
loadedModule.defaults.proxy = false;
41+
});
42+
43+
wrapModule('request', function wrapRequest (loadedModule) {
44+
// Global agent handles this automatically, if used (i.e. Node >= 10)
45+
if (global.GLOBAL_AGENT) return;
46+
47+
// Is this Request >= 2.17?
48+
// Before then proxy support isn't a problem anyway
49+
if (!loadedModule.defaults) return;
50+
51+
// Have we intercepted this already?
52+
if (loadedModule.INTERCEPTED_BY_HTTPTOOLKIT) return;
53+
54+
const fixedModule = loadedModule.defaults({ proxy: false });
55+
fixedModule.INTERCEPTED_BY_HTTPTOOLKIT = true;
56+
return fixedModule;
57+
});
58+
59+
wrapModule('superagent', function wrapSuperagent (loadedModule) {
60+
// Global agent handles this automatically, if used (i.e. Node >= 10)
61+
if (global.GLOBAL_AGENT) return;
62+
63+
// Have we intercepted this already?
64+
if (loadedModule.INTERCEPTED_BY_HTTPTOOLKIT) return;
65+
loadedModule.INTERCEPTED_BY_HTTPTOOLKIT = true;
66+
67+
// Global tunnel doesn't successfully reconfigure superagent.
68+
// To fix it, we forcibly override the agent property on every request.
69+
const originalRequestMethod = loadedModule.Request.prototype.request;
70+
loadedModule.Request.prototype.request = function () {
71+
if (this.url.indexOf('https:') === 0) {
72+
this._agent = require('https').globalAgent;
73+
} else {
74+
this._agent = require('http').globalAgent;
75+
}
76+
return originalRequestMethod.apply(this, arguments);
77+
};
78+
});
79+
80+
wrapModule('stripe', function wrapStripe (loadedModule) {
81+
if (loadedModule.INTERCEPTED_BY_HTTPTOOLKIT) return;
82+
83+
return Object.assign(
84+
function () {
85+
const result = loadedModule.apply(this, arguments);
86+
87+
// Set by global-tunnel in Node < 10 (or global-agent in 11.7+)
88+
result.setHttpAgent(require('https').globalAgent);
89+
return result;
90+
},
91+
loadedModule,
92+
{ INTERCEPTED_BY_HTTPTOOLKIT: true }
93+
);
94+
});

overrides/js/wrap-require.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Intercept calls to require() certain modules, to monkey-patch them.
3+
*
4+
* This modules intercepts all require calls. For all modules previously
5+
* registered via wrapModule, it runs the registered wrapper on the loaded
6+
* module before it is returned to the original require() call.
7+
*/
8+
9+
// Grab the built-in module loader that we're going to intercept
10+
const mod = require('module');
11+
const realLoad = mod._load;
12+
13+
const wrappers = {};
14+
15+
// Either false, or a list (initially empty) of modules whose wrapping is being
16+
// delayed. This is important for modules who require other modules that need
17+
// wrapping, to avoid issues with circular requires.
18+
let wrappingBlocked = false;
19+
20+
function fixModule(requestedName, filename, loadedModule) {
21+
const wrapper = wrappers[requestedName];
22+
23+
if (wrapper) {
24+
wrappingBlocked = wrapper.shouldBlockWrapping ? [] : false;
25+
26+
// wrap can either return a replacement, or mutate the module itself.
27+
const fixedModule = wrapper.wrap(loadedModule) || loadedModule;
28+
29+
if (fixedModule !== loadedModule && mod._cache[filename] && mod._cache[filename].exports) {
30+
mod._cache[filename].exports = fixedModule;
31+
}
32+
33+
if (wrappingBlocked) {
34+
wrappingBlocked.forEach(function (modDetails) {
35+
fixModule(modDetails.requestedName, modDetails.filename, modDetails.loadedModule);
36+
});
37+
wrappingBlocked = false;
38+
}
39+
40+
return fixedModule;
41+
} else {
42+
return loadedModule;
43+
}
44+
}
45+
46+
// Our hook into require():
47+
mod._load = function (requestedName, parent, isMain) {
48+
const filename = mod._resolveFilename(requestedName, parent, isMain);
49+
let loadedModule = realLoad.apply(this, arguments);
50+
51+
// Should always be set, but check just in case. This also allows
52+
// users to disable interception explicitly, if need be.
53+
if (!process.env.HTTP_TOOLKIT_ACTIVE) return loadedModule;
54+
55+
if (wrappingBlocked !== false) {
56+
wrappingBlocked.push({
57+
requestedName: requestedName,
58+
filename: filename,
59+
loadedModule: loadedModule
60+
});
61+
} else {
62+
loadedModule = fixModule(requestedName, filename, loadedModule);
63+
}
64+
65+
return loadedModule;
66+
};
67+
68+
// Register a wrapper for a given name. If shouldBlockWrapping is set, all wrapping
69+
// of modules require'd during the modules wrapper function will be delayed until
70+
// after it completes.
71+
module.exports = function wrapModule(
72+
requestedName,
73+
wrapperFunction,
74+
shouldBlockWrapping
75+
) {
76+
wrappers[requestedName] = {
77+
wrap: wrapperFunction,
78+
shouldBlockWrapping: shouldBlockWrapping || false
79+
};
80+
};

overrides/path/node

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ PATH="${PATH//`dirname "$0"`:/}"
66
real_node=`command -v node`
77
PATH="`dirname "$0"`:$PATH"
88

9-
PREPEND_PATH=`dirname "$0"`/prepend.js
9+
PREPEND_PATH=`dirname "$0"`/../js/prepend-node.js
1010

1111
# Call node with the given arguments, prefixed with our extra logic
1212
if command -v winpty >/dev/null 2>&1; then

overrides/path/node.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ REM Reset PATH, so its visible to node & subprocesses
1717
set PATH=%ORIGINALPATH%
1818

1919
REM Start Node for real, with an extra arg to inject our logic
20-
"%REAL_NODE%" -r "%WRAPPER_FOLDER%\prepend.js" %*
20+
"%REAL_NODE%" -r "%WRAPPER_FOLDER%\..\js\prepend-node.js" %*

overrides/path/prepend.js

Lines changed: 0 additions & 138 deletions
This file was deleted.

test/interceptors/fresh-terminal.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('Fresh terminal interceptor', function () {
5050
// Spawn node, as if it was run inside an intercepted terminal
5151
const terminalEnvOverrides = getTerminalEnvVars(server.port, httpsConfig, process.env);
5252
const nodeScript = fork(require.resolve('./terminal-js-test-script'), [], {
53-
execArgv: ['-r', require.resolve('../../overrides/path/prepend.js')],
53+
execArgv: ['-r', require.resolve('../../overrides/js/prepend-node.js')],
5454
env: Object.assign({}, process.env, terminalEnvOverrides)
5555
});
5656
await new Promise((resolve, reject) => {

0 commit comments

Comments
 (0)