feat(grpc-reflection): added reflection service to add capability to a users server

This commit is contained in:
Justin Timmons 2023-11-04 17:46:13 -04:00
parent 54df17727f
commit 215078f49a
7 changed files with 205 additions and 5 deletions

View File

@ -0,0 +1,22 @@
import * as path from 'path';
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { ReflectionService } from '../src';
const PROTO_PATH = path.join(__dirname, '../proto/sample/sample.proto');
const INCLUDE_PATH = path.join(__dirname, '../proto/sample/vendor');
const server = new grpc.Server();
const packageDefinition = protoLoader.loadSync(PROTO_PATH, { includeDirs: [INCLUDE_PATH] });
const reflection = new ReflectionService(packageDefinition);
reflection.addToServer(server);
server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => {
server.start();
});
// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);

View File

@ -15,12 +15,10 @@
"email": "justinmtimmons@gmail.com"
}
],
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
"files": [
"LICENSE",
"README.md",
"src",
"build",
"proto"
"build"
],
"license": "Apache-2.0",
"scripts": {

View File

@ -0,0 +1,14 @@
import * as protoLoader from '@grpc/proto-loader';
/** Options to use when loading protobuf files in this repo
*
* @remarks *must* match the proto-loader-gen-types usage in the package.json
* otherwise the generated types may not match the data coming into this service
*/
export const PROTO_LOADER_OPTS: protoLoader.Options = {
longs: String,
enums: String,
bytes: Array,
defaults: true,
oneofs: true
};

View File

@ -0,0 +1 @@
export { ReflectionService } from './service';

View File

@ -1,3 +1,4 @@
import * as path from 'path';
import {
FileDescriptorProto,
FileDescriptorSet,
@ -9,8 +10,11 @@ import * as protoLoader from '@grpc/proto-loader';
import { ExtensionNumberResponse__Output } from './generated/grpc/reflection/v1/ExtensionNumberResponse';
import { FileDescriptorResponse__Output } from './generated/grpc/reflection/v1/FileDescriptorResponse';
import { ListServiceResponse__Output } from './generated/grpc/reflection/v1/ListServiceResponse';
import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest';
import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse';
import { visit } from './protobuf-visitor';
import { scope } from './utils';
import { PROTO_LOADER_OPTS } from './constants';
export class ReflectionError extends Error {
constructor(
@ -139,6 +143,70 @@ export class ReflectionV1Implementation {
);
}
addToServer(server: Pick<grpc.Server, 'addService'>) {
const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1/reflection.proto');
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;
server.addService(pkg.grpc.reflection.v1.ServerReflection.service, {
ServerReflectionInfo: (
stream: grpc.ServerDuplexStream<ServerReflectionRequest, ServerReflectionResponse>
) => {
stream.on('end', () => stream.end());
stream.on('data', (message: ServerReflectionRequest) => {
stream.write(this.handleServerReflectionRequest(message));
});
}
});
}
/** Assemble a response for a single server reflection request in the stream */
handleServerReflectionRequest(message: ServerReflectionRequest): ServerReflectionResponse {
const response: ServerReflectionResponse = {
validHost: message.host,
originalRequest: message,
fileDescriptorResponse: undefined,
allExtensionNumbersResponse: undefined,
listServicesResponse: undefined,
errorResponse: undefined,
};
try {
if (message.listServices !== undefined) {
response.listServicesResponse = this.listServices(message.listServices);
} else if (message.fileContainingSymbol !== undefined) {
response.fileDescriptorResponse = this.fileContainingSymbol(message.fileContainingSymbol);
} else if (message.fileByFilename !== undefined) {
response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename);
} else if (message.fileContainingExtension !== undefined) {
const { containingType, extensionNumber } = message.fileContainingExtension;
response.fileDescriptorResponse = this.fileContainingExtension(containingType, extensionNumber);
} else if (message.allExtensionNumbersOfType) {
response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType);
} else {
throw new ReflectionError(
grpc.status.UNIMPLEMENTED,
`Unimplemented method for request: ${message}`,
);
}
} catch (e) {
if (e instanceof ReflectionError) {
response.errorResponse = {
errorCode: e.statusCode,
errorMessage: e.message,
};
} else {
response.errorResponse = {
errorCode: grpc.status.UNKNOWN,
errorMessage: 'Failed to process gRPC reflection request: unknown error',
};
}
}
return response;
}
/** List the full names of registered gRPC services
*
* note: the spec is unclear as to what the 'listServices' param can be; most

View File

@ -0,0 +1,42 @@
import * as path from 'path';
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest';
import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse';
import { PROTO_LOADER_OPTS } from './constants';
import { ReflectionV1Implementation } from './reflection-v1-implementation';
/** Analyzes a gRPC server and exposes methods to reflect on it
*
* NOTE: the files returned by this service may not match the handwritten ones 1:1.
* This is because proto-loader reorients files based on their package definition,
* combining any that have the same package.
*
* 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.
*
* @remarks as the v1 and v1alpha specs are identical, this implementation extends v1
* and just exposes it at the v1alpha package instead
*/
export class ReflectionV1AlphaImplementation extends ReflectionV1Implementation {
addToServer(server: Pick<grpc.Server, 'addService'>) {
const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1alpha/reflection.proto');
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;
server.addService(pkg.grpc.reflection.v1alpha.ServerReflection.service, {
ServerReflectionInfo: (
stream: grpc.ServerDuplexStream<ServerReflectionRequest, ServerReflectionResponse>
) => {
stream.on('end', () => stream.end());
stream.on('data', (message: ServerReflectionRequest) => {
stream.write(this.handleServerReflectionRequest(message));
});
}
});
}
}

View File

@ -0,0 +1,55 @@
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { ReflectionV1Implementation } from './reflection-v1-implementation';
import { ReflectionV1AlphaImplementation } from './reflection-v1alpha';
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
* it according to the gRPC Server Reflection API Specification
*
* @see https://github.com/grpc/grpc/blob/master/doc/server-reflection.md
*
* @remarks
*
* in order to keep backwards compatibility as the reflection schema evolves
* this service contains implementations for each of the published versions
*
* @privateRemarks
*
* this class acts mostly as a facade to several underlying implementations. This
* allows us to add or remove support for different versions of the reflection
* schema without affecting the consumer
*
*/
export class ReflectionService {
private readonly v1: ReflectionV1Implementation;
private readonly v1Alpha: ReflectionV1AlphaImplementation;
constructor(pkg: protoLoader.PackageDefinition, options?: ReflectionServerOptions) {
if (options.services) {
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'>) {
this.v1.addToServer(server);
this.v1Alpha.addToServer(server);
}
}