Skip to content

Commit 215078f

Browse files
committed
feat(grpc-reflection): added reflection service to add capability to a users server
1 parent 54df177 commit 215078f

File tree

7 files changed

+205
-5
lines changed

7 files changed

+205
-5
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as path from 'path';
2+
import * as grpc from '@grpc/grpc-js';
3+
import * as protoLoader from '@grpc/proto-loader';
4+
5+
import { ReflectionService } from '../src';
6+
7+
const PROTO_PATH = path.join(__dirname, '../proto/sample/sample.proto');
8+
const INCLUDE_PATH = path.join(__dirname, '../proto/sample/vendor');
9+
10+
const server = new grpc.Server();
11+
const packageDefinition = protoLoader.loadSync(PROTO_PATH, { includeDirs: [INCLUDE_PATH] });
12+
const reflection = new ReflectionService(packageDefinition);
13+
reflection.addToServer(server);
14+
15+
server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => {
16+
server.start();
17+
});
18+
19+
// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
20+
21+
22+

packages/grpc-reflection/package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@
1515
"email": "[email protected]"
1616
}
1717
],
18+
"main": "build/src/index.js",
19+
"types": "build/src/index.d.ts",
1820
"files": [
19-
"LICENSE",
20-
"README.md",
21-
"src",
22-
"build",
23-
"proto"
21+
"build"
2422
],
2523
"license": "Apache-2.0",
2624
"scripts": {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as protoLoader from '@grpc/proto-loader';
2+
3+
/** Options to use when loading protobuf files in this repo
4+
*
5+
* @remarks *must* match the proto-loader-gen-types usage in the package.json
6+
* otherwise the generated types may not match the data coming into this service
7+
*/
8+
export const PROTO_LOADER_OPTS: protoLoader.Options = {
9+
longs: String,
10+
enums: String,
11+
bytes: Array,
12+
defaults: true,
13+
oneofs: true
14+
};

packages/grpc-reflection/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ReflectionService } from './service';

