@libp2p/yamux 7.0.4-a02cb0461
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/dist/index.min.js +4 -0
- package/dist/index.min.js.map +7 -0
- package/dist/src/config.d.ts +30 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +41 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/constants.d.ts +12 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +22 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/decode.d.ts +34 -0
- package/dist/src/decode.d.ts.map +1 -0
- package/dist/src/decode.js +76 -0
- package/dist/src/decode.js.map +1 -0
- package/dist/src/encode.d.ts +3 -0
- package/dist/src/encode.d.ts.map +1 -0
- package/dist/src/encode.js +19 -0
- package/dist/src/encode.js.map +1 -0
- package/dist/src/errors.d.ts +40 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +70 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/frame.d.ts +54 -0
- package/dist/src/frame.d.ts.map +1 -0
- package/dist/src/frame.js +36 -0
- package/dist/src/frame.js.map +1 -0
- package/dist/src/index.d.ts +85 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +84 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/muxer.d.ts +91 -0
- package/dist/src/muxer.d.ts.map +1 -0
- package/dist/src/muxer.js +428 -0
- package/dist/src/muxer.js.map +1 -0
- package/dist/src/stream.d.ts +96 -0
- package/dist/src/stream.d.ts.map +1 -0
- package/dist/src/stream.js +253 -0
- package/dist/src/stream.js.map +1 -0
- package/package.json +95 -0
- package/src/config.ts +72 -0
- package/src/constants.ts +25 -0
- package/src/decode.ts +105 -0
- package/src/encode.ts +26 -0
- package/src/errors.ts +89 -0
- package/src/frame.ts +64 -0
- package/src/index.ts +90 -0
- package/src/muxer.ts +507 -0
- package/src/stream.ts +313 -0
package/src/frame.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export enum FrameType {
|
|
2
|
+
/** Used to transmit data. May transmit zero length payloads depending on the flags. */
|
|
3
|
+
Data = 0x0,
|
|
4
|
+
/** Used to updated the senders receive window size. This is used to implement per-session flow control. */
|
|
5
|
+
WindowUpdate = 0x1,
|
|
6
|
+
/** Used to measure RTT. It can also be used to heart-beat and do keep-alive over TCP. */
|
|
7
|
+
Ping = 0x2,
|
|
8
|
+
/** Used to close a session. */
|
|
9
|
+
GoAway = 0x3
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export enum Flag {
|
|
13
|
+
/** Signals the start of a new stream. May be sent with a data or window update message. Also sent with a ping to indicate outbound. */
|
|
14
|
+
SYN = 0x1,
|
|
15
|
+
/** Acknowledges the start of a new stream. May be sent with a data or window update message. Also sent with a ping to indicate response. */
|
|
16
|
+
ACK = 0x2,
|
|
17
|
+
/** Performs a half-close of a stream. May be sent with a data message or window update. */
|
|
18
|
+
FIN = 0x4,
|
|
19
|
+
/** Reset a stream immediately. May be sent with a data or window update message. */
|
|
20
|
+
RST = 0x8
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const flagCodes = Object.values(Flag).filter((x) => typeof x !== 'string') as Flag[]
|
|
24
|
+
|
|
25
|
+
export const YAMUX_VERSION = 0
|
|
26
|
+
|
|
27
|
+
export enum GoAwayCode {
|
|
28
|
+
NormalTermination = 0x0,
|
|
29
|
+
ProtocolError = 0x1,
|
|
30
|
+
InternalError = 0x2
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const HEADER_LENGTH = 12
|
|
34
|
+
|
|
35
|
+
export interface FrameHeader {
|
|
36
|
+
/**
|
|
37
|
+
* The version field is used for future backward compatibility.
|
|
38
|
+
* At the current time, the field is always set to 0, to indicate the initial version.
|
|
39
|
+
*/
|
|
40
|
+
version?: number
|
|
41
|
+
/** The type field is used to switch the frame message type. */
|
|
42
|
+
type: FrameType
|
|
43
|
+
/** The flags field is used to provide additional information related to the message type. */
|
|
44
|
+
flag: number
|
|
45
|
+
/**
|
|
46
|
+
* The StreamID field is used to identify the logical stream the frame is addressing.
|
|
47
|
+
* The client side should use odd ID's, and the server even.
|
|
48
|
+
* This prevents any collisions. Additionally, the 0 ID is reserved to represent the session.
|
|
49
|
+
*/
|
|
50
|
+
streamID: number
|
|
51
|
+
/**
|
|
52
|
+
* The meaning of the length field depends on the message type:
|
|
53
|
+
* Data - provides the length of bytes following the header
|
|
54
|
+
* Window update - provides a delta update to the window size
|
|
55
|
+
* Ping - Contains an opaque value, echoed back
|
|
56
|
+
* Go Away - Contains an error code
|
|
57
|
+
*/
|
|
58
|
+
length: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function stringifyHeader (header: FrameHeader): string {
|
|
62
|
+
const flags = flagCodes.filter(f => (header.flag & f) === f).map(f => Flag[f]).join('|')
|
|
63
|
+
return `streamID=${header.streamID} type=${FrameType[header.type]} flag=${flags} length=${header.length}`
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* This module is a JavaScript implementation of [Yamux from Hashicorp](https://github.com/hashicorp/yamux/blob/master/spec.md) designed to be used with [js-libp2p](https://github.com/libp2p/js-libp2p).
|
|
5
|
+
*
|
|
6
|
+
* @example Configure libp2p with Yamux
|
|
7
|
+
*
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createLibp2p } from 'libp2p'
|
|
10
|
+
* import { yamux } from '@libp2p/yamux'
|
|
11
|
+
*
|
|
12
|
+
* const node = await createLibp2p({
|
|
13
|
+
* // ... other options
|
|
14
|
+
* streamMuxers: [
|
|
15
|
+
* yamux()
|
|
16
|
+
* ]
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @example Using the low-level API
|
|
21
|
+
*
|
|
22
|
+
* ```js
|
|
23
|
+
* import { yamux } from '@libp2p/yamux'
|
|
24
|
+
* import { pipe } from 'it-pipe'
|
|
25
|
+
* import { duplexPair } from 'it-pair/duplex'
|
|
26
|
+
* import all from 'it-all'
|
|
27
|
+
*
|
|
28
|
+
* // Connect two yamux muxers to demo basic stream multiplexing functionality
|
|
29
|
+
*
|
|
30
|
+
* const clientMuxer = yamux({
|
|
31
|
+
* client: true,
|
|
32
|
+
* onIncomingStream: stream => {
|
|
33
|
+
* // echo data on incoming streams
|
|
34
|
+
* pipe(stream, stream)
|
|
35
|
+
* },
|
|
36
|
+
* onStreamEnd: stream => {
|
|
37
|
+
* // do nothing
|
|
38
|
+
* }
|
|
39
|
+
* })()
|
|
40
|
+
*
|
|
41
|
+
* const serverMuxer = yamux({
|
|
42
|
+
* client: false,
|
|
43
|
+
* onIncomingStream: stream => {
|
|
44
|
+
* // echo data on incoming streams
|
|
45
|
+
* pipe(stream, stream)
|
|
46
|
+
* },
|
|
47
|
+
* onStreamEnd: stream => {
|
|
48
|
+
* // do nothing
|
|
49
|
+
* }
|
|
50
|
+
* })()
|
|
51
|
+
*
|
|
52
|
+
* // `p` is our "connections", what we use to connect the two sides
|
|
53
|
+
* // In a real application, a connection is usually to a remote computer
|
|
54
|
+
* const p = duplexPair()
|
|
55
|
+
*
|
|
56
|
+
* // connect the muxers together
|
|
57
|
+
* pipe(p[0], clientMuxer, p[0])
|
|
58
|
+
* pipe(p[1], serverMuxer, p[1])
|
|
59
|
+
*
|
|
60
|
+
* // now either side can open streams
|
|
61
|
+
* const stream0 = clientMuxer.newStream()
|
|
62
|
+
* const stream1 = serverMuxer.newStream()
|
|
63
|
+
*
|
|
64
|
+
* // Send some data to the other side
|
|
65
|
+
* const encoder = new TextEncoder()
|
|
66
|
+
* const data = [encoder.encode('hello'), encoder.encode('world')]
|
|
67
|
+
* pipe(data, stream0)
|
|
68
|
+
*
|
|
69
|
+
* // Receive data back
|
|
70
|
+
* const result = await pipe(stream0, all)
|
|
71
|
+
*
|
|
72
|
+
* // close a stream
|
|
73
|
+
* stream1.close()
|
|
74
|
+
*
|
|
75
|
+
* // close the muxer
|
|
76
|
+
* clientMuxer.close()
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
import { Yamux } from './muxer.js'
|
|
81
|
+
import type { YamuxMuxer, YamuxMuxerInit } from './muxer.js'
|
|
82
|
+
import type { StreamMuxerFactory } from '@libp2p/interface'
|
|
83
|
+
|
|
84
|
+
export { GoAwayCode } from './frame.js'
|
|
85
|
+
export type { FrameHeader, FrameType } from './frame.js'
|
|
86
|
+
export type { YamuxMuxerInit }
|
|
87
|
+
|
|
88
|
+
export function yamux (init: YamuxMuxerInit = {}): () => StreamMuxerFactory<YamuxMuxer> {
|
|
89
|
+
return () => new Yamux(init)
|
|
90
|
+
}
|
package/src/muxer.ts
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { InvalidParametersError, MuxerClosedError, TooManyOutboundProtocolStreamsError, serviceCapabilities } from '@libp2p/interface'
|
|
2
|
+
import { AbstractStreamMuxer, repeatingTask } from '@libp2p/utils'
|
|
3
|
+
import { raceSignal } from 'race-signal'
|
|
4
|
+
import { Uint8ArrayList } from 'uint8arraylist'
|
|
5
|
+
import { defaultConfig, verifyConfig } from './config.js'
|
|
6
|
+
import { Decoder } from './decode.js'
|
|
7
|
+
import { encodeHeader } from './encode.js'
|
|
8
|
+
import { InvalidFrameError, isProtocolError, NotMatchingPingError, UnRequestedPingError } from './errors.js'
|
|
9
|
+
import { Flag, FrameType, GoAwayCode } from './frame.js'
|
|
10
|
+
import { StreamState, YamuxStream } from './stream.js'
|
|
11
|
+
import type { Config } from './config.js'
|
|
12
|
+
import type { Frame } from './decode.js'
|
|
13
|
+
import type { FrameHeader } from './frame.js'
|
|
14
|
+
import type { AbortOptions, MessageStream, StreamMuxerFactory } from '@libp2p/interface'
|
|
15
|
+
import type { RepeatingTask } from '@libp2p/utils'
|
|
16
|
+
|
|
17
|
+
function debugFrame (header: FrameHeader): any {
|
|
18
|
+
return {
|
|
19
|
+
type: FrameType[header.type],
|
|
20
|
+
flags: [
|
|
21
|
+
(header.flag & Flag.SYN) === Flag.SYN ? 'SYN' : undefined,
|
|
22
|
+
(header.flag & Flag.ACK) === Flag.ACK ? 'ACK' : undefined,
|
|
23
|
+
(header.flag & Flag.FIN) === Flag.FIN ? 'FIN' : undefined,
|
|
24
|
+
(header.flag & Flag.RST) === Flag.RST ? 'RST' : undefined
|
|
25
|
+
].filter(Boolean),
|
|
26
|
+
streamID: header.streamID,
|
|
27
|
+
length: header.length
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const YAMUX_PROTOCOL_ID = '/yamux/1.0.0'
|
|
32
|
+
|
|
33
|
+
export interface YamuxMuxerInit extends Partial<Config> {
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class Yamux implements StreamMuxerFactory {
|
|
37
|
+
protocol = YAMUX_PROTOCOL_ID
|
|
38
|
+
private readonly _init: Partial<Config>
|
|
39
|
+
|
|
40
|
+
constructor (init: Partial<Config> = {}) {
|
|
41
|
+
this._init = init
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
readonly [Symbol.toStringTag] = '@libp2p/yamux'
|
|
45
|
+
|
|
46
|
+
readonly [serviceCapabilities]: string[] = [
|
|
47
|
+
'@libp2p/stream-multiplexing'
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
createStreamMuxer (maConn: MessageStream): YamuxMuxer {
|
|
51
|
+
return new YamuxMuxer(maConn, {
|
|
52
|
+
...this._init
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CloseOptions extends AbortOptions {
|
|
58
|
+
reason?: GoAwayCode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ActivePing extends PromiseWithResolvers<number> {
|
|
62
|
+
id: number
|
|
63
|
+
start: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class YamuxMuxer extends AbstractStreamMuxer<YamuxStream> {
|
|
67
|
+
/** The next stream id to be used when initiating a new stream */
|
|
68
|
+
private nextStreamID: number
|
|
69
|
+
|
|
70
|
+
/** The next ping id to be used when pinging */
|
|
71
|
+
private nextPingID: number
|
|
72
|
+
/** Tracking info for the currently active ping */
|
|
73
|
+
private activePing?: ActivePing
|
|
74
|
+
/** Round trip time */
|
|
75
|
+
private rtt: number
|
|
76
|
+
|
|
77
|
+
/** True if client, false if server */
|
|
78
|
+
private client: boolean
|
|
79
|
+
|
|
80
|
+
private localGoAway?: GoAwayCode
|
|
81
|
+
private remoteGoAway?: GoAwayCode
|
|
82
|
+
|
|
83
|
+
/** Number of tracked inbound streams */
|
|
84
|
+
private numInboundStreams: number
|
|
85
|
+
/** Number of tracked outbound streams */
|
|
86
|
+
private numOutboundStreams: number
|
|
87
|
+
|
|
88
|
+
private decoder: Decoder
|
|
89
|
+
private keepAlive?: RepeatingTask
|
|
90
|
+
|
|
91
|
+
private enableKeepAlive: boolean
|
|
92
|
+
private keepAliveInterval: number
|
|
93
|
+
private maxInboundStreams: number
|
|
94
|
+
private maxOutboundStreams: number
|
|
95
|
+
|
|
96
|
+
constructor (maConn: MessageStream, init: YamuxMuxerInit = {}) {
|
|
97
|
+
super(maConn, {
|
|
98
|
+
...init,
|
|
99
|
+
protocol: YAMUX_PROTOCOL_ID,
|
|
100
|
+
name: 'yamux'
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
this.client = maConn.direction === 'outbound'
|
|
104
|
+
verifyConfig(init)
|
|
105
|
+
|
|
106
|
+
this.enableKeepAlive = init.enableKeepAlive ?? defaultConfig.enableKeepAlive
|
|
107
|
+
this.keepAliveInterval = init.keepAliveInterval ?? defaultConfig.keepAliveInterval
|
|
108
|
+
this.maxInboundStreams = init.maxInboundStreams ?? defaultConfig.maxInboundStreams
|
|
109
|
+
this.maxOutboundStreams = init.maxOutboundStreams ?? defaultConfig.maxOutboundStreams
|
|
110
|
+
|
|
111
|
+
this.decoder = new Decoder()
|
|
112
|
+
|
|
113
|
+
this.numInboundStreams = 0
|
|
114
|
+
this.numOutboundStreams = 0
|
|
115
|
+
|
|
116
|
+
// client uses odd streamIDs, server uses even streamIDs
|
|
117
|
+
this.nextStreamID = this.client ? 1 : 2
|
|
118
|
+
|
|
119
|
+
this.nextPingID = 0
|
|
120
|
+
this.rtt = -1
|
|
121
|
+
|
|
122
|
+
this.log.trace('muxer created')
|
|
123
|
+
|
|
124
|
+
if (this.enableKeepAlive) {
|
|
125
|
+
this.log.trace('muxer keepalive enabled interval=%s', this.keepAliveInterval)
|
|
126
|
+
this.keepAlive = repeatingTask(async (options) => {
|
|
127
|
+
try {
|
|
128
|
+
await this.ping(options)
|
|
129
|
+
} catch (err: any) {
|
|
130
|
+
// TODO: should abort here?
|
|
131
|
+
this.log.error('ping error: %s', err)
|
|
132
|
+
}
|
|
133
|
+
}, this.keepAliveInterval, {
|
|
134
|
+
// send an initial ping to establish RTT
|
|
135
|
+
runImmediately: true
|
|
136
|
+
})
|
|
137
|
+
this.keepAlive.start()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
onData (buf: Uint8Array | Uint8ArrayList): void {
|
|
142
|
+
for (const frame of this.decoder.emitFrames(buf)) {
|
|
143
|
+
this.handleFrame(frame)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onCreateStream (): YamuxStream {
|
|
148
|
+
if (this.remoteGoAway !== undefined) {
|
|
149
|
+
throw new MuxerClosedError('Muxer closed remotely')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this.localGoAway !== undefined) {
|
|
153
|
+
throw new MuxerClosedError('Muxer closed locally')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const id = this.nextStreamID
|
|
157
|
+
this.nextStreamID += 2
|
|
158
|
+
|
|
159
|
+
// check against our configured maximum number of outbound streams
|
|
160
|
+
if (this.numOutboundStreams >= this.maxOutboundStreams) {
|
|
161
|
+
throw new TooManyOutboundProtocolStreamsError('max outbound streams exceeded')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.log.trace('new outgoing stream id=%s', id)
|
|
165
|
+
|
|
166
|
+
const stream = this._newStream(id, StreamState.Init, 'outbound')
|
|
167
|
+
|
|
168
|
+
this.numOutboundStreams++
|
|
169
|
+
|
|
170
|
+
// send a window update to open the stream on the receiver end. do this in a
|
|
171
|
+
// microtask so the stream gets added to the streams array by the superclass
|
|
172
|
+
// before we send the SYN flag, otherwise we create a race condition whereby
|
|
173
|
+
// we can receive the ACK before the stream is added to the streams list
|
|
174
|
+
queueMicrotask(() => {
|
|
175
|
+
stream.sendWindowUpdate()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return stream
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Initiate a ping and wait for a response
|
|
183
|
+
*
|
|
184
|
+
* Note: only a single ping will be initiated at a time.
|
|
185
|
+
* If a ping is already in progress, a new ping will not be initiated.
|
|
186
|
+
*
|
|
187
|
+
* @returns the round-trip-time in milliseconds
|
|
188
|
+
*/
|
|
189
|
+
async ping (options?: AbortOptions): Promise<number> {
|
|
190
|
+
if (this.remoteGoAway !== undefined) {
|
|
191
|
+
throw new MuxerClosedError('Muxer closed remotely')
|
|
192
|
+
}
|
|
193
|
+
if (this.localGoAway !== undefined) {
|
|
194
|
+
throw new MuxerClosedError('Muxer closed locally')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (this.activePing != null) {
|
|
198
|
+
// an active ping is already in progress, piggyback off that
|
|
199
|
+
return raceSignal(this.activePing.promise, options?.signal)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// An active ping does not yet exist, handle the process here
|
|
203
|
+
// create active ping
|
|
204
|
+
this.activePing = Object.assign(Promise.withResolvers<number>(), {
|
|
205
|
+
id: this.nextPingID++,
|
|
206
|
+
start: Date.now()
|
|
207
|
+
})
|
|
208
|
+
// send ping
|
|
209
|
+
this.sendPing(this.activePing.id)
|
|
210
|
+
// await pong
|
|
211
|
+
try {
|
|
212
|
+
this.rtt = await raceSignal(this.activePing.promise, options?.signal)
|
|
213
|
+
} finally {
|
|
214
|
+
// clean-up active ping
|
|
215
|
+
this.activePing = undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return this.rtt
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get the ping round trip time
|
|
223
|
+
*
|
|
224
|
+
* Note: Will return 0 if no successful ping has yet been completed
|
|
225
|
+
*
|
|
226
|
+
* @returns the round-trip-time in milliseconds
|
|
227
|
+
*/
|
|
228
|
+
getRTT (): number {
|
|
229
|
+
return this.rtt
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Close the muxer
|
|
234
|
+
*/
|
|
235
|
+
async close (options: CloseOptions = {}): Promise<void> {
|
|
236
|
+
if (this.status !== 'open') {
|
|
237
|
+
// already closed
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const reason = options?.reason ?? GoAwayCode.NormalTermination
|
|
243
|
+
|
|
244
|
+
this.log.trace('muxer close reason=%s', GoAwayCode[reason])
|
|
245
|
+
|
|
246
|
+
await super.close(options)
|
|
247
|
+
|
|
248
|
+
// send reason to the other side, allow the other side to close gracefully
|
|
249
|
+
this.sendGoAway(reason)
|
|
250
|
+
} finally {
|
|
251
|
+
this.keepAlive?.stop()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
abort (err: Error): void {
|
|
256
|
+
if (this.status !== 'open') {
|
|
257
|
+
// already closed
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
super.abort(err)
|
|
263
|
+
|
|
264
|
+
let reason = GoAwayCode.InternalError
|
|
265
|
+
|
|
266
|
+
if (isProtocolError(err)) {
|
|
267
|
+
reason = err.reason
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If reason was provided, use that, otherwise use the presence of `err` to determine the reason
|
|
271
|
+
this.log.error('muxer abort reason=%s error=%s', reason, err)
|
|
272
|
+
|
|
273
|
+
// send reason to the other side, allow the other side to close gracefully
|
|
274
|
+
this.sendGoAway(reason)
|
|
275
|
+
} finally {
|
|
276
|
+
this.keepAlive?.stop()
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
onTransportClosed (): void {
|
|
281
|
+
try {
|
|
282
|
+
super.onTransportClosed()
|
|
283
|
+
} finally {
|
|
284
|
+
this.keepAlive?.stop()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Create a new stream */
|
|
289
|
+
private _newStream (streamId: number, state: StreamState, direction: 'inbound' | 'outbound'): YamuxStream {
|
|
290
|
+
if (this.streams.find(s => s.streamId === streamId) != null) {
|
|
291
|
+
throw new InvalidParametersError('Stream already exists with that id')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const stream = new YamuxStream({
|
|
295
|
+
...this.streamOptions,
|
|
296
|
+
id: `${streamId}`,
|
|
297
|
+
streamId,
|
|
298
|
+
state,
|
|
299
|
+
direction,
|
|
300
|
+
sendFrame: this.sendFrame.bind(this),
|
|
301
|
+
log: this.log.newScope(`${direction}:${streamId}`),
|
|
302
|
+
getRTT: this.getRTT.bind(this)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
stream.addEventListener('close', () => {
|
|
306
|
+
this.closeStream(streamId)
|
|
307
|
+
}, {
|
|
308
|
+
once: true
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return stream
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* closeStream is used to close a stream once both sides have
|
|
316
|
+
* issued a close.
|
|
317
|
+
*/
|
|
318
|
+
private closeStream (id: number): void {
|
|
319
|
+
if (this.client === (id % 2 === 0)) {
|
|
320
|
+
this.numInboundStreams--
|
|
321
|
+
} else {
|
|
322
|
+
this.numOutboundStreams--
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private handleFrame (frame: Frame): void {
|
|
327
|
+
const {
|
|
328
|
+
streamID,
|
|
329
|
+
type,
|
|
330
|
+
length
|
|
331
|
+
} = frame.header
|
|
332
|
+
|
|
333
|
+
this.log.trace('received frame %o', debugFrame(frame.header))
|
|
334
|
+
|
|
335
|
+
if (streamID === 0) {
|
|
336
|
+
switch (type) {
|
|
337
|
+
case FrameType.Ping:
|
|
338
|
+
{ this.handlePing(frame.header); return }
|
|
339
|
+
case FrameType.GoAway:
|
|
340
|
+
{ this.handleGoAway(length); return }
|
|
341
|
+
default:
|
|
342
|
+
// Invalid state
|
|
343
|
+
throw new InvalidFrameError('Invalid frame type')
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
switch (frame.header.type) {
|
|
347
|
+
case FrameType.Data:
|
|
348
|
+
case FrameType.WindowUpdate:
|
|
349
|
+
{ this.handleStreamMessage(frame); return }
|
|
350
|
+
default:
|
|
351
|
+
// Invalid state
|
|
352
|
+
throw new InvalidFrameError('Invalid frame type')
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private handlePing (header: FrameHeader): void {
|
|
358
|
+
// If the ping is initiated by the sender, send a response
|
|
359
|
+
if (header.flag === Flag.SYN) {
|
|
360
|
+
this.log.trace('received ping request pingId=%s', header.length)
|
|
361
|
+
this.sendPing(header.length, Flag.ACK)
|
|
362
|
+
} else if (header.flag === Flag.ACK) {
|
|
363
|
+
this.log.trace('received ping response pingId=%s', header.length)
|
|
364
|
+
this.handlePingResponse(header.length)
|
|
365
|
+
} else {
|
|
366
|
+
// Invalid state
|
|
367
|
+
throw new InvalidFrameError('Invalid frame flag')
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private handlePingResponse (pingId: number): void {
|
|
372
|
+
if (this.activePing === undefined) {
|
|
373
|
+
// this ping was not requested
|
|
374
|
+
throw new UnRequestedPingError('ping not requested')
|
|
375
|
+
}
|
|
376
|
+
if (this.activePing.id !== pingId) {
|
|
377
|
+
// this ping doesn't match our active ping request
|
|
378
|
+
throw new NotMatchingPingError('ping doesn\'t match our id')
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// valid ping response
|
|
382
|
+
this.activePing.resolve(Date.now() - this.activePing.start)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private handleGoAway (reason: GoAwayCode): void {
|
|
386
|
+
this.log.trace('received GoAway reason=%s', GoAwayCode[reason] ?? 'unknown')
|
|
387
|
+
this.remoteGoAway = reason
|
|
388
|
+
|
|
389
|
+
if (reason === GoAwayCode.NormalTermination) {
|
|
390
|
+
this.onTransportClosed()
|
|
391
|
+
} else {
|
|
392
|
+
// reset any streams that are still open and close the muxer
|
|
393
|
+
this.abort(new Error('Remote sent GoAway'))
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private handleStreamMessage (frame: Frame): void {
|
|
398
|
+
const { streamID, flag, type } = frame.header
|
|
399
|
+
|
|
400
|
+
if ((flag & Flag.SYN) === Flag.SYN) {
|
|
401
|
+
this.incomingStream(streamID)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const stream = this.streams.find(s => s.streamId === streamID)
|
|
405
|
+
if (stream === undefined) {
|
|
406
|
+
this.log.trace('frame for missing stream id=%s', streamID)
|
|
407
|
+
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
switch (type) {
|
|
412
|
+
case FrameType.WindowUpdate: {
|
|
413
|
+
stream.handleWindowUpdate(frame); return
|
|
414
|
+
}
|
|
415
|
+
case FrameType.Data: {
|
|
416
|
+
stream.handleData(frame); return
|
|
417
|
+
}
|
|
418
|
+
default:
|
|
419
|
+
throw new Error('unreachable')
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private incomingStream (id: number): void {
|
|
424
|
+
if (this.client !== (id % 2 === 0)) {
|
|
425
|
+
throw new InvalidParametersError('Both endpoints are clients')
|
|
426
|
+
}
|
|
427
|
+
if (this.streams.find(s => s.streamId === id)) {
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
this.log.trace('new incoming stream id=%s', id)
|
|
432
|
+
|
|
433
|
+
if (this.localGoAway !== undefined) {
|
|
434
|
+
// reject (reset) immediately if we are doing a go away
|
|
435
|
+
this.sendFrame({
|
|
436
|
+
type: FrameType.WindowUpdate,
|
|
437
|
+
flag: Flag.RST,
|
|
438
|
+
streamID: id,
|
|
439
|
+
length: 0
|
|
440
|
+
})
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// check against our configured maximum number of inbound streams
|
|
445
|
+
if (this.numInboundStreams >= this.maxInboundStreams) {
|
|
446
|
+
this.log('maxIncomingStreams exceeded, forcing stream reset')
|
|
447
|
+
this.sendFrame({
|
|
448
|
+
type: FrameType.WindowUpdate,
|
|
449
|
+
flag: Flag.RST,
|
|
450
|
+
streamID: id,
|
|
451
|
+
length: 0
|
|
452
|
+
})
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// allocate a new stream
|
|
457
|
+
const stream = this._newStream(id, StreamState.SYNReceived, 'inbound')
|
|
458
|
+
|
|
459
|
+
this.numInboundStreams++
|
|
460
|
+
|
|
461
|
+
// the stream should now be tracked
|
|
462
|
+
this.onRemoteStream(stream)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private sendFrame (header: FrameHeader, data?: Uint8ArrayList): boolean {
|
|
466
|
+
let encoded: Uint8Array | Uint8ArrayList
|
|
467
|
+
|
|
468
|
+
if (header.type === FrameType.Data) {
|
|
469
|
+
if (data == null) {
|
|
470
|
+
throw new InvalidFrameError('Invalid frame')
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
encoded = new Uint8ArrayList(encodeHeader(header), data)
|
|
474
|
+
} else {
|
|
475
|
+
encoded = encodeHeader(header)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
this.log.trace('sending frame %o', debugFrame(header))
|
|
479
|
+
|
|
480
|
+
return this.send(encoded)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private sendPing (pingId: number, flag: Flag = Flag.SYN): void {
|
|
484
|
+
if (flag === Flag.SYN) {
|
|
485
|
+
this.log.trace('sending ping request pingId=%s', pingId)
|
|
486
|
+
} else {
|
|
487
|
+
this.log.trace('sending ping response pingId=%s', pingId)
|
|
488
|
+
}
|
|
489
|
+
this.sendFrame({
|
|
490
|
+
type: FrameType.Ping,
|
|
491
|
+
flag,
|
|
492
|
+
streamID: 0,
|
|
493
|
+
length: pingId
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private sendGoAway (reason: GoAwayCode = GoAwayCode.NormalTermination): void {
|
|
498
|
+
this.log('sending GoAway reason=%s', GoAwayCode[reason])
|
|
499
|
+
this.localGoAway = reason
|
|
500
|
+
this.sendFrame({
|
|
501
|
+
type: FrameType.GoAway,
|
|
502
|
+
flag: 0,
|
|
503
|
+
streamID: 0,
|
|
504
|
+
length: reason
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
}
|