sliftutils 1.5.5 → 1.6.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/index.d.ts +42 -12
- package/misc/https/certs.d.ts +7 -10
- package/misc/https/certs.ts +43 -145
- package/misc/https/dns.d.ts +5 -0
- package/misc/https/dns.ts +18 -2
- package/misc/https/hostServer.d.ts +19 -0
- package/misc/https/hostServer.ts +132 -0
- package/misc/https/node-forge-ed25519.d.ts +7 -3
- package/package.json +1 -1
- package/render-utils/observer.tsx +2 -0
- package/testsite/browser.tsx +46 -0
- package/testsite/server.ts +41 -0
- package/testsite/serverInfoController.ts +19 -0
package/index.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ declare module "sliftutils/misc/https/certs" {
|
|
|
71
71
|
/// <reference types="node" />
|
|
72
72
|
import * as forge from "node-forge";
|
|
73
73
|
export declare const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
|
|
74
|
-
export declare const identityStorageKey = "
|
|
74
|
+
export declare const identityStorageKey = "machineCA_14";
|
|
75
75
|
export type IdentityStorageType = {
|
|
76
76
|
domain: string;
|
|
77
77
|
certB64: string;
|
|
@@ -90,9 +90,9 @@ declare module "sliftutils/misc/https/certs" {
|
|
|
90
90
|
keyPair: {
|
|
91
91
|
publicKey: forge.Ed25519PublicKey;
|
|
92
92
|
privateKey: forge.Ed25519PrivateKey;
|
|
93
|
-
}
|
|
93
|
+
};
|
|
94
94
|
}): X509KeyPair;
|
|
95
|
-
export declare function privateKeyToPem(
|
|
95
|
+
export declare function privateKeyToPem(key: forge.Ed25519PrivateKey): string;
|
|
96
96
|
export declare function parseCert(PEMorDER: string | Buffer): forge.pki.Certificate;
|
|
97
97
|
export declare function getPublicIdentifier(PEMorDER: string | Buffer): Buffer;
|
|
98
98
|
export declare const sign: (keyPair: {
|
|
@@ -101,8 +101,10 @@ declare module "sliftutils/misc/https/certs" {
|
|
|
101
101
|
export declare function verify(cert: string, signature: string, data: unknown): void;
|
|
102
102
|
export declare function validateCACert(domain: string, cert: string | Buffer): void;
|
|
103
103
|
export declare function validateCertificate(domain: string, cert: Buffer | string, issuerCert: Buffer | string): void;
|
|
104
|
-
export declare function generateKeyPair():
|
|
105
|
-
|
|
104
|
+
export declare function generateKeyPair(): {
|
|
105
|
+
publicKey: forge.Ed25519PublicKey;
|
|
106
|
+
privateKey: forge.Ed25519PrivateKey;
|
|
107
|
+
};
|
|
106
108
|
export declare function generateTestCA(domain: string): X509KeyPair;
|
|
107
109
|
export declare function createCertFromCA(config: {
|
|
108
110
|
CAKeyPair: X509KeyPair;
|
|
@@ -129,11 +131,6 @@ declare module "sliftutils/misc/https/certs" {
|
|
|
129
131
|
publicKey: Buffer;
|
|
130
132
|
}): boolean;
|
|
131
133
|
export declare function getThreadKeyCert(domain: string): X509KeyPair;
|
|
132
|
-
export declare const createTestBrowserKeyCert: {
|
|
133
|
-
(): Promise<X509KeyPair>;
|
|
134
|
-
reset(): void;
|
|
135
|
-
set(newValue: Promise<X509KeyPair>): void;
|
|
136
|
-
};
|
|
137
134
|
export declare function getOwnNodeId(): string;
|
|
138
135
|
export declare function getOwnNodeIdAllowUndefined(): string;
|
|
139
136
|
|
|
@@ -153,6 +150,34 @@ declare module "sliftutils/misc/https/dns" {
|
|
|
153
150
|
export declare function setRecord(type: string, key: string, value: string, proxied?: "proxied"): Promise<void>;
|
|
154
151
|
/** Keeps existing records */
|
|
155
152
|
export declare function addRecord(type: string, key: string, value: string, proxied?: "proxied"): Promise<void>;
|
|
153
|
+
/** Provide Cloudflare credentials directly (an API token, or a path to a file containing one), instead of relying on ./cloudflare.json */
|
|
154
|
+
export declare function setCloudflareCredentials(config: {
|
|
155
|
+
key?: string;
|
|
156
|
+
path?: string;
|
|
157
|
+
}): void;
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
declare module "sliftutils/misc/https/hostServer" {
|
|
162
|
+
export type HostServerConfig = {
|
|
163
|
+
/** Full domain to host on (e.g. "testsite.example.com"). The HTTPS cert is created for this domain and *.domain, so using a subdomain never touches the root domain (beyond its _acme-challenge TXT record). */
|
|
164
|
+
domain: string;
|
|
165
|
+
port: number;
|
|
166
|
+
/** Cloudflare API token (or a path to a file containing one). If neither is given, ./cloudflare.json is used. */
|
|
167
|
+
cloudflareApiToken?: string;
|
|
168
|
+
cloudflareApiTokenPath?: string;
|
|
169
|
+
/** Creates an unproxied A record pointing domain at this machine (publicIp, or our detected external IP) */
|
|
170
|
+
setDNSRecord?: boolean;
|
|
171
|
+
publicIp?: string;
|
|
172
|
+
allowHostnames?: string[];
|
|
173
|
+
};
|
|
174
|
+
/** Hosts a SocketFunction server on a real domain, with an automatically created and renewed Let's Encrypt HTTPS certificate (cached in the home folder, shared between processes on the machine). Expose your controllers (and any RequireController setup) before calling this. Returns the mounted nodeId. */
|
|
175
|
+
export declare function hostServer(config: HostServerConfig): Promise<string>;
|
|
176
|
+
/** Returns the cached HTTPS cert for the domain, creating/renewing it first if it is past this process's renewal threshold. Reads the disk cache on every call, so a renewal done by a parallel process is picked up instead of renewing again. */
|
|
177
|
+
export declare function getFreshHTTPSCert(domain: string): Promise<{
|
|
178
|
+
key: string;
|
|
179
|
+
cert: string;
|
|
180
|
+
}>;
|
|
156
181
|
|
|
157
182
|
}
|
|
158
183
|
|
|
@@ -201,10 +226,14 @@ declare module "sliftutils/misc/https/node-forge-ed25519" {
|
|
|
201
226
|
declare module "node-forge" {
|
|
202
227
|
declare type Ed25519PublicKey = {
|
|
203
228
|
publicKeyBytes: Buffer;
|
|
204
|
-
|
|
229
|
+
keyType: string;
|
|
230
|
+
verify(message: string | Buffer, signature: string): boolean;
|
|
231
|
+
};
|
|
205
232
|
declare type Ed25519PrivateKey = {
|
|
206
233
|
privateKeyBytes: Buffer;
|
|
207
|
-
|
|
234
|
+
keyType: string;
|
|
235
|
+
sign(message: string | Buffer): string;
|
|
236
|
+
};
|
|
208
237
|
class ed25519 {
|
|
209
238
|
static generateKeyPair(): { publicKey: Ed25519PublicKey, privateKey: Ed25519PrivateKey };
|
|
210
239
|
static privateKeyToPem(key: Ed25519PrivateKey): string;
|
|
@@ -213,6 +242,7 @@ declare module "sliftutils/misc/https/node-forge-ed25519" {
|
|
|
213
242
|
static publicKeyFromPem(pem: string): Ed25519PublicKey;
|
|
214
243
|
}
|
|
215
244
|
}
|
|
245
|
+
|
|
216
246
|
}
|
|
217
247
|
|
|
218
248
|
declare module "sliftutils/misc/https/persistentLocalStorage" {
|
package/misc/https/certs.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/// <reference types="node" />
|
|
5
5
|
import * as forge from "node-forge";
|
|
6
6
|
export declare const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
|
|
7
|
-
export declare const identityStorageKey = "
|
|
7
|
+
export declare const identityStorageKey = "machineCA_14";
|
|
8
8
|
export type IdentityStorageType = {
|
|
9
9
|
domain: string;
|
|
10
10
|
certB64: string;
|
|
@@ -23,9 +23,9 @@ export declare function createX509(config: {
|
|
|
23
23
|
keyPair: {
|
|
24
24
|
publicKey: forge.Ed25519PublicKey;
|
|
25
25
|
privateKey: forge.Ed25519PrivateKey;
|
|
26
|
-
}
|
|
26
|
+
};
|
|
27
27
|
}): X509KeyPair;
|
|
28
|
-
export declare function privateKeyToPem(
|
|
28
|
+
export declare function privateKeyToPem(key: forge.Ed25519PrivateKey): string;
|
|
29
29
|
export declare function parseCert(PEMorDER: string | Buffer): forge.pki.Certificate;
|
|
30
30
|
export declare function getPublicIdentifier(PEMorDER: string | Buffer): Buffer;
|
|
31
31
|
export declare const sign: (keyPair: {
|
|
@@ -34,8 +34,10 @@ export declare const sign: (keyPair: {
|
|
|
34
34
|
export declare function verify(cert: string, signature: string, data: unknown): void;
|
|
35
35
|
export declare function validateCACert(domain: string, cert: string | Buffer): void;
|
|
36
36
|
export declare function validateCertificate(domain: string, cert: Buffer | string, issuerCert: Buffer | string): void;
|
|
37
|
-
export declare function generateKeyPair():
|
|
38
|
-
|
|
37
|
+
export declare function generateKeyPair(): {
|
|
38
|
+
publicKey: forge.Ed25519PublicKey;
|
|
39
|
+
privateKey: forge.Ed25519PrivateKey;
|
|
40
|
+
};
|
|
39
41
|
export declare function generateTestCA(domain: string): X509KeyPair;
|
|
40
42
|
export declare function createCertFromCA(config: {
|
|
41
43
|
CAKeyPair: X509KeyPair;
|
|
@@ -62,10 +64,5 @@ export declare function verifyMachineIdForPublicKey(config: {
|
|
|
62
64
|
publicKey: Buffer;
|
|
63
65
|
}): boolean;
|
|
64
66
|
export declare function getThreadKeyCert(domain: string): X509KeyPair;
|
|
65
|
-
export declare const createTestBrowserKeyCert: {
|
|
66
|
-
(): Promise<X509KeyPair>;
|
|
67
|
-
reset(): void;
|
|
68
|
-
set(newValue: Promise<X509KeyPair>): void;
|
|
69
|
-
};
|
|
70
67
|
export declare function getOwnNodeId(): string;
|
|
71
68
|
export declare function getOwnNodeIdAllowUndefined(): string;
|
package/misc/https/certs.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module.allowclient = true;
|
|
4
4
|
|
|
5
|
+
// NOTE: We can't use crypto.subtle (non-extractable CryptoKeys) for our keys, because subtle is asynchronous, and everything here (key generation, cert creation, signing) must be available synchronously. The only real benefit of subtle is that a cross-site scripting attack can't exfiltrate the key. Which, while nice, is of minor benefit, as the cross-site script can already do quite a bit of damage anyway with access. And if that happens, the first thing the user is probably going to do is reset all their credentials, which solves the case of the key being exfiltrated anyway (by telling the server to stop trusting all identities).
|
|
6
|
+
// NOTE: We are just not going to support HTTPS browser certs. Our code is purely for identification, and so it only supports ED25519.
|
|
7
|
+
|
|
5
8
|
// https://www.rfc-editor.org/rfc/rfc5280#page-42
|
|
6
9
|
|
|
7
10
|
import { setFlag } from "socket-function/require/compileFlags";
|
|
@@ -27,7 +30,7 @@ const timeInDay = 1000 * 60 * 60 * 24;
|
|
|
27
30
|
|
|
28
31
|
export const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
|
|
29
32
|
|
|
30
|
-
export const identityStorageKey = "
|
|
33
|
+
export const identityStorageKey = "machineCA_14";
|
|
31
34
|
export type IdentityStorageType = { domain: string; certB64: string; keyB64: string };
|
|
32
35
|
|
|
33
36
|
function getIdentityStore(domain: string) {
|
|
@@ -52,19 +55,16 @@ export function createX509(
|
|
|
52
55
|
keyPair: {
|
|
53
56
|
publicKey: forge.Ed25519PublicKey;
|
|
54
57
|
privateKey: forge.Ed25519PrivateKey;
|
|
55
|
-
}
|
|
58
|
+
};
|
|
56
59
|
}
|
|
57
60
|
): X509KeyPair {
|
|
58
61
|
return measureBlock(function createX509() {
|
|
59
62
|
let { domain, issuer, lifeSpan, keyPair } = config;
|
|
60
63
|
|
|
61
64
|
let certObj = forge.pki.createCertificate();
|
|
62
|
-
certObj.publicKey = keyPair.publicKey;
|
|
65
|
+
certObj.publicKey = keyPair.publicKey as unknown as forge.pki.PublicKey;
|
|
63
66
|
certObj.serialNumber = "01";
|
|
64
|
-
// Give it 5 minutes before now. If we give it too much time, it can look like the cert is really
|
|
65
|
-
// old, which will trigger various processes to try to get a fresher one (as if it lasts for
|
|
66
|
-
// 1 hour, but we set notBefore to 1 month ago, it looks 1 month old, and so almost expired,
|
|
67
|
-
// when it isn't...)
|
|
67
|
+
// Give it 5 minutes before now. If we give it too much time, it can look like the cert is really old, which will trigger various processes to try to get a fresher one (as if it lasts for 1 hour, but we set notBefore to 1 month ago, it looks 1 month old, and so almost expired, when it isn't...)
|
|
68
68
|
certObj.validity.notBefore = new Date(Date.now() - 1000 * 60 * 5);
|
|
69
69
|
certObj.validity.notAfter = new Date(Date.now() + lifeSpan);
|
|
70
70
|
|
|
@@ -94,15 +94,10 @@ export function createX509(
|
|
|
94
94
|
{ type: 2, value: domain },
|
|
95
95
|
{ type: 2, value: "*." + domain },
|
|
96
96
|
{ type: 2, value: localHostDomain },
|
|
97
|
-
// NOTE: No longer allow 127.0.0.1, to make this more secure. We might enable this
|
|
98
|
-
// behavior behind a flag, for development.
|
|
99
|
-
//{ type: 7, ip: "127.0.0.1" }
|
|
97
|
+
// NOTE: No longer allow 127.0.0.1 ({ type: 7, ip: "127.0.0.1" }), to make this more secure. We might enable this behavior behind a flag, for development.
|
|
100
98
|
]
|
|
101
99
|
},
|
|
102
|
-
// NOTE: nameConstraints require our forked node-forge:
|
|
103
|
-
// "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
104
|
-
// Chrome ignores them (https://bugs.chromium.org/p/chromium/issues/detail?id=1072083),
|
|
105
|
-
// but our own validation (validateCACert/validateCertificate) enforces them.
|
|
100
|
+
// NOTE: nameConstraints require our forked node-forge ("node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6"). Chrome ignores them (https://bugs.chromium.org/p/chromium/issues/detail?id=1072083), but our own validation (validateCACert/validateCertificate) enforces them.
|
|
106
101
|
{
|
|
107
102
|
name: "nameConstraints",
|
|
108
103
|
permittedSubtrees: [
|
|
@@ -116,9 +111,9 @@ export function createX509(
|
|
|
116
111
|
|
|
117
112
|
measureBlock(function sign() {
|
|
118
113
|
if (issuer === "self") {
|
|
119
|
-
certObj.sign(keyPair.privateKey as any
|
|
114
|
+
certObj.sign(keyPair.privateKey as any);
|
|
120
115
|
} else {
|
|
121
|
-
certObj.sign(privateKeyFromPem(issuer.key.toString()) as any
|
|
116
|
+
certObj.sign(forge.ed25519.privateKeyFromPem(issuer.key.toString()) as any);
|
|
122
117
|
}
|
|
123
118
|
});
|
|
124
119
|
|
|
@@ -131,75 +126,37 @@ export function createX509(
|
|
|
131
126
|
});
|
|
132
127
|
});
|
|
133
128
|
}
|
|
134
|
-
export function privateKeyToPem(
|
|
135
|
-
|
|
136
|
-
return forge.ed25519.privateKeyToPem(buffer);
|
|
137
|
-
}
|
|
138
|
-
return forge.pki.privateKeyToPem(buffer);
|
|
139
|
-
}
|
|
140
|
-
function privateKeyFromPem(pem: string) {
|
|
141
|
-
// We want to guess the type correctly, as caught exceptions make debugging annoying
|
|
142
|
-
if (pem.length < 200) {
|
|
143
|
-
try {
|
|
144
|
-
return forge.ed25519.privateKeyFromPem(pem);
|
|
145
|
-
} catch { }
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
return forge.pki.privateKeyFromPem(pem);
|
|
149
|
-
} catch {
|
|
150
|
-
return forge.ed25519.privateKeyFromPem(pem);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
function publicKeyFromCert(cert: string) {
|
|
154
|
-
return parseCert(cert).publicKey;
|
|
129
|
+
export function privateKeyToPem(key: forge.Ed25519PrivateKey) {
|
|
130
|
+
return forge.ed25519.privateKeyToPem(key);
|
|
155
131
|
}
|
|
156
132
|
export function parseCert(PEMorDER: string | Buffer) {
|
|
157
133
|
return forge.pki.certificateFromPem(normalizeCertToPEM(PEMorDER));
|
|
158
134
|
}
|
|
159
135
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if ("publicKeyBytes" in publicKey) {
|
|
165
|
-
return Buffer.from(publicKey.publicKeyBytes as any);
|
|
136
|
+
function getED25519PublicKey(certParsed: forge.pki.Certificate): forge.Ed25519PublicKey {
|
|
137
|
+
let publicKey = certParsed.publicKey;
|
|
138
|
+
if (!("publicKeyBytes" in publicKey)) {
|
|
139
|
+
throw new Error(`Only ED25519 certificates are supported, the certificate public key is not ED25519 (subject: ${certParsed.subject.getField("CN")?.value})`);
|
|
166
140
|
}
|
|
167
|
-
return
|
|
141
|
+
return publicKey as unknown as forge.Ed25519PublicKey;
|
|
168
142
|
}
|
|
169
143
|
|
|
170
|
-
|
|
171
|
-
|
|
144
|
+
// Gets a unique value to represent the public key
|
|
145
|
+
export function getPublicIdentifier(PEMorDER: string | Buffer): Buffer {
|
|
146
|
+
return Buffer.from(getED25519PublicKey(parseCert(PEMorDER)).publicKeyBytes);
|
|
172
147
|
}
|
|
173
148
|
|
|
174
|
-
// EQUIVALENT TO: `crypto.createSign("SHA256").update(JSON.stringify(payload)).sign(keyCert.key, "binary")`
|
|
175
149
|
export const sign = measureWrap(function sign(keyPair: { key: string | Buffer }, data: unknown): string {
|
|
176
150
|
let dataStr = JSON.stringify(data);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return privateKey.sign(dataStr);
|
|
180
|
-
} else {
|
|
181
|
-
let privateKey = forge.pki.privateKeyFromPem(keyPair.key.toString());
|
|
182
|
-
const md = forge.md.sha256.create();
|
|
183
|
-
md.update(dataStr);
|
|
184
|
-
return privateKey.sign(md);
|
|
185
|
-
}
|
|
151
|
+
let privateKey = forge.ed25519.privateKeyFromPem(keyPair.key.toString());
|
|
152
|
+
return privateKey.sign(dataStr);
|
|
186
153
|
});
|
|
187
154
|
|
|
188
155
|
export function verify(cert: string, signature: string, data: unknown) {
|
|
189
156
|
let certObj = parseCert(cert);
|
|
190
|
-
let publicKey = certObj
|
|
157
|
+
let publicKey = getED25519PublicKey(certObj);
|
|
191
158
|
let dataStr = JSON.stringify(data);
|
|
192
|
-
|
|
193
|
-
if ("publicKeyBytes" in publicKey) {
|
|
194
|
-
// ed25519 verifies the raw message (it hashes internally)
|
|
195
|
-
verified = (publicKey as unknown as { verify(message: string, signature: string): boolean }).verify(dataStr, signature);
|
|
196
|
-
} else {
|
|
197
|
-
// RSA verifies against the digest, matching the digest sign() creates
|
|
198
|
-
const md = forge.md.sha256.create();
|
|
199
|
-
md.update(dataStr);
|
|
200
|
-
verified = (publicKey as forge.pki.rsa.PublicKey).verify(md.digest().bytes(), signature);
|
|
201
|
-
}
|
|
202
|
-
if (!verified) {
|
|
159
|
+
if (!publicKey.verify(dataStr, signature)) {
|
|
203
160
|
throw new Error(`Signature verification failed. Signature: ${JSON.stringify(signature)} | Data: ${ellipsize(dataStr, 1024)}`);
|
|
204
161
|
}
|
|
205
162
|
}
|
|
@@ -212,8 +169,7 @@ function normalizeCertToPEM(PEMorDER: string | Buffer): string {
|
|
|
212
169
|
return "-----BEGIN CERTIFICATE-----\n" + PEMorDER + "\n-----END CERTIFICATE-----";
|
|
213
170
|
}
|
|
214
171
|
|
|
215
|
-
// Base32 (RFC 4648, lowercase, unpadded), as domain names are case-insensitive,
|
|
216
|
-
// which rules out base64/hex-with-case
|
|
172
|
+
// Base32 (RFC 4648, lowercase, unpadded), as domain names are case-insensitive, which rules out base64/hex-with-case
|
|
217
173
|
const base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
218
174
|
function encodeBase32(bytes: Buffer): string {
|
|
219
175
|
let result = "";
|
|
@@ -233,14 +189,12 @@ function encodeBase32(bytes: Buffer): string {
|
|
|
233
189
|
return result;
|
|
234
190
|
}
|
|
235
191
|
|
|
236
|
-
function getDomainPartFromPublicKey(publicKey: { publicKeyBytes: Buffer } |
|
|
192
|
+
function getDomainPartFromPublicKey(publicKey: { publicKeyBytes: Buffer } | Buffer) {
|
|
237
193
|
let bytes: Buffer;
|
|
238
194
|
if ("publicKeyBytes" in publicKey) {
|
|
239
195
|
bytes = publicKey.publicKeyBytes;
|
|
240
|
-
} else if (publicKey instanceof Buffer) {
|
|
241
|
-
bytes = publicKey;
|
|
242
196
|
} else {
|
|
243
|
-
bytes =
|
|
197
|
+
bytes = publicKey;
|
|
244
198
|
}
|
|
245
199
|
return "b" + encodeBase32(Buffer.from(sha265.sha256.array(Buffer.from(bytes)))).slice(0, 20);
|
|
246
200
|
}
|
|
@@ -255,9 +209,7 @@ export function validateCACert(domain: string, cert: string | Buffer) {
|
|
|
255
209
|
|
|
256
210
|
let rootDomainParsed = [domainParts.shift(), domainParts.shift()].reverse().join(".");
|
|
257
211
|
if (rootDomainParsed !== domain) {
|
|
258
|
-
// This is important, as our trust store contains more then just OUR certificates,
|
|
259
|
-
// so if we allow any domains then real domains can impersonate anyone! It has to
|
|
260
|
-
// be one OUR domain to be trusted!
|
|
212
|
+
// This is important, as our trust store contains more then just OUR certificates, so if we allow any domains then real domains can impersonate anyone! It has to be OUR domain to be trusted!
|
|
261
213
|
throw new Error(`Certificate root domain should be ${domain}, but is ${rootDomainParsed}`);
|
|
262
214
|
}
|
|
263
215
|
// TODO: Maybe just skip if it isn't a hash string?
|
|
@@ -266,7 +218,7 @@ export function validateCACert(domain: string, cert: string | Buffer) {
|
|
|
266
218
|
}
|
|
267
219
|
|
|
268
220
|
let certExpectedPublicKeyPart = (domainParts.shift() || "").split("-").slice(-1)[0];
|
|
269
|
-
let certActualPublicKeyPart = getDomainPartFromPublicKey(certParsed
|
|
221
|
+
let certActualPublicKeyPart = getDomainPartFromPublicKey(getED25519PublicKey(certParsed));
|
|
270
222
|
if (certExpectedPublicKeyPart !== certActualPublicKeyPart) {
|
|
271
223
|
throw new Error(`Certificate public key in the url is ${certExpectedPublicKeyPart}, but in the cert is ${certActualPublicKeyPart}`);
|
|
272
224
|
}
|
|
@@ -281,8 +233,7 @@ export function validateCACert(domain: string, cert: string | Buffer) {
|
|
|
281
233
|
throw new Error(`Certificate must have nameConstraints.permittedSubtrees`);
|
|
282
234
|
}
|
|
283
235
|
let subtreeValues = subtrees.map((x: any) => x.value);
|
|
284
|
-
// Ignore localhostDomain, as it can always safely be allowed (the same machine
|
|
285
|
-
// is always allowed).
|
|
236
|
+
// Ignore localhostDomain, as it can always safely be allowed (the same machine is always allowed).
|
|
286
237
|
subtreeValues = subtreeValues.filter((x: string) => x !== localhostDomain);
|
|
287
238
|
if (subtreeValues.length !== 1 || subtreeValues[0] !== subject) {
|
|
288
239
|
throw new Error(`Certificate must have a single constrained domain (had ${JSON.stringify(subtreeValues)})`);
|
|
@@ -312,14 +263,14 @@ export function validateCertificate(domain: string, cert: Buffer | string, issue
|
|
|
312
263
|
let issuerCertParsed = parseCert(issuerCert);
|
|
313
264
|
|
|
314
265
|
let issuerExpectedPublicKeyPart = domainParts.shift() || "";
|
|
315
|
-
let issuerActualPublicKeyPart = getDomainPartFromPublicKey(issuerCertParsed
|
|
266
|
+
let issuerActualPublicKeyPart = getDomainPartFromPublicKey(getED25519PublicKey(issuerCertParsed));
|
|
316
267
|
if (issuerExpectedPublicKeyPart !== issuerActualPublicKeyPart) {
|
|
317
268
|
throw new Error(`Issuer public key in the url is ${issuerExpectedPublicKeyPart}, but in the cert is ${issuerActualPublicKeyPart}`);
|
|
318
269
|
}
|
|
319
270
|
|
|
320
271
|
// Take the last part
|
|
321
272
|
let certExpectedPublicKeyPart = domainParts.shift() || "";
|
|
322
|
-
let certActualPublicKeyPart = getDomainPartFromPublicKey(certParsed
|
|
273
|
+
let certActualPublicKeyPart = getDomainPartFromPublicKey(getED25519PublicKey(certParsed));
|
|
323
274
|
if (certExpectedPublicKeyPart !== certActualPublicKeyPart) {
|
|
324
275
|
throw new Error(`Certificate public key in the url is ${certExpectedPublicKeyPart}, but in the cert is ${certActualPublicKeyPart}`);
|
|
325
276
|
}
|
|
@@ -334,8 +285,7 @@ export function validateCertificate(domain: string, cert: Buffer | string, issue
|
|
|
334
285
|
throw new Error(`CA must have nameConstraints.permittedSubtrees`);
|
|
335
286
|
}
|
|
336
287
|
let subtreeValues = subtrees.map((x: any) => x.value);
|
|
337
|
-
// Ignore localhostDomain, as it can always safely be allowed (the same machine
|
|
338
|
-
// is always allowed).
|
|
288
|
+
// Ignore localhostDomain, as it can always safely be allowed (the same machine is always allowed).
|
|
339
289
|
subtreeValues = subtreeValues.filter((x: string) => x !== localhostDomain);
|
|
340
290
|
if (subtreeValues.length !== 1) {
|
|
341
291
|
throw new Error(`CA must have a single constrained domain (had ${JSON.stringify(subtreeValues)})`);
|
|
@@ -367,16 +317,7 @@ function validateAltNames(certParsed: forge.pki.Certificate, subject: string) {
|
|
|
367
317
|
!(
|
|
368
318
|
x === subject
|
|
369
319
|
|| x.endsWith("." + subject)
|
|
370
|
-
//
|
|
371
|
-
// and even to generate a real cert, so, we should jsut do that, and keep it secure.
|
|
372
|
-
// If we need this for development we can put it behind a flag, so non-development
|
|
373
|
-
// instances are still secure.
|
|
374
|
-
// // Also allow 127.0.0.1, for local testing, for now?
|
|
375
|
-
// // - This might be insecure if these are trusted by the browser,
|
|
376
|
-
// // as then anyone that stores any cookies in this ip can have
|
|
377
|
-
// // the cookies stolen. But... if this is just cross-server...
|
|
378
|
-
// // I don't see how this could cause a security vulnerability.
|
|
379
|
-
// || x === Buffer.from([127, 0, 0, 1]).toString()
|
|
320
|
+
// NOTE: We don't allow 127.0.0.1 (|| x === Buffer.from([127, 0, 0, 1]).toString()), because it is so easy to publish a 127.0.0.1 A record, and even to generate a real cert, so we should just do that, and keep it secure. If we need this for development we can put it behind a flag, so non-development instances are still secure.
|
|
380
321
|
)
|
|
381
322
|
)
|
|
382
323
|
) {
|
|
@@ -385,29 +326,10 @@ function validateAltNames(certParsed: forge.pki.Certificate, subject: string) {
|
|
|
385
326
|
}
|
|
386
327
|
|
|
387
328
|
|
|
388
|
-
// NOTE: We can't use crypto.subtle (non-extractable CryptoKeys) for our keys, because subtle
|
|
389
|
-
// is asynchronous, and everything here (key generation, cert creation, signing) must be
|
|
390
|
-
// available synchronously. The only real benefit of subtle is that a cross-site scripting
|
|
391
|
-
// attack can't exfiltrate the key. Which, while nice, is of minor benefit, as the cross-site
|
|
392
|
-
// script can already do quite a bit of damage anyway with access. And if that happens, the
|
|
393
|
-
// first thing the user is probably going to do is reset all their credentials, which solves
|
|
394
|
-
// the case of the key being exfiltrated anyway (by telling the server to stop trusting
|
|
395
|
-
// all identities).
|
|
396
329
|
export function generateKeyPair() {
|
|
397
330
|
return measureBlock(function generateKeyPair() {
|
|
398
|
-
// NOTE: We use ED25519 because it can
|
|
399
|
-
|
|
400
|
-
// NOTE: ED25519 doens't have great support in browsers, but we shouldn't need self signed certificates
|
|
401
|
-
// in the browser anyway.
|
|
402
|
-
// - https://security.stackexchange.com/a/236943/282367
|
|
403
|
-
//let keyPair = forge.ed25519.generateKeyPair();
|
|
404
|
-
let keyPair = forge.pki.rsa.generateKeyPair();
|
|
405
|
-
return keyPair;
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
export function generateRSAKeyPair() {
|
|
409
|
-
return measureBlock(function generateKeyPair() {
|
|
410
|
-
return forge.pki.rsa.generateKeyPair();
|
|
331
|
+
// NOTE: We use ED25519 because it can generate keys about 10X faster than RSA (which is still slow, ~6ms on my machine, so we DEFINITELY don't want it to be 10X slower!) - https://security.stackexchange.com/a/236943/282367
|
|
332
|
+
return forge.ed25519.generateKeyPair();
|
|
411
333
|
});
|
|
412
334
|
}
|
|
413
335
|
|
|
@@ -415,10 +337,6 @@ export function generateTestCA(domain: string) {
|
|
|
415
337
|
const keyPair = generateKeyPair();
|
|
416
338
|
let caPublicKeyPart = getDomainPartFromPublicKey(keyPair.publicKey);
|
|
417
339
|
let fullDomain = `${caPublicKeyPart}.${domain}`;
|
|
418
|
-
if (!isNode()) {
|
|
419
|
-
fullDomain = `${caPublicKeyPart}.${domain}`;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
340
|
return createX509({ domain: fullDomain, issuer: "self", keyPair, lifeSpan: timeInDay * 365 * 20 });
|
|
423
341
|
}
|
|
424
342
|
|
|
@@ -449,12 +367,7 @@ let identityCA = cache((domain: string) => lazy((): X509KeyPair => {
|
|
|
449
367
|
return result;
|
|
450
368
|
}));
|
|
451
369
|
|
|
452
|
-
// IMPORTANT! We do not embed any debug info in this domain. If we did, it would be useful,
|
|
453
|
-
// but... potentally a security vulnerability, as if the debug info (such as a prefix)
|
|
454
|
-
// is used to identify what a certificate is for, it would be easy for an attack to
|
|
455
|
-
// forge this (as the debug info won't be secured). So it is much better to keep
|
|
456
|
-
// the certificate opaque, and then require any metadata to be actually vetted
|
|
457
|
-
// (and hopefully stored in a UI, showing IP, time, etc).
|
|
370
|
+
// IMPORTANT! We do not embed any debug info in this domain. If we did, it would be useful, but... potentally a security vulnerability, as if the debug info (such as a prefix) is used to identify what a certificate is for, it would be easy for an attack to forge this (as the debug info won't be secured). So it is much better to keep the certificate opaque, and then require any metadata to be actually vetted (and hopefully stored in a UI, showing IP, time, etc).
|
|
458
371
|
export function createCertFromCA(config: {
|
|
459
372
|
CAKeyPair: X509KeyPair;
|
|
460
373
|
}): X509KeyPair {
|
|
@@ -488,11 +401,7 @@ export function decodeNodeId(nodeId: string, domain: string, allowMissingThreadI
|
|
|
488
401
|
return undefined;
|
|
489
402
|
}
|
|
490
403
|
let parts = locationObj.address.split(".");
|
|
491
|
-
// NOTE: We have to only allow localhost domains on our own domain, as the underlying domain
|
|
492
|
-
// gets stripped when we're looking at the machineId. So if we allowed localhost domains on
|
|
493
|
-
// other domains, a server could trick us into connecting to it, and then once the connection
|
|
494
|
-
// is established, it could talk back and we would think it has a localhost machineId, which
|
|
495
|
-
// is implicitly trusted, which would then give it access to everything.
|
|
404
|
+
// NOTE: We have to only allow localhost domains on our own domain, as the underlying domain gets stripped when we're looking at the machineId. So if we allowed localhost domains on other domains, a server could trick us into connecting to it, and then once the connection is established, it could talk back and we would think it has a localhost machineId, which is implicitly trusted, which would then give it access to everything.
|
|
496
405
|
if (locationObj.address === `127-0-0-1.${domain}` && nodeId.includes(":")) {
|
|
497
406
|
return {
|
|
498
407
|
threadId: "",
|
|
@@ -542,9 +451,7 @@ export async function setIdentityCARaw(domain: string, json: string) {
|
|
|
542
451
|
resetAllNodeCallFactories();
|
|
543
452
|
}
|
|
544
453
|
|
|
545
|
-
// NOTE: The identity CA is available synchronously (storage is fs/localStorage, both
|
|
546
|
-
// synchronous), so this only exists for backwards compatibility with startup code
|
|
547
|
-
// that awaits it.
|
|
454
|
+
// NOTE: The identity CA is available synchronously (storage is fs/localStorage, both synchronous), so this only exists for backwards compatibility with startup code that awaits it.
|
|
548
455
|
export async function loadIdentityCA(domain: string) {
|
|
549
456
|
identityCA(domain)();
|
|
550
457
|
}
|
|
@@ -552,8 +459,7 @@ export function getIdentityCA(domain: string): X509KeyPair {
|
|
|
552
459
|
return identityCA(domain)();
|
|
553
460
|
}
|
|
554
461
|
|
|
555
|
-
// TODO: Replace this with a database, so it is easy for us to trust CAs
|
|
556
|
-
// cross machine, and even have multiple users, etc, etc.
|
|
462
|
+
// TODO: Replace this with a database, so it is easy for us to trust CAs cross machine, and even have multiple users, etc, etc.
|
|
557
463
|
export function getIdentityCAPromise(domain: string): X509KeyPair {
|
|
558
464
|
return identityCA(domain)();
|
|
559
465
|
}
|
|
@@ -578,11 +484,8 @@ export function verifyMachineIdForPublicKey(config: {
|
|
|
578
484
|
}
|
|
579
485
|
|
|
580
486
|
// NOTE: We don't have a cache per CA, as... the CA should be set first
|
|
581
|
-
//
|
|
582
|
-
// TODO: Regenerate certificates after enough time (as thread certs should be relatively short lived,
|
|
583
|
-
// so it is plausible for them to expire)
|
|
584
|
-
// - We will also need to provide a callback so that users of the cert can update the cert they
|
|
585
|
-
// are using as well.
|
|
487
|
+
// TODO: Maybe throw if they try to change the CA after they generate any certificates?
|
|
488
|
+
// TODO: Regenerate certificates after enough time (as thread certs should be relatively short lived, so it is plausible for them to expire). We will also need to provide a callback so that users of the cert can update the cert they are using as well.
|
|
586
489
|
export function getThreadKeyCert(domain: string) {
|
|
587
490
|
return getThreadKeyCertBase(domain)();
|
|
588
491
|
}
|
|
@@ -591,11 +494,6 @@ const getThreadKeyCertBase = cache((domain: string) => lazy(() => {
|
|
|
591
494
|
return createCertFromCA({ CAKeyPair: ca });
|
|
592
495
|
}));
|
|
593
496
|
|
|
594
|
-
export const createTestBrowserKeyCert = lazy(async () => {
|
|
595
|
-
let keyPair = generateRSAKeyPair();
|
|
596
|
-
return await createX509({ domain: "test", issuer: "self", keyPair, lifeSpan: timeInDay * 365 * 20 });
|
|
597
|
-
});
|
|
598
|
-
|
|
599
497
|
export function getOwnNodeId(): string {
|
|
600
498
|
let nodeId = SocketFunction.mountedNodeId;
|
|
601
499
|
if (!nodeId) {
|
package/misc/https/dns.d.ts
CHANGED
|
@@ -11,3 +11,8 @@ export declare function deleteRecord(type: string, key: string, value: string):
|
|
|
11
11
|
export declare function setRecord(type: string, key: string, value: string, proxied?: "proxied"): Promise<void>;
|
|
12
12
|
/** Keeps existing records */
|
|
13
13
|
export declare function addRecord(type: string, key: string, value: string, proxied?: "proxied"): Promise<void>;
|
|
14
|
+
/** Provide Cloudflare credentials directly (an API token, or a path to a file containing one), instead of relying on ./cloudflare.json */
|
|
15
|
+
export declare function setCloudflareCredentials(config: {
|
|
16
|
+
key?: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
}): void;
|
package/misc/https/dns.ts
CHANGED
|
@@ -114,16 +114,32 @@ export async function addRecord(type: string, key: string, value: string, proxie
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
let credsOverride: { key: string } | undefined;
|
|
118
|
+
/** Provide Cloudflare credentials directly (an API token, or a path to a file containing one), instead of relying on ./cloudflare.json */
|
|
119
|
+
export function setCloudflareCredentials(config: { key?: string; path?: string }) {
|
|
120
|
+
let key = config.key;
|
|
121
|
+
if (!key && config.path) {
|
|
122
|
+
key = fs.readFileSync(config.path, "utf8").trim();
|
|
123
|
+
}
|
|
124
|
+
if (!key) {
|
|
125
|
+
throw new Error(`Must provide either key or path in setCloudflareCredentials, received ${JSON.stringify(Object.keys(config))}`);
|
|
126
|
+
}
|
|
127
|
+
credsOverride = { key };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const getCloudflareCredsFromFile = lazy(async (): Promise<{ key: string; }> => {
|
|
118
131
|
const path = "cloudflare.json";
|
|
119
132
|
if (!fs.existsSync(path)) {
|
|
120
|
-
throw new Error(`Must add cloudflare.json file to root of project.`);
|
|
133
|
+
throw new Error(`Must add cloudflare.json file to root of project (or call setCloudflareCredentials).`);
|
|
121
134
|
}
|
|
122
135
|
let creds = JSON.parse(fs.readFileSync(path, "utf8")) as { key: string; };
|
|
123
136
|
return {
|
|
124
137
|
key: creds.key,
|
|
125
138
|
};
|
|
126
139
|
});
|
|
140
|
+
async function getCloudflareCreds() {
|
|
141
|
+
return credsOverride || await getCloudflareCredsFromFile();
|
|
142
|
+
}
|
|
127
143
|
|
|
128
144
|
async function cloudflareGETCall<T>(path: string, params?: { [key: string]: string }): Promise<T> {
|
|
129
145
|
let url = new URL(`https://api.cloudflare.com/client/v4` + path);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type HostServerConfig = {
|
|
2
|
+
/** Full domain to host on (e.g. "testsite.example.com"). The HTTPS cert is created for this domain and *.domain, so using a subdomain never touches the root domain (beyond its _acme-challenge TXT record). */
|
|
3
|
+
domain: string;
|
|
4
|
+
port: number;
|
|
5
|
+
/** Cloudflare API token (or a path to a file containing one). If neither is given, ./cloudflare.json is used. */
|
|
6
|
+
cloudflareApiToken?: string;
|
|
7
|
+
cloudflareApiTokenPath?: string;
|
|
8
|
+
/** Creates an unproxied A record pointing domain at this machine (publicIp, or our detected external IP) */
|
|
9
|
+
setDNSRecord?: boolean;
|
|
10
|
+
publicIp?: string;
|
|
11
|
+
allowHostnames?: string[];
|
|
12
|
+
};
|
|
13
|
+
/** Hosts a SocketFunction server on a real domain, with an automatically created and renewed Let's Encrypt HTTPS certificate (cached in the home folder, shared between processes on the machine). Expose your controllers (and any RequireController setup) before calling this. Returns the mounted nodeId. */
|
|
14
|
+
export declare function hostServer(config: HostServerConfig): Promise<string>;
|
|
15
|
+
/** Returns the cached HTTPS cert for the domain, creating/renewing it first if it is past this process's renewal threshold. Reads the disk cache on every call, so a renewal done by a parallel process is picked up instead of renewing again. */
|
|
16
|
+
export declare function getFreshHTTPSCert(domain: string): Promise<{
|
|
17
|
+
key: string;
|
|
18
|
+
cert: string;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
4
|
+
import { timeInMinute } from "socket-function/src/misc";
|
|
5
|
+
import { delay } from "socket-function/src/batching";
|
|
6
|
+
import { getExternalIP } from "socket-function/src/networking";
|
|
7
|
+
import { magenta } from "socket-function/src/formatting/logColors";
|
|
8
|
+
import { getThreadKeyCert, loadIdentityCA } from "./certs";
|
|
9
|
+
import { generateCert, getAccountKey, parseCert } from "./httpsCerts";
|
|
10
|
+
import { setCloudflareCredentials, setRecord } from "./dns";
|
|
11
|
+
|
|
12
|
+
// Renew somewhere randomly between 40% and 60% of the way through the cert lifetime. The random threshold staggers parallel processes on the same machine, so usually one process renews early, and the others see the renewed cert on disk (we re-read the disk before every renewal check) and never renew themselves.
|
|
13
|
+
const RENEW_THRESHOLD_MIN = 0.4;
|
|
14
|
+
const RENEW_THRESHOLD_MAX = 0.6;
|
|
15
|
+
const CERT_CHECK_INTERVAL = timeInMinute * 15;
|
|
16
|
+
|
|
17
|
+
// One threshold per process, so a process is consistently early or late relative to its siblings
|
|
18
|
+
const renewThreshold = RENEW_THRESHOLD_MIN + Math.random() * (RENEW_THRESHOLD_MAX - RENEW_THRESHOLD_MIN);
|
|
19
|
+
|
|
20
|
+
export type HostServerConfig = {
|
|
21
|
+
/** Full domain to host on (e.g. "testsite.example.com"). The HTTPS cert is created for this domain and *.domain, so using a subdomain never touches the root domain (beyond its _acme-challenge TXT record). */
|
|
22
|
+
domain: string;
|
|
23
|
+
port: number;
|
|
24
|
+
/** Cloudflare API token (or a path to a file containing one). If neither is given, ./cloudflare.json is used. */
|
|
25
|
+
cloudflareApiToken?: string;
|
|
26
|
+
cloudflareApiTokenPath?: string;
|
|
27
|
+
/** Creates an unproxied A record pointing domain at this machine (publicIp, or our detected external IP) */
|
|
28
|
+
setDNSRecord?: boolean;
|
|
29
|
+
publicIp?: string;
|
|
30
|
+
allowHostnames?: string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Hosts a SocketFunction server on a real domain, with an automatically created and renewed Let's Encrypt HTTPS certificate (cached in the home folder, shared between processes on the machine). Expose your controllers (and any RequireController setup) before calling this. Returns the mounted nodeId. */
|
|
34
|
+
export async function hostServer(config: HostServerConfig): Promise<string> {
|
|
35
|
+
let { domain, port } = config;
|
|
36
|
+
if (config.cloudflareApiToken || config.cloudflareApiTokenPath) {
|
|
37
|
+
setCloudflareCredentials({ key: config.cloudflareApiToken, path: config.cloudflareApiTokenPath });
|
|
38
|
+
}
|
|
39
|
+
// The identity CA always lives on the root domain (nodeIds are threadHash.machineHash.root.tld)
|
|
40
|
+
let rootDomain = domain.split(".").slice(-2).join(".");
|
|
41
|
+
await loadIdentityCA(rootDomain);
|
|
42
|
+
|
|
43
|
+
if (config.setDNSRecord) {
|
|
44
|
+
let ip = config.publicIp || await getExternalIP();
|
|
45
|
+
await setRecord("A", domain, ip);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let keyCert = await getFreshHTTPSCert(domain);
|
|
49
|
+
let certListeners: ((value: { key: string; cert: string }) => void)[] = [];
|
|
50
|
+
void runCertRenewalLoop(domain, newKeyCert => {
|
|
51
|
+
keyCert = newKeyCert;
|
|
52
|
+
for (let listener of certListeners) {
|
|
53
|
+
listener(newKeyCert);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let nodeId = await SocketFunction.mount({
|
|
58
|
+
public: true,
|
|
59
|
+
port,
|
|
60
|
+
...getThreadKeyCert(rootDomain),
|
|
61
|
+
SNICerts: {
|
|
62
|
+
[domain]: callback => {
|
|
63
|
+
callback(keyCert);
|
|
64
|
+
certListeners.push(callback);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
allowHostnames: config.allowHostnames,
|
|
68
|
+
});
|
|
69
|
+
console.log(magenta(`Hosting https://${domain}:${port} (nodeId ${nodeId})`));
|
|
70
|
+
return nodeId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getCertDiskPath(domain: string) {
|
|
74
|
+
return os.homedir() + `/httpscert_${domain}.json`;
|
|
75
|
+
}
|
|
76
|
+
function readCertFromDisk(domain: string): { key: string; cert: string } | undefined {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(fs.readFileSync(getCertDiskPath(domain), "utf8")) as { key: string; cert: string };
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function getRenewTime(certPem: string, threshold: number) {
|
|
84
|
+
let certObj = parseCert(certPem);
|
|
85
|
+
let start = +new Date(certObj.validity.notBefore);
|
|
86
|
+
let end = +new Date(certObj.validity.notAfter);
|
|
87
|
+
return start + (end - start) * threshold;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Returns the cached HTTPS cert for the domain, creating/renewing it first if it is past this process's renewal threshold. Reads the disk cache on every call, so a renewal done by a parallel process is picked up instead of renewing again. */
|
|
91
|
+
export async function getFreshHTTPSCert(domain: string): Promise<{ key: string; cert: string }> {
|
|
92
|
+
let keyCert = readCertFromDisk(domain);
|
|
93
|
+
if (keyCert && getRenewTime(keyCert.cert, renewThreshold) > Date.now()) {
|
|
94
|
+
return keyCert;
|
|
95
|
+
}
|
|
96
|
+
if (keyCert) {
|
|
97
|
+
console.log(magenta(`HTTPS cert for ${domain} is past ${(renewThreshold * 100).toFixed(0)}% of its lifetime, renewing`));
|
|
98
|
+
} else {
|
|
99
|
+
console.log(magenta(`No HTTPS cert on disk for ${domain}, creating one`));
|
|
100
|
+
}
|
|
101
|
+
let accountKey = await getAccountKey(domain);
|
|
102
|
+
try {
|
|
103
|
+
keyCert = await generateCert({ accountKey, domain, altDomains: ["*." + domain] });
|
|
104
|
+
} catch (e) {
|
|
105
|
+
if (String(e).includes("authorization must be pending")) {
|
|
106
|
+
// Another process is mid-renewal. Wait for it to finish, then re-check the disk.
|
|
107
|
+
console.log(`Certificate authorization is pending in another process, waiting 2 minutes`);
|
|
108
|
+
await delay(timeInMinute * 2);
|
|
109
|
+
return await getFreshHTTPSCert(domain);
|
|
110
|
+
}
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
fs.writeFileSync(getCertDiskPath(domain), JSON.stringify({ key: keyCert.key, cert: keyCert.cert }));
|
|
114
|
+
return { key: keyCert.key, cert: keyCert.cert };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function runCertRenewalLoop(domain: string, onNewCert: (keyCert: { key: string; cert: string }) => void) {
|
|
118
|
+
let lastCert = readCertFromDisk(domain)?.cert;
|
|
119
|
+
while (true) {
|
|
120
|
+
await delay(CERT_CHECK_INTERVAL);
|
|
121
|
+
try {
|
|
122
|
+
let keyCert = await getFreshHTTPSCert(domain);
|
|
123
|
+
if (keyCert.cert !== lastCert) {
|
|
124
|
+
lastCert = keyCert.cert;
|
|
125
|
+
console.log(magenta(`HTTPS cert for ${domain} updated, applying to running server`));
|
|
126
|
+
onNewCert(keyCert);
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error(`Failed to check/renew HTTPS cert for ${domain}`, e);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
declare module "node-forge" {
|
|
4
4
|
declare type Ed25519PublicKey = {
|
|
5
5
|
publicKeyBytes: Buffer;
|
|
6
|
-
|
|
6
|
+
keyType: string;
|
|
7
|
+
verify(message: string | Buffer, signature: string): boolean;
|
|
8
|
+
};
|
|
7
9
|
declare type Ed25519PrivateKey = {
|
|
8
10
|
privateKeyBytes: Buffer;
|
|
9
|
-
|
|
11
|
+
keyType: string;
|
|
12
|
+
sign(message: string | Buffer): string;
|
|
13
|
+
};
|
|
10
14
|
class ed25519 {
|
|
11
15
|
static generateKeyPair(): { publicKey: Ed25519PublicKey, privateKey: Ed25519PrivateKey };
|
|
12
16
|
static privateKeyToPem(key: Ed25519PrivateKey): string;
|
|
@@ -14,4 +18,4 @@ declare module "node-forge" {
|
|
|
14
18
|
static publicKeyToPem(key: Ed25519PublicKey): string;
|
|
15
19
|
static publicKeyFromPem(pem: string): Ed25519PublicKey;
|
|
16
20
|
}
|
|
17
|
-
}
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module.allowclient = true;
|
|
2
|
+
|
|
3
|
+
import preact from "preact";
|
|
4
|
+
import { observable } from "mobx";
|
|
5
|
+
import { observer } from "../render-utils/observer";
|
|
6
|
+
import { isNode } from "socket-function/src/misc";
|
|
7
|
+
import { css } from "typesafecss";
|
|
8
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
9
|
+
import { ServerInfoController } from "./serverInfoController";
|
|
10
|
+
|
|
11
|
+
@observer
|
|
12
|
+
class TestPage extends preact.Component {
|
|
13
|
+
synced = observable({
|
|
14
|
+
serverOSName: "",
|
|
15
|
+
error: "",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
componentDidMount() {
|
|
19
|
+
void (async () => {
|
|
20
|
+
try {
|
|
21
|
+
let nodeId = SocketFunction.connect({ address: location.hostname, port: +location.port || 443 });
|
|
22
|
+
this.synced.serverOSName = await ServerInfoController.nodes[nodeId].getServerOSName();
|
|
23
|
+
} catch (e) {
|
|
24
|
+
this.synced.error = String(e);
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render() {
|
|
30
|
+
return <div className={css.vbox(8).pad2(16)}>
|
|
31
|
+
<div>sliftutils hostServer test page</div>
|
|
32
|
+
<div>
|
|
33
|
+
{this.synced.error && `Error: ${this.synced.error}`
|
|
34
|
+
|| this.synced.serverOSName && `Server OS (via SocketFunction): ${this.synced.serverOSName}`
|
|
35
|
+
|| "Asking the server for its OS name..."}
|
|
36
|
+
</div>
|
|
37
|
+
</div>;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
if (isNode()) return;
|
|
43
|
+
preact.render(<TestPage />, document.body);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
process.env.NODE_ENV = "production";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
5
|
+
import { RequireController } from "socket-function/require/RequireController";
|
|
6
|
+
import { hostServer } from "../misc/https/hostServer";
|
|
7
|
+
import { ServerInfoController } from "./serverInfoController";
|
|
8
|
+
// Import browser code, so it is allowed to be required by the client
|
|
9
|
+
import "./browser";
|
|
10
|
+
|
|
11
|
+
const DOMAIN = "testsite.vidgridweb.com";
|
|
12
|
+
const PORT = 4443;
|
|
13
|
+
|
|
14
|
+
process.on("unhandledRejection", (error) => {
|
|
15
|
+
console.error("Unhandled promise rejection:", error);
|
|
16
|
+
});
|
|
17
|
+
process.on("uncaughtException", (error) => {
|
|
18
|
+
console.error("Uncaught exception:", error);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
RequireController.allowAllNodeModules();
|
|
23
|
+
SocketFunction.expose(RequireController);
|
|
24
|
+
SocketFunction.expose(ServerInfoController);
|
|
25
|
+
SocketFunction.setDefaultHTTPCall(RequireController, "requireHTML", {
|
|
26
|
+
requireCalls: ["./testsite/browser.tsx"],
|
|
27
|
+
});
|
|
28
|
+
RequireController.addStaticRoot(path.resolve("."));
|
|
29
|
+
|
|
30
|
+
await hostServer({
|
|
31
|
+
domain: DOMAIN,
|
|
32
|
+
port: PORT,
|
|
33
|
+
cloudflareApiTokenPath: os.homedir() + "/vidgridweb.com.key",
|
|
34
|
+
setDNSRecord: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main().catch(e => {
|
|
39
|
+
console.error(e);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module.allowclient = true;
|
|
2
|
+
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
5
|
+
|
|
6
|
+
class ServerInfoControllerBase {
|
|
7
|
+
// Innocuous server-only information, to prove the browser is really talking to the server
|
|
8
|
+
async getServerOSName(): Promise<string> {
|
|
9
|
+
return `${os.type()} ${os.release()} (${os.platform()})`;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ServerInfoController = SocketFunction.register(
|
|
14
|
+
"ServerInfoController-7f3b1c2a",
|
|
15
|
+
new ServerInfoControllerBase(),
|
|
16
|
+
() => ({
|
|
17
|
+
getServerOSName: {},
|
|
18
|
+
})
|
|
19
|
+
);
|