Skip to content

Commit cd0caf5

Browse files
committed
feat: enable custom lookup for httpclient and fetch
fix: httpclient_next lookup
1 parent 918c21f commit cd0caf5

File tree

9 files changed

+197
-7
lines changed

9 files changed

+197
-7
lines changed

lib/core/fetch_factory.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ if (mainNodejsVersion >= 20) {
2121
} else {
2222
this.logger.warn('[egg-security] please configure `config.security.ssrf` first');
2323
}
24+
if (this.config.httpclient.lookup) {
25+
clientOptions.lookup = this.config.httpclient.lookup;
26+
}
2427
ssrfFetchFactory = new FetchFactory();
2528
ssrfFetchFactory.setClientOptions(clientOptions);
2629
}

lib/core/httpclient.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class HttpClient extends urllib.HttpClient2 {
3030
} else {
3131
args.tracer = args.tracer || this.app.tracer;
3232
}
33+
if (this.app.config.httpclient?.lookup) {
34+
args.lookup = this.app.config.httpclient.lookup;
35+
}
3336

3437
try {
3538
return await super.request(url, args);

lib/core/httpclient_next.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class HttpClientNext extends HttpClient {
3232
app,
3333
defaultArgs: options.request,
3434
allowH2: options.allowH2,
35+
lookup: options.lookup || app.config.httpclient?.lookup,
3536
// use on egg-security ssrf
3637
// https://github.com/eggjs/egg-security/blob/master/lib/extend/safe_curl.js#L11
3738
checkAddress: options.checkAddress,
@@ -62,8 +63,12 @@ class HttpClientNext extends HttpClient {
6263
} else {
6364
this.app.logger.warn('[egg-security] please configure `config.security.ssrf` first');
6465
}
66+
if (!options.options && this.app.config.httpclient.lookup) {
67+
options.lookup = this.app.config.httpclient.lookup;
68+
}
6569
this[SSRF_HTTPCLIENT] = new HttpClientNext(this.app, {
6670
checkAddress: ssrfConfig.checkAddress,
71+
lookup: options.lookup,
6772
});
6873
}
6974
return await this[SSRF_HTTPCLIENT].request(url, options);

lib/egg.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ class EggApplication extends EggCore {
5252
this.ContextHttpClient = ContextHttpClient;
5353
this.HttpClient = HttpClient;
5454
this.HttpClientNext = HttpClientNext;
55+
this.loader.loadConfig();
5556
this.FetchFactory = FetchFactory;
5657
if (FetchFactory) {
57-
this.FetchFactory.setClientOptions();
58+
const lookup = this.config?.httpclient?.lookup;
59+
this.FetchFactory.setClientOptions({ lookup });
5860
this.fetch = FetchFactory.fetch;
5961
this.safeFetch = safeFetch.bind(this);
6062
}
6163

62-
this.loader.loadConfig();
63-
6464
/**
6565
* messenger instance
6666
* @member {Messenger}
@@ -297,11 +297,15 @@ class EggApplication extends EggCore {
297297
* Create a new HttpClient instance with custom options
298298
* @param {Object} [options] HttpClient init options
299299
*/
300-
createHttpClient(options) {
300+
createHttpClient(options = {}) {
301301
let httpClient;
302+
if (this.config.httpclient?.lookup) {
303+
options.lookup = this.config.httpclient.lookup
304+
}
305+
302306
if (this.config.httpclient.useHttpClientNext || this.config.httpclient.allowH2) {
303307
httpClient = new this.HttpClientNext(this, options);
304-
} else if (this.config.httpclient.enableDNSCache) {
308+
} else if (this.config.httpclient?.enableDNSCache) {
305309
httpClient = new DNSCacheHttpClient(this, options);
306310
} else {
307311
httpClient = new this.HttpClient(this, options);
@@ -495,7 +499,7 @@ class EggApplication extends EggCore {
495499
return this.config.env;
496500
}
497501
/* eslint no-empty-function: off */
498-
set env(_) {}
502+
set env(_) { }
499503

500504
/**
501505
* app.proxy delegate app.config.proxy
@@ -506,7 +510,7 @@ class EggApplication extends EggCore {
506510
return this.config.proxy;
507511
}
508512
/* eslint no-empty-function: off */
509-
set proxy(_) {}
513+
set proxy(_) { }
510514

511515
/**
512516
* create a singleton instance
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
module.exports = function* () {
4+
let args;
5+
if (this.query.host) {
6+
args = {};
7+
args.headers = { host: this.query.host };
8+
}
9+
if (this.query.Host) {
10+
args = {};
11+
args.headers = { Host: this.query.Host };
12+
}
13+
if (this.query.disableDNSCache) {
14+
args = { enableDNSCache: false };
15+
}
16+
const result = yield this.curl(this.query.url, args);
17+
this.status = result.status;
18+
this.set(result.headers);
19+
this.body = result.data;
20+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
module.exports = app => {
4+
app.get('/', app.controller.home);
5+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
3+
exports.httpclient = {
4+
lookup: function (hostname, options, callback) {
5+
const IP_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
6+
if (IP_REGEX.test(hostname)) {
7+
const family = typeof options.family === 'number' ? options.family : 4;
8+
console.log(
9+
`[dns-resolver] literal IP ${hostname} lookup, bypassing cache`
10+
);
11+
if (options.all) {
12+
callback(null, [{ address: hostname, family }]);
13+
} else {
14+
callback(null, hostname, family);
15+
}
16+
} else {
17+
console.log(
18+
`[dns-resolver] custom lookup for hostname: ${hostname}`
19+
);
20+
21+
const resultIp = '127.0.0.1'
22+
if (options.all) {
23+
callback(null, [{ address: resultIp, family: 4 }]);
24+
} else {
25+
callback(null, resultIp, 4);
26+
}
27+
}
28+
},
29+
request: {
30+
timeout: 2000,
31+
},
32+
httpAgent: {
33+
keepAlive: false,
34+
},
35+
httpsAgent: {
36+
keepAlive: false,
37+
},
38+
};
39+
40+
exports.keys = 'test key';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "dns_resolver-app"
3+
}

test/lib/core/dns_resolver.test.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const utils = require('../../utils');
2+
const Koa = require('koa');
3+
const http = require('http');
4+
const assert = require('assert');
5+
6+
const startServer = (ip = '127.0.0.1') => {
7+
let localServer;
8+
return new Promise((resolve, reject) => {
9+
const app = new Koa();
10+
app.use(async ctx => {
11+
if (ctx.path === '/get_headers') {
12+
ctx.body = {
13+
headers: ctx.request.headers,
14+
host: ctx.request.headers.host,
15+
};
16+
return;
17+
}
18+
ctx.body = `${ctx.method} ${ctx.path}`;
19+
});
20+
localServer = http.createServer(app.callback());
21+
22+
localServer.listen(0, err => {
23+
if (err) return reject(err);
24+
console.log(`Local test server ${ip} started on port`, localServer.address().port);
25+
return resolve({ url: `http://${ip}:` + localServer.address().port, server: localServer });
26+
});
27+
});
28+
};
29+
30+
31+
32+
describe('test/lib/core/dns_resolver.test.js', () => {
33+
let app;
34+
let url1;
35+
let url2;
36+
let server1;
37+
let server2;
38+
39+
before(async () => {
40+
server1 = await startServer('127.0.0.1')
41+
server2 = await startServer('127.0.0.2')
42+
if (!server1 || !server2) {
43+
throw new Error('start local server failed');
44+
}
45+
process.once('exit', () => {
46+
if (server1 && server1.server) server1.server.close();
47+
if (server2 && server2.server) server2.server.close();
48+
})
49+
app = utils.app('apps/dns_resolver');
50+
await app.ready();
51+
url1 = server1.url
52+
url1 = url1.replace('127.0.0.1', 'localhost');
53+
url2 = server2.url
54+
url2 = url2.replace('127.0.0.2', 'localhost');
55+
});
56+
57+
it('should surpass dns resolve', async () => {
58+
let res = await app.curl(server1.url + '/get_headers', { dataType: 'json' });
59+
assert(res.status === 200);
60+
});
61+
62+
it('should curl work', async () => {
63+
let res = await app.curl(url1 + '/get_headers', { dataType: 'json' });
64+
assert(res.status === 200);
65+
});
66+
67+
it('should fetch also work', async () => {
68+
let res = await app.fetch(url1 + '/get_headers', { dataType: 'json' });
69+
assert(res.status === 200);
70+
});
71+
72+
it('should use dns custom lookup and catch error', async () => {
73+
try {
74+
if (server1?.server) await server1.server.close();
75+
// will resolve to 127.0.0.1
76+
const res = await app.curl(url1 + '/get_headers', { dataType: 'json' });
77+
assert(res.status !== 200);
78+
} catch (err) {
79+
console.error(`\nRequest Error\n`, err.message);
80+
assert(err);
81+
assert(err.message.includes('ECONNREFUSED'));
82+
}
83+
});
84+
85+
it('should safeCurl also work', async () => {
86+
let res = await app.curl(url2 + '/get_headers', { dataType: 'json' });
87+
assert(res.status === 200);
88+
});
89+
90+
it('should safeFetch also work', async () => {
91+
let res = await app.safeFetch(url2 + '/get_headers', { dataType: 'json' });
92+
assert(res.status === 200);
93+
});
94+
95+
it('should fetch fail', async () => {
96+
try {
97+
if (server1?.server) await server1.server.close();
98+
// will resolve to 127.0.0.1
99+
const res = await app.fetch(url1 + '/get_headers', { dataType: 'json' });
100+
assert(res.status !== 200);
101+
} catch (err) {
102+
console.error(`\nRequest Error\n`, err.message);
103+
assert(err);
104+
assert(err.message.includes('fetch failed'));
105+
}
106+
});
107+
});

0 commit comments

Comments
 (0)