packages/grpc-reflection/src/reflection-v1-implementation.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as path from 'path';
12
import {
23
FileDescriptorProto,
34
FileDescriptorSet,
@@ -9,8 +10,11 @@ import * as protoLoader from '@grpc/proto-loader';
910
import { ExtensionNumberResponse__Output } from './generated/grpc/reflection/v1/ExtensionNumberResponse';
1011
import { FileDescriptorResponse__Output } from './generated/grpc/reflection/v1/FileDescriptorResponse';
1112
import { ListServiceResponse__Output } from './generated/grpc/reflection/v1/ListServiceResponse';
13+
import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest';
14+
import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse';
1215
import { visit } from './protobuf-visitor';
1316
import { scope } from './utils';
17+
import { PROTO_LOADER_OPTS } from './constants';
1418

1519
export class ReflectionError extends Error {
1620
constructor(
@@ -139,6 +143,70 @@ export class ReflectionV1Implementation {
139143
);
140144
}
141145

146+
addToServer(server: Pick<grpc.Server, 'addService'>) {
147+
const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1/reflection.proto');
148+
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
149+
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;
150+
151+
server.addService(pkg.grpc.reflection.v1.ServerReflection.service, {
152+
ServerReflectionInfo: (
153+
stream: grpc.ServerDuplexStream<ServerReflectionRequest, ServerReflectionResponse>
154+
) => {
155+
stream.on('end', () => stream.end());
156+
157+
stream.on('data', (message: ServerReflectionRequest) => {
158+
stream.write(this.handleServerReflectionRequest(message));
159+
});
160+
}
161+
});
162+
}
163+
164+
/** Assemble a response for a single server reflection request in the stream */
165+
handleServerReflectionRequest(message: ServerReflectionRequest): ServerReflectionResponse {
166+
const response: ServerReflectionResponse = {
167+
validHost: message.host,
168+
originalRequest: message,
169+
fileDescriptorResponse: undefined,
170+
allExtensionNumbersResponse: undefined,
171+
listServicesResponse: undefined,
172+
errorResponse: undefined,
173+
};
174+
175+
try {
176+
if (message.listServices !== undefined) {
177+
response.listServicesResponse = this.listServices(message.listServices);
178+
} else if (message.fileContainingSymbol !== undefined) {
179+
response.fileDescriptorResponse = this.fileContainingSymbol(message.fileContainingSymbol);
180+
} else if (message.fileByFilename !== undefined) {
181+
response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename);
182+
} else if (message.fileContainingExtension !== undefined) {
183+
const { containingType, extensionNumber } = message.fileContainingExtension;
184+
response.fileDescriptorResponse = this.fileContainingExtension(containingType, extensionNumber);
185+
} else if (message.allExtensionNumbersOfType) {
186+
response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType);
187+
} else {
188+
throw new ReflectionError(
189+
grpc.status.UNIMPLEMENTED,
190+
`Unimplemented method for request: ${message}`,
191+
);
192+
}
193+
} catch (e) {
194+
if (e instanceof ReflectionError) {
195+
response.errorResponse = {
196+
errorCode: e.statusCode,
197+
errorMessage: e.message,
198+
};
199+
} else {
200+
response.errorResponse = {
201+
errorCode: grpc.status.UNKNOWN,
202+
errorMessage: 'Failed to process gRPC reflection request: unknown error',
203+
};
204+
}
205+
}
206+
207+
return response;
208+
}
209+
142210
/** List the full names of registered gRPC services
143211
*
144212
* note: the spec is unclear as to what the 'listServices' param can be; most
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as path from 'path';
2+
3+
import * as grpc from '@grpc/grpc-js';
4+
import * as protoLoader from '@grpc/proto-loader';
5+
6+
import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest';
7+
import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse';
8+
import { PROTO_LOADER_OPTS } from './constants';
9+
import { ReflectionV1Implementation } from './reflection-v1-implementation';
10+
11+
12+
/** Analyzes a gRPC server and exposes methods to reflect on it
13+
*
14+
* NOTE: the files returned by this service may not match the handwritten ones 1:1.
15+
* This is because proto-loader reorients files based on their package definition,
16+
* combining any that have the same package.
17+
*
18+
* For example: if files 'a.proto' and 'b.proto' are both for the same package 'c' then
19+
* we will always return a reference to a combined 'c.proto' instead of the 2 files.
20+
*
21+
* @remarks as the v1 and v1alpha specs are identical, this implementation extends v1
22+
* and just exposes it at the v1alpha package instead
23+
*/
24+
export class ReflectionV1AlphaImplementation extends ReflectionV1Implementation {
25+
addToServer(server: Pick<grpc.Server, 'addService'>) {
26+
const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1alpha/reflection.proto');
27+
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
28+
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;
29+
30+
server.addService(pkg.grpc.reflection.v1alpha.ServerReflection.service, {
31+
ServerReflectionInfo: (
32+
stream: grpc.ServerDuplexStream<ServerReflectionRequest, ServerReflectionResponse>
33+
) => {
34+
stream.on('end', () => stream.end());
35+
36+
stream.on('data', (message: ServerReflectionRequest) => {
37+
stream.write(this.handleServerReflectionRequest(message));
38+
});
39+
}
40+
});
41+
}
42+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as grpc from '@grpc/grpc-js';
2+
import * as protoLoader from '@grpc/proto-loader';
3+
4+
import { ReflectionV1Implementation } from './reflection-v1-implementation';
5+
import { ReflectionV1AlphaImplementation } from './reflection-v1alpha';
6+
7+
interface ReflectionServerOptions {
8+
/** whitelist of fully-qualified service names to expose. (Default: expose all) */
9+
services?: string[];
10+
}
11+
12+
/** Analyzes a gRPC package and exposes endpoints providing information about
13+
* it according to the gRPC Server Reflection API Specification
14+
*
15+
* @see https://github.com/grpc/grpc/blob/master/doc/server-reflection.md
16+
*
17+
* @remarks
18+
*
19+
* in order to keep backwards compatibility as the reflection schema evolves
20+
* this service contains implementations for each of the published versions
21+
*
22+
* @privateRemarks
23+
*
24+
* this class acts mostly as a facade to several underlying implementations. This
25+
* allows us to add or remove support for different versions of the reflection
26+
* schema without affecting the consumer
27+
*
28+
*/
29+
export class ReflectionService {
30+
private readonly v1: ReflectionV1Implementation;
31+
private readonly v1Alpha: ReflectionV1AlphaImplementation;
32+
33+
constructor(pkg: protoLoader.PackageDefinition, options?: ReflectionServerOptions) {
34+
35+
if (options.services) {
36+
const whitelist = new Set(options.services);
37+
38+
for (const key in Object.keys(pkg)) {
39+
const value = pkg[key];
40+
const isService = value.format !== 'Protocol Buffer 3 DescriptorProto' && value.format !== 'Protocol Buffer 3 EnumDescriptorProto';
41+
if (isService && !whitelist.has(key)) {
42+
delete pkg[key];
43+
}
44+
}
45+
}
46+
47+
this.v1 = new ReflectionV1Implementation(pkg);
48+
this.v1Alpha = new ReflectionV1AlphaImplementation(pkg);
49+
}
50+
51+
addToServer(server: Pick<grpc.Server, 'addService'>) {
52+
this.v1.addToServer(server);
53+
this.v1Alpha.addToServer(server);
54+
}
55+
}

0 commit comments

Comments
 (0)