diff --git a/dnsconfig.js b/dnsconfig.js index 8545ad4caf..892fefc1da 100644 --- a/dnsconfig.js +++ b/dnsconfig.js @@ -133,6 +133,19 @@ for (var subdomain in domains) { if (data.records.URL) { records.push(A(subdomainName, IP("192.0.2.1"), CF_PROXY_ON)); } + + // Manage service records + + // Discord TXT verification + if (data.services.discord) { + if (Array.isArray(data.services.discord)) { + for (var txt in data.services.discord) { + records.push(TXT("_discord." + subdomainName, data.services.discord[txt])); + } + } else { + records.push(TXT("_discord." + subdomainName, data.services.discord)); + } + } } var reserved = require("./util/reserved.json"); diff --git a/domains/william.json b/domains/william.json index 76ebc8e5a1..798c154f40 100644 --- a/domains/william.json +++ b/domains/william.json @@ -11,5 +11,8 @@ "/github": "https://github.com/wdhdev" }, "redirect_paths": true + }, + "services": { + "discord": "dh=4e444050c7f0e742327d027ebdf50a39f1203acd" } } diff --git a/tests/domains.test.js b/tests/domains.test.js index 7b07e567b7..269ba3fc7c 100644 --- a/tests/domains.test.js +++ b/tests/domains.test.js @@ -105,3 +105,16 @@ t("Users are limited to one single character subdomain", (t) => { t.pass(); }); + +t("Disallow nested subdomains when parent has specific service records", (t) => { + files.forEach((file) => { + const subdomain = file.replace(/\.json$/, ""); + const data = getDomainData(subdomain); + + if(data?.services?.discord) { + t.false(files.includes(`_discord.${file}`), `${file}: Nested subdomain "_discord.${subdomain}" should not exist when services.discord is present`); + } + }); + + t.pass(); +}); diff --git a/tests/json.test.js b/tests/json.test.js index dfe388803b..9171442149 100644 --- a/tests/json.test.js +++ b/tests/json.test.js @@ -11,7 +11,8 @@ const requiredFields = { const optionalFields = { proxied: "boolean", - redirect_config: "object" + redirect_config: "object", + services: "object" }; const requiredOwnerFields = { @@ -27,12 +28,7 @@ const optionalRedirectConfigFields = { redirect_paths: "boolean" }; -const blockedFields = [ - "domain", - "internal", - "reserved", - "subdomain" -]; +const blockedFields = ["domain", "internal", "reserved", "subdomain"]; const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const hostnameRegex = /^(?=.{1,253}$)(?:(?:[_a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+[a-zA-Z]{2,63}$/; @@ -117,10 +113,7 @@ async function validateFileName(t, file) { !internalDomains.some((i) => subdomain.endsWith(`.${i}`)), `${file}: Subdomain name is registered internally` ); - t.true( - !reservedDomains.some((r) => subdomain.endsWith(`.${r}`)), - `${file}: Subdomain name is reserved` - ); + t.true(!reservedDomains.some((r) => subdomain.endsWith(`.${r}`)), `${file}: Subdomain name is reserved`); const rootSubdomain = subdomain.split(".").pop(); t.false(rootSubdomain.startsWith("_"), `${file}: Root subdomains should not start with an underscore`); @@ -157,6 +150,15 @@ async function processFile(file, t) { validateFields(t, data.redirect_config, optionalRedirectConfigFields, file, "redirect_config"); } + if (data.services) { + if (data.services.discord) { + t.true( + Array.isArray(data.services.discord) || typeof data.services.discord === "string", + `${file}: Discord service should be an array or string` + ); + } + } + for (const field of blockedFields) { t.true(!data.hasOwnProperty(field), `${file}: Disallowed field: ${field}`); } diff --git a/tests/pr.test.js b/tests/pr.test.js index 6afa1ff857..976db563f9 100644 --- a/tests/pr.test.js +++ b/tests/pr.test.js @@ -3,7 +3,10 @@ const fs = require("fs-extra"); const path = require("path"); const requiredEnvVars = ["PR_AUTHOR", "PR_AUTHOR_ID"]; -const trustedUsers = require("../util/trusted.json").map((u) => u.id.toString()); +const trustedUsers = require("../util/trusted.json"); + +const trusted = trustedUsers.map((u) => u.id.toString()); +const admins = trustedUsers.filter((u) => u.admin).map((u) => u.id.toString()); function getDomainData(subdomain) { try { @@ -28,17 +31,24 @@ t("Users can only update their own subdomains", (t) => { .filter((file) => file.name.startsWith("domains/")) .map((file) => path.basename(file.name)); - if ((!changedJSONFiles && !deletedFiles) || trustedUsers.includes(prAuthorId)) return t.pass(); + if (!changedJSONFiles && !deletedFiles) return t.pass(); if (process.env.PR_LABELS && process.env.PR_LABELS.includes("ci: bypass-owner-check")) return t.pass(); changedJSONFiles.forEach((file) => { const subdomain = file.replace(/\.json$/, ""); const data = getDomainData(subdomain); - t.true( - data.owner.username.toLowerCase() === prAuthor, - `${subdomain}: ${prAuthor} is not authorized to update ${subdomain}.is-a.dev` - ); + if (data.owner.username === "is-a-dev") { + t.true( + admins.includes(prAuthorId), + `${subdomain}: ${prAuthor} is not authorized to update ${subdomain}.is-a.dev` + ); + } else { + t.true( + data.owner.username.toLowerCase() === prAuthor || trusted.includes(prAuthorId), + `${file}: ${prAuthor} is not authorized to update ${subdomain}.is-a.dev` + ); + } }); deletedJSONFiles.forEach((file) => { @@ -52,10 +62,17 @@ t("Users can only update their own subdomains", (t) => { .join("\n") ); - t.true( - data.owner.username.toLowerCase() === prAuthor, - `${file}: ${prAuthor} is not authorized to delete ${subdomain}.is-a.dev` - ); + if (data.owner.username === "is-a-dev") { + t.true( + admins.includes(prAuthorId), + `${subdomain}: ${prAuthor} is not authorized to delete ${subdomain}.is-a.dev` + ); + } else { + t.true( + data.owner.username.toLowerCase() === prAuthor || trusted.includes(prAuthorId), + `${file}: ${prAuthor} is not authorized to delete ${subdomain}.is-a.dev` + ); + } }); } diff --git a/tests/records.test.js b/tests/records.test.js index cf2d561978..f6f4e7960b 100644 --- a/tests/records.test.js +++ b/tests/records.test.js @@ -268,7 +268,7 @@ function validateRecordValues(t, data, file) { } } -t("All files should have valid record types", (t) => { +t("All files should have valid records", (t) => { files.forEach((file) => { const data = getDomainData(file); const recordKeys = Object.keys(data.records); @@ -328,3 +328,26 @@ t("Root subdomains should have at least one usable record", (t) => { ); }); }); + +t("All files should have valid service records", (t) => { + files.forEach((file) => { + const data = getDomainData(file); + + if (data?.services?.discord) { + const discord = Array.isArray(data.services.discord) ? data.services.discord : [data.services.discord]; + + discord.forEach((value) => { + const token = value.split("=")[1]; + + t.true(value.startsWith("dh="), `${file}: Discord service record should start with "dh="`); + t.true(token.length === 40, `${file}: Discord service token should be 40 characters long`); + t.true( + isValidHexadecimal(token), + `${file}: Discord service token should be a valid hexadecimal string` + ); + }); + } + }); + + t.pass(); +}); diff --git a/util/raw-api.js b/util/raw-api.js index e414915025..2ac24acb78 100644 --- a/util/raw-api.js +++ b/util/raw-api.js @@ -110,6 +110,32 @@ fs.readdir(directoryPath, function (err, files) { v1.push(itemV1); v2.push(itemV2); + if (item.services) { + if (item.services.discord) { + const discord = Array.isArray(item.services.discord) + ? item.services.discord + : [item.services.discord]; + + v1.push({ + domain: `_discord.${item.domain}`, + subdomain: `_discord.${item.subdomain}`, + owner: item.owner, + record: { + TXT: discord + } + }); + + v2.push({ + domain: `_discord.${item.domain}`, + subdomain: `_discord.${item.subdomain}`, + owner: item.owner, + records: { + TXT: discord + } + }); + } + } + processedCount++; if (processedCount === files.length) { fs.writeFile("raw-api/index.json", JSON.stringify(v1), (err) => { diff --git a/util/trusted.json b/util/trusted.json index c1eab71cc8..d0847dbaa8 100644 --- a/util/trusted.json +++ b/util/trusted.json @@ -1,11 +1,31 @@ [ - { "username": "21Z", "id": 69188068 }, - { "username": "DEV-DIBSTER", "id": 76603072 }, - { "username": "iostpa", "id": 69630113 }, - { "username": "is-a-dev-bot", "id": 106927495 }, - { "username": "orangci", "id": 90866414 }, - { "username": "Stef-00012", "id": 81536172 }, - { "username": "SX-9", "id": 90962949 }, - { "username": "WDH-2", "id": 115670568 }, - { "username": "wdhdev", "id": 87287585 } + { + "username": "21Z", + "id": 69188068 + }, + { + "username": "DEV-DIBSTER", + "id": 76603072 + }, + { + "username": "iostpa", + "id": 69630113 + }, + { + "username": "orangci", + "id": 90866414 + }, + { + "username": "Stef-00012", + "id": 81536172 + }, + { + "username": "SX-9", + "id": 90962949 + }, + { + "username": "wdhdev", + "id": 87287585, + "admin": true + } ]