Skip to content

Commit 4fa4d4b

Browse files
authored
feat: enable custom lookup for httpclient and fetch (#5749)
enable custom dns lookup by setting app config (curl, safeCurl, fetch, safeFetch supported)
1 parent 5b4e3be commit 4fa4d4b

18 files changed

Lines changed: 369 additions & 44 deletions

File tree

index.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import accepts = require('accepts');
22
import { AsyncLocalStorage } from 'async_hooks';
33
import { EventEmitter } from 'events'
44
import { Readable } from 'stream';
5-
import { Socket } from 'net';
5+
import { Socket, LookupFunction } from 'net';
66
import { IncomingMessage, ServerResponse } from 'http';
77
import KoaApplication = require('koa');
88
import KoaRouter = require('koa-router');
@@ -317,6 +317,8 @@ declare module 'egg' {
317317
useHttpClientNext?: boolean;
318318
/** Allow to use HTTP2 first, only work on `useHttpClientNext = true`. Default is `false` */
319319
allowH2?: boolean;
320+
/** Custom lookup function for DNS resolution */
321+
lookup?: LookupFunction;
320322
}
321323

322324
export interface EggAppConfig {
@@ -1297,4 +1299,4 @@ declare module 'egg' {
12971299
export interface Singleton<T> {
12981300
get(id: string): T;
12991301
}
1300-
}
1302+
}

lib/core/fetch_factory.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const debug = require('util').debuglog('egg:lib:core:fetch_factory');
22

33
const mainNodejsVersion = parseInt(process.versions.node.split('.')[0]);
44
let FetchFactory;
5+
let fetch;
6+
let fetchInitialized = false;
57
let safeFetch;
68
let ssrfFetchFactory;
79

@@ -12,15 +14,31 @@ if (mainNodejsVersion >= 20) {
1214
FetchFactory = urllib4.FetchFactory;
1315
debug('urllib4 enable');
1416

17+
18+
fetch = function fetch(url, init) {
19+
if (!fetchInitialized) {
20+
const clientOptions = {};
21+
if (this.config.httpclient?.lookup) {
22+
clientOptions.lookup = this.config.httpclient.lookup;
23+
}
24+
FetchFactory.setClientOptions(clientOptions);
25+
fetchInitialized = true;
26+
}
27+
return FetchFactory.fetch(url, init);
28+
};
29+
1530
safeFetch = function safeFetch(url, init) {
1631
if (!ssrfFetchFactory) {
17-
const ssrfConfig = this.config.security.ssrf;
32+
const ssrfConfig = this.config.security?.ssrf;
1833
const clientOptions = {};
1934
if (ssrfConfig?.checkAddress) {
2035
clientOptions.checkAddress = ssrfConfig.checkAddress;
2136
} else {
2237
this.logger.warn('[egg-security] please configure `config.security.ssrf` first');
2338
}
39+
if (this.config.httpclient?.lookup) {
40+
clientOptions.lookup = this.config.httpclient.lookup;
41+
}
2442
ssrfFetchFactory = new FetchFactory();
2543
ssrfFetchFactory.setClientOptions(clientOptions);
2644
}
@@ -34,4 +52,5 @@ if (mainNodejsVersion >= 20) {
3452
module.exports = {
3553
FetchFactory,
3654
safeFetch,
55+
fetch,
3756
};

lib/core/httpclient.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class HttpClient extends urllib.HttpClient2 {
3030
} else {
3131
args.tracer = args.tracer || this.app.tracer;
3232
}
33+
args.lookup = args.lookup ?? this.app.config.httpclient?.lookup;
3334

3435
try {
3536
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: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const Messenger = require('./core/messenger');
1414
const DNSCacheHttpClient = require('./core/dnscache_httpclient');
1515
const HttpClient = require('./core/httpclient');
1616
const HttpClientNext = require('./core/httpclient_next');
17-
const { FetchFactory, safeFetch } = require('./core/fetch_factory');
17+
const { FetchFactory, safeFetch, fetch } = require('./core/fetch_factory');
1818
const createLoggers = require('./core/logger');
1919
const Singleton = require('./core/singleton');
2020
const utils = require('./core/utils');
@@ -54,11 +54,9 @@ class EggApplication extends EggCore {
5454
this.HttpClientNext = HttpClientNext;
5555
this.FetchFactory = FetchFactory;
5656
if (FetchFactory) {
57-
this.FetchFactory.setClientOptions();
58-
this.fetch = FetchFactory.fetch;
57+
this.fetch = fetch.bind(this);
5958
this.safeFetch = safeFetch.bind(this);
6059
}
61-
6260
this.loader.loadConfig();
6361

6462
/**
@@ -297,11 +295,13 @@ class EggApplication extends EggCore {
297295
* Create a new HttpClient instance with custom options
298296
* @param {Object} [options] HttpClient init options
299297
*/
300-
createHttpClient(options) {
298+
createHttpClient(options = {}) {
301299
let httpClient;
300+
options.lookup = options.lookup ?? this.config.httpclient.lookup;
301+
302302
if (this.config.httpclient.useHttpClientNext || this.config.httpclient.allowH2) {
303303
httpClient = new this.HttpClientNext(this, options);
304-
} else if (this.config.httpclient.enableDNSCache) {
304+
} else if (this.config.httpclient?.enableDNSCache) {
305305
httpClient = new DNSCacheHttpClient(this, options);
306306
} else {
307307
httpClient = new this.HttpClient(this, options);
@@ -495,7 +495,7 @@ class EggApplication extends EggCore {
495495
return this.config.env;
496496
}
497497
/* eslint no-empty-function: off */
498-
set env(_) {}
498+
set env(_) { }
499499

500500
/**
501501
* app.proxy delegate app.config.proxy
@@ -506,7 +506,7 @@ class EggApplication extends EggCore {
506506
return this.config.proxy;
507507
}
508508
/* eslint no-empty-function: off */
509-
set proxy(_) {}
509+
set proxy(_) { }
510510

511511
/**
512512
* create a singleton instance

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@
7979
"eslint": "^8.23.1",
8080
"eslint-config-egg": "^12.0.0",
8181
"formstream": "^1.1.1",
82-
"https-pem": "^3.0.0",
8382
"jsdoc": "^3.6.11",
8483
"koa": "^2.13.4",
8584
"koa-static": "^5.0.0",
85+
"node-forge": "^1.3.3",
8686
"node-libs-browser": "^2.2.1",
8787
"pedding": "^1.1.0",
8888
"prettier": "^2.7.1",
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 = async 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 = await 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+
}

0 commit comments

Comments
 (0)