schangxiang@126.com
2025-09-19 0821aa23eabe557c0d9ef5dbe6989c68be35d1fe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
'use strict';
 
const dns = require('mz/dns');
const LRU = require('ylru');
const urlparse = require('url').parse;
const HttpClient = require('./httpclient');
const utility = require('utility');
 
const IP_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
const DNSLOOKUP = Symbol('DNSCacheHttpClient#dnslookup');
const UPDATE_DNS = Symbol('DNSCacheHttpClient#updateDNS');
 
class DNSCacheHttpClient extends HttpClient {
  constructor(app) {
    super(app);
    this.dnsCacheLookupInterval = this.app.config.httpclient.dnsCacheLookupInterval;
    this.dnsCache = new LRU(this.app.config.httpclient.dnsCacheMaxLength);
  }
 
  request(url, args, callback) {
    // request(url, callback)
    if (typeof args === 'function') {
      callback = args;
      args = null;
    }
 
    // the callback style
    if (callback) {
      this.app.deprecate('[dnscache_httpclient] We now support async for this function, so callback isn\'t recommended.');
      this[DNSLOOKUP](url, args)
        .then(result => {
          return super.request(result.url, result.args);
        })
        .then(result => process.nextTick(() => callback(null, result.data, result.res)))
        .catch(err => process.nextTick(() => callback(err)));
      return;
    }
 
    // the Promise style
    return (async () => {
      const result = await this[DNSLOOKUP](url, args);
      return super.request(result.url, result.args);
    })();
  }
 
  async [DNSLOOKUP](url, args) {
    const parsed = typeof url === 'string' ? urlparse(url) : url;
    // hostname must exists
    const hostname = parsed.hostname;
 
    // don't lookup when hostname is IP
    if (hostname && IP_REGEX.test(hostname)) {
      return { url, args };
    }
 
    args = args || {};
    args.headers = args.headers || {};
    // set host header is not exists
    if (!args.headers.host && !args.headers.Host) {
      // host must combine with hostname:port, node won't use `parsed.host`
      args.headers.host = parsed.port ? `${hostname}:${parsed.port}` : hostname;
    }
 
    const record = this.dnsCache.get(hostname);
    const now = Date.now();
    if (record) {
      if (now - record.timestamp >= this.dnsCacheLookupInterval) {
        // make sure next request don't refresh dns query
        record.timestamp = now;
        this[UPDATE_DNS](hostname, args).catch(err => this.app.emit('error', err));
      }
 
      return { url: formatDnsLookupUrl(hostname, url, record.ip), args };
    }
 
    const address = await this[UPDATE_DNS](hostname, args);
    return { url: formatDnsLookupUrl(hostname, url, address), args };
  }
 
  async [UPDATE_DNS](hostname, args) {
    const logger = args.ctx ? args.ctx.coreLogger : this.app.coreLogger;
    try {
      const [ address ] = await dns.lookup(hostname, { family: 4 });
      logger.info('[dnscache_httpclient] dns lookup success: %s => %s',
        hostname, address);
      this.dnsCache.set(hostname, { timestamp: Date.now(), ip: address });
      return address;
    } catch (err) {
      err.message = `[dnscache_httpclient] dns lookup error: ${hostname} => ${err.message}`;
      throw err;
    }
  }
 
}
 
module.exports = DNSCacheHttpClient;
 
function formatDnsLookupUrl(host, url, address) {
  if (typeof url === 'string') return url.replace(host, address);
  const urlObj = utility.assign({}, url);
  urlObj.hostname = urlObj.hostname.replace(host, address);
  if (urlObj.host) {
    urlObj.host = urlObj.host.replace(host, address);
  }
  return urlObj;
}