mirror of https://github.com/grpc/grpc-node.git
Merge pull request #1364 from murgatroid99/grpc-js_uri_parsing
grpc-js: Use a more structured representation of URIs internally
This commit is contained in:
commit
ae61562be2
|
@ -39,6 +39,7 @@ import { trace, log } from './logging';
|
|||
import { SubchannelAddress } from './subchannel';
|
||||
import { MaxMessageSizeFilterFactory } from './max-message-size-filter';
|
||||
import { mapProxyName } from './http_proxy';
|
||||
import { GrpcUri, parseUri, uriToString } from './uri-parser';
|
||||
|
||||
export enum ConnectivityState {
|
||||
CONNECTING,
|
||||
|
@ -136,8 +137,9 @@ export class ChannelImplementation implements Channel {
|
|||
private connectivityStateWatchers: ConnectivityStateWatcher[] = [];
|
||||
private defaultAuthority: string;
|
||||
private filterStackFactory: FilterStackFactory;
|
||||
private target: GrpcUri;
|
||||
constructor(
|
||||
private target: string,
|
||||
target: string,
|
||||
private readonly credentials: ChannelCredentials,
|
||||
private readonly options: ChannelOptions
|
||||
) {
|
||||
|
@ -164,14 +166,24 @@ export class ChannelImplementation implements Channel {
|
|||
);
|
||||
}
|
||||
}
|
||||
const originalTargetUri = parseUri(target);
|
||||
if (originalTargetUri === null) {
|
||||
throw new Error(`Could not parse target name "${target}"`);
|
||||
}
|
||||
if (this.options['grpc.default_authority']) {
|
||||
this.defaultAuthority = this.options['grpc.default_authority'] as string;
|
||||
} else {
|
||||
this.defaultAuthority = getDefaultAuthority(target);
|
||||
this.defaultAuthority = getDefaultAuthority(originalTargetUri);
|
||||
}
|
||||
const proxyMapResult = mapProxyName(target, options);
|
||||
const proxyMapResult = mapProxyName(originalTargetUri, options);
|
||||
this.target = proxyMapResult.target;
|
||||
this.options = Object.assign({}, this.options, proxyMapResult.extraOptions);
|
||||
|
||||
const targetUri = parseUri(target);
|
||||
if (targetUri === null) {
|
||||
throw new Error(`Could not parse target name "${target}"`);
|
||||
}
|
||||
this.target = targetUri;
|
||||
/* The global boolean parameter to getSubchannelPool has the inverse meaning to what
|
||||
* the grpc.use_local_subchannel_pool channel option means. */
|
||||
this.subchannelPool = getSubchannelPool(
|
||||
|
@ -422,7 +434,7 @@ export class ChannelImplementation implements Channel {
|
|||
}
|
||||
|
||||
getTarget() {
|
||||
return this.target;
|
||||
return uriToString(this.target);
|
||||
}
|
||||
|
||||
getConnectivityState(tryToConnect: boolean) {
|
||||
|
|
|
@ -15,11 +15,9 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { URL } from 'url';
|
||||
import { log } from './logging';
|
||||
import { LogVerbosity } from './constants';
|
||||
import { getDefaultAuthority } from './resolver';
|
||||
import { parseTarget } from './resolver-dns';
|
||||
import { Socket } from 'net';
|
||||
import * as http from 'http';
|
||||
import * as tls from 'tls';
|
||||
|
@ -30,6 +28,7 @@ import {
|
|||
subchannelAddressToString,
|
||||
} from './subchannel';
|
||||
import { ChannelOptions } from './channel-options';
|
||||
import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser';
|
||||
|
||||
const TRACER_NAME = 'proxy';
|
||||
|
||||
|
@ -61,31 +60,30 @@ function getProxyInfo(): ProxyInfo {
|
|||
} else {
|
||||
return {};
|
||||
}
|
||||
let proxyUrl: URL;
|
||||
try {
|
||||
proxyUrl = new URL(proxyEnv);
|
||||
} catch (e) {
|
||||
const proxyUrl = parseUri(proxyEnv);
|
||||
if (proxyUrl === null) {
|
||||
log(LogVerbosity.ERROR, `cannot parse value of "${envVar}" env var`);
|
||||
return {};
|
||||
}
|
||||
if (proxyUrl.protocol !== 'http:') {
|
||||
if (proxyUrl.scheme !== 'http') {
|
||||
log(
|
||||
LogVerbosity.ERROR,
|
||||
`"${proxyUrl.protocol}" scheme not supported in proxy URI`
|
||||
`"${proxyUrl.scheme}" scheme not supported in proxy URI`
|
||||
);
|
||||
return {};
|
||||
}
|
||||
const splitPath = proxyUrl.path.split('@');
|
||||
let host: string;
|
||||
let userCred: string | null = null;
|
||||
if (proxyUrl.username) {
|
||||
if (proxyUrl.password) {
|
||||
log(LogVerbosity.INFO, 'userinfo found in proxy URI');
|
||||
userCred = `${proxyUrl.username}:${proxyUrl.password}`;
|
||||
} else {
|
||||
userCred = proxyUrl.username;
|
||||
}
|
||||
if (splitPath.length === 2) {
|
||||
log(LogVerbosity.INFO, 'userinfo found in proxy URI');
|
||||
userCred = splitPath[0];
|
||||
host = splitPath[1];
|
||||
} else {
|
||||
host = proxyUrl.path;
|
||||
}
|
||||
const result: ProxyInfo = {
|
||||
address: proxyUrl.host,
|
||||
address: host,
|
||||
};
|
||||
if (userCred) {
|
||||
result.creds = userCred;
|
||||
|
@ -113,12 +111,12 @@ function getNoProxyHostList(): string[] {
|
|||
}
|
||||
|
||||
export interface ProxyMapResult {
|
||||
target: string;
|
||||
target: GrpcUri;
|
||||
extraOptions: ChannelOptions;
|
||||
}
|
||||
|
||||
export function mapProxyName(
|
||||
target: string,
|
||||
target: GrpcUri,
|
||||
options: ChannelOptions
|
||||
): ProxyMapResult {
|
||||
const noProxyResult: ProxyMapResult = {
|
||||
|
@ -129,11 +127,11 @@ export function mapProxyName(
|
|||
if (!proxyInfo.address) {
|
||||
return noProxyResult;
|
||||
}
|
||||
const parsedTarget = parseTarget(target);
|
||||
if (!parsedTarget) {
|
||||
const hostPort = splitHostPort(target.path);
|
||||
if (!hostPort) {
|
||||
return noProxyResult;
|
||||
}
|
||||
const serverHost = parsedTarget.host;
|
||||
const serverHost = hostPort.host;
|
||||
for (const host of getNoProxyHostList()) {
|
||||
if (host === serverHost) {
|
||||
trace('Not using proxy for target in no_proxy list: ' + target);
|
||||
|
@ -141,20 +139,20 @@ export function mapProxyName(
|
|||
}
|
||||
}
|
||||
const extraOptions: ChannelOptions = {
|
||||
'grpc.http_connect_target': target,
|
||||
'grpc.http_connect_target': uriToString(target),
|
||||
};
|
||||
if (proxyInfo.creds) {
|
||||
extraOptions['grpc.http_connect_creds'] = proxyInfo.creds;
|
||||
}
|
||||
return {
|
||||
target: `dns:${proxyInfo.address}`,
|
||||
target: { path: proxyInfo.address },
|
||||
extraOptions: extraOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProxyConnectionResult {
|
||||
socket?: Socket;
|
||||
realTarget?: string;
|
||||
realTarget?: GrpcUri;
|
||||
}
|
||||
|
||||
export function getProxiedConnection(
|
||||
|
@ -166,9 +164,13 @@ export function getProxiedConnection(
|
|||
return Promise.resolve<ProxyConnectionResult>({});
|
||||
}
|
||||
const realTarget = channelOptions['grpc.http_connect_target'] as string;
|
||||
const parsedTarget = parseTarget(realTarget)!;
|
||||
const parsedTarget = parseUri(realTarget);
|
||||
if (parsedTarget === null) {
|
||||
return Promise.resolve<ProxyConnectionResult>({});
|
||||
}
|
||||
const options: http.RequestOptions = {
|
||||
method: 'CONNECT',
|
||||
path: parsedTarget.path,
|
||||
};
|
||||
// Connect to the subchannel address as a proxy
|
||||
if (isTcpSubchannelAddress(address)) {
|
||||
|
@ -177,11 +179,6 @@ export function getProxiedConnection(
|
|||
} else {
|
||||
options.socketPath = address.path;
|
||||
}
|
||||
if (parsedTarget.port === undefined) {
|
||||
options.path = parsedTarget.host;
|
||||
} else {
|
||||
options.path = `${parsedTarget.host}:${parsedTarget.port}`;
|
||||
}
|
||||
if ('grpc.http_connect_creds' in channelOptions) {
|
||||
options.headers = {
|
||||
'Proxy-Authorization':
|
||||
|
@ -205,6 +202,10 @@ export function getProxiedConnection(
|
|||
' through proxy ' +
|
||||
proxyAddressString
|
||||
);
|
||||
resolve({
|
||||
socket,
|
||||
realTarget: parsedTarget,
|
||||
});
|
||||
if ('secureContext' in connectionOptions) {
|
||||
/* The proxy is connecting to a TLS server, so upgrade this socket
|
||||
* connection to a TLS connection.
|
||||
|
@ -212,16 +213,16 @@ export function getProxiedConnection(
|
|||
* See https://github.com/grpc/grpc-node/pull/1369 for more info. */
|
||||
const cts = tls.connect({
|
||||
...connectionOptions,
|
||||
host: getDefaultAuthority(realTarget),
|
||||
host: getDefaultAuthority(parsedTarget),
|
||||
socket: socket,
|
||||
}, () => {
|
||||
resolve({ socket: cts, realTarget });
|
||||
resolve({ socket: cts, realTarget: parsedTarget });
|
||||
}
|
||||
);
|
||||
} else {
|
||||
resolve({
|
||||
socket,
|
||||
realTarget,
|
||||
realTarget: parsedTarget,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -29,6 +29,8 @@ import { Metadata } from './metadata';
|
|||
import * as logging from './logging';
|
||||
import { LogVerbosity } from './constants';
|
||||
import { SubchannelAddress, TcpSubchannelAddress } from './subchannel';
|
||||
import { GrpcUri, uriToString, splitHostPort } from './uri-parser';
|
||||
import { isIPv6, isIPv4 } from 'net';
|
||||
|
||||
const TRACER_NAME = 'dns_resolver';
|
||||
|
||||
|
@ -36,67 +38,14 @@ function trace(text: string): void {
|
|||
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
|
||||
}
|
||||
|
||||
/* These regular expressions match IP addresses with optional ports in different
|
||||
* formats. In each case, capture group 1 contains the address, and capture
|
||||
* group 2 contains the port number, if present */
|
||||
/**
|
||||
* Matches 4 groups of up to 3 digits each, separated by periods, optionally
|
||||
* followed by a colon and a number.
|
||||
*/
|
||||
const IPV4_REGEX = /^(\d{1,3}(?:\.\d{1,3}){3})(?::(\d+))?$/;
|
||||
/**
|
||||
* Matches any number of groups of up to 4 hex digits (case insensitive)
|
||||
* separated by 1 or more colons. This variant does not match a port number.
|
||||
*/
|
||||
const IPV6_REGEX = /^([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)$/i;
|
||||
/**
|
||||
* Matches the same as the IPv6_REGEX, surrounded by square brackets, and
|
||||
* optionally followed by a colon and a number.
|
||||
*/
|
||||
const IPV6_BRACKET_REGEX = /^\[([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)\](?::(\d+))?$/i;
|
||||
|
||||
/**
|
||||
* Matches `[dns:][//authority/]host[:port]`, where `authority` and `host` are
|
||||
* both arbitrary sequences of dot-separated strings of alphanumeric characters
|
||||
* and `port` is a sequence of digits. Group 1 contains the hostname and group
|
||||
* 2 contains the port number if provided.
|
||||
*/
|
||||
const DNS_REGEX = /^(?:dns:)?(?:\/\/(?:[a-zA-Z0-9-]+\.?)+\/)?((?:[a-zA-Z0-9-]+\.?)+)(?::(\d+))?$/;
|
||||
|
||||
/**
|
||||
* The default TCP port to connect to if not explicitly specified in the target.
|
||||
*/
|
||||
const DEFAULT_PORT = '443';
|
||||
const DEFAULT_PORT = 443;
|
||||
|
||||
const resolveTxtPromise = util.promisify(dns.resolveTxt);
|
||||
const dnsLookupPromise = util.promisify(dns.lookup);
|
||||
|
||||
/**
|
||||
* Attempt to parse a target string as an IP address
|
||||
* @param target
|
||||
* @return An "IP:port" string in an array if parsing was successful, `null` otherwise
|
||||
*/
|
||||
function parseIP(target: string): SubchannelAddress[] | null {
|
||||
/* These three regular expressions are all mutually exclusive, so we just
|
||||
* want the first one that matches the target string, if any do. */
|
||||
const ipv4Match = IPV4_REGEX.exec(target);
|
||||
const match =
|
||||
ipv4Match || IPV6_REGEX.exec(target) || IPV6_BRACKET_REGEX.exec(target);
|
||||
if (match === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ipv6 addresses should be bracketed
|
||||
const addr = match[1];
|
||||
let port: string;
|
||||
if (match[2]) {
|
||||
port = match[2];
|
||||
} else {
|
||||
port = DEFAULT_PORT;
|
||||
}
|
||||
return [{ host: addr, port: +port }];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge any number of arrays into a single alternating array
|
||||
* @param arrays
|
||||
|
@ -127,7 +76,7 @@ function mergeArrays<T>(...arrays: T[][]): T[] {
|
|||
class DnsResolver implements Resolver {
|
||||
private readonly ipResult: SubchannelAddress[] | null;
|
||||
private readonly dnsHostname: string | null;
|
||||
private readonly port: string | null;
|
||||
private readonly port: number | null;
|
||||
private pendingLookupPromise: Promise<dns.LookupAddress[]> | null = null;
|
||||
private pendingTxtPromise: Promise<string[][]> | null = null;
|
||||
private latestLookupResult: TcpSubchannelAddress[] | null = null;
|
||||
|
@ -135,19 +84,27 @@ class DnsResolver implements Resolver {
|
|||
private latestServiceConfigError: StatusObject | null = null;
|
||||
private percentage: number;
|
||||
private defaultResolutionError: StatusObject;
|
||||
constructor(private target: string, private listener: ResolverListener) {
|
||||
trace('Resolver constructed for target ' + target);
|
||||
this.ipResult = parseIP(target);
|
||||
const dnsMatch = DNS_REGEX.exec(target);
|
||||
if (dnsMatch === null) {
|
||||
constructor(private target: GrpcUri, private listener: ResolverListener) {
|
||||
trace('Resolver constructed for target ' + uriToString(target));
|
||||
const hostPort = splitHostPort(target.path);
|
||||
if (hostPort === null) {
|
||||
this.ipResult = null;
|
||||
this.dnsHostname = null;
|
||||
this.port = null;
|
||||
} else {
|
||||
this.dnsHostname = dnsMatch[1];
|
||||
if (dnsMatch[2]) {
|
||||
this.port = dnsMatch[2];
|
||||
if (isIPv4(hostPort.host) || isIPv6(hostPort.host)) {
|
||||
this.ipResult = [
|
||||
{
|
||||
host: hostPort.host,
|
||||
port: hostPort.port ?? DEFAULT_PORT,
|
||||
},
|
||||
];
|
||||
this.dnsHostname = null;
|
||||
this.port = null;
|
||||
} else {
|
||||
this.port = DEFAULT_PORT;
|
||||
this.ipResult = null;
|
||||
this.dnsHostname = hostPort.host;
|
||||
this.port = hostPort.port ?? DEFAULT_PORT;
|
||||
}
|
||||
}
|
||||
this.percentage = Math.random() * 100;
|
||||
|
@ -308,19 +265,13 @@ class DnsResolver implements Resolver {
|
|||
* the IP address. For DNS targets, it is the hostname.
|
||||
* @param target
|
||||
*/
|
||||
static getDefaultAuthority(target: string): string {
|
||||
const ipMatch =
|
||||
IPV4_REGEX.exec(target) ||
|
||||
IPV6_REGEX.exec(target) ||
|
||||
IPV6_BRACKET_REGEX.exec(target);
|
||||
if (ipMatch) {
|
||||
return ipMatch[1];
|
||||
static getDefaultAuthority(target: GrpcUri): string {
|
||||
const hostPort = splitHostPort(target.path);
|
||||
if (hostPort !== null) {
|
||||
return hostPort.host;
|
||||
} else {
|
||||
throw new Error(`Failed to parse target ${uriToString(target)}`);
|
||||
}
|
||||
const dnsMatch = DNS_REGEX.exec(target);
|
||||
if (dnsMatch) {
|
||||
return dnsMatch[1];
|
||||
}
|
||||
throw new Error(`Failed to parse target ${target}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,7 +280,7 @@ class DnsResolver implements Resolver {
|
|||
* "dns:" prefix and as the default resolver.
|
||||
*/
|
||||
export function setup(): void {
|
||||
registerResolver('dns:', DnsResolver);
|
||||
registerResolver('dns', DnsResolver);
|
||||
registerDefaultResolver(DnsResolver);
|
||||
}
|
||||
|
||||
|
@ -337,19 +288,3 @@ export interface DnsUrl {
|
|||
host: string;
|
||||
port?: string;
|
||||
}
|
||||
|
||||
export function parseTarget(target: string): DnsUrl | null {
|
||||
const match =
|
||||
IPV4_REGEX.exec(target) ??
|
||||
IPV6_REGEX.exec(target) ??
|
||||
IPV6_BRACKET_REGEX.exec(target) ??
|
||||
DNS_REGEX.exec(target);
|
||||
if (match) {
|
||||
return {
|
||||
host: match[1],
|
||||
port: match[2] ?? undefined,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,23 +21,18 @@ import {
|
|||
registerDefaultResolver,
|
||||
} from './resolver';
|
||||
import { SubchannelAddress } from './subchannel';
|
||||
|
||||
function getUdsName(target: string): string {
|
||||
/* Due to how this resolver is registered, it should only be constructed
|
||||
* with strings that start with 'unix:'. Other strings may result in
|
||||
* nonsensical output. If the string starts with 'unix://' that entire
|
||||
* prefix needs to be ignored */
|
||||
if (target.startsWith('unix://')) {
|
||||
return target.substring(7);
|
||||
} else {
|
||||
return target.substring(5);
|
||||
}
|
||||
}
|
||||
import { GrpcUri } from './uri-parser';
|
||||
|
||||
class UdsResolver implements Resolver {
|
||||
private addresses: SubchannelAddress[] = [];
|
||||
constructor(target: string, private listener: ResolverListener) {
|
||||
this.addresses = [{ path: getUdsName(target) }];
|
||||
constructor(target: GrpcUri, private listener: ResolverListener) {
|
||||
let path: string;
|
||||
if (target.authority === '') {
|
||||
path = '/' + target.path;
|
||||
} else {
|
||||
path = target.path;
|
||||
}
|
||||
this.addresses = [{ path }];
|
||||
}
|
||||
updateResolution(): void {
|
||||
process.nextTick(
|
||||
|
@ -48,11 +43,11 @@ class UdsResolver implements Resolver {
|
|||
);
|
||||
}
|
||||
|
||||
static getDefaultAuthority(target: string): string {
|
||||
static getDefaultAuthority(target: GrpcUri): string {
|
||||
return 'localhost';
|
||||
}
|
||||
}
|
||||
|
||||
export function setup() {
|
||||
registerResolver('unix:', UdsResolver);
|
||||
registerResolver('unix', UdsResolver);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import * as resolver_dns from './resolver-dns';
|
|||
import * as resolver_uds from './resolver-uds';
|
||||
import { StatusObject } from './call-stream';
|
||||
import { SubchannelAddress } from './subchannel';
|
||||
import { GrpcUri, uriToString } from './uri-parser';
|
||||
|
||||
/**
|
||||
* A listener object passed to the resolver's constructor that provides name
|
||||
|
@ -62,17 +63,17 @@ export interface Resolver {
|
|||
}
|
||||
|
||||
export interface ResolverConstructor {
|
||||
new (target: string, listener: ResolverListener): Resolver;
|
||||
new (target: GrpcUri, listener: ResolverListener): Resolver;
|
||||
/**
|
||||
* Get the default authority for a target. This loosely corresponds to that
|
||||
* target's hostname. Throws an error if this resolver class cannot parse the
|
||||
* `target`.
|
||||
* @param target
|
||||
*/
|
||||
getDefaultAuthority(target: string): string;
|
||||
getDefaultAuthority(target: GrpcUri): string;
|
||||
}
|
||||
|
||||
const registeredResolvers: { [prefix: string]: ResolverConstructor } = {};
|
||||
const registeredResolvers: { [scheme: string]: ResolverConstructor } = {};
|
||||
let defaultResolver: ResolverConstructor | null = null;
|
||||
|
||||
/**
|
||||
|
@ -83,10 +84,10 @@ let defaultResolver: ResolverConstructor | null = null;
|
|||
* @param resolverClass
|
||||
*/
|
||||
export function registerResolver(
|
||||
prefix: string,
|
||||
scheme: string,
|
||||
resolverClass: ResolverConstructor
|
||||
) {
|
||||
registeredResolvers[prefix] = resolverClass;
|
||||
registeredResolvers[scheme] = resolverClass;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,18 +106,24 @@ export function registerDefaultResolver(resolverClass: ResolverConstructor) {
|
|||
* @param listener
|
||||
*/
|
||||
export function createResolver(
|
||||
target: string,
|
||||
target: GrpcUri,
|
||||
listener: ResolverListener
|
||||
): Resolver {
|
||||
for (const prefix of Object.keys(registeredResolvers)) {
|
||||
if (target.startsWith(prefix)) {
|
||||
return new registeredResolvers[prefix](target, listener);
|
||||
if (target.scheme !== undefined && target.scheme in registeredResolvers) {
|
||||
return new registeredResolvers[target.scheme](target, listener);
|
||||
} else {
|
||||
if (defaultResolver !== null) {
|
||||
/* If the scheme does not correspond to a registered scheme, we assume
|
||||
* that the whole thing is the path, and the scheme was pulled out
|
||||
* incorrectly. For example, it is valid to parse "localhost:80" as
|
||||
* having a scheme of "localhost" and a path of 80, but that is not
|
||||
* how the resolver should see it */
|
||||
return new defaultResolver({ path: uriToString(target) }, listener);
|
||||
}
|
||||
}
|
||||
if (defaultResolver !== null) {
|
||||
return new defaultResolver(target, listener);
|
||||
}
|
||||
throw new Error(`No resolver could be created for target ${target}`);
|
||||
throw new Error(
|
||||
`No resolver could be created for target ${uriToString(target)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -124,16 +131,16 @@ export function createResolver(
|
|||
* error if no registered name resolver can parse that target string.
|
||||
* @param target
|
||||
*/
|
||||
export function getDefaultAuthority(target: string): string {
|
||||
for (const prefix of Object.keys(registeredResolvers)) {
|
||||
if (target.startsWith(prefix)) {
|
||||
return registeredResolvers[prefix].getDefaultAuthority(target);
|
||||
export function getDefaultAuthority(target: GrpcUri): string {
|
||||
if (target.scheme !== undefined && target.scheme in registeredResolvers) {
|
||||
return registeredResolvers[target.scheme].getDefaultAuthority(target);
|
||||
} else {
|
||||
if (defaultResolver !== null) {
|
||||
// See comment in createResolver for why we handle the target like this
|
||||
return defaultResolver.getDefaultAuthority({ path: uriToString(target) });
|
||||
}
|
||||
}
|
||||
if (defaultResolver !== null) {
|
||||
return defaultResolver.getDefaultAuthority(target);
|
||||
}
|
||||
throw new Error(`Invalid target ${target}`);
|
||||
throw new Error(`Invalid target ${uriToString(target)}`);
|
||||
}
|
||||
|
||||
export function registerAll() {
|
||||
|
|
|
@ -35,6 +35,7 @@ import { Metadata } from './metadata';
|
|||
import * as logging from './logging';
|
||||
import { LogVerbosity } from './constants';
|
||||
import { SubchannelAddress } from './subchannel';
|
||||
import { GrpcUri } from './uri-parser';
|
||||
|
||||
const TRACER_NAME = 'resolving_load_balancer';
|
||||
|
||||
|
@ -126,7 +127,7 @@ export class ResolvingLoadBalancer implements LoadBalancer {
|
|||
* implmentation
|
||||
*/
|
||||
constructor(
|
||||
private target: string,
|
||||
private target: GrpcUri,
|
||||
private channelControlHelper: ChannelControlHelper,
|
||||
private defaultServiceConfig: ServiceConfig | null
|
||||
) {
|
||||
|
|
|
@ -52,6 +52,7 @@ import {
|
|||
TcpSubchannelAddress,
|
||||
isTcpSubchannelAddress,
|
||||
} from './subchannel';
|
||||
import { parseUri } from './uri-parser';
|
||||
|
||||
interface BindResult {
|
||||
port: number;
|
||||
|
@ -225,6 +226,11 @@ export class Server {
|
|||
throw new TypeError('callback must be a function');
|
||||
}
|
||||
|
||||
const portUri = parseUri(port);
|
||||
if (portUri === null) {
|
||||
throw new Error(`Could not parse port "${port}"`);
|
||||
}
|
||||
|
||||
const serverOptions: http2.ServerOptions = {};
|
||||
if ('grpc.max_concurrent_streams' in this.options) {
|
||||
serverOptions.settings = {
|
||||
|
@ -392,7 +398,7 @@ export class Server {
|
|||
},
|
||||
};
|
||||
|
||||
const resolver = createResolver(port, resolverListener);
|
||||
const resolver = createResolver(portUri, resolverListener);
|
||||
resolver.updateResolution();
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
subchannelAddressEqual,
|
||||
} from './subchannel';
|
||||
import { ChannelCredentials } from './channel-credentials';
|
||||
import { GrpcUri, uriToString } from './uri-parser';
|
||||
|
||||
// 10 seconds in milliseconds. This value is arbitrary.
|
||||
/**
|
||||
|
@ -114,13 +115,13 @@ export class SubchannelPool {
|
|||
* @param channelCredentials
|
||||
*/
|
||||
getOrCreateSubchannel(
|
||||
channelTarget: string,
|
||||
channelTargetUri: GrpcUri,
|
||||
subchannelTarget: SubchannelAddress,
|
||||
channelArguments: ChannelOptions,
|
||||
channelCredentials: ChannelCredentials
|
||||
): Subchannel {
|
||||
this.ensureCleanupTask();
|
||||
|
||||
const channelTarget = uriToString(channelTargetUri);
|
||||
if (channelTarget in this.pool) {
|
||||
const subchannelObjArray = this.pool[channelTarget];
|
||||
for (const subchannelObj of subchannelObjArray) {
|
||||
|
@ -141,7 +142,7 @@ export class SubchannelPool {
|
|||
}
|
||||
// If we get here, no matching subchannel was found
|
||||
const subchannel = new Subchannel(
|
||||
channelTarget,
|
||||
channelTargetUri,
|
||||
subchannelTarget,
|
||||
channelArguments,
|
||||
channelCredentials
|
||||
|
|
|
@ -28,6 +28,7 @@ import * as logging from './logging';
|
|||
import { LogVerbosity } from './constants';
|
||||
import { getProxiedConnection, ProxyConnectionResult } from './http_proxy';
|
||||
import * as net from 'net';
|
||||
import { GrpcUri } from './uri-parser';
|
||||
import { ConnectionOptions } from 'tls';
|
||||
|
||||
const clientVersion = require('../../package.json').version;
|
||||
|
@ -200,7 +201,7 @@ export class Subchannel {
|
|||
* connection
|
||||
*/
|
||||
constructor(
|
||||
private channelTarget: string,
|
||||
private channelTarget: GrpcUri,
|
||||
private subchannelAddress: SubchannelAddress,
|
||||
private options: ChannelOptions,
|
||||
private credentials: ChannelCredentials
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
export interface GrpcUri {
|
||||
scheme?: string;
|
||||
authority?: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* The groups correspond to URI parts as follows:
|
||||
* 1. scheme
|
||||
* 2. authority
|
||||
* 3. path
|
||||
*/
|
||||
const URI_REGEX = /^(?:([A-Za-z0-9+.-]+):)?(?:\/\/([^/]*)\/)?(.+)$/;
|
||||
|
||||
export function parseUri(uriString: string): GrpcUri | null {
|
||||
const parsedUri = URI_REGEX.exec(uriString);
|
||||
if (parsedUri === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
scheme: parsedUri[1],
|
||||
authority: parsedUri[2],
|
||||
path: parsedUri[3],
|
||||
};
|
||||
}
|
||||
|
||||
export interface HostPort {
|
||||
host: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
const NUMBER_REGEX = /^\d+$/;
|
||||
|
||||
export function splitHostPort(path: string): HostPort | null {
|
||||
if (path.startsWith('[')) {
|
||||
const hostEnd = path.indexOf(']');
|
||||
if (hostEnd === -1) {
|
||||
return null;
|
||||
}
|
||||
const host = path.substring(1, hostEnd);
|
||||
/* Only an IPv6 address should be in bracketed notation, and an IPv6
|
||||
* address should have at least one colon */
|
||||
if (host.indexOf(':') === -1) {
|
||||
return null;
|
||||
}
|
||||
if (path.length > hostEnd + 1) {
|
||||
if (path[hostEnd + 1] === ':') {
|
||||
const portString = path.substring(hostEnd + 2);
|
||||
if (NUMBER_REGEX.test(portString)) {
|
||||
return {
|
||||
host: host,
|
||||
port: +portString,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
host,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const splitPath = path.split(':');
|
||||
/* Exactly one colon means that this is host:port. Zero colons means that
|
||||
* there is no port. And multiple colons means that this is a bare IPv6
|
||||
* address with no port */
|
||||
if (splitPath.length === 2) {
|
||||
if (NUMBER_REGEX.test(splitPath[1])) {
|
||||
return {
|
||||
host: splitPath[0],
|
||||
port: +splitPath[1],
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
host: path,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function uriToString(uri: GrpcUri): string {
|
||||
let result = '';
|
||||
if (uri.scheme !== undefined) {
|
||||
result += uri.scheme + ':';
|
||||
}
|
||||
if (uri.authority !== undefined) {
|
||||
result += '//' + uri.authority + '/';
|
||||
}
|
||||
result += uri.path;
|
||||
return result;
|
||||
}
|
|
@ -22,6 +22,7 @@ import * as resolverManager from '../src/resolver';
|
|||
import { ServiceConfig } from '../src/service-config';
|
||||
import { StatusObject } from '../src/call-stream';
|
||||
import { SubchannelAddress, isTcpSubchannelAddress } from '../src/subchannel';
|
||||
import { parseUri, GrpcUri } from '../src/uri-parser';
|
||||
|
||||
describe('Name Resolver', () => {
|
||||
describe('DNS Names', function() {
|
||||
|
@ -31,7 +32,7 @@ describe('Name Resolver', () => {
|
|||
resolverManager.registerAll();
|
||||
});
|
||||
it('Should resolve localhost properly', done => {
|
||||
const target = 'localhost:50051';
|
||||
const target = parseUri('localhost:50051')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -66,7 +67,7 @@ describe('Name Resolver', () => {
|
|||
resolver.updateResolution();
|
||||
});
|
||||
it('Should default to port 443', done => {
|
||||
const target = 'localhost';
|
||||
const target = parseUri('localhost')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -101,7 +102,7 @@ describe('Name Resolver', () => {
|
|||
resolver.updateResolution();
|
||||
});
|
||||
it('Should correctly represent an ipv4 address', done => {
|
||||
const target = '1.2.3.4';
|
||||
const target = parseUri('1.2.3.4')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -128,7 +129,7 @@ describe('Name Resolver', () => {
|
|||
resolver.updateResolution();
|
||||
});
|
||||
it('Should correctly represent an ipv6 address', done => {
|
||||
const target = '::1';
|
||||
const target = parseUri('::1')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -155,7 +156,7 @@ describe('Name Resolver', () => {
|
|||
resolver.updateResolution();
|
||||
});
|
||||
it('Should correctly represent a bracketed ipv6 address', done => {
|
||||
const target = '[::1]:50051';
|
||||
const target = parseUri('[::1]:50051')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -182,7 +183,7 @@ describe('Name Resolver', () => {
|
|||
resolver.updateResolution();
|
||||
});
|
||||
it('Should resolve a public address', done => {
|
||||
const target = 'example.com';
|
||||
const target = parseUri('example.com')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -202,7 +203,7 @@ describe('Name Resolver', () => {
|
|||
resolver.updateResolution();
|
||||
});
|
||||
it('Should resolve a name with multiple dots', done => {
|
||||
const target = 'loopback4.unittest.grpc.io';
|
||||
const target = parseUri('loopback4.unittest.grpc.io')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -231,7 +232,7 @@ describe('Name Resolver', () => {
|
|||
/* TODO(murgatroid99): re-enable this test, once we can get the IPv6 result
|
||||
* consistently */
|
||||
it.skip('Should resolve a DNS name to an IPv6 address', done => {
|
||||
const target = 'loopback6.unittest.grpc.io';
|
||||
const target = parseUri('loopback6.unittest.grpc.io')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -258,7 +259,7 @@ describe('Name Resolver', () => {
|
|||
resolver.updateResolution();
|
||||
});
|
||||
it('Should resolve a DNS name to IPv4 and IPv6 addresses', done => {
|
||||
const target = 'loopback46.unittest.grpc.io';
|
||||
const target = parseUri('loopback46.unittest.grpc.io')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -289,7 +290,7 @@ describe('Name Resolver', () => {
|
|||
it('Should resolve a name with a hyphen', done => {
|
||||
/* TODO(murgatroid99): Find or create a better domain name to test this with.
|
||||
* This is just the first one I found with a hyphen. */
|
||||
const target = 'network-tools.com';
|
||||
const target = parseUri('network-tools.com')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -310,8 +311,8 @@ describe('Name Resolver', () => {
|
|||
});
|
||||
it('Should resolve gRPC interop servers', done => {
|
||||
let completeCount = 0;
|
||||
const target1 = 'grpc-test.sandbox.googleapis.com';
|
||||
const target2 = 'grpc-test4.sandbox.googleapis.com';
|
||||
const target1 = parseUri('grpc-test.sandbox.googleapis.com')!;
|
||||
const target2 = parseUri('grpc-test4.sandbox.googleapis.com')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -332,13 +333,13 @@ describe('Name Resolver', () => {
|
|||
};
|
||||
const resolver1 = resolverManager.createResolver(target1, listener);
|
||||
resolver1.updateResolution();
|
||||
const resolver2 = resolverManager.createResolver(target1, listener);
|
||||
const resolver2 = resolverManager.createResolver(target2, listener);
|
||||
resolver2.updateResolution();
|
||||
});
|
||||
});
|
||||
describe('UDS Names', () => {
|
||||
it('Should handle a relative Unix Domain Socket name', done => {
|
||||
const target = 'unix:socket';
|
||||
const target = parseUri('unix:socket')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -362,7 +363,7 @@ describe('Name Resolver', () => {
|
|||
resolver.updateResolution();
|
||||
});
|
||||
it('Should handle an absolute Unix Domain Socket name', done => {
|
||||
const target = 'unix:///tmp/socket';
|
||||
const target = parseUri('unix:///tmp/socket')!;
|
||||
const listener: resolverManager.ResolverListener = {
|
||||
onSuccessfulResolution: (
|
||||
addressList: SubchannelAddress[],
|
||||
|
@ -393,14 +394,15 @@ describe('Name Resolver', () => {
|
|||
return [];
|
||||
}
|
||||
|
||||
static getDefaultAuthority(target: string): string {
|
||||
static getDefaultAuthority(target: GrpcUri): string {
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
it('Should return the correct authority if a different resolver has been registered', () => {
|
||||
const target = 'other://name';
|
||||
resolverManager.registerResolver('other:', OtherResolver);
|
||||
const target = parseUri('other:name')!;
|
||||
console.log(target);
|
||||
resolverManager.registerResolver('other', OtherResolver);
|
||||
|
||||
const authority = resolverManager.getDefaultAuthority(target);
|
||||
assert.equal(authority, 'other');
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as uriParser from '../src/uri-parser';
|
||||
|
||||
describe('URI Parser', function(){
|
||||
describe('parseUri', function() {
|
||||
const expectationList: {target: string, result: uriParser.GrpcUri | null}[] = [
|
||||
{target: 'localhost', result: {scheme: undefined, authority: undefined, path: 'localhost'}},
|
||||
/* This looks weird, but it's OK because the resolver selection code will handle it */
|
||||
{target: 'localhost:80', result: {scheme: 'localhost', authority: undefined, path: '80'}},
|
||||
{target: 'dns:localhost', result: {scheme: 'dns', authority: undefined, path: 'localhost'}},
|
||||
{target: 'dns:///localhost', result: {scheme: 'dns', authority: '', path: 'localhost'}},
|
||||
{target: 'dns://authority/localhost', result: {scheme: 'dns', authority: 'authority', path: 'localhost'}},
|
||||
{target: '//authority/localhost', result: {scheme: undefined, authority: 'authority', path: 'localhost'}},
|
||||
// Regression test for https://github.com/grpc/grpc-node/issues/1359
|
||||
{target: 'dns:foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443', result: {scheme: 'dns', authority: undefined, path: 'foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443'}}
|
||||
];
|
||||
for (const {target, result} of expectationList) {
|
||||
it (target, function() {
|
||||
assert.deepStrictEqual(uriParser.parseUri(target), result);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('splitHostPort', function() {
|
||||
const expectationList: {path: string, result: uriParser.HostPort | null}[] = [
|
||||
{path: 'localhost', result: {host: 'localhost'}},
|
||||
{path: 'localhost:123', result: {host: 'localhost', port: 123}},
|
||||
{path: '12345:6789', result: {host: '12345', port: 6789}},
|
||||
{path: '[::1]:123', result: {host: '::1', port: 123}},
|
||||
{path: '[::1]', result: {host: '::1'}},
|
||||
{path: '[', result: null},
|
||||
{path: '[123]', result: null},
|
||||
// Regression test for https://github.com/grpc/grpc-node/issues/1359
|
||||
{path: 'foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443', result: {host: 'foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443'}}
|
||||
];
|
||||
for (const {path, result} of expectationList) {
|
||||
it(path, function() {
|
||||
assert.deepStrictEqual(uriParser.splitHostPort(path), result);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue