@gjsify/tls 0.3.13 → 0.3.15
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/lib/esm/index.js +344 -351
- package/package.json +7 -7
package/lib/esm/index.js
CHANGED
|
@@ -1,377 +1,370 @@
|
|
|
1
1
|
import Gio from "@girs/gio-2.0";
|
|
2
2
|
import GLib from "@girs/glib-2.0";
|
|
3
|
-
import {
|
|
3
|
+
import { Server, Socket } from "node:net";
|
|
4
4
|
import { createNodeError, deferEmit } from "@gjsify/utils";
|
|
5
|
+
|
|
6
|
+
//#region src/index.ts
|
|
5
7
|
const DEFAULT_MIN_VERSION = "TLSv1.2";
|
|
6
8
|
const DEFAULT_MAX_VERSION = "TLSv1.3";
|
|
7
9
|
const DEFAULT_CIPHERS = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384";
|
|
10
|
+
/** Returns a list of supported TLS cipher names (subset; implementation-defined). */
|
|
8
11
|
function getCiphers() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
return [
|
|
13
|
+
"aes-128-gcm",
|
|
14
|
+
"aes-256-gcm",
|
|
15
|
+
"chacha20-poly1305",
|
|
16
|
+
"aes-128-cbc",
|
|
17
|
+
"aes-256-cbc"
|
|
18
|
+
];
|
|
16
19
|
}
|
|
20
|
+
/** Removes a trailing dot from a fully-qualified domain name. */
|
|
17
21
|
function unfqdn(host) {
|
|
18
|
-
|
|
22
|
+
return host.endsWith(".") ? host.slice(0, -1) : host;
|
|
19
23
|
}
|
|
24
|
+
/** Splits a hostname into parts, lower-cased, after removing trailing dots. */
|
|
20
25
|
function splitHost(host) {
|
|
21
|
-
|
|
26
|
+
return unfqdn(host).toLowerCase().split(".");
|
|
22
27
|
}
|
|
28
|
+
/** Wildcard-match a hostname part list against a pattern (supports leading *). */
|
|
23
29
|
function checkWildcard(hostParts, pattern) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const patParts = splitHost(pattern);
|
|
31
|
+
if (patParts.length !== hostParts.length) return false;
|
|
32
|
+
if (patParts[0] === "*") {
|
|
33
|
+
return patParts.slice(1).join(".") === hostParts.slice(1).join(".");
|
|
34
|
+
}
|
|
35
|
+
return patParts.every((p, i) => p === hostParts[i]);
|
|
30
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Verifies that the certificate `cert` is valid for `hostname`.
|
|
39
|
+
* Returns an Error if the check fails, or undefined on success.
|
|
40
|
+
*
|
|
41
|
+
* Reference: Node.js lib/tls.js exports.checkServerIdentity
|
|
42
|
+
*/
|
|
31
43
|
function checkServerIdentity(hostname, cert) {
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return err;
|
|
81
|
-
}
|
|
82
|
-
return void 0;
|
|
83
|
-
}
|
|
84
|
-
class TLSSocket extends Socket {
|
|
85
|
-
encrypted = true;
|
|
86
|
-
authorized = false;
|
|
87
|
-
authorizationError;
|
|
88
|
-
alpnProtocol = false;
|
|
89
|
-
/** @internal */
|
|
90
|
-
_tlsConnection = null;
|
|
91
|
-
constructor(socket, options) {
|
|
92
|
-
super();
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* @internal Wire the TLS connection's I/O streams into this socket
|
|
96
|
-
* so that read/write operations go through the encrypted channel.
|
|
97
|
-
*/
|
|
98
|
-
_setupTlsStreams(tlsConn) {
|
|
99
|
-
this._tlsConnection = tlsConn;
|
|
100
|
-
this._inputStream = tlsConn.get_input_stream();
|
|
101
|
-
this._outputStream = tlsConn.get_output_stream();
|
|
102
|
-
this._connection = tlsConn;
|
|
103
|
-
}
|
|
104
|
-
/** Get the peer certificate info. */
|
|
105
|
-
getPeerCertificate(_detailed) {
|
|
106
|
-
if (!this._tlsConnection) return {};
|
|
107
|
-
try {
|
|
108
|
-
const cert = this._tlsConnection.get_peer_certificate();
|
|
109
|
-
if (!cert) return {};
|
|
110
|
-
return {
|
|
111
|
-
subject: {},
|
|
112
|
-
issuer: {},
|
|
113
|
-
valid_from: "",
|
|
114
|
-
valid_to: ""
|
|
115
|
-
};
|
|
116
|
-
} catch {
|
|
117
|
-
return {};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
/** Get the negotiated TLS protocol version. */
|
|
121
|
-
getProtocol() {
|
|
122
|
-
if (!this._tlsConnection) return null;
|
|
123
|
-
try {
|
|
124
|
-
const proto = this._tlsConnection.get_protocol_version();
|
|
125
|
-
switch (proto) {
|
|
126
|
-
case Gio.TlsProtocolVersion.TLS_1_0:
|
|
127
|
-
return "TLSv1";
|
|
128
|
-
case Gio.TlsProtocolVersion.TLS_1_1:
|
|
129
|
-
return "TLSv1.1";
|
|
130
|
-
case Gio.TlsProtocolVersion.TLS_1_2:
|
|
131
|
-
return "TLSv1.2";
|
|
132
|
-
case Gio.TlsProtocolVersion.TLS_1_3:
|
|
133
|
-
return "TLSv1.3";
|
|
134
|
-
default:
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
} catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
/** Get the cipher info. */
|
|
142
|
-
getCipher() {
|
|
143
|
-
if (!this._tlsConnection) return null;
|
|
144
|
-
try {
|
|
145
|
-
const name = this._tlsConnection.get_ciphersuite_name();
|
|
146
|
-
return { name: name || "unknown", version: this.getProtocol() || "unknown" };
|
|
147
|
-
} catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/** Get the negotiated ALPN protocol. */
|
|
152
|
-
getAlpnProtocol() {
|
|
153
|
-
if (!this._tlsConnection) return false;
|
|
154
|
-
try {
|
|
155
|
-
const proto = this._tlsConnection.get_negotiated_protocol();
|
|
156
|
-
return proto || false;
|
|
157
|
-
} catch {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
44
|
+
const subject = cert.subject;
|
|
45
|
+
const altNames = cert.subjectaltname;
|
|
46
|
+
const dnsNames = [];
|
|
47
|
+
const ips = [];
|
|
48
|
+
hostname = String(hostname);
|
|
49
|
+
if (altNames) {
|
|
50
|
+
const parts = altNames.split(", ");
|
|
51
|
+
for (const name of parts) {
|
|
52
|
+
if (name.startsWith("DNS:")) {
|
|
53
|
+
dnsNames.push(name.slice(4));
|
|
54
|
+
} else if (name.startsWith("IP Address:")) {
|
|
55
|
+
ips.push(name.slice(11).trim());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
let valid = false;
|
|
60
|
+
let reason = "Unknown reason";
|
|
61
|
+
hostname = unfqdn(hostname);
|
|
62
|
+
const isIPv4 = /^(\d{1,3}\.){3}\d{1,3}$/.test(hostname);
|
|
63
|
+
const isIPv6 = hostname.includes(":");
|
|
64
|
+
if (isIPv4 || isIPv6) {
|
|
65
|
+
valid = ips.some((ip) => ip.toLowerCase() === hostname.toLowerCase());
|
|
66
|
+
if (!valid) reason = `IP: ${hostname} is not in the cert's list: ${ips.join(", ")}`;
|
|
67
|
+
} else if (dnsNames.length > 0 || subject?.CN) {
|
|
68
|
+
const hostParts = splitHost(hostname);
|
|
69
|
+
if (dnsNames.length > 0) {
|
|
70
|
+
valid = dnsNames.some((pattern) => checkWildcard(hostParts, pattern));
|
|
71
|
+
if (!valid) reason = `Host: ${hostname}. is not in the cert's altnames: ${altNames}`;
|
|
72
|
+
} else {
|
|
73
|
+
const cn = subject.CN;
|
|
74
|
+
if (Array.isArray(cn)) {
|
|
75
|
+
valid = cn.some((c) => checkWildcard(hostParts, c));
|
|
76
|
+
} else if (cn) {
|
|
77
|
+
valid = checkWildcard(hostParts, cn);
|
|
78
|
+
}
|
|
79
|
+
if (!valid) reason = `Host: ${hostname}. is not cert's CN: ${cn}`;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
reason = "Cert does not contain a DNS name";
|
|
83
|
+
}
|
|
84
|
+
if (!valid) {
|
|
85
|
+
const err = new Error(reason);
|
|
86
|
+
err.reason = reason;
|
|
87
|
+
err.host = hostname;
|
|
88
|
+
err.cert = cert;
|
|
89
|
+
return err;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
161
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* TLSSocket wraps a net.Socket with TLS via Gio.TlsConnection.
|
|
95
|
+
*/
|
|
96
|
+
var TLSSocket = class extends Socket {
|
|
97
|
+
encrypted = true;
|
|
98
|
+
authorized = false;
|
|
99
|
+
authorizationError;
|
|
100
|
+
alpnProtocol = false;
|
|
101
|
+
/** @internal */
|
|
102
|
+
_tlsConnection = null;
|
|
103
|
+
constructor(socket, options) {
|
|
104
|
+
super();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* @internal Wire the TLS connection's I/O streams into this socket
|
|
108
|
+
* so that read/write operations go through the encrypted channel.
|
|
109
|
+
*/
|
|
110
|
+
_setupTlsStreams(tlsConn) {
|
|
111
|
+
this._tlsConnection = tlsConn;
|
|
112
|
+
this._inputStream = tlsConn.get_input_stream();
|
|
113
|
+
this._outputStream = tlsConn.get_output_stream();
|
|
114
|
+
this._connection = tlsConn;
|
|
115
|
+
}
|
|
116
|
+
/** Get the peer certificate info. */
|
|
117
|
+
getPeerCertificate(_detailed) {
|
|
118
|
+
if (!this._tlsConnection) return {};
|
|
119
|
+
try {
|
|
120
|
+
const cert = this._tlsConnection.get_peer_certificate();
|
|
121
|
+
if (!cert) return {};
|
|
122
|
+
return {
|
|
123
|
+
subject: {},
|
|
124
|
+
issuer: {},
|
|
125
|
+
valid_from: "",
|
|
126
|
+
valid_to: ""
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Get the negotiated TLS protocol version. */
|
|
133
|
+
getProtocol() {
|
|
134
|
+
if (!this._tlsConnection) return null;
|
|
135
|
+
try {
|
|
136
|
+
const proto = this._tlsConnection.get_protocol_version();
|
|
137
|
+
switch (proto) {
|
|
138
|
+
case Gio.TlsProtocolVersion.TLS_1_0: return "TLSv1";
|
|
139
|
+
case Gio.TlsProtocolVersion.TLS_1_1: return "TLSv1.1";
|
|
140
|
+
case Gio.TlsProtocolVersion.TLS_1_2: return "TLSv1.2";
|
|
141
|
+
case Gio.TlsProtocolVersion.TLS_1_3: return "TLSv1.3";
|
|
142
|
+
default: return null;
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Get the cipher info. */
|
|
149
|
+
getCipher() {
|
|
150
|
+
if (!this._tlsConnection) return null;
|
|
151
|
+
try {
|
|
152
|
+
const name = this._tlsConnection.get_ciphersuite_name();
|
|
153
|
+
return {
|
|
154
|
+
name: name || "unknown",
|
|
155
|
+
version: this.getProtocol() || "unknown"
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Get the negotiated ALPN protocol. */
|
|
162
|
+
getAlpnProtocol() {
|
|
163
|
+
if (!this._tlsConnection) return false;
|
|
164
|
+
try {
|
|
165
|
+
const proto = this._tlsConnection.get_negotiated_protocol();
|
|
166
|
+
return proto || false;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Create a TLS client connection.
|
|
174
|
+
*
|
|
175
|
+
* Connects via TCP first (using net.Socket.connect), then upgrades
|
|
176
|
+
* the connection to TLS using Gio.TlsClientConnection.
|
|
177
|
+
*/
|
|
162
178
|
function connect(options, callback) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
|
-
} catch (err) {
|
|
222
|
-
socket.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
socket.connect({ port, host });
|
|
226
|
-
return socket;
|
|
179
|
+
const socket = new TLSSocket(undefined, options);
|
|
180
|
+
if (callback) {
|
|
181
|
+
socket.once("secureConnect", callback);
|
|
182
|
+
}
|
|
183
|
+
const port = options.port || 443;
|
|
184
|
+
const host = options.host || "localhost";
|
|
185
|
+
const servername = options.servername || host;
|
|
186
|
+
const rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
187
|
+
socket.once("connect", () => {
|
|
188
|
+
const rawConnection = socket._connection;
|
|
189
|
+
if (!rawConnection) {
|
|
190
|
+
socket.destroy(new Error("No underlying connection for TLS upgrade"));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const connectable = Gio.NetworkAddress.new(servername, port);
|
|
195
|
+
const tlsConn = Gio.TlsClientConnection.new(rawConnection, connectable);
|
|
196
|
+
tlsConn.set_server_identity(connectable);
|
|
197
|
+
if (options.ALPNProtocols && options.ALPNProtocols.length > 0) {
|
|
198
|
+
try {
|
|
199
|
+
tlsConn.set_advertised_protocols(options.ALPNProtocols);
|
|
200
|
+
} catch {}
|
|
201
|
+
}
|
|
202
|
+
if (!rejectUnauthorized) {
|
|
203
|
+
tlsConn.connect("accept-certificate", () => true);
|
|
204
|
+
}
|
|
205
|
+
const cancellable = new Gio.Cancellable();
|
|
206
|
+
tlsConn.handshake_async(GLib.PRIORITY_DEFAULT, cancellable, (_source, asyncResult) => {
|
|
207
|
+
try {
|
|
208
|
+
tlsConn.handshake_finish(asyncResult);
|
|
209
|
+
socket.authorized = true;
|
|
210
|
+
socket._setupTlsStreams(tlsConn);
|
|
211
|
+
socket.alpnProtocol = socket.getAlpnProtocol();
|
|
212
|
+
socket._reading = false;
|
|
213
|
+
socket._startReading();
|
|
214
|
+
socket.emit("secureConnect");
|
|
215
|
+
} catch (err) {
|
|
216
|
+
socket.authorized = false;
|
|
217
|
+
socket.authorizationError = err instanceof Error ? err.message : String(err);
|
|
218
|
+
if (rejectUnauthorized) {
|
|
219
|
+
socket.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
220
|
+
} else {
|
|
221
|
+
socket._setupTlsStreams(tlsConn);
|
|
222
|
+
socket.emit("secureConnect");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
} catch (err) {
|
|
227
|
+
socket.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
socket.connect({
|
|
231
|
+
port,
|
|
232
|
+
host
|
|
233
|
+
});
|
|
234
|
+
return socket;
|
|
227
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Create a TLS secure context.
|
|
238
|
+
*/
|
|
228
239
|
function createSecureContext(options) {
|
|
229
|
-
|
|
240
|
+
return { context: options || {} };
|
|
230
241
|
}
|
|
231
242
|
const rootCertificates = [];
|
|
243
|
+
/**
|
|
244
|
+
* Build a Gio.TlsCertificate from PEM cert+key strings.
|
|
245
|
+
*/
|
|
232
246
|
function buildGioCertificate(cert, key) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return Gio.TlsCertificate.new_from_pem(pem, pem.length);
|
|
238
|
-
}
|
|
239
|
-
class TLSServer extends Server {
|
|
240
|
-
_tlsCertificate = null;
|
|
241
|
-
_tlsOptions;
|
|
242
|
-
_sniContexts = /* @__PURE__ */ new Map();
|
|
243
|
-
constructor(options, secureConnectionListener) {
|
|
244
|
-
super();
|
|
245
|
-
this._tlsOptions = options || {};
|
|
246
|
-
if (secureConnectionListener) {
|
|
247
|
-
this.on("secureConnection", secureConnectionListener);
|
|
248
|
-
}
|
|
249
|
-
if (this._tlsOptions.cert) {
|
|
250
|
-
try {
|
|
251
|
-
this._tlsCertificate = buildGioCertificate(this._tlsOptions.cert, this._tlsOptions.key);
|
|
252
|
-
} catch (err) {
|
|
253
|
-
deferEmit(this, "error", createNodeError(err, "createServer", {}));
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Add a context for SNI (Server Name Indication).
|
|
259
|
-
*/
|
|
260
|
-
addContext(hostname, context) {
|
|
261
|
-
if (context.cert) {
|
|
262
|
-
try {
|
|
263
|
-
const cert = buildGioCertificate(context.cert, context.key);
|
|
264
|
-
this._sniContexts.set(hostname, cert);
|
|
265
|
-
} catch (err) {
|
|
266
|
-
this.emit("error", createNodeError(err, "addContext", {}));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
listen(...args) {
|
|
271
|
-
this.on("connection", (socket) => {
|
|
272
|
-
this._upgradeTls(socket);
|
|
273
|
-
});
|
|
274
|
-
return super.listen(...args);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Upgrade a raw TCP socket to TLS using Gio.TlsServerConnection.
|
|
278
|
-
*/
|
|
279
|
-
_upgradeTls(socket) {
|
|
280
|
-
const rawConnection = socket._connection;
|
|
281
|
-
if (!rawConnection) {
|
|
282
|
-
const err = new Error("Cannot upgrade socket: no underlying connection");
|
|
283
|
-
this.emit("tlsClientError", err, socket);
|
|
284
|
-
socket.destroy();
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
if (!this._tlsCertificate) {
|
|
288
|
-
const err = new Error("TLS server has no certificate configured");
|
|
289
|
-
this.emit("tlsClientError", err, socket);
|
|
290
|
-
socket.destroy();
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
const tlsConn = Gio.TlsServerConnection.new(
|
|
295
|
-
rawConnection,
|
|
296
|
-
this._tlsCertificate
|
|
297
|
-
);
|
|
298
|
-
if (this._tlsOptions.requestCert) {
|
|
299
|
-
tlsConn.authenticationMode = this._tlsOptions.rejectUnauthorized !== false ? Gio.TlsAuthenticationMode.REQUIRED : Gio.TlsAuthenticationMode.REQUESTED;
|
|
300
|
-
} else {
|
|
301
|
-
tlsConn.authenticationMode = Gio.TlsAuthenticationMode.NONE;
|
|
302
|
-
}
|
|
303
|
-
if (this._tlsOptions.rejectUnauthorized === false) {
|
|
304
|
-
tlsConn.connect(
|
|
305
|
-
"accept-certificate",
|
|
306
|
-
() => true
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
if (this._tlsOptions.ALPNProtocols && this._tlsOptions.ALPNProtocols.length > 0) {
|
|
310
|
-
try {
|
|
311
|
-
tlsConn.set_advertised_protocols(this._tlsOptions.ALPNProtocols);
|
|
312
|
-
} catch {
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
const cancellable = new Gio.Cancellable();
|
|
316
|
-
tlsConn.handshake_async(
|
|
317
|
-
GLib.PRIORITY_DEFAULT,
|
|
318
|
-
cancellable,
|
|
319
|
-
(_source, asyncResult) => {
|
|
320
|
-
try {
|
|
321
|
-
tlsConn.handshake_finish(asyncResult);
|
|
322
|
-
const tlsSocket = new TLSSocket();
|
|
323
|
-
tlsSocket.encrypted = true;
|
|
324
|
-
tlsSocket.authorized = true;
|
|
325
|
-
tlsSocket._setupTlsStreams(tlsConn);
|
|
326
|
-
tlsSocket.alpnProtocol = tlsSocket.getAlpnProtocol();
|
|
327
|
-
tlsSocket._startReading();
|
|
328
|
-
this.emit("secureConnection", tlsSocket);
|
|
329
|
-
} catch (err) {
|
|
330
|
-
const nodeErr = createNodeError(err, "handshake", {});
|
|
331
|
-
this.emit("tlsClientError", nodeErr, socket);
|
|
332
|
-
socket.destroy();
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
);
|
|
336
|
-
} catch (err) {
|
|
337
|
-
const nodeErr = createNodeError(err, "tls_wrap", {});
|
|
338
|
-
this.emit("tlsClientError", nodeErr, socket);
|
|
339
|
-
socket.destroy();
|
|
340
|
-
}
|
|
341
|
-
}
|
|
247
|
+
const certStr = Array.isArray(cert) ? cert.map((c) => typeof c === "string" ? c : c.toString("utf-8")).join("\n") : typeof cert === "string" ? cert : cert.toString("utf-8");
|
|
248
|
+
const keyStr = key ? Array.isArray(key) ? key.map((k) => typeof k === "string" ? k : k.toString("utf-8")).join("\n") : typeof key === "string" ? key : key.toString("utf-8") : "";
|
|
249
|
+
const pem = keyStr ? `${certStr}\n${keyStr}` : certStr;
|
|
250
|
+
return Gio.TlsCertificate.new_from_pem(pem, pem.length);
|
|
342
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* TLSServer wraps a net.Server to accept TLS connections.
|
|
254
|
+
*/
|
|
255
|
+
var TLSServer = class extends Server {
|
|
256
|
+
_tlsCertificate = null;
|
|
257
|
+
_tlsOptions;
|
|
258
|
+
_sniContexts = new Map();
|
|
259
|
+
constructor(options, secureConnectionListener) {
|
|
260
|
+
super();
|
|
261
|
+
this._tlsOptions = options || {};
|
|
262
|
+
if (secureConnectionListener) {
|
|
263
|
+
this.on("secureConnection", secureConnectionListener);
|
|
264
|
+
}
|
|
265
|
+
if (this._tlsOptions.cert) {
|
|
266
|
+
try {
|
|
267
|
+
this._tlsCertificate = buildGioCertificate(this._tlsOptions.cert, this._tlsOptions.key);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
deferEmit(this, "error", createNodeError(err, "createServer", {}));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Add a context for SNI (Server Name Indication).
|
|
275
|
+
*/
|
|
276
|
+
addContext(hostname, context) {
|
|
277
|
+
if (context.cert) {
|
|
278
|
+
try {
|
|
279
|
+
const cert = buildGioCertificate(context.cert, context.key);
|
|
280
|
+
this._sniContexts.set(hostname, cert);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
this.emit("error", createNodeError(err, "addContext", {}));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
listen(...args) {
|
|
287
|
+
this.on("connection", (socket) => {
|
|
288
|
+
this._upgradeTls(socket);
|
|
289
|
+
});
|
|
290
|
+
return super.listen(...args);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Upgrade a raw TCP socket to TLS using Gio.TlsServerConnection.
|
|
294
|
+
*/
|
|
295
|
+
_upgradeTls(socket) {
|
|
296
|
+
const rawConnection = socket._connection;
|
|
297
|
+
if (!rawConnection) {
|
|
298
|
+
const err = new Error("Cannot upgrade socket: no underlying connection");
|
|
299
|
+
this.emit("tlsClientError", err, socket);
|
|
300
|
+
socket.destroy();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (!this._tlsCertificate) {
|
|
304
|
+
const err = new Error("TLS server has no certificate configured");
|
|
305
|
+
this.emit("tlsClientError", err, socket);
|
|
306
|
+
socket.destroy();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const tlsConn = Gio.TlsServerConnection.new(rawConnection, this._tlsCertificate);
|
|
311
|
+
if (this._tlsOptions.requestCert) {
|
|
312
|
+
tlsConn.authenticationMode = this._tlsOptions.rejectUnauthorized !== false ? Gio.TlsAuthenticationMode.REQUIRED : Gio.TlsAuthenticationMode.REQUESTED;
|
|
313
|
+
} else {
|
|
314
|
+
tlsConn.authenticationMode = Gio.TlsAuthenticationMode.NONE;
|
|
315
|
+
}
|
|
316
|
+
if (this._tlsOptions.rejectUnauthorized === false) {
|
|
317
|
+
tlsConn.connect("accept-certificate", () => true);
|
|
318
|
+
}
|
|
319
|
+
if (this._tlsOptions.ALPNProtocols && this._tlsOptions.ALPNProtocols.length > 0) {
|
|
320
|
+
try {
|
|
321
|
+
tlsConn.set_advertised_protocols(this._tlsOptions.ALPNProtocols);
|
|
322
|
+
} catch {}
|
|
323
|
+
}
|
|
324
|
+
const cancellable = new Gio.Cancellable();
|
|
325
|
+
tlsConn.handshake_async(GLib.PRIORITY_DEFAULT, cancellable, (_source, asyncResult) => {
|
|
326
|
+
try {
|
|
327
|
+
tlsConn.handshake_finish(asyncResult);
|
|
328
|
+
const tlsSocket = new TLSSocket();
|
|
329
|
+
tlsSocket.encrypted = true;
|
|
330
|
+
tlsSocket.authorized = true;
|
|
331
|
+
tlsSocket._setupTlsStreams(tlsConn);
|
|
332
|
+
tlsSocket.alpnProtocol = tlsSocket.getAlpnProtocol();
|
|
333
|
+
tlsSocket._startReading();
|
|
334
|
+
this.emit("secureConnection", tlsSocket);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
const nodeErr = createNodeError(err, "handshake", {});
|
|
337
|
+
this.emit("tlsClientError", nodeErr, socket);
|
|
338
|
+
socket.destroy();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
} catch (err) {
|
|
342
|
+
const nodeErr = createNodeError(err, "tls_wrap", {});
|
|
343
|
+
this.emit("tlsClientError", nodeErr, socket);
|
|
344
|
+
socket.destroy();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
343
348
|
function createServer(optionsOrListener, secureConnectionListener) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
349
|
+
if (typeof optionsOrListener === "function") {
|
|
350
|
+
return new TLSServer(undefined, optionsOrListener);
|
|
351
|
+
}
|
|
352
|
+
return new TLSServer(optionsOrListener, secureConnectionListener);
|
|
348
353
|
}
|
|
349
|
-
var
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
};
|
|
363
|
-
export {
|
|
364
|
-
DEFAULT_CIPHERS,
|
|
365
|
-
DEFAULT_MAX_VERSION,
|
|
366
|
-
DEFAULT_MIN_VERSION,
|
|
367
|
-
TLSServer as Server,
|
|
368
|
-
TLSServer,
|
|
369
|
-
TLSSocket,
|
|
370
|
-
checkServerIdentity,
|
|
371
|
-
connect,
|
|
372
|
-
createSecureContext,
|
|
373
|
-
createServer,
|
|
374
|
-
index_default as default,
|
|
375
|
-
getCiphers,
|
|
376
|
-
rootCertificates
|
|
354
|
+
var src_default = {
|
|
355
|
+
TLSSocket,
|
|
356
|
+
TLSServer,
|
|
357
|
+
Server: TLSServer,
|
|
358
|
+
connect,
|
|
359
|
+
createServer,
|
|
360
|
+
createSecureContext,
|
|
361
|
+
checkServerIdentity,
|
|
362
|
+
getCiphers,
|
|
363
|
+
rootCertificates,
|
|
364
|
+
DEFAULT_MIN_VERSION,
|
|
365
|
+
DEFAULT_MAX_VERSION,
|
|
366
|
+
DEFAULT_CIPHERS
|
|
377
367
|
};
|
|
368
|
+
|
|
369
|
+
//#endregion
|
|
370
|
+
export { DEFAULT_CIPHERS, DEFAULT_MAX_VERSION, DEFAULT_MIN_VERSION, TLSServer as Server, TLSServer, TLSSocket, checkServerIdentity, connect, createSecureContext, createServer, src_default as default, getCiphers, rootCertificates };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/tls",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"description": "Node.js tls module for Gjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
"tls"
|
|
31
31
|
],
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@gjsify/cli": "^0.3.
|
|
34
|
-
"@gjsify/unit": "^0.3.
|
|
33
|
+
"@gjsify/cli": "^0.3.15",
|
|
34
|
+
"@gjsify/unit": "^0.3.15",
|
|
35
35
|
"@types/node": "^25.6.0",
|
|
36
36
|
"typescript": "^6.0.3"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@girs/gio-2.0": "
|
|
40
|
-
"@girs/glib-2.0": "
|
|
41
|
-
"@gjsify/net": "^0.3.
|
|
42
|
-
"@gjsify/utils": "^0.3.
|
|
39
|
+
"@girs/gio-2.0": "2.88.0-4.0.0-rc.9",
|
|
40
|
+
"@girs/glib-2.0": "2.88.0-4.0.0-rc.9",
|
|
41
|
+
"@gjsify/net": "^0.3.15",
|
|
42
|
+
"@gjsify/utils": "^0.3.15"
|
|
43
43
|
}
|
|
44
44
|
}
|