quico 0.1.3 → 0.2.0
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 +381 -216
- package/index.cjs +45 -0
- package/index.d.ts +158 -0
- package/index.js +46 -23
- package/package.json +83 -60
- package/src/agent.js +275 -0
- package/src/client.js +439 -0
- package/src/crypto.js +519 -0
- package/src/h3.js +911 -0
- package/src/h3_client.js +317 -0
- package/src/h3_server.js +523 -0
- package/src/quic_connection.js +1238 -0
- package/src/server.js +346 -0
- package/src/streams.js +471 -0
- package/src/tls_bridge.js +211 -0
- package/src/transport.js +651 -0
- package/src/utils.js +290 -0
- package/src/webtransport.js +708 -0
- package/CHANGELOG.md +0 -31
- package/examples/webtransport_datagram_echo_server/client.html +0 -58
- package/examples/webtransport_datagram_echo_server/server.js +0 -23
- package/h3_server.js +0 -416
- package/h3_socket.js +0 -473
- package/libs/crypto.js +0 -1841
- package/libs/h3.js +0 -1461
- package/libs/utils.js +0 -404
- package/quic_socket.js +0 -1550
- package/quico.svg +0 -1
package/index.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quico — TypeScript declarations
|
|
3
|
+
* Pure JavaScript QUIC, HTTP/3, QPACK & WebTransport for Node.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/// <reference types="node" />
|
|
7
|
+
|
|
8
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
9
|
+
import { Duplex, Readable, Writable } from 'node:stream';
|
|
10
|
+
import { EventEmitter } from 'node:events';
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// Server
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
type RequestHandler = (req: QuicoIncomingMessage, res: QuicoServerResponse) => void;
|
|
17
|
+
|
|
18
|
+
interface ServerOptions {
|
|
19
|
+
key?: string | Buffer;
|
|
20
|
+
cert?: string | Buffer;
|
|
21
|
+
ca?: string | Buffer;
|
|
22
|
+
SNICallback?: (servername: string, cb: (err: Error | null, ctx: any) => void) => void;
|
|
23
|
+
maxConnections?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface QuicoServer extends EventEmitter {
|
|
27
|
+
listen(port: number, callback?: () => void): this;
|
|
28
|
+
listen(port: number, host: string, callback?: () => void): this;
|
|
29
|
+
close(callback?: () => void): void;
|
|
30
|
+
on(event: 'request', listener: RequestHandler): this;
|
|
31
|
+
on(event: 'listening', listener: () => void): this;
|
|
32
|
+
on(event: 'error', listener: (err: Error) => void): this;
|
|
33
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface QuicoIncomingMessage extends IncomingMessage {
|
|
37
|
+
/** QUIC stream ID */
|
|
38
|
+
streamId?: number;
|
|
39
|
+
|
|
40
|
+
// WebTransport events (available after res.writeHead(200) on WT sessions)
|
|
41
|
+
on(event: 'stream', listener: (stream: Duplex) => void): this;
|
|
42
|
+
on(event: 'unidirectionalStream', listener: (stream: Readable) => void): this;
|
|
43
|
+
on(event: 'datagram', listener: (data: Buffer) => void): this;
|
|
44
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface QuicoServerResponse extends ServerResponse {
|
|
48
|
+
// WebTransport methods (available after res.writeHead(200) on WT sessions)
|
|
49
|
+
sendDatagram?(data: Buffer | Uint8Array | string): void;
|
|
50
|
+
createBidirectionalStream?(): Duplex;
|
|
51
|
+
createUnidirectionalStream?(): Writable;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
// ============================================================
|
|
56
|
+
// Client
|
|
57
|
+
// ============================================================
|
|
58
|
+
|
|
59
|
+
interface RequestOptions {
|
|
60
|
+
hostname?: string;
|
|
61
|
+
host?: string;
|
|
62
|
+
port?: number;
|
|
63
|
+
path?: string;
|
|
64
|
+
method?: string;
|
|
65
|
+
headers?: Record<string, string | string[]>;
|
|
66
|
+
agent?: QuicoAgent;
|
|
67
|
+
rejectUnauthorized?: boolean;
|
|
68
|
+
timeout?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface QuicoClientRequest extends Writable {
|
|
72
|
+
on(event: 'response', listener: (res: QuicoIncomingMessage) => void): this;
|
|
73
|
+
on(event: 'error', listener: (err: Error) => void): this;
|
|
74
|
+
on(event: 'timeout', listener: () => void): this;
|
|
75
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
76
|
+
end(data?: string | Buffer): this;
|
|
77
|
+
abort(): void;
|
|
78
|
+
setTimeout(ms: number, callback?: () => void): this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
// ============================================================
|
|
83
|
+
// Agent
|
|
84
|
+
// ============================================================
|
|
85
|
+
|
|
86
|
+
interface QuicoAgent {
|
|
87
|
+
destroy(): void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
// ============================================================
|
|
92
|
+
// WebTransport
|
|
93
|
+
// ============================================================
|
|
94
|
+
|
|
95
|
+
interface WebTransportOptions {
|
|
96
|
+
/** Skip certificate verification (default: true for self-signed) */
|
|
97
|
+
rejectUnauthorized?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
declare class WebTransport extends EventEmitter {
|
|
101
|
+
constructor(url: string, options?: WebTransportOptions);
|
|
102
|
+
|
|
103
|
+
/** Resolves when the WebTransport session is established */
|
|
104
|
+
readonly ready: Promise<void>;
|
|
105
|
+
/** Current state: 'connecting' | 'connected' | 'closed' */
|
|
106
|
+
readonly state: string;
|
|
107
|
+
/** The URL of the WebTransport session */
|
|
108
|
+
readonly url: string;
|
|
109
|
+
|
|
110
|
+
/** WHATWG-style datagram interface */
|
|
111
|
+
readonly datagrams: {
|
|
112
|
+
readonly readable: ReadableStream<Uint8Array>;
|
|
113
|
+
readonly writable: WritableStream<Uint8Array>;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Create a bidirectional stream (Node.js Duplex + WHATWG .readable/.writable) */
|
|
117
|
+
createBidirectionalStream(): Promise<Duplex & {
|
|
118
|
+
readonly readable: ReadableStream<Uint8Array>;
|
|
119
|
+
readonly writable: WritableStream<Uint8Array>;
|
|
120
|
+
}>;
|
|
121
|
+
|
|
122
|
+
/** Create a unidirectional stream for writing */
|
|
123
|
+
createUnidirectionalStream(): Promise<Writable>;
|
|
124
|
+
|
|
125
|
+
/** Send an unreliable datagram */
|
|
126
|
+
sendDatagram(data: Buffer | Uint8Array): void;
|
|
127
|
+
|
|
128
|
+
/** Close the WebTransport session */
|
|
129
|
+
close(info?: { closeCode?: number; reason?: string }): void;
|
|
130
|
+
|
|
131
|
+
on(event: 'datagram', listener: (data: Buffer) => void): this;
|
|
132
|
+
on(event: 'close', listener: () => void): this;
|
|
133
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// Main module
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
declare const quico: {
|
|
142
|
+
createServer(options: ServerOptions, handler?: RequestHandler): QuicoServer;
|
|
143
|
+
|
|
144
|
+
request(url: string | URL, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
|
|
145
|
+
request(url: string | URL, options: RequestOptions, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
|
|
146
|
+
request(options: RequestOptions, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
|
|
147
|
+
|
|
148
|
+
get(url: string | URL, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
|
|
149
|
+
get(url: string | URL, options: RequestOptions, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
|
|
150
|
+
get(options: RequestOptions, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
|
|
151
|
+
|
|
152
|
+
globalAgent: QuicoAgent;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default quico;
|
|
156
|
+
export { WebTransport };
|
|
157
|
+
export { QuicoServer, QuicoIncomingMessage, QuicoServerResponse, QuicoClientRequest, QuicoAgent };
|
|
158
|
+
export { ServerOptions, RequestOptions, RequestHandler, WebTransportOptions };
|
package/index.js
CHANGED
|
@@ -1,23 +1,46 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* quico: HTTP/3 and QUIC implementation for Node.js
|
|
3
|
-
* Copyright 2025 colocohen
|
|
4
|
-
*
|
|
5
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
* you may not use this file except in compliance with the License.
|
|
7
|
-
* You may obtain a copy of the License at:
|
|
8
|
-
*
|
|
9
|
-
* https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
*
|
|
11
|
-
* This file is part of the open-source project hosted at:
|
|
12
|
-
* https://github.com/colocohen/quico
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
/*
|
|
2
|
+
* quico: HTTP/3 and QUIC implementation for Node.js
|
|
3
|
+
* Copyright 2025 colocohen
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* This file is part of the open-source project hosted at:
|
|
12
|
+
* https://github.com/colocohen/quico
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createServer } from './src/server.js';
|
|
16
|
+
import { request, get } from './src/client.js';
|
|
17
|
+
import { Agent, globalAgent } from './src/agent.js';
|
|
18
|
+
import { IncomingMessage, ServerResponse, ClientRequest } from './src/streams.js';
|
|
19
|
+
import { createSecureContext } from './src/tls_bridge.js';
|
|
20
|
+
import { WebTransport } from './src/webtransport.js';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
createServer,
|
|
24
|
+
request,
|
|
25
|
+
get,
|
|
26
|
+
Agent,
|
|
27
|
+
globalAgent,
|
|
28
|
+
IncomingMessage,
|
|
29
|
+
ServerResponse,
|
|
30
|
+
ClientRequest,
|
|
31
|
+
createSecureContext,
|
|
32
|
+
WebTransport
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
createServer,
|
|
37
|
+
request,
|
|
38
|
+
get,
|
|
39
|
+
Agent,
|
|
40
|
+
globalAgent,
|
|
41
|
+
IncomingMessage,
|
|
42
|
+
ServerResponse,
|
|
43
|
+
ClientRequest,
|
|
44
|
+
createSecureContext,
|
|
45
|
+
WebTransport
|
|
46
|
+
};
|
package/package.json
CHANGED
|
@@ -1,60 +1,83 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "quico",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "quico",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.cjs",
|
|
7
|
+
"module": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./index.js",
|
|
11
|
+
"require": "./index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"types": "index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"index.cjs",
|
|
21
|
+
"index.d.ts",
|
|
22
|
+
"src/",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"quic",
|
|
28
|
+
"http3",
|
|
29
|
+
"http",
|
|
30
|
+
"https",
|
|
31
|
+
"h3",
|
|
32
|
+
"quic-node",
|
|
33
|
+
"http3-node",
|
|
34
|
+
"quic-server",
|
|
35
|
+
"quic-client",
|
|
36
|
+
"http3-server",
|
|
37
|
+
"http3-client",
|
|
38
|
+
"h3-node",
|
|
39
|
+
"webtransport",
|
|
40
|
+
"websocket",
|
|
41
|
+
"datagram",
|
|
42
|
+
"udp",
|
|
43
|
+
"qpack",
|
|
44
|
+
"tls",
|
|
45
|
+
"tls1.3",
|
|
46
|
+
"tls13",
|
|
47
|
+
"nodejs",
|
|
48
|
+
"backend",
|
|
49
|
+
"real-time",
|
|
50
|
+
"low-latency",
|
|
51
|
+
"transport",
|
|
52
|
+
"http-server",
|
|
53
|
+
"socketio"
|
|
54
|
+
],
|
|
55
|
+
"author": {
|
|
56
|
+
"name": "colocohen",
|
|
57
|
+
"email": "support@quicojs.dev"
|
|
58
|
+
},
|
|
59
|
+
"license": "Apache-2.0",
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "https://github.com/colocohen/quico.git"
|
|
63
|
+
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"email": "support@quicojs.dev",
|
|
66
|
+
"url": "https://github.com/colocohen/quico/issues"
|
|
67
|
+
},
|
|
68
|
+
"homepage": "https://github.com/colocohen/quico#readme",
|
|
69
|
+
"funding": [
|
|
70
|
+
{
|
|
71
|
+
"type": "github",
|
|
72
|
+
"url": "https://github.com/sponsors/colocohen"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"type": "opencollective",
|
|
76
|
+
"url": "https://opencollective.com/quicojs"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"flat-ranges": "^2.0.0",
|
|
81
|
+
"lemon-tls": "^0.2.0"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quico: HTTP/3 and QUIC implementation for Node.js
|
|
3
|
+
* Copyright 2025 colocohen
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* This file is part of the open-source project hosted at:
|
|
12
|
+
* https://github.com/colocohen/quico
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { EventEmitter } from 'node:events';
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// Agent — connection pooling + protocol discovery
|
|
20
|
+
//
|
|
21
|
+
// Mirrors node:https.Agent API surface.
|
|
22
|
+
// Caches which protocol works per host so subsequent requests
|
|
23
|
+
// skip the discovery step.
|
|
24
|
+
//
|
|
25
|
+
// Options:
|
|
26
|
+
// http2: true (default) — allow HTTP/2 in fallback chain
|
|
27
|
+
// http1: true (default) — allow HTTP/1.1 in fallback chain
|
|
28
|
+
// h3Timeout: 3000 — ms before falling back from H3
|
|
29
|
+
// ============================================================
|
|
30
|
+
|
|
31
|
+
class Agent extends EventEmitter {
|
|
32
|
+
|
|
33
|
+
constructor(options) {
|
|
34
|
+
super();
|
|
35
|
+
|
|
36
|
+
options = options || {};
|
|
37
|
+
|
|
38
|
+
this.keepAlive = options.keepAlive !== false;
|
|
39
|
+
this.keepAliveMsecs = options.keepAliveMsecs || 1000;
|
|
40
|
+
this.maxSockets = options.maxSockets || 256;
|
|
41
|
+
this.maxFreeSockets = options.maxFreeSockets || 32;
|
|
42
|
+
this.timeout = options.timeout || 30000;
|
|
43
|
+
|
|
44
|
+
// Protocol flags (defaults for all requests through this agent)
|
|
45
|
+
this.http2 = options.http2 !== false;
|
|
46
|
+
this.http1 = options.http1 !== false;
|
|
47
|
+
|
|
48
|
+
// H3 connect timeout before falling back (ms)
|
|
49
|
+
this.h3Timeout = options.h3Timeout || 3000;
|
|
50
|
+
|
|
51
|
+
// Protocol cache: "host:port" → { protocol, ts, altSvcMaxAge }
|
|
52
|
+
// protocol: 'h3' | 'h2' | 'https'
|
|
53
|
+
this._protocolCache = {};
|
|
54
|
+
|
|
55
|
+
// Active QUIC connections: "host:port" → QUICConnection
|
|
56
|
+
// Allows multiplexing multiple requests over one QUIC connection
|
|
57
|
+
this._h3Pool = {};
|
|
58
|
+
|
|
59
|
+
// Active HTTP/2 sessions: "host:port" → http2.ClientHttp2Session
|
|
60
|
+
this._h2Pool = {};
|
|
61
|
+
|
|
62
|
+
// Request count per host (for maxSockets enforcement)
|
|
63
|
+
this._activeCounts = {};
|
|
64
|
+
|
|
65
|
+
// Sweep interval — clean stale pool entries
|
|
66
|
+
this._sweepTimer = setInterval(() => this._sweep(), 15000);
|
|
67
|
+
if (this._sweepTimer.unref) this._sweepTimer.unref();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// ============================================================
|
|
72
|
+
// Protocol cache
|
|
73
|
+
// ============================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get cached protocol for a host, or null if unknown.
|
|
77
|
+
*/
|
|
78
|
+
getProtocol(host, port) {
|
|
79
|
+
var key = host + ':' + (port || 443);
|
|
80
|
+
var entry = this._protocolCache[key];
|
|
81
|
+
if (!entry) return null;
|
|
82
|
+
|
|
83
|
+
// Check expiry (Alt-Svc max-age, default 24h)
|
|
84
|
+
var maxAge = entry.altSvcMaxAge || 86400000;
|
|
85
|
+
if (Date.now() - entry.ts > maxAge) {
|
|
86
|
+
delete this._protocolCache[key];
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return entry.protocol;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record which protocol works for a host.
|
|
95
|
+
*/
|
|
96
|
+
setProtocol(host, port, protocol, altSvcMaxAge) {
|
|
97
|
+
var key = host + ':' + (port || 443);
|
|
98
|
+
this._protocolCache[key] = {
|
|
99
|
+
protocol: protocol,
|
|
100
|
+
ts: Date.now(),
|
|
101
|
+
altSvcMaxAge: altSvcMaxAge || 86400000
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
// ============================================================
|
|
107
|
+
// Alt-Svc header parsing
|
|
108
|
+
// ============================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse Alt-Svc header from an HTTP/1.1 or HTTP/2 response.
|
|
112
|
+
* If it advertises h3, cache it for future requests.
|
|
113
|
+
*
|
|
114
|
+
* Examples:
|
|
115
|
+
* Alt-Svc: h3=":443"; ma=86400
|
|
116
|
+
* Alt-Svc: h3=":443", h3-29=":443"
|
|
117
|
+
*/
|
|
118
|
+
parseAltSvc(host, port, altSvcValue) {
|
|
119
|
+
if (!altSvcValue || altSvcValue === 'clear') {
|
|
120
|
+
var key = host + ':' + (port || 443);
|
|
121
|
+
delete this._protocolCache[key];
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
var entries = altSvcValue.split(',');
|
|
126
|
+
for (var i = 0; i < entries.length; i++) {
|
|
127
|
+
var entry = entries[i].trim();
|
|
128
|
+
var match = entry.match(/^h3(?:-\d+)?="([^"]*)"(.*)$/);
|
|
129
|
+
if (match) {
|
|
130
|
+
var authority = match[1];
|
|
131
|
+
var params = match[2] || '';
|
|
132
|
+
|
|
133
|
+
var maMatch = params.match(/ma=(\d+)/);
|
|
134
|
+
var maxAge = maMatch ? parseInt(maMatch[1]) * 1000 : 86400000;
|
|
135
|
+
|
|
136
|
+
var h3Port = port;
|
|
137
|
+
var colonIdx = authority.lastIndexOf(':');
|
|
138
|
+
if (colonIdx >= 0) {
|
|
139
|
+
var parsed = parseInt(authority.substring(colonIdx + 1));
|
|
140
|
+
if (!isNaN(parsed)) h3Port = parsed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.setProtocol(host, h3Port, 'h3', maxAge);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
// ============================================================
|
|
151
|
+
// H3 connection pool
|
|
152
|
+
// Stores { quic, h3, udpSocket, _nextStreamId } objects
|
|
153
|
+
// from h3_client.js for multiplexing.
|
|
154
|
+
// ============================================================
|
|
155
|
+
|
|
156
|
+
getH3Connection(host, port) {
|
|
157
|
+
var key = host + ':' + (port || 443);
|
|
158
|
+
var conn = this._h3Pool[key];
|
|
159
|
+
if (!conn) return null;
|
|
160
|
+
var state = conn.quic ? conn.quic.state : 'closed';
|
|
161
|
+
if (state === 'closed' || state === 'draining') {
|
|
162
|
+
delete this._h3Pool[key];
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return conn;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setH3Connection(host, port, conn) {
|
|
169
|
+
var key = host + ':' + (port || 443);
|
|
170
|
+
this._h3Pool[key] = conn;
|
|
171
|
+
if (conn.quic) {
|
|
172
|
+
conn.quic.on('close', () => {
|
|
173
|
+
if (this._h3Pool[key] === conn) delete this._h3Pool[key];
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
// ============================================================
|
|
180
|
+
// H2 session pool
|
|
181
|
+
// ============================================================
|
|
182
|
+
|
|
183
|
+
getH2Session(host, port) {
|
|
184
|
+
var key = host + ':' + (port || 443);
|
|
185
|
+
var session = this._h2Pool[key];
|
|
186
|
+
if (!session) return null;
|
|
187
|
+
if (session.closed || session.destroyed) {
|
|
188
|
+
delete this._h2Pool[key];
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return session;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setH2Session(host, port, session) {
|
|
195
|
+
var key = host + ':' + (port || 443);
|
|
196
|
+
this._h2Pool[key] = session;
|
|
197
|
+
session.on('close', () => {
|
|
198
|
+
if (this._h2Pool[key] === session) delete this._h2Pool[key];
|
|
199
|
+
});
|
|
200
|
+
session.on('error', () => {
|
|
201
|
+
if (this._h2Pool[key] === session) delete this._h2Pool[key];
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
// ============================================================
|
|
207
|
+
// Active request tracking
|
|
208
|
+
// ============================================================
|
|
209
|
+
|
|
210
|
+
canCreateSocket(host, port) {
|
|
211
|
+
var key = host + ':' + (port || 443);
|
|
212
|
+
return (this._activeCounts[key] || 0) < this.maxSockets;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
trackRequest(host, port) {
|
|
216
|
+
var key = host + ':' + (port || 443);
|
|
217
|
+
this._activeCounts[key] = (this._activeCounts[key] || 0) + 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
releaseRequest(host, port) {
|
|
221
|
+
var key = host + ':' + (port || 443);
|
|
222
|
+
if (this._activeCounts[key]) {
|
|
223
|
+
this._activeCounts[key]--;
|
|
224
|
+
if (this._activeCounts[key] <= 0) delete this._activeCounts[key];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
// ============================================================
|
|
230
|
+
// Sweep stale entries
|
|
231
|
+
// ============================================================
|
|
232
|
+
|
|
233
|
+
_sweep() {
|
|
234
|
+
var now = Date.now();
|
|
235
|
+
|
|
236
|
+
for (var key in this._protocolCache) {
|
|
237
|
+
var entry = this._protocolCache[key];
|
|
238
|
+
var maxAge = entry.altSvcMaxAge || 86400000;
|
|
239
|
+
if (now - entry.ts > maxAge) delete this._protocolCache[key];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (var key in this._h3Pool) {
|
|
243
|
+
var conn = this._h3Pool[key];
|
|
244
|
+
var state = conn.quic ? conn.quic.state : 'closed';
|
|
245
|
+
if (state === 'closed' || state === 'draining') delete this._h3Pool[key];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (var key in this._h2Pool) {
|
|
249
|
+
var session = this._h2Pool[key];
|
|
250
|
+
if (session.closed || session.destroyed) delete this._h2Pool[key];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
destroy() {
|
|
255
|
+
if (this._sweepTimer) { clearInterval(this._sweepTimer); this._sweepTimer = null; }
|
|
256
|
+
|
|
257
|
+
for (var key in this._h3Pool) {
|
|
258
|
+
try { this._h3Pool[key].quic.close(0, 'agent destroy'); } catch (e) {}
|
|
259
|
+
}
|
|
260
|
+
this._h3Pool = {};
|
|
261
|
+
|
|
262
|
+
for (var key in this._h2Pool) {
|
|
263
|
+
try { this._h2Pool[key].close(); } catch (e) {}
|
|
264
|
+
}
|
|
265
|
+
this._h2Pool = {};
|
|
266
|
+
|
|
267
|
+
this._protocolCache = {};
|
|
268
|
+
this._activeCounts = {};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
var globalAgent = new Agent();
|
|
274
|
+
|
|
275
|
+
export { Agent, globalAgent };
|