UNPKG

owltech

Version:
410 lines 17.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const http2 = require("http2"); const stream_1 = require("stream"); const call_credentials_1 = require("./call-credentials"); const constants_1 = require("./constants"); const metadata_1 = require("./metadata"); const { HTTP2_HEADER_STATUS, HTTP2_HEADER_CONTENT_TYPE, NGHTTP2_CANCEL } = http2.constants; var ReadState; (function (ReadState) { ReadState[ReadState["NO_DATA"] = 0] = "NO_DATA"; ReadState[ReadState["READING_SIZE"] = 1] = "READING_SIZE"; ReadState[ReadState["READING_MESSAGE"] = 2] = "READING_MESSAGE"; })(ReadState || (ReadState = {})); class Http2CallStream extends stream_1.Duplex { constructor(methodName, channel, options, filterStackFactory) { super({ objectMode: true }); this.methodName = methodName; this.channel = channel; this.options = options; this.credentials = call_credentials_1.CallCredentials.createEmpty(); this.http2Stream = null; this.pendingRead = false; this.pendingWrite = null; this.pendingWriteCallback = null; this.pendingFinalCallback = null; this.readState = ReadState.NO_DATA; this.readCompressFlag = Buffer.alloc(1); this.readPartialSize = Buffer.alloc(4); this.readSizeRemaining = 4; this.readMessageSize = 0; this.readPartialMessage = []; this.readMessageRemaining = 0; this.isReadFilterPending = false; this.canPush = false; this.unpushedReadMessages = []; this.unfilteredReadMessages = []; // Status code mapped from :status. To be used if grpc-status is not received this.mappedStatusCode = constants_1.Status.UNKNOWN; // Promise objects that are re-assigned to resolving promises when headers // or trailers received. Processing headers/trailers is asynchronous, so we // can use these objects to await their completion. This helps us establish // order of precedence when obtaining the status of the call. this.handlingHeaders = Promise.resolve(); this.handlingTrailers = Promise.resolve(); // This is populated (non-null) if and only if the call has ended this.finalStatus = null; this.filterStack = filterStackFactory.createFilter(this); } /** * On first call, emits a 'status' event with the given StatusObject. * Subsequent calls are no-ops. * @param status The status of the call. */ endCall(status) { if (this.finalStatus === null) { this.finalStatus = status; this.emit('status', status); } } handleFilterError(error) { this.cancelWithStatus(constants_1.Status.INTERNAL, error.message); } handleFilteredRead(message) { /* If we the call has already ended, we don't want to do anything with * this message. Dropping it on the floor is correct behavior */ if (this.finalStatus !== null) { return; } this.isReadFilterPending = false; if (this.canPush) { if (!this.push(message)) { this.canPush = false; this.http2Stream.pause(); } } else { this.unpushedReadMessages.push(message); } if (this.unfilteredReadMessages.length > 0) { /* nextMessage is guaranteed not to be undefined because unfilteredReadMessages is non-empty */ const nextMessage = this.unfilteredReadMessages.shift(); this.filterReceivedMessage(nextMessage); } } filterReceivedMessage(framedMessage) { /* If we the call has already ended, we don't want to do anything with * this message. Dropping it on the floor is correct behavior */ if (this.finalStatus !== null) { return; } if (framedMessage === null) { if (this.canPush) { this.push(null); } else { this.unpushedReadMessages.push(null); } return; } this.isReadFilterPending = true; this.filterStack.receiveMessage(Promise.resolve(framedMessage)) .then(this.handleFilteredRead.bind(this), this.handleFilterError.bind(this)); } tryPush(messageBytes) { if (this.isReadFilterPending) { this.unfilteredReadMessages.push(messageBytes); } else { this.filterReceivedMessage(messageBytes); } } handleTrailers(headers) { const code = this.mappedStatusCode; const details = ''; let metadata; try { metadata = metadata_1.Metadata.fromHttp2Headers(headers); } catch (e) { metadata = new metadata_1.Metadata(); } const status = { code, details, metadata }; this.handlingTrailers = (async () => { let finalStatus; try { // Attempt to assign final status. finalStatus = await this.filterStack.receiveTrailers(Promise.resolve(status)); } catch (error) { await this.handlingHeaders; // This is a no-op if the call was already ended when handling headers. this.endCall({ code: constants_1.Status.INTERNAL, details: 'Failed to process received status', metadata: new metadata_1.Metadata() }); return; } // It's possible that headers were received but not fully handled yet. // Give the headers handler an opportunity to end the call first, // if an error occurred. await this.handlingHeaders; // This is a no-op if the call was already ended when handling headers. this.endCall(finalStatus); })(); } attachHttp2Stream(stream) { if (this.finalStatus !== null) { stream.close(NGHTTP2_CANCEL); } else { this.http2Stream = stream; stream.on('response', (headers, flags) => { switch (headers[HTTP2_HEADER_STATUS]) { // TODO(murgatroid99): handle 100 and 101 case '400': this.mappedStatusCode = constants_1.Status.INTERNAL; break; case '401': this.mappedStatusCode = constants_1.Status.UNAUTHENTICATED; break; case '403': this.mappedStatusCode = constants_1.Status.PERMISSION_DENIED; break; case '404': this.mappedStatusCode = constants_1.Status.UNIMPLEMENTED; break; case '429': case '502': case '503': case '504': this.mappedStatusCode = constants_1.Status.UNAVAILABLE; break; default: this.mappedStatusCode = constants_1.Status.UNKNOWN; } if (flags & http2.constants.NGHTTP2_FLAG_END_STREAM) { this.handleTrailers(headers); } else { let metadata; try { metadata = metadata_1.Metadata.fromHttp2Headers(headers); } catch (error) { this.endCall({ code: constants_1.Status.UNKNOWN, details: error.message, metadata: new metadata_1.Metadata() }); return; } this.handlingHeaders = this.filterStack.receiveMetadata(Promise.resolve(metadata)) .then((finalMetadata) => { this.emit('metadata', finalMetadata); }) .catch((error) => { this.destroyHttp2Stream(); this.endCall({ code: constants_1.Status.UNKNOWN, details: error.message, metadata: new metadata_1.Metadata() }); }); } }); stream.on('trailers', this.handleTrailers.bind(this)); stream.on('data', (data) => { let readHead = 0; let toRead; while (readHead < data.length) { switch (this.readState) { case ReadState.NO_DATA: this.readCompressFlag = data.slice(readHead, readHead + 1); readHead += 1; this.readState = ReadState.READING_SIZE; this.readPartialSize.fill(0); this.readSizeRemaining = 4; this.readMessageSize = 0; this.readMessageRemaining = 0; this.readPartialMessage = []; break; case ReadState.READING_SIZE: toRead = Math.min(data.length - readHead, this.readSizeRemaining); data.copy(this.readPartialSize, 4 - this.readSizeRemaining, readHead, readHead + toRead); this.readSizeRemaining -= toRead; readHead += toRead; // readSizeRemaining >=0 here if (this.readSizeRemaining === 0) { this.readMessageSize = this.readPartialSize.readUInt32BE(0); this.readMessageRemaining = this.readMessageSize; if (this.readMessageRemaining > 0) { this.readState = ReadState.READING_MESSAGE; } else { this.tryPush(Buffer.concat([this.readCompressFlag, this.readPartialSize])); this.readState = ReadState.NO_DATA; } } break; case ReadState.READING_MESSAGE: toRead = Math.min(data.length - readHead, this.readMessageRemaining); this.readPartialMessage.push(data.slice(readHead, readHead + toRead)); this.readMessageRemaining -= toRead; readHead += toRead; // readMessageRemaining >=0 here if (this.readMessageRemaining === 0) { // At this point, we have read a full message const framedMessageBuffers = [ this.readCompressFlag, this.readPartialSize ].concat(this.readPartialMessage); const framedMessage = Buffer.concat(framedMessageBuffers, this.readMessageSize + 5); this.tryPush(framedMessage); this.readState = ReadState.NO_DATA; } break; default: throw new Error('This should never happen'); } } }); stream.on('end', () => { this.tryPush(null); }); stream.on('close', async (errorCode) => { let code; let details = ''; switch (errorCode) { case http2.constants.NGHTTP2_REFUSED_STREAM: code = constants_1.Status.UNAVAILABLE; break; case http2.constants.NGHTTP2_CANCEL: code = constants_1.Status.CANCELLED; break; case http2.constants.NGHTTP2_ENHANCE_YOUR_CALM: code = constants_1.Status.RESOURCE_EXHAUSTED; details = 'Bandwidth exhausted'; break; case http2.constants.NGHTTP2_INADEQUATE_SECURITY: code = constants_1.Status.PERMISSION_DENIED; details = 'Protocol not secure enough'; break; default: code = constants_1.Status.INTERNAL; } // This guarantees that if trailers were received, the value of the // 'grpc-status' header takes precedence for emitted status data. await this.handlingTrailers; // This is a no-op if trailers were received at all. // This is OK, because status codes emitted here correspond to more // catastrophic issues that prevent us from receiving trailers in the // first place. this.endCall({ code, details, metadata: new metadata_1.Metadata() }); }); stream.on('error', (err) => { this.endCall({ code: constants_1.Status.INTERNAL, details: 'Internal HTTP2 error', metadata: new metadata_1.Metadata() }); }); if (!this.pendingRead) { stream.pause(); } if (this.pendingWrite) { if (!this.pendingWriteCallback) { throw new Error('Invalid state in write handling code'); } stream.write(this.pendingWrite, this.pendingWriteCallback); } if (this.pendingFinalCallback) { stream.end(this.pendingFinalCallback); } } } sendMetadata(metadata) { this.channel._startHttp2Stream(this.options.host, this.methodName, this, metadata); } destroyHttp2Stream() { // The http2 stream could already have been destroyed if cancelWithStatus // is called in response to an internal http2 error. if (this.http2Stream !== null && !this.http2Stream.destroyed) { /* TODO(murgatroid99): Determine if we want to send different RST_STREAM * codes based on the status code */ this.http2Stream.close(NGHTTP2_CANCEL); } } cancelWithStatus(status, details) { this.destroyHttp2Stream(); (async () => { // If trailers are currently being processed, the call should be ended // by handleTrailers instead. await this.handlingTrailers; this.endCall({ code: status, details, metadata: new metadata_1.Metadata() }); })(); } getDeadline() { return this.options.deadline; } getCredentials() { return this.credentials; } setCredentials(credentials) { this.credentials = credentials; } getStatus() { return this.finalStatus; } getPeer() { throw new Error('Not yet implemented'); } getMethod() { return this.methodName; } getHost() { return this.options.host; } _read(size) { /* If we have already emitted a status, we should not emit any more * messages and we should communicate that the stream has ended */ if (this.finalStatus !== null) { this.push(null); return; } this.canPush = true; if (this.http2Stream === null) { this.pendingRead = true; } else { while (this.unpushedReadMessages.length > 0) { const nextMessage = this.unpushedReadMessages.shift(); this.canPush = this.push(nextMessage); if (nextMessage === null || (!this.canPush)) { this.canPush = false; return; } } /* Only resume reading from the http2Stream if we don't have any pending * messages to emit, and we haven't gotten the signal to stop pushing * messages */ this.http2Stream.resume(); } } _write(chunk, encoding, cb) { this.filterStack.sendMessage(Promise.resolve(chunk)).then((message) => { if (this.http2Stream === null) { this.pendingWrite = message.message; this.pendingWriteCallback = cb; } else { this.http2Stream.write(message.message, cb); } }, this.handleFilterError.bind(this)); } _final(cb) { if (this.http2Stream === null) { this.pendingFinalCallback = cb; } else { this.http2Stream.end(cb); } } } exports.Http2CallStream = Http2CallStream; //# sourceMappingURL=call-stream.js.map