@aiti-ai/ddns-mocks 0.0.0-stage → 0.1.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/dist/aliyun.d.ts +36 -0
- package/dist/aliyun.js +103 -0
- package/dist/assets/mock-ca.key +28 -0
- package/dist/assets/mock-ca.pem +19 -0
- package/dist/assets/mock-server.key +28 -0
- package/dist/assets/mock-server.pem +21 -0
- package/dist/cloudflare.d.ts +38 -0
- package/dist/cloudflare.js +105 -0
- package/dist/custom.d.ts +24 -0
- package/dist/custom.js +22 -0
- package/dist/dnspod.d.ts +33 -0
- package/dist/dnspod.js +104 -0
- package/dist/duckdns.d.ts +24 -0
- package/dist/duckdns.js +41 -0
- package/dist/dyndns.d.ts +26 -0
- package/dist/dyndns.js +30 -0
- package/dist/freedns.d.ts +23 -0
- package/dist/freedns.js +28 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +83 -0
- package/dist/loose.d.ts +58 -0
- package/dist/loose.js +1017 -0
- package/dist/rest.d.ts +41 -0
- package/dist/rest.js +279 -0
- package/dist/route53.d.ts +32 -0
- package/dist/route53.js +64 -0
- package/dist/tls.d.ts +42 -0
- package/dist/tls.js +140 -0
- package/package.json +26 -4
- package/README.md +0 -3
package/dist/aliyun.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** aliyun mock — in-memory alidns.aliyuncs.com RPC endpoint. Verifies the
|
|
2
|
+
* HMAC-SHA1 signature (when a secret is given) exactly like the real API,
|
|
3
|
+
* so signature bugs surface in tests. */
|
|
4
|
+
export interface AliyunMockOptions {
|
|
5
|
+
accessKeyId?: string;
|
|
6
|
+
/** Fixed listen port (default: kernel-assigned free port). */
|
|
7
|
+
port?: number;
|
|
8
|
+
accessKeySecret?: string;
|
|
9
|
+
records?: Array<{
|
|
10
|
+
rr: string;
|
|
11
|
+
type: string;
|
|
12
|
+
value: string;
|
|
13
|
+
ttl?: number;
|
|
14
|
+
}>;
|
|
15
|
+
zone: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AliyunMockState {
|
|
18
|
+
records: Array<{
|
|
19
|
+
recordId: string;
|
|
20
|
+
rr: string;
|
|
21
|
+
type: string;
|
|
22
|
+
value: string;
|
|
23
|
+
ttl: number;
|
|
24
|
+
}>;
|
|
25
|
+
requests: Array<{
|
|
26
|
+
action: string;
|
|
27
|
+
params: URLSearchParams;
|
|
28
|
+
signatureValid: boolean | null;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export interface AliyunMockHandle {
|
|
32
|
+
url: string;
|
|
33
|
+
state: AliyunMockState;
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export declare function startAliyunMock(opts: AliyunMockOptions): Promise<AliyunMockHandle>;
|
package/dist/aliyun.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** aliyun mock — in-memory alidns.aliyuncs.com RPC endpoint. Verifies the
|
|
2
|
+
* HMAC-SHA1 signature (when a secret is given) exactly like the real API,
|
|
3
|
+
* so signature bugs surface in tests. */
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { randomUUID, createHmac } from "node:crypto";
|
|
6
|
+
// ── aliyun 签名链(内联自 ddns core,mock 钉死真实 wire 行为)──
|
|
7
|
+
function aliyunPercent(s) {
|
|
8
|
+
return encodeURIComponent(s).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
9
|
+
}
|
|
10
|
+
function aliyunCanonicalQuery(params) {
|
|
11
|
+
return Object.entries(params)
|
|
12
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
13
|
+
.map(([k, v]) => `${aliyunPercent(k)}=${aliyunPercent(v)}`)
|
|
14
|
+
.join("&");
|
|
15
|
+
}
|
|
16
|
+
function aliyunStringToSign(params) {
|
|
17
|
+
return `GET&%2F&${aliyunPercent(aliyunCanonicalQuery(params))}`;
|
|
18
|
+
}
|
|
19
|
+
function aliyunSign(accessKeySecret, params) {
|
|
20
|
+
return createHmac("sha1", `${accessKeySecret}&`).update(aliyunStringToSign(params)).digest("base64");
|
|
21
|
+
}
|
|
22
|
+
export async function startAliyunMock(opts) {
|
|
23
|
+
const keyId = opts.accessKeyId ?? "AKID";
|
|
24
|
+
const keySecret = opts.accessKeySecret;
|
|
25
|
+
const state = {
|
|
26
|
+
records: (opts.records ?? []).map((r) => ({ recordId: randomUUID(), ttl: r.ttl ?? 600, ...r })),
|
|
27
|
+
requests: [],
|
|
28
|
+
};
|
|
29
|
+
const json = (res, status, body) => {
|
|
30
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
31
|
+
res.end(JSON.stringify(body));
|
|
32
|
+
};
|
|
33
|
+
const fail = (res, code, message) => json(res, 400, { Code: code, Message: message, RequestId: randomUUID() });
|
|
34
|
+
const server = createServer((req, res) => {
|
|
35
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
36
|
+
const params = new URLSearchParams(url.search);
|
|
37
|
+
const action = params.get("Action") ?? "";
|
|
38
|
+
const signature = params.get("Signature") ?? "";
|
|
39
|
+
// signature verification (mirrors the real gateway): recompute over the
|
|
40
|
+
// canonical query of every param EXCEPT Signature
|
|
41
|
+
let signatureValid = null;
|
|
42
|
+
if (keySecret && signature) {
|
|
43
|
+
const rest = Object.fromEntries([...params.entries()].filter(([k]) => k !== "Signature"));
|
|
44
|
+
signatureValid = aliyunSign(keySecret, rest) === signature;
|
|
45
|
+
if (!signatureValid)
|
|
46
|
+
return fail(res, "SignatureDoesNotMatch", "Specified signature is not matched with our calculation.");
|
|
47
|
+
}
|
|
48
|
+
state.requests.push({ action, params, signatureValid });
|
|
49
|
+
switch (action) {
|
|
50
|
+
case "DescribeDomainRecords": {
|
|
51
|
+
const domain = params.get("DomainName") ?? opts.zone;
|
|
52
|
+
if (domain !== opts.zone)
|
|
53
|
+
return fail(res, "InvalidDomainName.NoExist", "The specified domain name does not exist.");
|
|
54
|
+
const records = state.records.map((r) => ({
|
|
55
|
+
RecordId: r.recordId,
|
|
56
|
+
RR: r.rr,
|
|
57
|
+
Type: r.type,
|
|
58
|
+
Value: r.value,
|
|
59
|
+
TTL: String(r.ttl),
|
|
60
|
+
}));
|
|
61
|
+
return json(res, 200, { RequestId: randomUUID(), TotalCount: records.length, DomainRecords: { Record: records } });
|
|
62
|
+
}
|
|
63
|
+
case "AddDomainRecord": {
|
|
64
|
+
const rec = {
|
|
65
|
+
recordId: randomUUID(),
|
|
66
|
+
rr: params.get("RR") ?? "@",
|
|
67
|
+
type: params.get("Type") ?? "A",
|
|
68
|
+
value: params.get("Value") ?? "",
|
|
69
|
+
ttl: Number(params.get("TTL") ?? 600),
|
|
70
|
+
};
|
|
71
|
+
state.records.push(rec);
|
|
72
|
+
return json(res, 200, { RequestId: randomUUID(), RecordId: rec.recordId });
|
|
73
|
+
}
|
|
74
|
+
case "UpdateDomainRecord": {
|
|
75
|
+
const rec = state.records.find((r) => r.recordId === params.get("RecordId"));
|
|
76
|
+
if (!rec)
|
|
77
|
+
return fail(res, "RecordNotExist", "The specified record does not exist.");
|
|
78
|
+
rec.rr = params.get("RR") ?? rec.rr;
|
|
79
|
+
rec.type = params.get("Type") ?? rec.type;
|
|
80
|
+
rec.value = params.get("Value") ?? rec.value;
|
|
81
|
+
rec.ttl = Number(params.get("TTL") ?? rec.ttl);
|
|
82
|
+
return json(res, 200, { RequestId: randomUUID(), RecordId: rec.recordId });
|
|
83
|
+
}
|
|
84
|
+
case "DeleteDomainRecord": {
|
|
85
|
+
const idx = state.records.findIndex((r) => r.recordId === params.get("RecordId"));
|
|
86
|
+
if (idx < 0)
|
|
87
|
+
return fail(res, "RecordNotExist", "The specified record does not exist.");
|
|
88
|
+
state.records.splice(idx, 1);
|
|
89
|
+
return json(res, 200, { RequestId: randomUUID(), RecordId: params.get("RecordId") });
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return fail(res, "InvalidAction", `Specified action is not supported: ${action}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
await new Promise((resolve) => server.listen(opts.port ?? 0, "127.0.0.1", resolve));
|
|
96
|
+
const addr = server.address();
|
|
97
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
98
|
+
return {
|
|
99
|
+
url: `http://127.0.0.1:${port}`,
|
|
100
|
+
state,
|
|
101
|
+
stop: () => new Promise((resolve) => server.close(() => resolve())),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-----BEGIN PRIVATE KEY-----
|
|
2
|
+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCZNofmGlwQLAVn
|
|
3
|
+
Z9jxTTz3o32KhoWXtUNqrsIr46Nu78zuCaNZr+9B0tMXGFpIqjmxcaovei+9fY9H
|
|
4
|
+
AUBg/3tbW3ku8efQTPkD/EatXTEFrBM07T8UBATLhLfaN2xvzAskKp27jTnpjsvV
|
|
5
|
+
G3/9trCaO2OlUONXtvoQEnyn4jyCLnG9CLPhxoinNdJBUZsuZvYcNIKF1A4IRcW1
|
|
6
|
+
5zVU5zduhZ7s+8yLs6uYEp5gE6jH6kmKL6Bo5ZKO4lTpbc6Xbl+/TUMPaqXP1ApN
|
|
7
|
+
UKzTDDRiMKqK4EiDyUl4uHSotHYx26c37gJAk4pbu0tSOQQXntKRR/3BknCOIURR
|
|
8
|
+
/fpjc8/NAgMBAAECggEACvd3RMYwPswX2u34/lSr/4HJU6gLWe+UksdNiLyzjxZF
|
|
9
|
+
EejIs3u/YXPIvkd09uyQUp4zM1DIqASbw9ZYGFlRKBnlE7InCKbp0Bo3qOMZxjEp
|
|
10
|
+
8sjkkfczrKmkS09aSKpPRdhzD+Cs+4XIG0nroGp6O5/yovumOjoTriDUoWO1NxAl
|
|
11
|
+
kWzPnQfGzfeve/EwmuqYxFyLjMXcHFm2J2swQvp7m1nPA1rwea0LV4YXLWXsCQNI
|
|
12
|
+
tg9OyxO5qyN8jtGGNFVcbwswU3eIsHIoOXTxyMPqvOsnrrLinT5jkb8NJXJrwEvy
|
|
13
|
+
vgdLXMC8GRfxejExNDT69/os9zBw/CVgR9Gx+HiGtQKBgQDUsmguhVS9iWODlxyY
|
|
14
|
+
iaaJuxYDRwSo4ed0zqQ3KfqVHYAACr4ivpnqH/N/pbroGQAUEnA5DSURn9EfqT/8
|
|
15
|
+
XyjP2xPEpHNAQCsIH+a8fOo+BM7Zj0b6S7Ed9Tob1NF7rREV6E0mCshBaYEbZqlN
|
|
16
|
+
dj7OThKurVxBPFRqeZ3nRxAhlwKBgQC4Z97zK3i7r7jMF85+PqKk3UE1xfbNiSeN
|
|
17
|
+
XNjP4YnFam4bdspAY0+CPkBDC+1ifpnIr5MLU/lvzjTAVu5oFXpmqse+jzXRuaKI
|
|
18
|
+
SIb1UYKjNINYfpi+I/T7GbKwHdsp9SwyI0JDu1PxXdeZEvsXM7qLASrjfxjerwuI
|
|
19
|
+
FbdZAmK+OwKBgHde7WN2hvmc2tHmhDEDyAiyjAco22+g4VLICpe/Iph/pu/fF5jO
|
|
20
|
+
WflxFyrDq6NJLKcXNg5+1xgOFsLrLTMJgREuxeht1xTvLndv2AJfJOPLtP+bDt24
|
|
21
|
+
j13d3E/0X+FGXzlpV6sLEBrOCJyFQG1t+I+2d3u7ZfRi1ImxWbKMSnxfAoGAPJGx
|
|
22
|
+
g0GmGE+xbSfspT7BgVFDF/x8AiqomSAD/tPfwr4giFjOObPk66yywtLqF83piSdS
|
|
23
|
+
LwhYB8c9QGfNStazK4GwtgCn9sj4f7cCzRUGycB329tJJDr/PnLEZGCKGg/zgkxe
|
|
24
|
+
eYzQF5lxNVAstQRCo2GwyqR2cL4iFYK8hDnqYP0CgYEAucMoDpX96KjWuxJU52NG
|
|
25
|
+
kfpMI1Jaq1ViyXfQ4J6Ag037ySB40X4nkjuEuNPGPWcIcL4cKaLI8hXQCkXxXuuO
|
|
26
|
+
9xrsqHztA1vCki5HRmhi/l4fGbP3tGxH6EsCiR4Hp5hODTlnEbVBo2z19OqHLQks
|
|
27
|
+
eeBTcR46lJU6yDbRDQfiqLw=
|
|
28
|
+
-----END PRIVATE KEY-----
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDHzCCAgegAwIBAgIUa0F8AroOLrX2jEHC3B66CFkSB30wDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwFzEVMBMGA1UEAwwMZGRucyBtb2NrIENBMB4XDTI2MDgzMDA3NTAwNFoXDTM2
|
|
4
|
+
MDgyNzA3NTAwNFowFzEVMBMGA1UEAwwMZGRucyBtb2NrIENBMIIBIjANBgkqhkiG
|
|
5
|
+
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmTaH5hpcECwFZ2fY8U0896N9ioaFl7VDaq7C
|
|
6
|
+
K+Ojbu/M7gmjWa/vQdLTFxhaSKo5sXGqL3ovvX2PRwFAYP97W1t5LvHn0Ez5A/xG
|
|
7
|
+
rV0xBawTNO0/FAQEy4S32jdsb8wLJCqdu4056Y7L1Rt//bawmjtjpVDjV7b6EBJ8
|
|
8
|
+
p+I8gi5xvQiz4caIpzXSQVGbLmb2HDSChdQOCEXFtec1VOc3boWe7PvMi7OrmBKe
|
|
9
|
+
YBOox+pJii+gaOWSjuJU6W3Ol25fv01DD2qlz9QKTVCs0ww0YjCqiuBIg8lJeLh0
|
|
10
|
+
qLR2MdunN+4CQJOKW7tLUjkEF57SkUf9wZJwjiFEUf36Y3PPzQIDAQABo2MwYTAd
|
|
11
|
+
BgNVHQ4EFgQUqhYXNe0bvTC3Pl7Lcw2dwJf0C54wHwYDVR0jBBgwFoAUqhYXNe0b
|
|
12
|
+
vTC3Pl7Lcw2dwJf0C54wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
|
|
13
|
+
DQYJKoZIhvcNAQELBQADggEBAFBHsTniggP0c3TbXattz518eXggQmCgSCpU3Rw9
|
|
14
|
+
BQvgjxAhO9fSmunutrjRRHo4etX/w0wCnsUXVDLberyBZKgOFfK7PIuYcNKxpKbU
|
|
15
|
+
6goiU914diRDIKdLmRl7bjwWoI65FWZZgLqvEbzByWsY9qLceChENDNRCdzakctU
|
|
16
|
+
qXi3tiYFPrQ2EJdZRU3vigQkxDS8uqrv2IAMmiM9UDAAqdYX92VOXnqzWWpkG2Ek
|
|
17
|
+
fg680qFmY5niT3Av7dBtvA93VZpfVmzFv/ls/M8oSkfqhffSpMUfjCFYqBv3XZsD
|
|
18
|
+
9e20JpqkOELlUGNok20fhCe59ZA8bdMyn9XkAiyKEoM4NLE=
|
|
19
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-----BEGIN PRIVATE KEY-----
|
|
2
|
+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCsVCMXqgAl3ro0
|
|
3
|
+
QEg8d3u9cdb/wxwDNm5rnOhdD9jd+FVdIRfEjfABbVbW7nDHJ0wA/Fvvs0fPKNoz
|
|
4
|
+
Yf8ts78EpAJhhIXscieUCFnuTTJvlW8R23HwHaZt+iF5/SH/eUHlg2Zmq3GGDq5U
|
|
5
|
+
0REUAYPGHPOzrfY/hLXd3Qk/SCNTfboU3Kg8AU1eb0SucmJIax7+IXh4wT8SGJdB
|
|
6
|
+
8cUhRq1frfNF0MZHeX4wcidRD+aW3I5pDNUKh7h4Ncmvc9boc4XMYzt9hIUB5ljS
|
|
7
|
+
yWfgrCSXE1iTARhhDb90czJq+HtwWw32BAkfAzkxDxtmiC8hI26m8YKsDauaObC2
|
|
8
|
+
qHpg3xgPAgMBAAECggEAN1YTKLOltYNmToX/zC7f9K7cB6fq1FtDrNmnQc7naCuo
|
|
9
|
+
ma2ZOLwNQCAQnRbW7xR+0ug2S2vJ2Ms4+fvzWYUZBw/1VcQczxa+/8mIuz1dvzgA
|
|
10
|
+
gP7zANQ8j58c5dOzeZpB30XI+Zn+67nGICU8MyzjWBgO53kqlC30ymgCYqgy88lp
|
|
11
|
+
9bxElMlXaZqecjcS62dBev/xyA7bGCz7tfMv9+/7LAR5VOHIdXajEoIIRbj8cLRD
|
|
12
|
+
xWkwSGW1nPiuLPEgHHTztJulMUM8gpGmR9h8Uw3nCDDldSzlynMX2nQtzx2TPzdC
|
|
13
|
+
FOTCV3pOBmUJbaAuJlDhGKtWH3GdLTCmUUQhw3KpeQKBgQDfMbTmR2uE9B8MyGxP
|
|
14
|
+
wwpHcC791UFaxH+IxPj+eBANvLZBa6wfBj7eMqCQlXRJFo98NqbveHp1AjGjMYeA
|
|
15
|
+
UaDg7TiCBVmRBEJOjaIoGQ29orh+w/cbI4XZhENrXbq1TAzzrQBC10Pslo+AJHNx
|
|
16
|
+
C+OYXwY+8auc2tj/ytTVXYJT9wKBgQDFqHniPWdnDLlO/sdUzoXB1+1+g1pJ83HA
|
|
17
|
+
+0qPboaAckIh4nl/erS+9gakR/3OhDpOrXgB9K7riddSWBl399RWyrnWqONlosGh
|
|
18
|
+
8Iz5b9kCuOtGPnadQ0MHuBILOBfF7Oah2MXjzRUula40nETO1wU/ME7iQUfUPN1h
|
|
19
|
+
ESq5p9kmqQKBgHRP6g7rfzutIJU/yToT0UKDUunS/Zj4t+GljIDIPs1535zeKLeO
|
|
20
|
+
QKr4xTYzvILuL94yePJo3aAmgYmjIRMc9vLgjMctVIy/JHLdWnj8/W0q5GyjYR4S
|
|
21
|
+
eAlv5NaJ5Off9wvxxyYOGoaru6Ky3Do4UOYx9wZMWPuIDPfYUuvSLD3nAoGBAK6O
|
|
22
|
+
Q9115tyN0xUdUcHmhR+UZsvEircmRD5ubgFPVX8X6KluDWh0YC0IrwLkBRQdmDSl
|
|
23
|
+
Y4cupE+kssCoxw04k5B2hjo5emDmpGQ0QPuEODz8O+MUBMKLhSXK/DxmNDB6L4+d
|
|
24
|
+
Fd5A3kT+sRMLMoUMRTjxm6iaiCm5+rPfakD9J3vJAoGAM1/JgP3RYr7nvk2BcbD+
|
|
25
|
+
O6/893QlsMcZvkikSWp6Qexum3qa8+MFMoPpQkWhfSkjIAOudsS0Y/jzG8vJXnNq
|
|
26
|
+
mVNg9CelAtIvse83lOe9taRDyAkzdcHXAorbIB1ZyoeobkDj5DMqXx/3JAIAyFeh
|
|
27
|
+
uaOVmtghx31mNCe5GUS0H1o=
|
|
28
|
+
-----END PRIVATE KEY-----
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDZzCCAk+gAwIBAgIUWoGE9BH89lxIwuU9xy6AZFY/YfUwDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwFzEVMBMGA1UEAwwMZGRucyBtb2NrIENBMB4XDTI2MDgzMDA3NTAwNVoXDTM2
|
|
4
|
+
MDgyNzA3NTAwNVowHTEbMBkGA1UEAwwSYXBpLmNsb3VkZmxhcmUuY29tMIIBIjAN
|
|
5
|
+
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArFQjF6oAJd66NEBIPHd7vXHW/8Mc
|
|
6
|
+
AzZua5zoXQ/Y3fhVXSEXxI3wAW1W1u5wxydMAPxb77NHzyjaM2H/LbO/BKQCYYSF
|
|
7
|
+
7HInlAhZ7k0yb5VvEdtx8B2mbfohef0h/3lB5YNmZqtxhg6uVNERFAGDxhzzs632
|
|
8
|
+
P4S13d0JP0gjU326FNyoPAFNXm9ErnJiSGse/iF4eME/EhiXQfHFIUatX63zRdDG
|
|
9
|
+
R3l+MHInUQ/mltyOaQzVCoe4eDXJr3PW6HOFzGM7fYSFAeZY0sln4KwklxNYkwEY
|
|
10
|
+
YQ2/dHMyavh7cFsN9gQJHwM5MQ8bZogvISNupvGCrA2rmjmwtqh6YN8YDwIDAQAB
|
|
11
|
+
o4GkMIGhMF8GA1UdEQRYMFaCEmFwaS5jbG91ZGZsYXJlLmNvbYIJZG5zYXBpLmNu
|
|
12
|
+
ghNhbGlkbnMuYWxpeXVuY3MuY29tgg93d3cuZHVja2Rucy5vcmeCCWxvY2FsaG9z
|
|
13
|
+
dIcEfwAAATAdBgNVHQ4EFgQUxCD4eRMWklwxHlxJGmb9GMFq4pcwHwYDVR0jBBgw
|
|
14
|
+
FoAUqhYXNe0bvTC3Pl7Lcw2dwJf0C54wDQYJKoZIhvcNAQELBQADggEBAIrb+5KS
|
|
15
|
+
oaUsImjHYNu2fMBbiLF+WgJSGqhWAmudsV+msis56F7r/H2c5KTZb6Sk5y80DDdX
|
|
16
|
+
4To/+e5TyJeX/jVnQ0A+lqmFdmbnibNCHPAVmuQKc2QFi0m0XBgGyGMRiR9TqzkB
|
|
17
|
+
ngv1hqm0ALDKgU1z8vcZl4vbWXtgrOllxWFcvVrzFgdZACNSds7twMuCX0A47hiy
|
|
18
|
+
8GvhRvV7gdrS4CAAoA62XvZZ17UCsYTKFO26LZ8fTasJTnFZfdg+/8QcAMABLzgi
|
|
19
|
+
slq9VZkvsAiEDOajS42wJ6jk4uMzDs4d0zn2tqWqvk+Wfcrz61JvG1Ve3lYK1EyX
|
|
20
|
+
8iYT42RsR236Ly0=
|
|
21
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** cloudflare mock — an in-memory implementation of the Cloudflare REST v4
|
|
2
|
+
* DNS API, so the real provider can be exercised end-to-end with no network
|
|
3
|
+
* (mirrors ddns-updater/lexicon tests, which stub the same endpoints). */
|
|
4
|
+
export interface CloudflareMockOptions {
|
|
5
|
+
/** Token the mock accepts in Authorization: Bearer. Default "test-token". */
|
|
6
|
+
token?: string;
|
|
7
|
+
/** Fixed listen port (default: kernel-assigned free port). */
|
|
8
|
+
port?: number;
|
|
9
|
+
/** Pre-seeded zones: name → records. */
|
|
10
|
+
zones?: Record<string, Array<{
|
|
11
|
+
type: string;
|
|
12
|
+
name: string;
|
|
13
|
+
content: string;
|
|
14
|
+
ttl?: number;
|
|
15
|
+
proxied?: boolean;
|
|
16
|
+
}>>;
|
|
17
|
+
}
|
|
18
|
+
export interface CloudflareMockState {
|
|
19
|
+
zones: Map<string, {
|
|
20
|
+
id: string;
|
|
21
|
+
records: Array<Record<string, unknown>>;
|
|
22
|
+
}>;
|
|
23
|
+
/** Every request the mock served (method, url, body). */
|
|
24
|
+
requests: Array<{
|
|
25
|
+
method: string;
|
|
26
|
+
url: string;
|
|
27
|
+
body?: unknown;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export interface CloudflareMockHandle {
|
|
31
|
+
/** Base URL to inject into the provider (cfg.baseUrl). */
|
|
32
|
+
url: string;
|
|
33
|
+
state: CloudflareMockState;
|
|
34
|
+
/** Wait until the mock is listening. */
|
|
35
|
+
ready: Promise<void>;
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export declare function startCloudflareMock(opts?: CloudflareMockOptions): Promise<CloudflareMockHandle>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** cloudflare mock — an in-memory implementation of the Cloudflare REST v4
|
|
2
|
+
* DNS API, so the real provider can be exercised end-to-end with no network
|
|
3
|
+
* (mirrors ddns-updater/lexicon tests, which stub the same endpoints). */
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
export async function startCloudflareMock(opts = {}) {
|
|
7
|
+
const token = opts.token ?? "test-token";
|
|
8
|
+
const state = { zones: new Map(), requests: [] };
|
|
9
|
+
for (const [name, records] of Object.entries(opts.zones ?? {})) {
|
|
10
|
+
state.zones.set(name, {
|
|
11
|
+
id: randomUUID(),
|
|
12
|
+
records: records.map((r) => ({ id: randomUUID(), ...r })),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const authOk = (req) => req.headers.authorization === `Bearer ${token}`;
|
|
16
|
+
const json = (res, status, body) => {
|
|
17
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
18
|
+
res.end(JSON.stringify(body));
|
|
19
|
+
};
|
|
20
|
+
const fail = (res, status, code, message) => json(res, status, { success: false, errors: [{ code, message }] });
|
|
21
|
+
const server = createServer((req, res) => {
|
|
22
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
23
|
+
// Real Cloudflare API paths are /client/v4/zones... — the mock serves at
|
|
24
|
+
// <baseUrl>/client/v4 (provider baseUrl = API root), so strip the prefix.
|
|
25
|
+
const pathname = url.pathname.replace(/^\/client\/v4/, "") || "/";
|
|
26
|
+
const parts = pathname.split("/").filter(Boolean); // zones, :id, dns_records, :rid
|
|
27
|
+
let body = "";
|
|
28
|
+
req.on("data", (c) => (body += c));
|
|
29
|
+
req.on("end", () => {
|
|
30
|
+
const parsed = body ? JSON.parse(body) : undefined;
|
|
31
|
+
state.requests.push({ method: req.method ?? "GET", url: url.pathname + url.search, body: parsed });
|
|
32
|
+
if (!authOk(req))
|
|
33
|
+
return fail(res, 403, 9109, "Invalid Token");
|
|
34
|
+
// GET /zones?name= — result_info is required by lexicon's
|
|
35
|
+
// authenticate() (it paginates on result_info.total_pages).
|
|
36
|
+
if (parts[0] === "zones" && parts.length === 1 && req.method === "GET") {
|
|
37
|
+
const name = url.searchParams.get("name");
|
|
38
|
+
const zones = [...state.zones.entries()]
|
|
39
|
+
.filter(([n]) => !name || n === name)
|
|
40
|
+
.map(([n, z]) => ({ id: z.id, name: n }));
|
|
41
|
+
return json(res, 200, {
|
|
42
|
+
success: true,
|
|
43
|
+
result: zones,
|
|
44
|
+
result_info: { page: 1, per_page: 100, count: zones.length, total_count: zones.length, total_pages: 1 },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// GET /zones/:id/dns_records[?type&name]
|
|
48
|
+
if (parts[0] === "zones" && parts[2] === "dns_records" && parts.length === 3 && req.method === "GET") {
|
|
49
|
+
const zone = [...state.zones.values()].find((z) => z.id === parts[1]);
|
|
50
|
+
if (!zone)
|
|
51
|
+
return fail(res, 404, 6003, "Zone not found");
|
|
52
|
+
let records = zone.records;
|
|
53
|
+
const type = url.searchParams.get("type");
|
|
54
|
+
const name = url.searchParams.get("name");
|
|
55
|
+
if (type)
|
|
56
|
+
records = records.filter((r) => r.type === type);
|
|
57
|
+
if (name)
|
|
58
|
+
records = records.filter((r) => r.name === name);
|
|
59
|
+
// lexicon 的 list_records 读取 result_info 分页(total_pages/page)
|
|
60
|
+
return json(res, 200, {
|
|
61
|
+
success: true,
|
|
62
|
+
result: records,
|
|
63
|
+
result_info: { page: 1, per_page: 100, count: records.length, total_count: records.length, total_pages: 1 },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// POST /zones/:id/dns_records
|
|
67
|
+
if (parts[0] === "zones" && parts[2] === "dns_records" && parts.length === 3 && req.method === "POST") {
|
|
68
|
+
const zone = [...state.zones.values()].find((z) => z.id === parts[1]);
|
|
69
|
+
if (!zone)
|
|
70
|
+
return fail(res, 404, 6003, "Zone not found");
|
|
71
|
+
const rec = { id: randomUUID(), ...parsed };
|
|
72
|
+
zone.records.push(rec);
|
|
73
|
+
return json(res, 200, { success: true, result: rec });
|
|
74
|
+
}
|
|
75
|
+
// PUT /zones/:id/dns_records/:rid
|
|
76
|
+
if (parts[0] === "zones" && parts[2] === "dns_records" && parts.length === 4 && req.method === "PUT") {
|
|
77
|
+
const zone = [...state.zones.values()].find((z) => z.id === parts[1]);
|
|
78
|
+
const rec = zone?.records.find((r) => r.id === parts[3]);
|
|
79
|
+
if (!zone || !rec)
|
|
80
|
+
return fail(res, 404, 81044, "Record not found");
|
|
81
|
+
Object.assign(rec, parsed);
|
|
82
|
+
return json(res, 200, { success: true, result: rec });
|
|
83
|
+
}
|
|
84
|
+
// DELETE /zones/:id/dns_records/:rid
|
|
85
|
+
if (parts[0] === "zones" && parts[2] === "dns_records" && parts.length === 4 && req.method === "DELETE") {
|
|
86
|
+
const zone = [...state.zones.values()].find((z) => z.id === parts[1]);
|
|
87
|
+
const idx = zone?.records.findIndex((r) => r.id === parts[3]) ?? -1;
|
|
88
|
+
if (!zone || idx < 0)
|
|
89
|
+
return fail(res, 404, 81044, "Record not found");
|
|
90
|
+
zone.records.splice(idx, 1);
|
|
91
|
+
return json(res, 200, { success: true, result: { id: parts[3] } });
|
|
92
|
+
}
|
|
93
|
+
return fail(res, 404, 7000, `Unknown endpoint ${url.pathname}`);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
await new Promise((resolve) => server.listen(opts.port ?? 0, "127.0.0.1", resolve));
|
|
97
|
+
const addr = server.address();
|
|
98
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
99
|
+
return {
|
|
100
|
+
url: `http://127.0.0.1:${port}/client/v4`,
|
|
101
|
+
state,
|
|
102
|
+
ready: Promise.resolve(),
|
|
103
|
+
stop: () => new Promise((resolve) => server.close(() => resolve())),
|
|
104
|
+
};
|
|
105
|
+
}
|
package/dist/custom.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** custom mock — a generic webhook endpoint for the custom provider (Go
|
|
2
|
+
* ddns-updater custom semantics): records every request's query params and
|
|
3
|
+
* returns a configurable response body so success_regex matching can be
|
|
4
|
+
* tested both ways. */
|
|
5
|
+
export interface CustomMockOptions {
|
|
6
|
+
/** Fixed listen port (default: kernel-assigned free port). */
|
|
7
|
+
port?: number;
|
|
8
|
+
/** Response body for every request (default "good"). */
|
|
9
|
+
responseText?: string;
|
|
10
|
+
/** HTTP status for every request (default 200). */
|
|
11
|
+
status?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface CustomMockState {
|
|
14
|
+
requests: Array<{
|
|
15
|
+
url: string;
|
|
16
|
+
query: URLSearchParams;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export interface CustomMockHandle {
|
|
20
|
+
url: string;
|
|
21
|
+
state: CustomMockState;
|
|
22
|
+
stop(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function startCustomMock(opts?: CustomMockOptions): Promise<CustomMockHandle>;
|
package/dist/custom.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** custom mock — a generic webhook endpoint for the custom provider (Go
|
|
2
|
+
* ddns-updater custom semantics): records every request's query params and
|
|
3
|
+
* returns a configurable response body so success_regex matching can be
|
|
4
|
+
* tested both ways. */
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
export async function startCustomMock(opts = {}) {
|
|
7
|
+
const state = { requests: [] };
|
|
8
|
+
const server = createServer((req, res) => {
|
|
9
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
10
|
+
state.requests.push({ url: url.pathname + url.search, query: url.searchParams });
|
|
11
|
+
res.writeHead(opts.status ?? 200, { "content-type": "text/plain" });
|
|
12
|
+
res.end(opts.responseText ?? "good");
|
|
13
|
+
});
|
|
14
|
+
await new Promise((resolve) => server.listen(opts.port ?? 0, "127.0.0.1", resolve));
|
|
15
|
+
const addr = server.address();
|
|
16
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
17
|
+
return {
|
|
18
|
+
url: `http://127.0.0.1:${port}/update`,
|
|
19
|
+
state,
|
|
20
|
+
stop: () => new Promise((resolve) => server.close(() => resolve())),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/dnspod.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** dnspod mock — in-memory dnsapi.cn (Record.List/Create/Modify/Remove),
|
|
2
|
+
* form-encoded POST like the real service. */
|
|
3
|
+
export interface DnspodMockOptions {
|
|
4
|
+
token?: string;
|
|
5
|
+
/** Fixed listen port (default: kernel-assigned free port). */
|
|
6
|
+
port?: number;
|
|
7
|
+
records?: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
value: string;
|
|
11
|
+
ttl?: number;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export interface DnspodMockState {
|
|
15
|
+
records: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
type: string;
|
|
19
|
+
value: string;
|
|
20
|
+
ttl: number;
|
|
21
|
+
line: string;
|
|
22
|
+
}>;
|
|
23
|
+
requests: Array<{
|
|
24
|
+
action: string;
|
|
25
|
+
params: URLSearchParams;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
export interface DnspodMockHandle {
|
|
29
|
+
url: string;
|
|
30
|
+
state: DnspodMockState;
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export declare function startDnspodMock(opts?: DnspodMockOptions): Promise<DnspodMockHandle>;
|
package/dist/dnspod.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** dnspod mock — in-memory dnsapi.cn (Record.List/Create/Modify/Remove),
|
|
2
|
+
* form-encoded POST like the real service. */
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
export async function startDnspodMock(opts = {}) {
|
|
6
|
+
const token = opts.token ?? "1,test-token";
|
|
7
|
+
const state = {
|
|
8
|
+
records: (opts.records ?? []).map((r) => ({ id: randomUUID(), line: "默认", ttl: r.ttl ?? 600, ...r })),
|
|
9
|
+
requests: [],
|
|
10
|
+
};
|
|
11
|
+
const json = (res, body) => {
|
|
12
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
13
|
+
res.end(JSON.stringify(body));
|
|
14
|
+
};
|
|
15
|
+
const ok = (res, data = {}) => json(res, { status: { code: "1", message: "Action completed successful" }, data });
|
|
16
|
+
// 真实 DNSPod 的 Record.* 响应字段在 TOP level({status, records/record/id})
|
|
17
|
+
// 而非 data 包裹 —— 用 okTop 模拟真实形状
|
|
18
|
+
const okTop = (res, body = {}) => json(res, { status: { code: "1", message: "Action completed successful" }, ...body });
|
|
19
|
+
const fail = (res, code, message) => json(res, { status: { code, message } });
|
|
20
|
+
const server = createServer((req, res) => {
|
|
21
|
+
let body = "";
|
|
22
|
+
req.on("data", (c) => (body += c));
|
|
23
|
+
req.on("end", () => {
|
|
24
|
+
const params = new URLSearchParams(body);
|
|
25
|
+
const action = (req.url ?? "").replace(/^\//, "").split("?")[0] ?? "";
|
|
26
|
+
state.requests.push({ action, params });
|
|
27
|
+
if (params.get("login_token") !== token)
|
|
28
|
+
return fail(res, "6", "Invalid Token");
|
|
29
|
+
switch (action) {
|
|
30
|
+
case "Domain.Info": {
|
|
31
|
+
// lexicon's dnspod authenticate() calls this first — domain.id is
|
|
32
|
+
// required, status.code must be "1".
|
|
33
|
+
const domain = params.get("domain");
|
|
34
|
+
const exists = state.records.some((r) => r.name.endsWith(`.${domain}`) || r.name === domain);
|
|
35
|
+
if (!exists)
|
|
36
|
+
return fail(res, "8", "Domain not found");
|
|
37
|
+
return okTop(res, { id: randomUUID(), name: domain });
|
|
38
|
+
}
|
|
39
|
+
case "Record.List": {
|
|
40
|
+
const domain = params.get("domain");
|
|
41
|
+
const sub = params.get("sub_domain");
|
|
42
|
+
const type = params.get("record_type");
|
|
43
|
+
let records = state.records;
|
|
44
|
+
if (domain)
|
|
45
|
+
records = records.filter((r) => r.name.endsWith(`.${domain}`) || r.name === domain);
|
|
46
|
+
if (sub && sub !== "@")
|
|
47
|
+
records = records.filter((r) => r.name.split(".")[0] === sub);
|
|
48
|
+
if (type)
|
|
49
|
+
records = records.filter((r) => r.type === type);
|
|
50
|
+
// real DNSPod returns the SHORT label as "name" (not the FQDN)
|
|
51
|
+
const shaped = records.map((r) => ({
|
|
52
|
+
...r,
|
|
53
|
+
name: r.name === domain ? "@" : r.name.replace(`.${domain}`, ""),
|
|
54
|
+
}));
|
|
55
|
+
return okTop(res, { records: shaped });
|
|
56
|
+
}
|
|
57
|
+
case "Record.Create": {
|
|
58
|
+
const domain = params.get("domain") ?? "";
|
|
59
|
+
const sub = params.get("sub_domain") ?? "";
|
|
60
|
+
const owner = sub === "@" ? "" : `${sub}.`;
|
|
61
|
+
const rec = {
|
|
62
|
+
id: randomUUID(),
|
|
63
|
+
name: `${owner}${domain}`,
|
|
64
|
+
type: params.get("record_type") ?? "A",
|
|
65
|
+
value: params.get("value") ?? "",
|
|
66
|
+
ttl: Number(params.get("ttl") ?? 600),
|
|
67
|
+
line: params.get("record_line") ?? "默认",
|
|
68
|
+
};
|
|
69
|
+
state.records.push(rec);
|
|
70
|
+
// 真实 DNSPod 返回 {record: {id, name, status, ...}} —— 模拟真实形状
|
|
71
|
+
return okTop(res, { record: { id: rec.id, name: rec.name, status: "enabled" } });
|
|
72
|
+
}
|
|
73
|
+
case "Record.Modify": {
|
|
74
|
+
const id = params.get("record_id");
|
|
75
|
+
const rec = state.records.find((r) => r.id === id);
|
|
76
|
+
if (!rec)
|
|
77
|
+
return fail(res, "8", "Record not found");
|
|
78
|
+
rec.type = params.get("record_type") ?? rec.type;
|
|
79
|
+
rec.value = params.get("value") ?? rec.value;
|
|
80
|
+
rec.ttl = Number(params.get("ttl") ?? rec.ttl);
|
|
81
|
+
return okTop(res, { id });
|
|
82
|
+
}
|
|
83
|
+
case "Record.Remove": {
|
|
84
|
+
const id = params.get("record_id");
|
|
85
|
+
const idx = state.records.findIndex((r) => r.id === id);
|
|
86
|
+
if (idx < 0)
|
|
87
|
+
return fail(res, "8", "Record not found");
|
|
88
|
+
state.records.splice(idx, 1);
|
|
89
|
+
return okTop(res, { id });
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return fail(res, "15", `Unknown action ${action}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
await new Promise((resolve) => server.listen(opts.port ?? 0, "127.0.0.1", resolve));
|
|
97
|
+
const addr = server.address();
|
|
98
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
99
|
+
return {
|
|
100
|
+
url: `http://127.0.0.1:${port}`,
|
|
101
|
+
state,
|
|
102
|
+
stop: () => new Promise((resolve) => server.close(() => resolve())),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** duckdns mock — in-memory /v3/update endpoint (GET form like the real
|
|
2
|
+
* service; "OK"/"KO" responses exactly as duckdns returns them, so Go
|
|
3
|
+
* ddns-updater and Python lexicon duckdns implementations both work). */
|
|
4
|
+
export interface DuckdnsMockOptions {
|
|
5
|
+
token?: string;
|
|
6
|
+
/** Fixed listen port (default: kernel-assigned free port). */
|
|
7
|
+
port?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DuckdnsMockState {
|
|
10
|
+
/** label → { ipv4?, ipv6? } as last pushed. */
|
|
11
|
+
ips: Map<string, {
|
|
12
|
+
ipv4?: string;
|
|
13
|
+
ipv6?: string;
|
|
14
|
+
}>;
|
|
15
|
+
requests: Array<{
|
|
16
|
+
url: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export interface DuckdnsMockHandle {
|
|
20
|
+
url: string;
|
|
21
|
+
state: DuckdnsMockState;
|
|
22
|
+
stop(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function startDuckdnsMock(opts?: DuckdnsMockOptions): Promise<DuckdnsMockHandle>;
|
package/dist/duckdns.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** duckdns mock — in-memory /v3/update endpoint (GET form like the real
|
|
2
|
+
* service; "OK"/"KO" responses exactly as duckdns returns them, so Go
|
|
3
|
+
* ddns-updater and Python lexicon duckdns implementations both work). */
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
export async function startDuckdnsMock(opts = {}) {
|
|
6
|
+
const token = opts.token ?? "test-token";
|
|
7
|
+
const state = { ips: new Map(), requests: [] };
|
|
8
|
+
const server = createServer((req, res) => {
|
|
9
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
10
|
+
state.requests.push({ url: url.pathname + url.search });
|
|
11
|
+
// The real duckdns service keys everything on the token — Go ddns-updater
|
|
12
|
+
// hits /update, lexicon hits /, our client hits /v3/update. Answer every
|
|
13
|
+
// path identically so all three implementations work against the mock.
|
|
14
|
+
const domains = url.searchParams.get("domains") ?? "";
|
|
15
|
+
const tok = url.searchParams.get("token") ?? "";
|
|
16
|
+
const ip = url.searchParams.get("ip");
|
|
17
|
+
const ipv6 = url.searchParams.get("ipv6");
|
|
18
|
+
res.writeHead(200, { "content-type": "text/plain" });
|
|
19
|
+
if (tok !== token)
|
|
20
|
+
return res.end("KO");
|
|
21
|
+
if (!domains)
|
|
22
|
+
return res.end("KO");
|
|
23
|
+
for (const label of domains.split(",")) {
|
|
24
|
+
const entry = state.ips.get(label) ?? {};
|
|
25
|
+
if (ip !== null)
|
|
26
|
+
entry.ipv4 = ip;
|
|
27
|
+
if (ipv6 !== null)
|
|
28
|
+
entry.ipv6 = ipv6;
|
|
29
|
+
state.ips.set(label, entry);
|
|
30
|
+
}
|
|
31
|
+
res.end("OK");
|
|
32
|
+
});
|
|
33
|
+
await new Promise((resolve) => server.listen(opts.port ?? 0, "127.0.0.1", resolve));
|
|
34
|
+
const addr = server.address();
|
|
35
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
36
|
+
return {
|
|
37
|
+
url: `http://127.0.0.1:${port}`,
|
|
38
|
+
state,
|
|
39
|
+
stop: () => new Promise((resolve) => server.close(() => resolve())),
|
|
40
|
+
};
|
|
41
|
+
}
|