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:
Michael Lumish 2020-04-20 12:19:18 -07:00 committed by GitHub
commit ae61562be2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 325 additions and 191 deletions

View File

@ -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) {

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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() {

View File

@ -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
) {

View File

@ -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();
}

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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');

View File

@ -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);
});
}
});
});