diff --git a/packages/grpc-js-core/src/call-credentials-filter.ts b/packages/grpc-js-core/src/call-credentials-filter.ts index e432935b..73759410 100644 --- a/packages/grpc-js-core/src/call-credentials-filter.ts +++ b/packages/grpc-js-core/src/call-credentials-filter.ts @@ -7,13 +7,27 @@ import {BaseFilter, Filter, FilterFactory} from './filter'; import {Metadata} from './metadata'; export class CallCredentialsFilter extends BaseFilter implements Filter { - constructor(private readonly credentials: CallCredentials) { + private serviceUrl: string; + constructor( + private readonly credentials: CallCredentials, + private readonly host: string, + private readonly path: string) { super(); + let splitPath: string[] = path.split('/'); + let serviceName: string = ''; + /* The standard path format is "/{serviceName}/{methodName}", so if we split + * by '/', the first item should be empty and the second should be the + * service name */ + if (splitPath.length >= 2) { + serviceName = splitPath[1]; + } + /* Currently, call credentials are only allowed on HTTPS connections, so we + * can assume that the scheme is "https" */ + this.serviceUrl = `https://${host}/${serviceName}`; } async sendMetadata(metadata: Promise): Promise { - // TODO(kjin): pass real service URL to generateMetadata - let credsMetadata = this.credentials.generateMetadata({ service_url: '' }); + let credsMetadata = this.credentials.generateMetadata({ service_url: this.serviceUrl }); let resultMetadata = await metadata; resultMetadata.merge(await credsMetadata); return resultMetadata; @@ -29,6 +43,8 @@ export class CallCredentialsFilterFactory implements createFilter(callStream: CallStream): CallCredentialsFilter { return new CallCredentialsFilter( - this.credentials.compose(callStream.getCredentials())); + this.credentials.compose(callStream.getCredentials()), + callStream.getHost(), + callStream.getMethod()); } } diff --git a/packages/grpc-js-core/src/call-stream.ts b/packages/grpc-js-core/src/call-stream.ts index 3a403d68..80962008 100644 --- a/packages/grpc-js-core/src/call-stream.ts +++ b/packages/grpc-js-core/src/call-stream.ts @@ -17,6 +17,7 @@ export interface CallStreamOptions { deadline: Deadline; credentials: CallCredentials; flags: number; + host: string; } export type CallOptions = Partial; @@ -44,6 +45,8 @@ export type CallStream = { /* If the return value is null, the call has not ended yet. Otherwise, it has * ended with the specified status */ getStatus(): StatusObject|null; + getMethod(): string; + getHost(): string; } & EmitterAugmentation1<'metadata', Metadata> & EmitterAugmentation1<'status', StatusObject> & ObjectDuplex; @@ -355,6 +358,14 @@ export class Http2CallStream extends Duplex implements CallStream { throw new Error('Not yet implemented'); } + getMethod(): string { + return this.methodName; + } + + getHost(): string { + return this.options.host; + } + _read(size: number) { if (this.http2Stream === null) { this.pendingRead = true; diff --git a/packages/grpc-js-core/src/channel.ts b/packages/grpc-js-core/src/channel.ts index a4d04023..8e8e38e7 100644 --- a/packages/grpc-js-core/src/channel.ts +++ b/packages/grpc-js-core/src/channel.ts @@ -82,7 +82,8 @@ export interface Channel extends EventEmitter { export class Http2Channel extends EventEmitter implements Channel { private readonly userAgent: string; - private readonly authority: url.URL; + private readonly target: url.URL; + private readonly defaultAuthority: string; private connectivityState: ConnectivityState = ConnectivityState.IDLE; /* For now, we have up to one subchannel, which will exist as long as we are * connecting or trying to connect */ @@ -146,7 +147,7 @@ export class Http2Channel extends EventEmitter implements Channel { let subChannel: http2.ClientHttp2Session; let secureContext = this.credentials.getSecureContext(); if (secureContext === null) { - subChannel = http2.connect(this.authority); + subChannel = http2.connect(this.target); } else { const connectionOptions: http2.SecureClientSessionOptions = { secureContext, @@ -161,7 +162,7 @@ export class Http2Channel extends EventEmitter implements Channel { } connectionOptions.servername = sslTargetNameOverride; } - subChannel = http2.connect(this.authority, connectionOptions); + subChannel = http2.connect(this.target, connectionOptions); } this.subChannel = subChannel; let now = new Date(); @@ -195,9 +196,15 @@ export class Http2Channel extends EventEmitter implements Channel { private readonly options: Partial) { super(); if (credentials.getSecureContext() === null) { - this.authority = new url.URL(`http://${address}`); + this.target = new url.URL(`http://${address}`); } else { - this.authority = new url.URL(`https://${address}`); + this.target = new url.URL(`https://${address}`); + } + // TODO(murgatroid99): Add more centralized handling of channel options + if (this.options['grpc.default_authority']) { + this.defaultAuthority = this.options['grpc.default_authority'] as string; + } else { + this.defaultAuthority = this.target.host; } this.filterStackFactory = new FilterStackFactory([ new CompressionFilterFactory(this), @@ -220,20 +227,16 @@ export class Http2Channel extends EventEmitter implements Channel { } private startHttp2Stream( - methodName: string, stream: Http2CallStream, metadata: Metadata) { + authority: string, + methodName: string, + stream: Http2CallStream, + metadata: Metadata) { let finalMetadata: Promise = stream.filterStack.sendMetadata(Promise.resolve(metadata.clone())); Promise.all([finalMetadata, this.connect()]) .then(([metadataValue]) => { let headers = metadataValue.toHttp2Headers(); - let host: string; - // TODO(murgatroid99): Add more centralized handling of channel options - if (this.options['grpc.default_authority']) { - host = this.options['grpc.default_authority'] as string; - } else { - host = this.authority.hostname; - } - headers[HTTP2_HEADER_AUTHORITY] = host; + headers[HTTP2_HEADER_AUTHORITY] = authority; headers[HTTP2_HEADER_USER_AGENT] = this.userAgent; headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc'; headers[HTTP2_HEADER_METHOD] = 'POST'; @@ -249,7 +252,7 @@ export class Http2Channel extends EventEmitter implements Channel { /* In this case, we lost the connection while finalizing * metadata. That should be very unusual */ setImmediate(() => { - this.startHttp2Stream(methodName, stream, metadata); + this.startHttp2Stream(authority, methodName, stream, metadata); }); } }).catch((error: Error & { code: number }) => { @@ -267,11 +270,12 @@ export class Http2Channel extends EventEmitter implements Channel { let finalOptions: CallStreamOptions = { deadline: options.deadline === undefined ? Infinity : options.deadline, credentials: options.credentials || CallCredentials.createEmpty(), - flags: options.flags || 0 + flags: options.flags || 0, + host: options.host || this.defaultAuthority }; let stream: Http2CallStream = new Http2CallStream(methodName, finalOptions, this.filterStackFactory); - this.startHttp2Stream(methodName, stream, metadata); + this.startHttp2Stream(finalOptions.host, methodName, stream, metadata); return stream; } diff --git a/packages/grpc-js-core/test/test-call-stream.ts b/packages/grpc-js-core/test/test-call-stream.ts index fb8f5b02..f3a4f617 100644 --- a/packages/grpc-js-core/test/test-call-stream.ts +++ b/packages/grpc-js-core/test/test-call-stream.ts @@ -77,7 +77,8 @@ describe('CallStream', () => { const callStreamArgs = { deadline: Infinity, credentials: CallCredentials.createEmpty(), - flags: 0 + flags: 0, + host: '' }; const filterStackFactory = new FilterStackFactory([]); const message = 'eat this message'; // 16 bytes