Skip to content

Commit 65242ae

Browse files
committed
feat: enable custom lookup for httpclient and fetch
fix: httpclient_next lookup fix: lookup param chore: delete log
1 parent 918c21f commit 65242ae

File tree

9 files changed

+193
-7
lines changed

9 files changed

+193
-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 (!args.lookup && 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.lookup && 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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
if (options.all) {
9+
callback(null, [{ address: hostname, family }]);
10+
} else {
11+
callback(null, hostname, family);
12+
}
13+
} else {
14+
const resultIp = '127.0.0.1'
15+
if (options.all) {
16+
callback(null, [{ address: resultIp, family: 4 }]);
17+
} else {
18+
callback(null, resultIp, 4);
19+
}
20+
}
21+
},
22+
request: {
23+
timeout: 2000,
24+
},
25+
httpAgent: {
26+
keepAlive: false,
27+
},
28+
httpsAgent: {
29+
keepAlive: false,
30+
},
31+
};
32+
33+
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: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
return resolve({ url: `http://${ip}:` + localServer.address().port, server: localServer });
25+
});
26+
});
27+
};
28+
29+
30+
31+
describe('test/lib/core/dns_resolver.test.js', () => {
32+
let app;
33+
let url1;
34+
let url2;
35+
let server1;
36+
let server2;
37+
38+
process.once('exit', () => {
39+
if (server1?.server?.close) server1.server.close();
40+
if (server2?.server?.close) server2.server.close();
41+
})
42+
43+
before(async () => {
44+
server1 = await startServer('127.0.0.1');
45+
server2 = await startServer('127.0.0.2');
46+
if (!server1 || !server2) {
47+
throw new Error('start local server failed');
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+
after(() => {
58+
if (server1?.server?.close) server1.server.close();
59+
if (server2?.server?.close) server2.server.close();
60+
})
61+
62+
it('should surpass dns resolve', async () => {
63+
let res = await app.curl(server1.url + '/get_headers', { dataType: 'json' });
64+
assert(res.status === 200);
65+
});
66+
67+
it('should curl work', async () => {
68+
let res = await app.curl(url1 + '/get_headers', { dataType: 'json' });
69+
assert(res.status === 200);
70+
});
71+
72+
it('should fetch also work', async () => {
73+
let res = await app.fetch(url1 + '/get_headers', { dataType: 'json' });
74+
assert(res.status === 200);
75+
});
76+
77+
it('should use dns custom lookup and catch error', async () => {
78+
try {
79+
if (server1?.server) await server1.server.close();
80+
// will resolve to 127.0.0.1
81+
const res = await app.curl(url1 + '/get_headers', { dataType: 'json' });
82+
assert(res.status !== 200);
83+
} catch (err) {
84+
assert(err);
85+
assert(err.message.includes('ECONNREFUSED'));
86+
}
87+
});
88+
89+
it('should safeCurl also work', async () => {
90+
let res = await app.curl(url2 + '/get_headers', { dataType: 'json' });
91+
assert(res.status === 200);
92+
});
93+
94+
it('should safeFetch also work', async () => {
95+
let res = await app.safeFetch(url2 + '/get_headers', { dataType: 'json' });
96+
assert(res.status === 200);
97+
});
98+
99+
it('should fetch fail', async () => {
100+
try {
101+
if (server1?.server) await server1.server.close();
102+
// will resolve to 127.0.0.1
103+
const res = await app.fetch(url1 + '/get_headers', { dataType: 'json' });
104+
assert(res.status !== 200);
105+
} catch (err) {
106+
assert(err);
107+
assert(err.message.includes('fetch failed'));
108+
}
109+
});
110+
});

0 commit comments

Comments
 (0)