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"
|
||||
}
|
||||
],
|
||||
"main": "build/src/index.js",
|
||||
"types": "build/src/index.d.ts",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"src",
|
||||
"build",
|
||||
"proto"
|
||||
"build"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"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 {
|
||||
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
|
||||
|
|
|
@ -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