Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

grpc-js: Add support for ipv4 and ipv6 schemes #1752

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
107 changes: 107 additions & 0 deletions packages/grpc-js/src/resolver-ip.ts
@@ -0,0 +1,107 @@
/*
* Copyright 2021 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 { isIPv4, isIPv6 } from "net";
import { StatusObject } from "./call-stream";
import { ChannelOptions } from "./channel-options";
import { LogVerbosity, Status } from "./constants";
import { Metadata } from "./metadata";
import { registerResolver, Resolver, ResolverListener } from "./resolver";
import { SubchannelAddress } from "./subchannel";
import { GrpcUri, splitHostPort, uriToString } from "./uri-parser";
import * as logging from './logging';

const TRACER_NAME = 'ip_resolver';

function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}

const IPV4_SCHEME = 'ipv4';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorts of feels weird that these are private strings here, but I guess there might not be any needs to expose a list of all the known schemes somewhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they were going to be exposed in any way, it would probably be through the registerResolver function, so the string constants in this file would still be private.

const IPV6_SCHEME = 'ipv6';

/**
* The default TCP port to connect to if not explicitly specified in the target.
*/
const DEFAULT_PORT = 443;

class IpResolver implements Resolver {
private addresses: SubchannelAddress[] = [];
private error: StatusObject | null = null;
constructor(
private target: GrpcUri,
private listener: ResolverListener,
channelOptions: ChannelOptions
) {
trace('Resolver constructed for target ' + uriToString(target));
const addresses: SubchannelAddress[] = [];
if (!(target.scheme === IPV4_SCHEME || target.scheme === IPV6_SCHEME)) {
this.error = {
code: Status.UNAVAILABLE,
details: `Unrecognized scheme ${target.scheme} in IP resolver`,
metadata: new Metadata()
};
return;
}
const pathList = target.path.split(',');
for (const path of pathList) {
const hostPort = splitHostPort(path);
if (hostPort === null) {
this.error = {
code: Status.UNAVAILABLE,
details: `Failed to parse ${target.scheme} address ${path}`,
metadata: new Metadata()
};
return;
}
if ((target.scheme === IPV4_SCHEME && !isIPv4(hostPort.host)) || (target.scheme === IPV6_SCHEME && !isIPv6(hostPort.host))) {
this.error = {
code: Status.UNAVAILABLE,
details: `Failed to parse ${target.scheme} address ${path}`,
metadata: new Metadata()
};
return;
}
addresses.push({
host: hostPort.host,
port: hostPort.port ?? DEFAULT_PORT
});
}
this.addresses = addresses;
trace('Parsed ' + target.scheme + ' address list ' + this.addresses);
}
updateResolution(): void {
process.nextTick(() => {
if (this.error) {
this.listener.onError(this.error)
} else {
this.listener.onSuccessfulResolution(this.addresses, null, null, null, {});
}
});
}
destroy(): void {
// This resolver owns no resources, so we do nothing here.
}

static getDefaultAuthority(target: GrpcUri): string {
return target.path.split(',')[0];
}
}

export function setup() {
registerResolver(IPV4_SCHEME, IpResolver);
registerResolver(IPV6_SCHEME, IpResolver);
}
2 changes: 2 additions & 0 deletions packages/grpc-js/src/resolver.ts
Expand Up @@ -18,6 +18,7 @@
import { MethodConfig, ServiceConfig } from './service-config';
import * as resolver_dns from './resolver-dns';
import * as resolver_uds from './resolver-uds';
import * as resolver_ip from './resolver-ip';
import { StatusObject } from './call-stream';
import { SubchannelAddress } from './subchannel';
import { GrpcUri, uriToString } from './uri-parser';
Expand Down Expand Up @@ -179,4 +180,5 @@ export function mapUriDefaultScheme(target: GrpcUri): GrpcUri | null {
export function registerAll() {
resolver_dns.setup();
resolver_uds.setup();
resolver_ip.setup();
}
180 changes: 180 additions & 0 deletions packages/grpc-js/test/test-resolver.ts
Expand Up @@ -388,6 +388,186 @@ describe('Name Resolver', () => {
resolver.updateResolution();
});
});
describe('IP Addresses', () => {
it('should handle one IPv4 address with no port', done => {
const target = resolverManager.mapUriDefaultScheme(parseUri('ipv4:127.0.0.1')!)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
addressList: SubchannelAddress[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
addressList.some(
addr =>
isTcpSubchannelAddress(addr) &&
addr.host === '127.0.0.1' &&
addr.port === 443
)
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle one IPv4 address with a port', done => {
const target = resolverManager.mapUriDefaultScheme(parseUri('ipv4:127.0.0.1:50051')!)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
addressList: SubchannelAddress[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
addressList.some(
addr =>
isTcpSubchannelAddress(addr) &&
addr.host === '127.0.0.1' &&
addr.port === 50051
)
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle multiple IPv4 addresses with different ports', done => {
const target = resolverManager.mapUriDefaultScheme(parseUri('ipv4:127.0.0.1:50051,127.0.0.1:50052')!)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
addressList: SubchannelAddress[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
addressList.some(
addr =>
isTcpSubchannelAddress(addr) &&
addr.host === '127.0.0.1' &&
addr.port === 50051
)
);
assert(
addressList.some(
addr =>
isTcpSubchannelAddress(addr) &&
addr.host === '127.0.0.1' &&
addr.port === 50052
)
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle one IPv6 address with no port', done => {
const target = resolverManager.mapUriDefaultScheme(parseUri('ipv6:::1')!)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
addressList: SubchannelAddress[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
addressList.some(
addr =>
isTcpSubchannelAddress(addr) &&
addr.host === '::1' &&
addr.port === 443
)
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle one IPv6 address with a port', done => {
const target = resolverManager.mapUriDefaultScheme(parseUri('ipv6:[::1]:50051')!)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
addressList: SubchannelAddress[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
addressList.some(
addr =>
isTcpSubchannelAddress(addr) &&
addr.host === '::1' &&
addr.port === 50051
)
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle multiple IPv6 addresses with different ports', done => {
const target = resolverManager.mapUriDefaultScheme(parseUri('ipv6:[::1]:50051,[::1]:50052')!)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
addressList: SubchannelAddress[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
addressList.some(
addr =>
isTcpSubchannelAddress(addr) &&
addr.host === '::1' &&
addr.port === 50051
)
);
assert(
addressList.some(
addr =>
isTcpSubchannelAddress(addr) &&
addr.host === '::1' &&
addr.port === 50052
)
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
});
describe('getDefaultAuthority', () => {
class OtherResolver implements resolverManager.Resolver {
updateResolution() {
Expand Down