mirror of https://github.com/grpc/grpc-node.git
Fix DNS result handling, special case localhost, resolve comment
This commit is contained in:
parent
026d681a84
commit
b7656e0644
|
|
@ -158,7 +158,7 @@ export class ChannelImplementation implements Channel {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// TODO: check channel arg for default service config
|
// TODO(murgatroid99): check channel arg for default service config
|
||||||
const defaultServiceConfig: ServiceConfig = {
|
const defaultServiceConfig: ServiceConfig = {
|
||||||
loadBalancingConfig: [],
|
loadBalancingConfig: [],
|
||||||
methodConfig: [],
|
methodConfig: [],
|
||||||
|
|
|
||||||
|
|
@ -69,15 +69,46 @@ const DEFAULT_PORT = '443';
|
||||||
*/
|
*/
|
||||||
const IPV6_SUPPORT_RANGE = '>= 12.6';
|
const IPV6_SUPPORT_RANGE = '>= 12.6';
|
||||||
|
|
||||||
const resolve4Promise = util.promisify(dns.resolve4);
|
/**
|
||||||
const resolve6Promise = util.promisify(dns.resolve6);
|
* Get a promise that always resolves with either the result of the function
|
||||||
|
* or the error if it failed.
|
||||||
|
* @param fn
|
||||||
|
*/
|
||||||
|
function resolvePromisify<TArg, TResult, TError>(
|
||||||
|
fn: (
|
||||||
|
arg: TArg,
|
||||||
|
callback: (error: TError | null, result: TResult) => void
|
||||||
|
) => void
|
||||||
|
): (arg: TArg) => Promise<TResult | TError> {
|
||||||
|
return arg =>
|
||||||
|
new Promise<TResult | TError>((resolve, reject) => {
|
||||||
|
fn(arg, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
resolve(error);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolve4Promise = resolvePromisify<
|
||||||
|
string,
|
||||||
|
string[],
|
||||||
|
NodeJS.ErrnoException
|
||||||
|
>(dns.resolve4);
|
||||||
|
const resolve6Promise = resolvePromisify<
|
||||||
|
string,
|
||||||
|
string[],
|
||||||
|
NodeJS.ErrnoException
|
||||||
|
>(dns.resolve6);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to parse a target string as an IP address
|
* Attempt to parse a target string as an IP address
|
||||||
* @param target
|
* @param target
|
||||||
* @return An "IP:port" string if parsing was successful, `null` otherwise
|
* @return An "IP:port" string in an array if parsing was successful, `null` otherwise
|
||||||
*/
|
*/
|
||||||
function parseIP(target: string): string | null {
|
function parseIP(target: string): string[] | null {
|
||||||
/* These three regular expressions are all mutually exclusive, so we just
|
/* These three regular expressions are all mutually exclusive, so we just
|
||||||
* want the first one that matches the target string, if any do. */
|
* want the first one that matches the target string, if any do. */
|
||||||
const match =
|
const match =
|
||||||
|
|
@ -94,7 +125,7 @@ function parseIP(target: string): string | null {
|
||||||
} else {
|
} else {
|
||||||
port = DEFAULT_PORT;
|
port = DEFAULT_PORT;
|
||||||
}
|
}
|
||||||
return `${addr}:${port}`;
|
return [`${addr}:${port}`];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -121,15 +152,20 @@ function mergeArrays<T>(...arrays: T[][]): T[] {
|
||||||
* Resolver implementation that handles DNS names and IP addresses.
|
* Resolver implementation that handles DNS names and IP addresses.
|
||||||
*/
|
*/
|
||||||
class DnsResolver implements Resolver {
|
class DnsResolver implements Resolver {
|
||||||
private readonly ipResult: string | null;
|
private readonly ipResult: string[] | null;
|
||||||
private readonly dnsHostname: string | null;
|
private readonly dnsHostname: string | null;
|
||||||
private readonly port: string | null;
|
private readonly port: string | null;
|
||||||
/* The promise results here contain, in order, the A record, the AAAA record,
|
/* The promise results here contain, in order, the A record, the AAAA record,
|
||||||
* and either the TXT record or an error if TXT resolution failed */
|
* and either the TXT record or an error if TXT resolution failed */
|
||||||
pendingResultPromise: Promise<
|
private pendingResultPromise: Promise<
|
||||||
[string[], string[], string[][] | Error]
|
[
|
||||||
|
string[] | NodeJS.ErrnoException,
|
||||||
|
string[] | NodeJS.ErrnoException,
|
||||||
|
string[][] | NodeJS.ErrnoException
|
||||||
|
]
|
||||||
> | null = null;
|
> | null = null;
|
||||||
percentage: number;
|
private percentage: number;
|
||||||
|
private defaultResolutionError: StatusObject;
|
||||||
constructor(private target: string, private listener: ResolverListener) {
|
constructor(private target: string, private listener: ResolverListener) {
|
||||||
this.ipResult = parseIP(target);
|
this.ipResult = parseIP(target);
|
||||||
const dnsMatch = DNS_REGEX.exec(target);
|
const dnsMatch = DNS_REGEX.exec(target);
|
||||||
|
|
@ -143,8 +179,21 @@ class DnsResolver implements Resolver {
|
||||||
} else {
|
} else {
|
||||||
this.port = DEFAULT_PORT;
|
this.port = DEFAULT_PORT;
|
||||||
}
|
}
|
||||||
|
if (this.dnsHostname === 'localhost') {
|
||||||
|
if (semver.satisfies(process.version, IPV6_SUPPORT_RANGE)) {
|
||||||
|
this.ipResult = [`::1:${this.port}`, `127.0.0.1:${this.port}`];
|
||||||
|
} else {
|
||||||
|
this.ipResult = [`127.0.0.1:${this.port}`];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.percentage = Math.random() * 100;
|
this.percentage = Math.random() * 100;
|
||||||
|
|
||||||
|
this.defaultResolutionError = {
|
||||||
|
code: Status.UNAVAILABLE,
|
||||||
|
details: `Name resolution failed for target ${this.target}`,
|
||||||
|
metadata: new Metadata(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -154,14 +203,14 @@ class DnsResolver implements Resolver {
|
||||||
private startResolution() {
|
private startResolution() {
|
||||||
if (this.ipResult !== null) {
|
if (this.ipResult !== null) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
this.listener.onSuccessfulResolution([this.ipResult!], null, null);
|
this.listener.onSuccessfulResolution(this.ipResult!, null, null);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.dnsHostname !== null) {
|
if (this.dnsHostname !== null) {
|
||||||
const hostname: string = this.dnsHostname;
|
const hostname: string = this.dnsHostname;
|
||||||
const aResult = resolve4Promise(hostname);
|
const aResult = resolve4Promise(hostname);
|
||||||
let aaaaResult: Promise<string[]>;
|
let aaaaResult: Promise<string[] | Error>;
|
||||||
if (semver.satisfies(process.version, IPV6_SUPPORT_RANGE)) {
|
if (semver.satisfies(process.version, IPV6_SUPPORT_RANGE)) {
|
||||||
aaaaResult = resolve6Promise(hostname);
|
aaaaResult = resolve6Promise(hostname);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -183,9 +232,34 @@ class DnsResolver implements Resolver {
|
||||||
this.pendingResultPromise.then(
|
this.pendingResultPromise.then(
|
||||||
([aRecord, aaaaRecord, txtRecord]) => {
|
([aRecord, aaaaRecord, txtRecord]) => {
|
||||||
this.pendingResultPromise = null;
|
this.pendingResultPromise = null;
|
||||||
|
/* dns.resolve4 and resolve6 return an error if there are no
|
||||||
|
* addresses for that family. If there are addresses for the other
|
||||||
|
* family we want to use them instead of considering that an overall
|
||||||
|
* resolution failure. The error code that indicates this situation
|
||||||
|
* is ENODATA */
|
||||||
|
if (aRecord instanceof Error) {
|
||||||
|
if (aRecord.code === 'ENODATA') {
|
||||||
|
aRecord = [];
|
||||||
|
} else {
|
||||||
|
this.listener.onError(this.defaultResolutionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (aaaaRecord instanceof Error) {
|
||||||
|
if (aaaaRecord.code === 'ENODATA') {
|
||||||
|
aaaaRecord = [];
|
||||||
|
} else {
|
||||||
|
this.listener.onError(this.defaultResolutionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
aRecord = aRecord.map(value => `${value}:${this.port}`);
|
aRecord = aRecord.map(value => `${value}:${this.port}`);
|
||||||
aaaaRecord = aaaaRecord.map(value => `[${value}]:${this.port}`);
|
aaaaRecord = aaaaRecord.map(value => `[${value}]:${this.port}`);
|
||||||
const allAddresses: string[] = mergeArrays(aaaaRecord, aRecord);
|
const allAddresses: string[] = mergeArrays(aaaaRecord, aRecord);
|
||||||
|
if (allAddresses.length === 0) {
|
||||||
|
this.listener.onError(this.defaultResolutionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let serviceConfig: ServiceConfig | null = null;
|
let serviceConfig: ServiceConfig | null = null;
|
||||||
let serviceConfigError: StatusObject | null = null;
|
let serviceConfigError: StatusObject | null = null;
|
||||||
if (txtRecord instanceof Error) {
|
if (txtRecord instanceof Error) {
|
||||||
|
|
@ -216,11 +290,7 @@ class DnsResolver implements Resolver {
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
this.pendingResultPromise = null;
|
this.pendingResultPromise = null;
|
||||||
this.listener.onError({
|
this.listener.onError(this.defaultResolutionError);
|
||||||
code: Status.UNAVAILABLE,
|
|
||||||
details: `Name resolution failed for target ${this.target}`,
|
|
||||||
metadata: new Metadata(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue