refactor(grpc-reflection): file cleanup and enabled ts strict mode

This commit is contained in:
Justin Timmons 2023-11-08 20:42:20 -05:00
parent 215078f49a
commit 3b4f92ee62
12 changed files with 77 additions and 66 deletions

View File

@ -9,14 +9,9 @@ const INCLUDE_PATH = path.join(__dirname, '../proto/sample/vendor');
const server = new grpc.Server(); const server = new grpc.Server();
const packageDefinition = protoLoader.loadSync(PROTO_PATH, { includeDirs: [INCLUDE_PATH] }); const packageDefinition = protoLoader.loadSync(PROTO_PATH, { includeDirs: [INCLUDE_PATH] });
const reflection = new ReflectionService(packageDefinition); const reflection = new ReflectionService(packageDefinition, { services: ['sample.SampleService'] });
reflection.addToServer(server); reflection.addToServer(server);
server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => { server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => {
server.start(); server.start();
}); });
// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);

View File

@ -9,6 +9,10 @@ service SampleService {
rpc Hello2 (HelloRequest) returns (CommonMessage) {} rpc Hello2 (HelloRequest) returns (CommonMessage) {}
} }
service IgnoreService {
rpc Hello (HelloRequest) returns (HelloResponse) {}
}
message HelloRequest { message HelloRequest {
string hello = 1; string hello = 1;
HelloNested nested = 2; HelloNested nested = 2;

View File

@ -0,0 +1,5 @@
/** Options to create a reflection server */
export interface ReflectionServerOptions {
/** whitelist of fully-qualified service names to expose. (Default: expose all) */
services?: string[];
}

View File

@ -101,7 +101,7 @@ export const visit = (file: FileDescriptorProto, visitor: Visitor): void => {
}); });
}; };
const packageName = file.getPackage(); const packageName = file.getPackage() || '';
file.getEnumTypeList().forEach((type) => processEnum(packageName, file, type)); file.getEnumTypeList().forEach((type) => processEnum(packageName, file, type));
file.getMessageTypeList().forEach((type) => processMessage(packageName, file, type)); file.getMessageTypeList().forEach((type) => processMessage(packageName, file, type));
file.getServiceList().forEach((service) => processService(packageName, file, service)); file.getServiceList().forEach((service) => processService(packageName, file, service));

View File

@ -7,14 +7,15 @@ import {
import * as grpc from '@grpc/grpc-js'; import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader'; import * as protoLoader from '@grpc/proto-loader';
import { ExtensionNumberResponse__Output } from './generated/grpc/reflection/v1/ExtensionNumberResponse'; import { ExtensionNumberResponse__Output } from '../generated/grpc/reflection/v1/ExtensionNumberResponse';
import { FileDescriptorResponse__Output } from './generated/grpc/reflection/v1/FileDescriptorResponse'; import { FileDescriptorResponse__Output } from '../generated/grpc/reflection/v1/FileDescriptorResponse';
import { ListServiceResponse__Output } from './generated/grpc/reflection/v1/ListServiceResponse'; import { ListServiceResponse__Output } from '../generated/grpc/reflection/v1/ListServiceResponse';
import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest'; import { ServerReflectionRequest } from '../generated/grpc/reflection/v1/ServerReflectionRequest';
import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse'; import { ServerReflectionResponse } from '../generated/grpc/reflection/v1/ServerReflectionResponse';
import { visit } from './protobuf-visitor'; import { visit } from './common/protobuf-visitor';
import { scope } from './utils'; import { scope } from './common/utils';
import { PROTO_LOADER_OPTS } from './constants'; import { PROTO_LOADER_OPTS } from './common/constants';
import { ReflectionServerOptions } from './common/interfaces';
export class ReflectionError extends Error { export class ReflectionError extends Error {
constructor( constructor(
@ -37,22 +38,27 @@ export class ReflectionError extends Error {
export class ReflectionV1Implementation { export class ReflectionV1Implementation {
/** The full list of proto files (including imported deps) that the gRPC server includes */ /** The full list of proto files (including imported deps) that the gRPC server includes */
private fileDescriptorSet = new FileDescriptorSet(); private readonly fileDescriptorSet = new FileDescriptorSet();
/** An index of proto files by file name (eg. 'sample.proto') */ /** An index of proto files by file name (eg. 'sample.proto') */
private fileNameIndex: Record<string, FileDescriptorProto> = {}; private readonly fileNameIndex: Record<string, FileDescriptorProto> = {};
/** An index of proto files by type extension relationship /** An index of proto files by type extension relationship
* *
* extensionIndex[<pkg>.<msg>][<field#>] contains a reference to the file containing an * extensionIndex[<pkg>.<msg>][<field#>] contains a reference to the file containing an
* extension for the type "<pkg>.<msg>" and field number "<field#>" * extension for the type "<pkg>.<msg>" and field number "<field#>"
*/ */
private extensionIndex: Record<string, Record<number, FileDescriptorProto>> = {}; private readonly extensionIndex: Record<string, Record<number, FileDescriptorProto>> = {};
/** An index of fully qualified symbol names (eg. 'sample.Message') to the files that contain them */ /** An index of fully qualified symbol names (eg. 'sample.Message') to the files that contain them */
private symbolMap: Record<string, FileDescriptorProto> = {}; private readonly symbolMap: Record<string, FileDescriptorProto> = {};
/** Options that the user provided for this service */
private readonly options?: ReflectionServerOptions;
constructor(root: protoLoader.PackageDefinition, options?: ReflectionServerOptions) {
this.options = options;
constructor(root: protoLoader.PackageDefinition) {
Object.values(root).forEach(({ fileDescriptorProtos }) => { Object.values(root).forEach(({ fileDescriptorProtos }) => {
// Add file descriptors to the FileDescriptorSet. // Add file descriptors to the FileDescriptorSet.
// We use the Array check here because a ServiceDefinition could have a method named the same thing // We use the Array check here because a ServiceDefinition could have a method named the same thing
@ -88,10 +94,10 @@ export class ReflectionV1Implementation {
extension: (fqn, file, ext) => { extension: (fqn, file, ext) => {
index(fqn, file); index(fqn, file);
const extendeeName = ext.getExtendee(); const extendeeName = ext.getExtendee() || '';
this.extensionIndex[extendeeName] = { this.extensionIndex[extendeeName] = {
...(this.extensionIndex[extendeeName] || {}), ...(this.extensionIndex[extendeeName] || {}),
[ext.getNumber()]: file, [ext.getNumber() || -1]: file,
}; };
}, },
}), }),
@ -126,25 +132,26 @@ export class ReflectionV1Implementation {
return; return;
} }
if (referencedFile !== sourceFile) { const fname = referencedFile.getName();
sourceFile.addDependency(referencedFile.getName()); if (referencedFile !== sourceFile && fname) {
sourceFile.addDependency(fname);
} }
}; };
this.fileDescriptorSet.getFileList().forEach((file) => this.fileDescriptorSet.getFileList().forEach((file) =>
visit(file, { visit(file, {
field: (fqn, file, field) => addReference(field.getTypeName(), file, scope(fqn)), field: (fqn, file, field) => addReference(field.getTypeName() || '', file, scope(fqn)),
extension: (fqn, file, ext) => addReference(ext.getTypeName(), file, scope(fqn)), extension: (fqn, file, ext) => addReference(ext.getTypeName() || '', file, scope(fqn)),
method: (fqn, file, method) => { method: (fqn, file, method) => {
addReference(method.getInputType(), file, scope(fqn)); addReference(method.getInputType() || '', file, scope(fqn));
addReference(method.getOutputType(), file, scope(fqn)); addReference(method.getOutputType() || '', file, scope(fqn));
}, },
}), }),
); );
} }
addToServer(server: Pick<grpc.Server, 'addService'>) { addToServer(server: Pick<grpc.Server, 'addService'>) {
const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1/reflection.proto'); const protoPath = path.join(__dirname, '../../proto/grpc/reflection/v1/reflection.proto');
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS); const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any; const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;
@ -180,8 +187,10 @@ export class ReflectionV1Implementation {
} else if (message.fileByFilename !== undefined) { } else if (message.fileByFilename !== undefined) {
response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename); response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename);
} else if (message.fileContainingExtension !== undefined) { } else if (message.fileContainingExtension !== undefined) {
const { containingType, extensionNumber } = message.fileContainingExtension; response.fileDescriptorResponse = this.fileContainingExtension(
response.fileDescriptorResponse = this.fileContainingExtension(containingType, extensionNumber); message.fileContainingExtension?.containingType || '',
message.fileContainingExtension?.extensionNumber || -1
);
} else if (message.allExtensionNumbersOfType) { } else if (message.allExtensionNumbersOfType) {
response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType); response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType);
} else { } else {
@ -224,7 +233,12 @@ export class ReflectionV1Implementation {
) )
.flat(); .flat();
return { service: services.map((service) => ({ name: service })) }; const whitelist = new Set(this.options?.services ?? undefined);
const exposedServices = this.options?.services ?
services.filter(service => whitelist.has(service))
: services;
return { service: exposedServices.map((service) => ({ name: service })) };
} }
/** Find the proto file(s) that declares the given fully-qualified symbol name /** Find the proto file(s) that declares the given fully-qualified symbol name

View File

@ -3,10 +3,10 @@ import * as path from 'path';
import * as grpc from '@grpc/grpc-js'; import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader'; import * as protoLoader from '@grpc/proto-loader';
import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest'; import { ServerReflectionRequest } from '../generated/grpc/reflection/v1/ServerReflectionRequest';
import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse'; import { ServerReflectionResponse } from '../generated/grpc/reflection/v1/ServerReflectionResponse';
import { PROTO_LOADER_OPTS } from './constants'; import { PROTO_LOADER_OPTS } from './common/constants';
import { ReflectionV1Implementation } from './reflection-v1-implementation'; import { ReflectionV1Implementation } from './reflection-v1';
/** Analyzes a gRPC server and exposes methods to reflect on it /** Analyzes a gRPC server and exposes methods to reflect on it
@ -18,12 +18,12 @@ import { ReflectionV1Implementation } from './reflection-v1-implementation';
* For example: if files 'a.proto' and 'b.proto' are both for the same package 'c' then * For example: if files 'a.proto' and 'b.proto' are both for the same package 'c' then
* we will always return a reference to a combined 'c.proto' instead of the 2 files. * we will always return a reference to a combined 'c.proto' instead of the 2 files.
* *
* @remarks as the v1 and v1alpha specs are identical, this implementation extends v1 * @privateRemarks as the v1 and v1alpha specs are identical, this implementation extends
* and just exposes it at the v1alpha package instead * reflection-v1 and exposes it at the v1alpha package instead
*/ */
export class ReflectionV1AlphaImplementation extends ReflectionV1Implementation { export class ReflectionV1AlphaImplementation extends ReflectionV1Implementation {
addToServer(server: Pick<grpc.Server, 'addService'>) { addToServer(server: Pick<grpc.Server, 'addService'>) {
const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1alpha/reflection.proto'); const protoPath = path.join(__dirname, '../../proto/grpc/reflection/v1alpha/reflection.proto');
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS); const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any; const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;

View File

@ -1,13 +1,9 @@
import * as grpc from '@grpc/grpc-js'; import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader'; import * as protoLoader from '@grpc/proto-loader';
import { ReflectionV1Implementation } from './reflection-v1-implementation'; import { ReflectionV1Implementation } from './implementations/reflection-v1';
import { ReflectionV1AlphaImplementation } from './reflection-v1alpha'; import { ReflectionV1AlphaImplementation } from './implementations/reflection-v1alpha';
import { ReflectionServerOptions } from './implementations/common/interfaces';
interface ReflectionServerOptions {
/** whitelist of fully-qualified service names to expose. (Default: expose all) */
services?: string[];
}
/** Analyzes a gRPC package and exposes endpoints providing information about /** Analyzes a gRPC package and exposes endpoints providing information about
* it according to the gRPC Server Reflection API Specification * it according to the gRPC Server Reflection API Specification
@ -31,21 +27,8 @@ export class ReflectionService {
private readonly v1Alpha: ReflectionV1AlphaImplementation; private readonly v1Alpha: ReflectionV1AlphaImplementation;
constructor(pkg: protoLoader.PackageDefinition, options?: ReflectionServerOptions) { constructor(pkg: protoLoader.PackageDefinition, options?: ReflectionServerOptions) {
this.v1 = new ReflectionV1Implementation(pkg, options);
if (options.services) { this.v1Alpha = new ReflectionV1AlphaImplementation(pkg, options);
const whitelist = new Set(options.services);
for (const key in Object.keys(pkg)) {
const value = pkg[key];
const isService = value.format !== 'Protocol Buffer 3 DescriptorProto' && value.format !== 'Protocol Buffer 3 EnumDescriptorProto';
if (isService && !whitelist.has(key)) {
delete pkg[key];
}
}
}
this.v1 = new ReflectionV1Implementation(pkg);
this.v1Alpha = new ReflectionV1AlphaImplementation(pkg);
} }
addToServer(server: Pick<grpc.Server, 'addService'>) { addToServer(server: Pick<grpc.Server, 'addService'>) {

View File

@ -3,14 +3,12 @@ import * as path from 'path';
import { FileDescriptorProto } from 'google-protobuf/google/protobuf/descriptor_pb'; import { FileDescriptorProto } from 'google-protobuf/google/protobuf/descriptor_pb';
import * as protoLoader from '@grpc/proto-loader'; import * as protoLoader from '@grpc/proto-loader';
import { ReflectionV1Implementation } from '../src/reflection-v1-implementation'; import { ReflectionV1Implementation } from '../src/implementations/reflection-v1';
describe('GrpcReflectionService', () => { describe('GrpcReflectionService', () => {
let reflectionService: ReflectionV1Implementation; let reflectionService: ReflectionV1Implementation;
beforeEach(async () => { beforeEach(async () => {
console.log(path.join(__dirname, '../proto/sample/sample.proto'));
console.log([path.join(__dirname, '../proto/sample/vendor')]);
const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), { const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), {
includeDirs: [path.join(__dirname, '../proto/sample/vendor')] includeDirs: [path.join(__dirname, '../proto/sample/vendor')]
}); });
@ -20,6 +18,18 @@ describe('GrpcReflectionService', () => {
describe('listServices()', () => { describe('listServices()', () => {
it('lists all services', () => { it('lists all services', () => {
const { service: services } = reflectionService.listServices('*');
assert.equal(services.length, 2);
assert(services.find((s) => s.name === 'sample.SampleService'));
});
it('whitelists services properly', () => {
const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), {
includeDirs: [path.join(__dirname, '../proto/sample/vendor')]
});
reflectionService = new ReflectionV1Implementation(root, { services: ['sample.SampleService'] });
const { service: services } = reflectionService.listServices('*'); const { service: services } = reflectionService.listServices('*');
assert.equal(services.length, 1); assert.equal(services.length, 1);
assert(services.find((s) => s.name === 'sample.SampleService')); assert(services.find((s) => s.name === 'sample.SampleService'));

View File

@ -1,6 +1,6 @@
import * as assert from 'assert'; import * as assert from 'assert';
import { scope } from '../src/utils'; import { scope } from '../src/implementations/common/utils';
describe('scope', () => { describe('scope', () => {
it('traverses upwards in the package scope', () => { it('traverses upwards in the package scope', () => {

View File

@ -9,7 +9,7 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"pretty": true, "pretty": true,
"sourceMap": true, "sourceMap": true,
"strictNullChecks": false, "strict": true,
"lib": ["es2017"], "lib": ["es2017"],
"outDir": "build", "outDir": "build",
"target": "es2017", "target": "es2017",