UNPKG

quic

Version:

A QUIC server/client implementation in Node.js.

400 lines 14.8 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); // **Github:** https://github.com/fidm/quic // // **License:** MIT const util_1 = require("util"); const stream_1 = require("stream"); const error_1 = require("./internal/error"); const constant_1 = require("./internal/constant"); const protocol_1 = require("./internal/protocol"); const frame_1 = require("./internal/frame"); const flowcontrol_1 = require("./internal/flowcontrol"); const common_1 = require("./internal/common"); const symbol_1 = require("./internal/symbol"); const debug = util_1.debuglog('quic:stream'); class Stream extends stream_1.Duplex { constructor(streamID, session, options) { options.allowHalfOpen = true; options.objectMode = false; super(options); this[symbol_1.kID] = streamID; this[symbol_1.kSession] = session; this[symbol_1.kState] = new StreamState(); this[symbol_1.kFC] = session.isClient ? // TODO: small window will make "packets loss" test failure new flowcontrol_1.StreamFlowController(constant_1.ReceiveStreamWindow, constant_1.DefaultMaxReceiveStreamWindowClient, session[symbol_1.kFC]) : new flowcontrol_1.StreamFlowController(constant_1.ReceiveStreamWindow, constant_1.DefaultMaxReceiveStreamWindowServer, session[symbol_1.kFC]); this.once('close', () => this[symbol_1.kState].lastActivityTime = Date.now()); debug(`session %s - new stream: %d`, session.id, streamID.valueOf()); } // The socket owned by this session get id() { return this[symbol_1.kID].valueOf(); } get session() { return this[symbol_1.kSession]; } get aborted() { return this[symbol_1.kState].aborted; } get destroyed() { return this[symbol_1.kState].destroyed; } get bytesRead() { return this[symbol_1.kFC].consumedOffset; } get bytesWritten() { return this[symbol_1.kFC].writtenOffset; } // close closes the stream with an error. close(err) { this[symbol_1.kState].localFIN = true; const offset = new protocol_1.Offset(this[symbol_1.kFC].writtenOffset); const rstStreamFrame = new frame_1.RstStreamFrame(this[symbol_1.kID], offset, error_1.StreamError.fromError(err)); debug(`stream %s - close stream, offset: %d, error: %j`, this.id, offset.valueOf(), err); return new Promise((resolve) => { this[symbol_1.kSession]._sendFrame(rstStreamFrame, (e) => { if (e != null) { this.destroy(e); } resolve(); }); }); } _write(chunk, encoding, callback) { if (this[symbol_1.kState].localFIN) { return callback(new error_1.StreamError('QUIC_RST_ACKNOWLEDGEMENT')); } if (!(chunk instanceof Buffer)) { chunk = Buffer.from(chunk, encoding); } if (chunk.length === 0) { return callback(null); } this[symbol_1.kState].outgoingChunksList.push(chunk, callback); this._tryFlushCallbacks(); } _writev(chunks, callback) { if (this[symbol_1.kState].localFIN) { return callback(new error_1.StreamError('QUIC_RST_ACKNOWLEDGEMENT')); } let len = 0; const list = []; for (const item of chunks) { // { chunk: ..., encoding: ... } let chunk = item.chunk; if (!(chunk instanceof Buffer)) { chunk = Buffer.from(chunk, item.encoding); } len += chunk.length; list.push(chunk); } if (len === 0) { return callback(null); } this[symbol_1.kState].outgoingChunksList.push(Buffer.concat(list, len), callback); this._tryFlushCallbacks(); } _final(callback) { this[symbol_1.kState].outgoingChunksList.push(null, callback); this._tryFlushCallbacks(); } _read(size = 0) { let data = this[symbol_1.kState].incomingSequencer.read(); while (data != null) { if (this.push(data) && size > data.length) { size -= data.length; data = this[symbol_1.kState].incomingSequencer.read(); continue; } break; } this[symbol_1.kFC].updateConsumedOffset(this[symbol_1.kState].incomingSequencer.consumedOffset); if (!this[symbol_1.kState].remoteFIN) { process.nextTick(() => this._trySendUpdateWindow()); } if (!this[symbol_1.kState].ended && this[symbol_1.kState].incomingSequencer.isFIN()) { this[symbol_1.kState].ended = true; this.push(null); } } _destroy(err, callback) { debug(`stream %s - stream destroyed, error: %j`, this.id, err); this[symbol_1.kSession][symbol_1.kState].liveStreamCount -= 1; const state = this[symbol_1.kState]; state.localFIN = true; state.remoteFIN = true; state.aborted = true; state.destroyed = true; state.finished = true; state.incomingSequencer.reset(); state.outgoingChunksList.reset(); err = error_1.StreamError.checkAny(err); if (err != null && err.isNoError) { err = null; } callback(err); } _sendBlockFrame() { this[symbol_1.kSession]._sendFrame(new frame_1.BlockedFrame(this[symbol_1.kID])); } _trySendUpdateWindow() { if (this[symbol_1.kFC].shouldUpdateWindow()) { const offset = this[symbol_1.kFC].updateWindowOffset(this[symbol_1.kSession][symbol_1.kRTT].msRTT); this[symbol_1.kSession]._sendWindowUpdate(new protocol_1.Offset(offset), this[symbol_1.kID]); } } _handleFrame(frame, rcvTime) { this[symbol_1.kState].lastActivityTime = rcvTime; const offset = frame.offset.valueOf(); const byteLen = frame.data == null ? 0 : frame.data.length; debug(`stream %s - received StreamFrame, offset: %d, data size: %d, isFIN: %s`, this.id, offset, byteLen, frame.isFIN); this[symbol_1.kFC].updateHighestReceived(offset + byteLen); if (this[symbol_1.kFC].isBlocked()) { this.emit('error', new Error('The window of byte offset overflowed')); this.close(error_1.StreamError.fromError(error_1.StreamError.QUIC_ERROR_PROCESSING_STREAM)); return; } if (frame.isFIN) { this[symbol_1.kState].remoteFIN = true; this[symbol_1.kState].incomingSequencer.setFinalOffset(offset + byteLen); } if (frame.data != null) { if (this[symbol_1.kState].incomingSequencer.hasOffset(offset)) { return; // duplicated frame } this[symbol_1.kState].incomingSequencer.push(frame); } this._read(); if (this[symbol_1.kState].incomingSequencer.byteLen > constant_1.MaxStreamReadCacheSize) { this.emit('error', new Error('Too large caching, stream data maybe lost')); this.destroy(error_1.StreamError.fromError(error_1.StreamError.QUIC_ERROR_PROCESSING_STREAM)); } } _handleRstFrame(frame, rcvTime) { this[symbol_1.kState].lastActivityTime = rcvTime; this[symbol_1.kState].remoteFIN = true; this[symbol_1.kState].incomingSequencer.setFinalOffset(frame.offset.valueOf()); debug(`stream %s - received RstStreamFrame, offset: %d, error: %j`, this.id, frame.offset.valueOf(), frame.error); if (this[symbol_1.kState].localFIN) { this.destroy(frame.error); } else { this.emit('error', frame.error); this.close(error_1.StreamError.fromError(error_1.StreamError.QUIC_RST_ACKNOWLEDGEMENT)); } return; } _tryFlushCallbacks() { const entry = this[symbol_1.kState].outgoingChunksList.first(); if (entry == null || this[symbol_1.kState].flushing) { return; } if (entry.data != null && !this._isRemoteWriteable(this[symbol_1.kSession][symbol_1.kState].maxPacketSize)) { return; } const callback = entry.callback; this[symbol_1.kState].flushing = true; this._flushData(entry.data, (err) => { this[symbol_1.kState].flushing = false; if (entry.checkConsumed()) { this[symbol_1.kState].outgoingChunksList.shift(); callback(err); } if (err == null && this[symbol_1.kState].outgoingChunksList.pendingCb > 0) { return this._tryFlushCallbacks(); } }); } _isRemoteWriteable(byteLen) { if (this[symbol_1.kFC].willBlocked(byteLen)) { // should wait for WINDOW_UPDATE debug(`stream %s - wait for WINDOW_UPDATE, writtenOffset: %d, maxSendOffset: %d, to write size: %d`, this.id, this[symbol_1.kFC].writtenOffset, this[symbol_1.kFC].maxSendOffset, byteLen); this._sendBlockFrame(); return false; } return true; } _flushData(bufv, callback) { let byteLen = 0; // bytes to write let nextByteLen = 0; // bytes for next write const offet = new protocol_1.Offset(this[symbol_1.kFC].writtenOffset); const streamFrame = new frame_1.StreamFrame(this[symbol_1.kID], offet, bufv == null); const packet = this[symbol_1.kSession]._newRegularPacket(); if (bufv != null) { byteLen = Math.min(bufv.length - bufv.end, this[symbol_1.kSession][symbol_1.kState].maxPacketSize - packet.headerLen() - streamFrame.headerLen(true)); bufv.walk(byteLen); nextByteLen = Math.min(byteLen, bufv.length - bufv.end); streamFrame.setData(bufv.buf.slice(bufv.start, bufv.end)); this[symbol_1.kFC].updateWrittenOffset(byteLen); } if (streamFrame.isFIN) { this[symbol_1.kState].localFIN = true; } debug(`stream %s - write streamFrame, isFIN: %s, offset: %d, data size: %d`, this.id, streamFrame.isFIN, streamFrame.offset.valueOf(), byteLen); packet.addFrames(streamFrame); packet.isRetransmittable = true; this[symbol_1.kSession]._sendPacket(packet, (err) => { // Packet Number length maybe increase 1 byte if (err != null || nextByteLen === 0 || !this._isRemoteWriteable(nextByteLen + 1)) { return callback(err); } this._flushData(bufv, callback); }); } } exports.Stream = Stream; class StreamState { constructor() { this.localFIN = false; // local endpoint will not send data this.remoteFIN = false; // remote endpoint should not send data this.flushing = false; this.ended = false; this.aborted = false; this.destroyed = false; this.finished = false; this.lastActivityTime = Date.now(); this.incomingSequencer = new StreamSequencer(); this.outgoingChunksList = new StreamDataList(); } } class StreamDataEntry { constructor(callback, buf) { this.callback = callback; this.next = null; this.data = buf == null ? null : new common_1.BufferVisitor(buf); } get byteLen() { return this.data == null ? 0 : this.data.length; } checkConsumed() { return this.data == null || this.data.end === this.data.length; } } class StreamDataList { constructor() { this.head = null; this.tail = null; this.pendingCb = 0; this.byteLen = 0; } reset() { this.head = null; this.tail = null; this.pendingCb = 0; this.byteLen = 0; } push(buf, callback) { const entry = new StreamDataEntry(callback, buf); if (this.tail != null) { this.tail.next = entry; } else { this.head = entry; } this.tail = entry; this.pendingCb += 1; this.byteLen += entry.byteLen; } first() { return this.head; } shift() { if (this.head == null) { return null; } const entry = this.head; if (this.pendingCb === 1) { this.head = this.tail = null; } else { this.head = this.head.next; } this.pendingCb -= 1; this.byteLen -= entry.byteLen; return entry; } } class StreamFrameEntry { constructor(frame, entry) { this.data = frame.data; this.offset = frame.offset.valueOf(); this.next = entry; } } // sequencer class StreamSequencer { constructor() { this.head = null; this.byteLen = 0; this.consumedOffset = 0; this.finalOffset = -1; this.pendingOffsets = new Set(); } hasOffset(offset) { if (offset < this.consumedOffset) { return true; } return this.pendingOffsets.has(offset); } reset() { this.head = null; this.byteLen = 0; this.consumedOffset = 0; this.finalOffset = -1; this.pendingOffsets.clear(); } setFinalOffset(offset) { this.finalOffset = offset; } isFIN() { return this.consumedOffset === this.finalOffset; } /** * @param {StreamFrame} */ push(frame) { const entry = new StreamFrameEntry(frame, null); const offset = entry.offset; this.pendingOffsets.add(offset); if (entry.data != null) { this.byteLen += entry.data.length; } if (this.head == null) { this.head = entry; } else if (this.head.offset > offset) { entry.next = this.head; this.head = entry; } else { let prev = this.head; while (true) { if (prev.next == null) { prev.next = entry; break; } if (prev.next.offset > offset) { entry.next = prev.next; prev.next = entry; break; } prev = prev.next; } } } read() { let data = null; if (this.head != null && this.consumedOffset === this.head.offset) { data = this.head.data; if (data != null) { this.pendingOffsets.delete(this.consumedOffset); this.byteLen -= data.length; this.consumedOffset += data.length; } this.head = this.head.next; } return data; } } //# sourceMappingURL=stream.js.map