diff --git a/packages/grpc-js-core/src/call-credentials-filter.ts b/packages/grpc-js-core/src/call-credentials-filter.ts index 4633ffe8..e432935b 100644 --- a/packages/grpc-js-core/src/call-credentials-filter.ts +++ b/packages/grpc-js-core/src/call-credentials-filter.ts @@ -12,8 +12,8 @@ export class CallCredentialsFilter extends BaseFilter implements Filter { } async sendMetadata(metadata: Promise): Promise { - // TODO(murgatroid99): pass real options to generateMetadata - let credsMetadata = this.credentials.generateMetadata({}); + // TODO(kjin): pass real service URL to generateMetadata + let credsMetadata = this.credentials.generateMetadata({ service_url: '' }); let resultMetadata = await metadata; resultMetadata.merge(await credsMetadata); return resultMetadata; diff --git a/packages/grpc-js-core/src/call-credentials.ts b/packages/grpc-js-core/src/call-credentials.ts index 278cb163..0c0f6d53 100644 --- a/packages/grpc-js-core/src/call-credentials.ts +++ b/packages/grpc-js-core/src/call-credentials.ts @@ -2,8 +2,10 @@ import {map, reduce} from 'lodash'; import {Metadata} from './metadata'; +export type CallMetadataOptions = { service_url: string; }; + export type CallMetadataGenerator = - (options: {}, cb: (err: Error|null, metadata?: Metadata) => void) => + (options: CallMetadataOptions, cb: (err: Error|null, metadata?: Metadata) => void) => void; /** @@ -15,7 +17,7 @@ export interface CallCredentials { * Asynchronously generates a new Metadata object. * @param options Options used in generating the Metadata object. */ - generateMetadata(options: {}): Promise; + generateMetadata(options: CallMetadataOptions): Promise; /** * Creates a new CallCredentials object from properties of both this and * another CallCredentials object. This object's metadata generator will be @@ -28,7 +30,7 @@ export interface CallCredentials { class ComposedCallCredentials implements CallCredentials { constructor(private creds: CallCredentials[]) {} - async generateMetadata(options: {}): Promise { + async generateMetadata(options: CallMetadataOptions): Promise { let base: Metadata = new Metadata(); let generated: Metadata[] = await Promise.all( map(this.creds, (cred) => cred.generateMetadata(options))); @@ -46,7 +48,7 @@ class ComposedCallCredentials implements CallCredentials { class SingleCallCredentials implements CallCredentials { constructor(private metadataGenerator: CallMetadataGenerator) {} - generateMetadata(options: {}): Promise { + generateMetadata(options: CallMetadataOptions): Promise { return new Promise((resolve, reject) => { this.metadataGenerator(options, (err, metadata) => { if (metadata !== undefined) { @@ -64,7 +66,7 @@ class SingleCallCredentials implements CallCredentials { } class EmptyCallCredentials implements CallCredentials { - generateMetadata(options: {}): Promise { + generateMetadata(options: CallMetadataOptions): Promise { return Promise.resolve(new Metadata()); } diff --git a/packages/grpc-js-core/src/call-stream.ts b/packages/grpc-js-core/src/call-stream.ts index 849d685f..3a403d68 100644 --- a/packages/grpc-js-core/src/call-stream.ts +++ b/packages/grpc-js-core/src/call-stream.ts @@ -271,9 +271,6 @@ export class Http2CallStream extends Duplex implements CallStream { let code: Status; let details = ''; switch (errorCode) { - case http2.constants.NGHTTP2_NO_ERROR: - code = Status.OK; - break; case http2.constants.NGHTTP2_REFUSED_STREAM: code = Status.UNAVAILABLE; break; diff --git a/packages/grpc-js-core/src/client.ts b/packages/grpc-js-core/src/client.ts index 74de1d3f..25fa7bc8 100644 --- a/packages/grpc-js-core/src/client.ts +++ b/packages/grpc-js-core/src/client.ts @@ -16,6 +16,10 @@ export interface UnaryCallback { (err: ServiceError|null, value?: ResponseType): void; } +/** + * A generic gRPC client. Primarily useful as a base class for all generated + * clients. + */ export class Client { private readonly [kChannel]: Channel; constructor( diff --git a/packages/grpc-js-core/src/index.ts b/packages/grpc-js-core/src/index.ts index b26c390b..5a3f6025 100644 --- a/packages/grpc-js-core/src/index.ts +++ b/packages/grpc-js-core/src/index.ts @@ -5,31 +5,73 @@ import { Client } from './client'; import { Status} from './constants'; import { makeClientConstructor, loadPackageDefinition } from './make-client'; import { Metadata } from './metadata'; +import { IncomingHttpHeaders } from 'http'; -const notImplementedFn = () => { throw new Error('Not implemented'); }; +export interface OAuth2Client { + getRequestMetadata: (url: string, callback: (err: Error|null, headers?: { Authorization: string }) => void) => void; +} + +/**** Client Credentials ****/ + +// Using assign only copies enumerable properties, which is what we want +export const credentials = Object.assign({ + /** + * Create a gRPC credential from a Google credential object. + * @param googleCredentials The authentication client to use. + * @return The resulting CallCredentials object. + */ + createFromGoogleCredential: (googleCredentials: OAuth2Client): CallCredentials => { + return CallCredentials.createFromMetadataGenerator((options, callback) => { + googleCredentials.getRequestMetadata(options.service_url, (err, headers) => { + if (err) { + callback(err); + return; + } + const metadata = new Metadata(); + metadata.add('authorization', headers!.Authorization); + callback(null, metadata); + }); + }); + }, + + /** + * Combine a ChannelCredentials with any number of CallCredentials into a + * single ChannelCredentials object. + * @param channelCredentials The ChannelCredentials object. + * @param callCredentials Any number of CallCredentials objects. + * @return The resulting ChannelCredentials object. + */ + combineChannelCredentials: ( + channelCredentials: ChannelCredentials, + ...callCredentials: CallCredentials[]): ChannelCredentials => { + return callCredentials.reduce((acc, other) => acc.compose(other), channelCredentials); + }, + + /** + * Combine any number of CallCredentials into a single CallCredentials object. + * @param first The first CallCredentials object. + * @param additional Any number of additional CallCredentials objects. + * @return The resulting CallCredentials object. + */ + combineCallCredentials: ( + first: CallCredentials, + ...additional: CallCredentials[]): CallCredentials => { + return additional.reduce((acc, other) => acc.compose(other), first); + } +}, ChannelCredentials, CallCredentials); + +/**** Metadata ****/ -// Metadata export { Metadata }; -// Client credentials - -export const credentials = { - createSsl: ChannelCredentials.createSsl, - createFromMetadataGenerator: CallCredentials.createFromMetadataGenerator, - createFromGoogleCredential: notImplementedFn /*TODO*/, - combineChannelCredentials: (first: ChannelCredentials, ...additional: CallCredentials[]) => additional.reduce((acc, other) => acc.compose(other), first), - combineCallCredentials: (first: CallCredentials, ...additional: CallCredentials[]) => additional.reduce((acc, other) => acc.compose(other), first), - createInsecure: ChannelCredentials.createInsecure -}; - -// Constants +/**** Constants ****/ export { Status as status // TODO: Other constants as well }; -// Client +/**** Client ****/ export { Client, @@ -37,4 +79,9 @@ export { makeClientConstructor, makeClientConstructor as makeGenericClientConstructor }; + +/** + * Close a Client object. + * @param client The client to close. + */ export const closeClient = (client: Client) => client.close(); diff --git a/packages/grpc-js-core/src/make-client.ts b/packages/grpc-js-core/src/make-client.ts index bc3299e8..f5433f57 100644 --- a/packages/grpc-js-core/src/make-client.ts +++ b/packages/grpc-js-core/src/make-client.ts @@ -130,7 +130,12 @@ export type GrpcObject = { [index: string]: GrpcObject | ServiceClientConstructor; }; -export function loadPackageDefinition(packageDef: PackageDefinition) { +/** + * Load a gRPC package definition as a gRPC object hierarchy. + * @param packageDef The package definition object. + * @return The resulting gRPC object. + */ +export function loadPackageDefinition(packageDef: PackageDefinition): GrpcObject { const result: GrpcObject = {}; for (const serviceFqn in packageDef) { const service = packageDef[serviceFqn]; diff --git a/packages/grpc-js-core/test/test-call-credentials.ts b/packages/grpc-js-core/test/test-call-credentials.ts index ab516ed9..64cfe140 100644 --- a/packages/grpc-js-core/test/test-call-credentials.ts +++ b/packages/grpc-js-core/test/test-call-credentials.ts @@ -5,18 +5,6 @@ import {Metadata} from '../src/metadata'; // Metadata generators -function makeGenerator(props: Array): CallMetadataGenerator { - return (options: {[propName: string]: string}, cb) => { - const metadata: Metadata = new Metadata(); - props.forEach((prop) => { - if (options[prop]) { - metadata.add(prop, options[prop]); - } - }); - cb(null, metadata); - }; -} - function makeAfterMsElapsedGenerator(ms: number): CallMetadataGenerator { return (options, cb) => { const metadata = new Metadata(); @@ -25,7 +13,11 @@ function makeAfterMsElapsedGenerator(ms: number): CallMetadataGenerator { }; } -const generateFromName: CallMetadataGenerator = makeGenerator(['name']); +const generateFromServiceURL: CallMetadataGenerator = (options, cb) => { + const metadata: Metadata = new Metadata(); + metadata.add('service_url', options.service_url); + cb(null, metadata); +}; const generateWithError: CallMetadataGenerator = (options, cb) => cb(new Error()); @@ -35,16 +27,16 @@ describe('CallCredentials', () => { describe('createFromMetadataGenerator', () => { it('should accept a metadata generator', () => { assert.doesNotThrow( - () => CallCredentials.createFromMetadataGenerator(generateFromName)); + () => CallCredentials.createFromMetadataGenerator(generateFromServiceURL)); }); }); describe('compose', () => { it('should accept a CallCredentials object and return a new object', () => { const callCredentials1 = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); const callCredentials2 = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); const combinedCredentials = callCredentials1.compose(callCredentials2); assert.notEqual(combinedCredentials, callCredentials1); assert.notEqual(combinedCredentials, callCredentials2); @@ -52,9 +44,9 @@ describe('CallCredentials', () => { it('should be chainable', () => { const callCredentials1 = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); const callCredentials2 = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); assert.doesNotThrow(() => { callCredentials1.compose(callCredentials2) .compose(callCredentials2) @@ -67,14 +59,14 @@ describe('CallCredentials', () => { it('should call the function passed to createFromMetadataGenerator', async () => { const callCredentials = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); let metadata: Metadata; try { - metadata = await callCredentials.generateMetadata({name: 'foo'}); + metadata = await callCredentials.generateMetadata({service_url: 'foo'}); } catch (err) { throw err; } - assert.deepEqual(metadata.get('name'), ['foo']); + assert.deepEqual(metadata.get('service_url'), ['foo']); }); it('should emit an error if the associated metadataGenerator does', @@ -83,7 +75,7 @@ describe('CallCredentials', () => { CallCredentials.createFromMetadataGenerator(generateWithError); let metadata: Metadata|null = null; try { - metadata = await callCredentials.generateMetadata({}); + metadata = await callCredentials.generateMetadata({service_url: ''}); } catch (err) { assert.ok(err instanceof Error); } @@ -115,13 +107,12 @@ describe('CallCredentials', () => { expected: ['150', '200', '50', '100'] } ]; - const options = {}; // Try each test case and make sure the msElapsed field is as expected await Promise.all(testCases.map(async (testCase) => { const {credentials, expected} = testCase; let metadata: Metadata; try { - metadata = await credentials.generateMetadata(options); + metadata = await credentials.generateMetadata({service_url: ''}); } catch (err) { throw err; } diff --git a/packages/grpc-js-core/test/test-call-stream.ts b/packages/grpc-js-core/test/test-call-stream.ts index 0e142e40..fb8f5b02 100644 --- a/packages/grpc-js-core/test/test-call-stream.ts +++ b/packages/grpc-js-core/test/test-call-stream.ts @@ -103,7 +103,7 @@ describe('CallStream', () => { assert2.afterMustCallsSatisfied(done); }); - it('should end a call with an error if a stream was closed', (done) => { + describe('should end a call with an error if a stream was closed', () => { const c = http2.constants; const s = Status; const errorCodeMapping = { @@ -121,21 +121,31 @@ describe('CallStream', () => { [c.NGHTTP2_ENHANCE_YOUR_CALM]: s.RESOURCE_EXHAUSTED, [c.NGHTTP2_INADEQUATE_SECURITY]: s.PERMISSION_DENIED }; - forOwn(errorCodeMapping, (value: Status | null, key) => { - const callStream = new Http2CallStream('foo', callStreamArgs, filterStackFactory); - const http2Stream = new ClientHttp2StreamMock({ - payload: Buffer.alloc(0), - frameLengths: [] + const keys = Object.keys(errorCodeMapping).map(key => Number(key)); + keys.forEach((key) => { + const value = errorCodeMapping[key]; + // A null value indicates: behavior isn't specified, so skip this test. + let maybeSkip = (fn: typeof it) => value ? fn : fn.skip; + maybeSkip(it)(`for error code ${key}`, () => { + return new Promise((resolve, reject) => { + const callStream = new Http2CallStream('foo', callStreamArgs, filterStackFactory); + const http2Stream = new ClientHttp2StreamMock({ + payload: Buffer.alloc(0), + frameLengths: [] + }); + callStream.attachHttp2Stream(http2Stream); + callStream.once('status', (status) => { + try { + assert.strictEqual(status.code, value); + resolve(); + } catch (e) { + reject(e); + } + }); + http2Stream.emit('close', Number(key)); + }); }); - callStream.attachHttp2Stream(http2Stream); - if (value !== null) { - callStream.once('status', assert2.mustCall((status) => { - assert.strictEqual(status.code, value); - })); - } - http2Stream.emit('streamClosed', Number(key)); }); - assert2.afterMustCallsSatisfied(done); }); it('should have functioning getters', (done) => {