quic
Version:
A QUIC server/client implementation in Node.js.
565 lines • 24.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
// **Github:** https://github.com/fidm/quic
//
// **License:** MIT
const util_1 = require("util");
const events_1 = require("events");
const constant_1 = require("./internal/constant");
const protocol_1 = require("./internal/protocol");
const symbol_1 = require("./internal/symbol");
const frame_1 = require("./internal/frame");
const packet_1 = require("./internal/packet");
const error_1 = require("./internal/error");
const flowcontrol_1 = require("./internal/flowcontrol");
const congestion_1 = require("./internal/congestion");
const socket_1 = require("./socket");
const stream_1 = require("./stream");
const common_1 = require("./internal/common");
const debug = util_1.debuglog('quic:session');
//
// *************** Session ***************
//
class Session extends events_1.EventEmitter {
constructor(id, type) {
super();
this[symbol_1.kID] = id;
this[symbol_1.kType] = type;
this[symbol_1.kStreams] = new Map();
this[symbol_1.kNextStreamID] = new protocol_1.StreamID(type === protocol_1.SessionType.SERVER ? 2 : 1);
this[symbol_1.kState] = new SessionState();
this[symbol_1.kACKHandler] = new ACKHandler();
this[symbol_1.kSocket] = null;
this[symbol_1.kVersion] = '';
this[symbol_1.kIntervalCheck] = null;
this[symbol_1.kNextPacketNumber] = new protocol_1.PacketNumber(1);
this[symbol_1.kUnackedPackets] = new common_1.Queue(); // up to 1000
this[symbol_1.kRTT] = new congestion_1.RTTStats();
this[symbol_1.kFC] = this.isClient ? // TODO
new flowcontrol_1.ConnectionFlowController(constant_1.ReceiveConnectionWindow, constant_1.DefaultMaxReceiveConnectionWindowClient) :
new flowcontrol_1.ConnectionFlowController(constant_1.ReceiveConnectionWindow, constant_1.DefaultMaxReceiveConnectionWindowServer);
}
get id() {
return this[symbol_1.kID].valueOf();
}
get version() {
return this[symbol_1.kVersion];
}
get isClient() {
return this[symbol_1.kType] === protocol_1.SessionType.CLIENT;
}
get destroyed() {
return this[symbol_1.kState].destroyed;
}
get localAddr() {
return {
address: this[symbol_1.kState].localAddress,
family: this[symbol_1.kState].localFamily,
port: this[symbol_1.kState].localPort,
socketAddress: this[symbol_1.kState].localAddr,
};
}
get remoteAddr() {
return {
address: this[symbol_1.kState].remoteAddress,
family: this[symbol_1.kState].remoteFamily,
port: this[symbol_1.kState].remotePort,
socketAddress: this[symbol_1.kState].remoteAddr,
};
}
_newRegularPacket() {
const packetNumber = this[symbol_1.kNextPacketNumber];
this[symbol_1.kNextPacketNumber] = packetNumber.nextNumber();
return new packet_1.RegularPacket(this[symbol_1.kID], packetNumber);
}
_sendFrame(frame, callback) {
const regularPacket = this._newRegularPacket();
regularPacket.addFrames(frame);
regularPacket.isRetransmittable = frame.isRetransmittable();
this._sendPacket(regularPacket, callback);
}
_sendStopWaitingFrame(leastUnacked) {
const regularPacket = this._newRegularPacket();
const frame = new frame_1.StopWaitingFrame(regularPacket.packetNumber, leastUnacked);
regularPacket.addFrames(frame);
regularPacket.isRetransmittable = false;
debug(`%s session %s - write StopWaitingFrame, packetNumber: %d, leastUnacked: %d`, protocol_1.SessionType[this[symbol_1.kType]], this.id, frame.packetNumber.valueOf(), leastUnacked);
this._sendPacket(regularPacket);
}
_retransmit(frame, rcvTime) {
const unackedPackets = this[symbol_1.kUnackedPackets];
debug(`%s session %s - start retransmit, count: %d, ackFrame: %j`, protocol_1.SessionType[this[symbol_1.kType]], this.id, unackedPackets.length, frame.valueOf());
let count = 0;
let packet = unackedPackets.first();
while (packet != null) {
const packetNumber = packet.packetNumber.valueOf();
if (packetNumber > frame.largestAcked) {
break; // wait for newest ack
}
else if (packetNumber === frame.largestAcked) {
this[symbol_1.kRTT].updateRTT(packet.sentTime, rcvTime, frame.delayTime);
}
if (frame.acksPacket(packetNumber)) {
unackedPackets.shift();
packet = unackedPackets.first();
continue;
}
unackedPackets.shift();
packet.setPacketNumber(this[symbol_1.kNextPacketNumber]);
this[symbol_1.kNextPacketNumber] = packet.packetNumber.nextNumber();
this._sendPacket(packet);
count += 1;
packet = unackedPackets.first();
}
debug(`%s session %s - finish retransmit, count: %d`, protocol_1.SessionType[this[symbol_1.kType]], this.id, count);
return count;
}
_sendPacket(packet, callback) {
const socket = this[symbol_1.kSocket];
if (callback == null) {
callback = (err) => {
if (err != null) {
this.destroy(err);
}
};
}
if (socket == null) {
return callback(error_1.QuicError.fromError(error_1.QuicError.QUIC_PACKET_WRITE_ERROR));
}
if (socket[symbol_1.kState].destroyed) {
return callback(error_1.QuicError.fromError(error_1.QuicError.QUIC_PACKET_WRITE_ERROR));
}
if (packet.isRegular()) {
const _packet = packet;
if (this.isClient && !this[symbol_1.kState].versionNegotiated) {
_packet.setVersion(this[symbol_1.kVersion]);
}
if (_packet.isRetransmittable) {
this[symbol_1.kUnackedPackets].push(packet);
if (this[symbol_1.kUnackedPackets].length > 4096) {
return callback(error_1.QuicError.fromError(error_1.QuicError.QUIC_TOO_MANY_OUTSTANDING_SENT_PACKETS));
}
}
debug(`%s session %s - write RegularPacket, packetNumber: %d, frames: %j`, protocol_1.SessionType[this[symbol_1.kType]], this.id, _packet.packetNumber.valueOf(), _packet.frames.map((frame) => frame.name));
}
socket_1.sendPacket(socket, packet, this[symbol_1.kState].remotePort, this[symbol_1.kState].remoteAddress, callback);
// debug(`%s session %s - write packet: %j`, this.id, packet.valueOf())
}
_sendWindowUpdate(offset, streamID) {
if (streamID == null) {
// update for session
streamID = new protocol_1.StreamID(0);
}
debug(`%s session %s - write WindowUpdateFrame, streamID: %d, offset: %d`, protocol_1.SessionType[this[symbol_1.kType]], this.id, streamID.valueOf(), offset);
this._sendFrame(new frame_1.WindowUpdateFrame(streamID, offset), (err) => {
if (err != null) {
this.emit('error', err);
}
});
}
_trySendAckFrame() {
const frame = this[symbol_1.kACKHandler].toFrame();
if (frame == null) {
return;
}
debug(`%s session %s - write AckFrame, lowestAcked: %d, largestAcked: %d, ackRanges: %j`, protocol_1.SessionType[this[symbol_1.kType]], this.id, frame.lowestAcked, frame.largestAcked, frame.ackRanges);
frame.setDelay();
this._sendFrame(frame, (err) => {
if (err != null) {
this.destroy(err);
}
});
}
_handleRegularPacket(packet, rcvTime, bufv) {
if (this.isClient && packet.nonce != null) {
// TODO
// this.cryptoSetup.SetDiversificationNonce(packet.nonce)
}
try {
packet.parseFrames(bufv);
}
catch (err) {
debug(`%s session %s - parsing frames error: %o`, err);
this.destroy(error_1.QuicError.fromError(err));
return;
}
const packetNumber = packet.packetNumber.valueOf();
this[symbol_1.kState].lastNetworkActivityTime = rcvTime;
if (this[symbol_1.kACKHandler].ack(packetNumber, rcvTime, packet.needAck())) {
this._trySendAckFrame();
}
debug(`%s session %s - received RegularPacket, packetNumber: %d, frames: %j`, protocol_1.SessionType[this[symbol_1.kType]], this.id, packetNumber, packet.frames.map((frame) => frame.name));
for (const frame of packet.frames) {
switch (frame.name) {
case 'STREAM':
this._handleStreamFrame(frame, rcvTime);
break;
case 'ACK':
this._handleACKFrame(frame, rcvTime);
break;
case 'STOP_WAITING':
// The STOP_WAITING frame is sent to inform the peer that it should not continue to
// wait for packets with packet numbers lower than a specified value.
// The resulting least unacked is the smallest packet number of any packet for which the sender is still awaiting an ack.
// If the receiver is missing any packets smaller than this value,
// the receiver should consider those packets to be irrecoverably lost.
this._handleStopWaitingFrame(frame);
break;
case 'WINDOW_UPDATE':
this._handleWindowUpdateFrame(frame);
break;
case 'BLOCKED':
// The BLOCKED frame is used to indicate to the remote endpoint that this endpoint is
// ready to send data (and has data to send), but is currently flow control blocked.
// It is a purely informational frame.
this._handleBlockedFrame(frame, rcvTime);
break;
case 'CONGESTION_FEEDBACK':
// The CONGESTION_FEEDBACK frame is an experimental frame currently not used.
break;
case 'PADDING':
// When this frame is encountered, the rest of the packet is expected to be padding bytes.
return;
case 'RST_STREAM':
this._handleRstStreamFrame(frame, rcvTime);
break;
case 'PING':
// The PING frame contains no payload.
// The receiver of a PING frame simply needs to ACK the packet containing this frame.
break;
case 'CONNECTION_CLOSE':
this.destroy(frame.error);
break;
case 'GOAWAY':
this[symbol_1.kState].shuttingDown = true;
this.emit('goaway');
break;
}
}
}
_handleStreamFrame(frame, rcvTime) {
const streamID = frame.streamID.valueOf();
let stream = this[symbol_1.kStreams].get(streamID);
if (stream == null) {
if (this[symbol_1.kState].shuttingDown) {
return;
}
stream = new stream_1.Stream(frame.streamID, this, {});
if (this[symbol_1.kState].liveStreamCount >= constant_1.DefaultMaxIncomingStreams) {
stream.close(error_1.QuicError.fromError(error_1.QuicError.QUIC_TOO_MANY_AVAILABLE_STREAMS));
return;
}
this[symbol_1.kStreams].set(streamID, stream);
this[symbol_1.kState].liveStreamCount += 1;
this.emit('stream', stream);
}
else if (stream.destroyed) {
return;
}
stream._handleFrame(frame, rcvTime);
}
_handleRstStreamFrame(frame, rcvTime) {
const streamID = frame.streamID.valueOf();
const stream = this[symbol_1.kStreams].get(streamID);
if (stream == null || stream.destroyed) {
return;
}
stream._handleRstFrame(frame, rcvTime);
}
_handleACKFrame(frame, rcvTime) {
// The sender must always close the connection if an unsent packet number is acked,
// so this mechanism automatically defeats any potential attackers.
if (frame.largestAcked >= this[symbol_1.kNextPacketNumber].valueOf()) {
this.destroy(error_1.QuicError.fromError(error_1.QuicError.QUIC_INTERNAL_ERROR));
return;
}
// It is recommended for the sender to send the most recent largest acked packet
// it has received in an ack as the stop waiting frame’s least unacked value.
if (frame.hasMissingRanges()) {
this._sendStopWaitingFrame(frame.largestAcked);
}
this._retransmit(frame, rcvTime);
}
_handleStopWaitingFrame(frame) {
this[symbol_1.kACKHandler].lowest(frame.leastUnacked.valueOf());
}
_handleWindowUpdateFrame(frame) {
// The stream ID can be 0, indicating this WINDOW_UPDATE applies to the connection level flow control window,
// or > 0 indicating that the specified stream should increase its flow control window.
const streamID = frame.streamID.valueOf();
const offset = frame.offset.valueOf();
debug(`%s session %s - received WindowUpdateFrame, streamID: %d, offset: %d`, protocol_1.SessionType[this[symbol_1.kType]], this.id, streamID, offset);
if (streamID === 0) {
this[symbol_1.kFC].updateMaxSendOffset(offset);
}
else {
const stream = this[symbol_1.kStreams].get(streamID);
if (stream != null && !stream.destroyed) {
if (stream[symbol_1.kFC].updateMaxSendOffset(offset)) {
stream._tryFlushCallbacks();
}
}
}
}
_handleBlockedFrame(frame, rcvTime) {
this[symbol_1.kFC].updateBlockedFrame(frame.streamID.valueOf(), rcvTime);
}
_intervalCheck(time) {
if (this.destroyed) {
return;
}
// The PING frame should be used to keep a connection alive when a stream is open.
if (this[symbol_1.kState].keepAlivePingSent && this[symbol_1.kStreams].size > 0 && (time - this[symbol_1.kState].lastNetworkActivityTime >= constant_1.PingFrameDelay)) {
this.ping().catch((err) => this.emit('error', err));
}
for (const stream of this[symbol_1.kStreams].values()) {
if (stream.destroyed) {
// clearup idle stream
if (time - stream[symbol_1.kState].lastActivityTime > this[symbol_1.kState].idleTimeout) {
this[symbol_1.kStreams].delete(stream.id);
}
}
else if (time - stream[symbol_1.kState].lastActivityTime > constant_1.MaxStreamWaitingTimeout) {
stream.emit('timeout');
}
}
this._trySendAckFrame();
return;
}
request(options) {
if (this[symbol_1.kState].shuttingDown) {
throw error_1.StreamError.fromError(error_1.StreamError.QUIC_STREAM_PEER_GOING_AWAY);
}
if (this[symbol_1.kState].liveStreamCount >= constant_1.DefaultMaxIncomingStreams) {
throw error_1.QuicError.fromError(error_1.QuicError.QUIC_TOO_MANY_OPEN_STREAMS);
}
const streamID = this[symbol_1.kNextStreamID];
this[symbol_1.kNextStreamID] = streamID.nextID();
const stream = new stream_1.Stream(streamID, this, (options == null ? {} : options));
this[symbol_1.kStreams].set(streamID.valueOf(), stream);
this[symbol_1.kState].liveStreamCount += 1;
return stream;
}
goaway(err) {
return new Promise((resolve) => {
if (this[symbol_1.kState].shuttingDown) {
return resolve();
}
this[symbol_1.kState].shuttingDown = true;
const frame = new frame_1.GoAwayFrame(this[symbol_1.kNextStreamID].prevID(), error_1.QuicError.fromError(err));
debug(`%s session %s - write GoAwayFrame, streamID: %d, error: %j`, protocol_1.SessionType[this[symbol_1.kType]], this.id, frame.streamID.valueOf(), frame.error);
this._sendFrame(frame, (_e) => {
resolve();
});
});
}
ping() {
return new Promise((resolve, reject) => {
debug(`%s session %s - write PingFrame`, protocol_1.SessionType[this[symbol_1.kType]], this.id);
this._sendFrame(new frame_1.PingFrame(), (err) => {
if (err != null) {
reject(err);
}
else {
resolve();
}
});
});
}
setTimeout(_msecs) {
return;
}
close(err) {
return new Promise((resolve) => {
if (this[symbol_1.kState].destroyed) {
return resolve();
}
const frame = new frame_1.ConnectionCloseFrame(error_1.QuicError.fromError(err));
debug(`%s session %s - write ConnectionCloseFrame, error: %j`, protocol_1.SessionType[this[symbol_1.kType]], this.id, frame.error);
this._sendFrame(frame, (e) => {
this.destroy(e);
resolve();
});
});
}
reset(_err) {
return new Promise((resolve) => {
if (this[symbol_1.kState].destroyed) {
return resolve();
}
const tags = new protocol_1.QuicTags(protocol_1.Tag.PRST);
tags.set(protocol_1.Tag.RNON, Buffer.allocUnsafe(8)); // TODO
tags.set(protocol_1.Tag.RSEQ, common_1.toBuffer(this[symbol_1.kNextPacketNumber].prevNumber()));
const localAddr = this[symbol_1.kState].localAddr;
if (localAddr != null) {
tags.set(protocol_1.Tag.CADR, common_1.toBuffer(localAddr));
}
const packet = new packet_1.ResetPacket(this[symbol_1.kID], tags);
debug(`%s session %s - write ResetPacket, packet: %j`, protocol_1.SessionType[this[symbol_1.kType]], this.id, packet);
this._sendPacket(packet, (e) => {
this.destroy(e);
resolve();
});
});
}
destroy(err) {
if (this[symbol_1.kState].destroyed) {
return;
}
debug(`%s session %s - session destroyed, error: %j`, protocol_1.SessionType[this[symbol_1.kType]], this.id, err);
err = error_1.QuicError.checkAny(err);
if (err != null && err.isNoError) {
err = null;
}
const socket = this[symbol_1.kSocket];
if (socket != null) {
socket[symbol_1.kState].conns.delete(this.id);
if (this.isClient && !socket[symbol_1.kState].destroyed && (socket[symbol_1.kState].exclusive || socket[symbol_1.kState].conns.size === 0)) {
socket.close();
socket[symbol_1.kState].destroyed = true;
}
this[symbol_1.kSocket] = null;
}
for (const stream of this[symbol_1.kStreams].values()) {
stream.destroy(err);
}
const timer = this[symbol_1.kIntervalCheck];
if (timer != null) {
clearInterval(timer);
}
this[symbol_1.kStreams].clear();
this[symbol_1.kUnackedPackets].reset();
if (err != null) {
this.emit('error', err);
}
if (!this[symbol_1.kState].destroyed) {
this[symbol_1.kState].destroyed = true;
process.nextTick(() => this.emit('close'));
}
return;
}
}
exports.Session = Session;
class SessionState {
constructor() {
this.localFamily = '';
this.localAddress = '';
this.localPort = 0;
this.localAddr = null; // SocketAddress
this.remoteFamily = '';
this.remoteAddress = '';
this.remotePort = 0;
this.remoteAddr = null; // SocketAddress
this.maxPacketSize = 0;
this.bytesRead = 0;
this.bytesWritten = 0;
this.idleTimeout = constant_1.DefaultIdleTimeout;
this.liveStreamCount = 0;
this.lastNetworkActivityTime = Date.now();
this.destroyed = false;
this.shutdown = false;
this.shuttingDown = false; // send or receive GOAWAY
this.versionNegotiated = false;
this.keepAlivePingSent = false;
}
}
exports.SessionState = SessionState;
class ACKHandler {
constructor() {
this.misshit = 0;
this.lowestAcked = 0;
this.largestAcked = 0;
this.numbersAcked = [];
this.largestAckedTime = 0;
this.lastAckedTime = Date.now();
}
lowest(packetNumber) {
if (packetNumber > this.lowestAcked) {
this.lowestAcked = packetNumber;
}
}
ack(packetNumber, rcvTime, needAck) {
if (packetNumber < this.lowestAcked) {
return false; // ignore
}
if (packetNumber > this.largestAcked) {
if (packetNumber - this.largestAcked > 1) {
this.misshit += 1;
}
this.largestAcked = packetNumber;
this.largestAckedTime = rcvTime;
}
else if (Math.abs(packetNumber - this.numbersAcked[0]) > 1) {
this.misshit += 1;
}
let shouldAck = this.numbersAcked.unshift(packetNumber) >= 511; // 256 blocks + 255 gaps, too many packets, should ack
if (!needAck && this.largestAcked - this.lowestAcked <= 1) {
// ACK frame
this.lowestAcked = this.largestAcked;
this.numbersAcked.length = 1;
return false;
}
if (this.misshit > 16) {
shouldAck = true;
}
const timeSpan = rcvTime - this.lastAckedTime;
if (timeSpan >= 512) {
shouldAck = true;
}
if (shouldAck) {
debug(`should ACK, largestAcked: %d, lowestAcked: %d, misshit: %d, numbersAcked: %d, timeSpan: %d`, this.largestAcked, this.lowestAcked, this.misshit, this.numbersAcked.length, timeSpan);
this.lastAckedTime = rcvTime;
}
return shouldAck;
}
toFrame() {
const numbersAcked = this.numbersAcked;
if (numbersAcked.length === 0) {
return null;
}
numbersAcked.sort((a, b) => b - a);
if (numbersAcked[0] <= this.lowestAcked) {
numbersAcked.length = 0;
this.largestAcked = this.lowestAcked;
return null;
}
const frame = new frame_1.AckFrame();
frame.largestAcked = this.largestAcked;
frame.largestAckedTime = this.largestAckedTime;
let range = new frame_1.AckRange(this.largestAcked, this.largestAcked);
// numbersAcked should include largestAcked and lowestAcked for this AGL
for (let i = 1, l = numbersAcked.length; i < l; i++) {
const num = numbersAcked[i];
if (num < this.lowestAcked) {
numbersAcked.length = i; // drop smaller numbers
break;
}
const ret = numbersAcked[i - 1] - num;
if (ret === 1) {
range.first = num;
}
else if (ret > 1) {
frame.ackRanges.push(range);
range = new frame_1.AckRange(num, num);
} // else ingnore
}
frame.lowestAcked = range.first;
if (range.last < frame.largestAcked) {
frame.ackRanges.push(range);
}
if (frame.ackRanges.length === 0) {
this.lowestAcked = this.largestAcked;
numbersAcked.length = 1;
}
else if (frame.ackRanges.length > 256) {
// if ackRanges.length > 256, ignore some ranges between
frame.ackRanges[255] = frame.ackRanges[frame.ackRanges.length - 1];
frame.ackRanges.length = 256;
}
debug(`after build AckFrame, largestAcked: %d, lowestAcked: %d, numbersAcked: %j`, this.largestAcked, this.lowestAcked, numbersAcked);
this.misshit = 0;
return frame;
}
}
exports.ACKHandler = ACKHandler;
//# sourceMappingURL=session.js.map