mirror of https://github.com/grpc/grpc-node.git
feat(grpc-reflection): added reflection service to add capability to a users server
This commit is contained in:
parent
54df17727f
commit
215078f49a
|
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,10 @@
|
||||||
"email": "justinmtimmons@gmail.com"
|
"email": "justinmtimmons@gmail.com"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"main": "build/src/index.js",
|
||||||
|
"types": "build/src/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"LICENSE",
|
"build"
|
||||||
"README.md",
|
|
||||||
"src",
|
|
||||||
"build",
|
|
||||||
"proto"
|
|
||||||
],
|
],
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export { ReflectionService } from './service';
|
|
@ -1,3 +1,4 @@
|
||||||
|
import * as path from 'path';
|
||||||
import {
|
import {
|
||||||
FileDescriptorProto,
|
FileDescriptorProto,
|
||||||
FileDescriptorSet,
|
FileDescriptorSet,
|
||||||
|
@ -9,8 +10,11 @@ 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 { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse';
|
||||||
import { visit } from './protobuf-visitor';
|
import { visit } from './protobuf-visitor';
|
||||||
import { scope } from './utils';
|
import { scope } from './utils';
|
||||||
|
import { PROTO_LOADER_OPTS } from './constants';
|
||||||
|
|
||||||
export class ReflectionError extends Error {
|
export class ReflectionError extends Error {
|
||||||
constructor(
|
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
|
/** List the full names of registered gRPC services
|
||||||
*
|
*
|
||||||
* note: the spec is unclear as to what the 'listServices' param can be; most
|
* note: the spec is unclear as to what the 'listServices' param can be; most
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue