This commit is contained in:
andrewstech
2024-10-16 09:39:09 +00:00
parent c6a8883183
commit 92c44c1ad8
31 changed files with 203 additions and 2059 deletions
-6
View File
@@ -1,6 +0,0 @@
DOMAIN_USER=username
DOMAIN_API_KEY=apikey
DOMAIN_API_HOST=api-example.com
DOMAIN_API_PORT=2087
DOMAIN_DOMAIN=example.com
DOMAIN_HOST_IP=69.69.69.69
-1
View File
@@ -1 +0,0 @@
use_nix
-18
View File
@@ -1,18 +0,0 @@
name: Checks
on: [pull_request, workflow_dispatch]
jobs:
validation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: bun install
- name: Run tests
run: bun test
-30
View File
@@ -1,30 +0,0 @@
name: Publish records
on:
push:
branches: [main]
workflow_dispatch:
jobs:
publish:
if: github.repository == 'is-a-dev/register'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: bun install
- run: bun test
- name: Publishing records
env:
CI: 1
NODE_ENV: production
ENV: production
DOMAIN_USER: ${{ secrets.DOMAIN_USER }}
DOMAIN_API_KEY: ${{ secrets.DOMAIN_API_KEY }}
DOMAIN_API_HOST: ${{ secrets.DOMAIN_API_HOST }}
DOMAIN_API_PORT: ${{ secrets.DOMAIN_API_PORT }}
DOMAIN_DOMAIN: ${{ secrets.DOMAIN_DOMAIN }}
DOMAIN_HOST_IP: ${{ secrets.DOMAIN_HOST_IP }}
run: bun run publish-records
+32
View File
@@ -0,0 +1,32 @@
name: Publish
on:
push:
branches: [main]
paths:
- "domains/**"
- ".github/workflows/publish.yml"
- "dnsconfig.js"
workflow_dispatch:
concurrency:
group: ${{ github.ref }}-publish
cancel-in-progress: false
jobs:
dns:
name: DNS
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo '{"cloudflare":{"TYPE":"CLOUDFLAREAPI","apitoken":"$CLOUDFLARE_API_TOKEN"}}' > ./creds.json
- name: Publish
uses: koenrh/dnscontrol-action@v3
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
with:
args: push
config_file: "dnsconfig.js"
+43
View File
@@ -0,0 +1,43 @@
name: Validation
on:
pull_request:
push:
branches: [main]
paths:
- "domains/**"
- ".github/workflows/validation.yml"
- "dnsconfig.js"
workflow_dispatch:
concurrency:
group: ${{ github.ref }}-validation
cancel-in-progress: true
jobs:
dns:
name: DNS
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check
uses: koenrh/dnscontrol-action@v3
with:
args: check
config_file: "dnsconfig.js"
json:
name: JSON
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: JSON Syntax Check
uses: limitusus/json-syntax-check@v2
with:
pattern: "\\.json$"
env:
BASE: "domains/"
-22
View File
@@ -1,22 +0,0 @@
FROM ubuntu:20.04
ENV TERM xterm
RUN ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime
RUN apt-get -y update
RUN apt-get install -y nodejs npm curl wget dnsutils certbot --fix-missing
RUN apt-get install -y unzip
RUN bash -c "curl -fsSL https://bun.sh/install | bash -s 'bun-v1.0.15'"
RUN ~/.bun/bin/bun -v
WORKDIR /opt/app
COPY bun.lockb .
COPY package.json .
RUN ~/.bun/bin/bun install
CMD ["sh", "-c", "cp -r node_modules code; cd code; tail -f /dev/null"]
BIN
View File
Binary file not shown.
-15
View File
@@ -1,15 +0,0 @@
{ nixpkgs ? import <nixpkgs> {} }:
let
inherit (nixpkgs) pkgs;
nixPackages = with pkgs; [
yarn
docker-compose
dnsutils
bun
];
in
pkgs.stdenv.mkDerivation {
name = "env";
buildInputs = nixPackages;
}
+128
View File
@@ -0,0 +1,128 @@
var regNone = NewRegistrar("none");
var providerCf = DnsProvider(NewDnsProvider("cloudflare"));
var proxy = {
on: { "cloudflare_proxy": "on" },
off: { "cloudflare_proxy": "off" }
};
// Function to validate filenames according to the given rules
function isValidFilename(filename) {
var regex = /^[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+(-[a-z0-9]+)*)*\.json$/;
return regex.test(filename);
}
function getDomainsList(filesPath) {
var result = [];
var files = glob.apply(null, [filesPath, true, '.json']);
for (var i = 0; i < files.length; i++) {
var basename = files[i].split('/').reverse()[0];
if (!isValidFilename(basename)) {
console.log(`Skipping invalid file: ${basename}`);
continue;
}
var name = basename.split('.')[0];
result.push({ name: name, data: require(files[i]) });
}
return result;
}
var domains = getDomainsList('./domains');
var commit = {};
for (var idx in domains) {
var domainData = domains[idx].data;
var proxyState = proxy.off; // disabled by default
if (!commit[domainData.record.domain]) {
commit[domainData.record.domain] = [];
}
if (domainData.record.proxied === false) {
proxyState = proxy.off;
}
// Handle CNAME record
if (domainData.record.CNAME) {
commit[domainData.record.domain].push(
CNAME(domainData.subdomain, domainData.record.CNAME + ".", proxyState)
);
}
// Handle A records
if (domainData.record.A) {
for (var a in domainData.record.A) {
commit[domainData.record.domain].push(
A(domainData.subdomain, IP(domainData.record.A[a]), proxyState)
);
}
}
// Handle AAAA records
if (domainData.record.AAAA) {
for (var aaaa in domainData.record.AAAA) {
commit[domainData.record.domain].push(
AAAA(domainData.subdomain, domainData.record.AAAA[aaaa], proxyState)
);
}
}
// Handle MX records
if (domainData.record.MX) {
for (var mx in domainData.record.MX) {
commit[domainData.record.domain].push(
MX(domainData.subdomain, 10, domainData.record.MX[mx] + ".")
);
}
}
// Handle URL records (redirect)
if (domainData.record.URL) {
commit[domainData.record.domain].push(
URL(domainData.subdomain, domainData.record.URL)
);
}
// Handle NS records
if (domainData.record.NS) {
for (var ns in domainData.record.NS) {
commit[domainData.domain].push(
NS(domainData.subdomain, domainData.record.NS[ns] + ".")
);
}
}
// Handle SRV records
if (domainData.record.SRV) {
for (var srv in domainData.record.SRV) {
var srvRecord = domainData.record.SRV[srv];
commit[domainData.domain].push(
SRV(domainData.subdomain, srvRecord.priority, srvRecord.weight, srvRecord.port, srvRecord.target + ".")
);
}
}
// Handle TXT records
if (domainData.record.TXT) {
if (Array.isArray(domainData.record.TXT)) {
for (var txt in domainData.record.TXT) {
commit[domainData.record.domain].push(
TXT(domainData.subdomain, domainData.record.TXT[txt])
);
}
} else {
commit[domainData.record.domain].push(
TXT(domainData.subdomain, domainData.record.TXT)
);
}
}
}
// Commit all DNS records
for (var domainName in commit) {
D(domainName, regNone, providerCf, commit[domainName]);
}
-8
View File
@@ -1,8 +0,0 @@
version: '3'
services:
dev:
build:
context: .
dockerfile: ./Dockerfile
volumes:
- ./:/opt/app/code
-27
View File
@@ -1,27 +0,0 @@
{
"name": "is-a-dev-core",
"version": "1.0.0",
"description": "Register *.is-a.dev domains for free",
"scripts": {
"lint": "eslint utils scripts domains --ext .json,.js",
"publish-records": "bun run -b ./scripts/register-domains.js",
"dc": "docker-compose -p is-a-dev",
"dc:start": "bun run dc up",
"dc:shell": "bun run dc run dev /bin/bash"
},
"repository": {
"type": "git",
"url": "https://github.com/is-a-dev/register"
},
"author": "Akshay Nair <phenax5@gmail.com>",
"license": "GPL-3.0",
"dependencies": {
"ip-regex": "^5.0.0",
"ramda": "^0.27.1"
},
"devDependencies": {
"eslint": "^7.11.0",
"eslint-plugin-json": "^2.1.2",
"eslint-plugin-node": "^11.1.0"
}
}
-8
View File
@@ -1,8 +0,0 @@
module.exports = {
hasLabel: (context, label) => {
const pr = context.payload.pull_request || context.payload.issue;
const { labels = [] } = pr;
return !!labels.find(({ name }) => name === label);
},
};
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
echo "$PWD";
echo "AUTH ::[$CERTBOT_VALIDATION]::[$CERTBOT_TOKEN]::[$CERTBOT_REMAINING_CHALLENGES]";
echo "[$CERTBOT_DOMAIN]";
sleep 1;
./scripts/certbot.sh acme_txt "$CERTBOT_VALIDATION";
echo "Going to sleep for a few minutes...";
# TODO: Check if $CERTBOT_VALIDATION == $(./scripts/certbot.sh get-acme)?
sleep $((3*60));
./scripts/certbot.sh check;
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env bash
export NODE_ENV=production
DRY_RUN=0
if_dry_run() { [[ $DRY_RUN == 1 ]] && echo "$1" || echo "$2"; }
generate_certificate() {
outdir=$(mktemp -d /tmp/is-a-dev-certbot.XXXXX);
certbot \
--config-dir $outdir/config \
--work-dir $outdir/work \
--logs-dir $outdir/logs \
certonly \
--manual \
--preferred-challenges=dns \
--manual-auth-hook=./scripts/certbot-auth.sh \
-m 'phenax5@gmail.com' \
-d '*.is-a.dev,is-a.dev' \
--agree-tos \
$(if_dry_run "--dry-run" "");
echo "+-----------------------------------------------+";
echo "| Certificate output: |";
echo "|= $outdir";
echo "+-----------------------------------------------+";
}
update_record() {
local method=$([[ "$1" == "add" ]] && echo "addZoneRecord" || echo "removeZoneRecord");
local type="$2";
local name="$3";
local address="$4";
local ttl=${5:-"1"};
bun -e "
const { domainService } = require('./utils/domain-service');
const { ENV, DOMAIN_DOMAIN } = require('./utils/constants');
const method = '$method';
const name = '$name';
const type = '$type';
const record = { name, type, address: '$address', ttl: $ttl };
async function main() {
if (method === 'removeZoneRecord') {
const data = await domainService.get({ customonly: 0, name: '$name.is-a.dev.', type });
if (data.length > 0) {
record.id = data[0].line;
} else {
throw new Error('Unable to find record');
}
}
console.log('Uploading $name to', DOMAIN_DOMAIN, '(', ENV, ')...');
const data = await domainService[method](record)();
console.log(data.cpanelresult ? data.cpanelresult.data : data);
}
main().catch(console.error);
"
}
update_acme_txt_record() {
update_record add TXT '_acme-challenge' "$1";
}
reset_acme() {
sleep 1;
update_record remove TXT '_acme-challenge' '';
update_record remove TXT '_acme-challenge' '';
# update_record add CNAME 'www' "is-a-dev.github.io";
}
get_acme() { dig +noall +answer _acme-challenge.is-a.dev TXT | awk '{print $5}'; }
case "$1" in
check) echo "TXT record:: $(get_acme)" ;;
get-acme) get_acme ;;
cert) generate_certificate ;;
acme_txt) update_acme_txt_record "$2" ;;
reset) reset_acme ;;
*) echo "Invalid command"; exit 1; ;;
esac
### STEPS ###
# Run ./scripts/certbot.sh cert
# cp -r /tmp/is-a-dev-whatever /opt/app/code/is-a-dev-cert
# Upload cert.pem and privkey.pem (from config/live/is-a.dev/) contents to SSL > Manage SSL Sites
# Run ./scripts/certbot.sh reset
-76
View File
@@ -1,76 +0,0 @@
const R = require("ramda");
const {
VALID_RECORD_TYPES,
DOMAIN_HOST_IP,
TTL,
ENV,
} = require("../utils/constants");
const { domainService: dc } = require("../utils/domain-service");
const { getDomains: gd } = require("../utils/get-domain");
const getRecords = R.compose(R.toPairs, R.pick(VALID_RECORD_TYPES));
const address = (type, value) => {
if ("URL" === type) return `${value}`.replace(/\/$/g, "");
if ("TXT" === type) return value;
return (type === "CNAME" ? `${value}`.toLowerCase() : `${value}`).replace(
/[/.]$/g,
"",
);
};
const toHostList = R.chain((data) => {
// URL redirection must contain explicit A record
// Wildcard A record breaks when used with MX
// Ref: https://github.com/is-a-dev/register/issues/2365
if ((data.record.URL && data.record.MX) || data.name === "@") {
data.record.A = [DOMAIN_HOST_IP];
}
const records = getRecords(data.record);
return R.chain(([recordType, values]) => {
const valueList = Array.isArray(values) ? values : [values];
return valueList.map((value, index) => ({
name: data.name,
type: recordType,
address: address(recordType, value),
ttl: TTL,
...(recordType === "MX" ? { priority: index + 20 } : {}),
}));
}, records);
});
const registerDomains = async ({
domainService,
getDomains,
log = () => {},
}) => {
const domains = await getDomains().then(toHostList);
if (domains.length === 0)
return Promise.reject(new Error("Nothing to register"));
log(`${domains.length} records found`);
return domainService.updateHosts(domains);
};
const main = async () => {
console.log(`Running in ${ENV} mode`);
const result = await registerDomains({
domainService: dc,
getDomains: gd,
log: console.log,
});
console.log(result);
};
if (require.main === module) {
main().catch((e) => {
console.error(e);
process.exit(1);
});
} else {
module.exports = { toHostList, registerDomains };
}
-48
View File
@@ -1,48 +0,0 @@
const getInstructions = () => `
The changes have been published!! It should reflect in less than 24 hours.
## Here's what you need to do next
If your domain points to a server you own, add \`domain-name.is-a.dev\` to your server config. For https, you will have to configure ssl certificate to allow the new subdomain.
### For github pages users,
* Go to your github page repo (\`user/user.github.io\`)
* Open up the **settings** tab
* Scroll down to the **Github pages** section
* In the **Custom domain** text input, enter the domain you registered (\`domain-name.is-a.dev\`)
* Check the **Enforce HTTPS** checkbox below the input
* Give it some time to reflect and you should be good to go
## Need help with your domain?
If you are having trouble setting up your domain, [create an issue](https://github.com/is-a-dev/register/issues/new/choose). I will try my best to get back to you asap!
## Made a mistake in the record?
Don't worry, you can create a new pull request with the corrections
## Love/Hate the service?
**Love it?** Leave it a **star**! Also consider donating so that I can keep this service running forever!
**Hate it?** Please leave your feedback by [creating an issue](https://github.com/is-a-dev/register/issues/new/choose). I would really like to keep improving this service for other users.
## Wanna support this project?
Help me in my mission to keep this service alive forever by donating!
<a href="https://www.buymeacoffee.com/phenax" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="28" width="119"></a> <a href="https://liberapay.com/phenax" target="_blank"><img src="https://img.shields.io/badge/liberapay-donate-yellow.svg?style=for-the-badge" alt="Liberapay recurring donation button" /></a>
`;
module.exports = {
async instructions(context, github) {
const pr = context.payload.issue || context.payload.pull_request;
const { number } = pr;
await github.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number,
body: getInstructions(),
});
},
};
-11
View File
@@ -1,11 +0,0 @@
#!/usr/bin/env sh
sed \
-e 's/\.json.*$/.is-a.dev/g' \
-e 's/^\s*domains\///g' \
-e '/^\s*$/d' \
| while read domain; do
echo "$domain";
dig "$domain" +noall +answer && echo "done" || echo "x";
done;
-210
View File
@@ -1,210 +0,0 @@
const R = require("ramda");
const { CpanelClient } = require("../utils/lib/cpanel");
const mockFetch =
(expectRequest, decorate = R.identity) =>
(reqUrl, request) => {
expectRequest(reqUrl, request);
return Promise.resolve({
json: async () => decorate(request),
});
};
describe("Cpanel client", () => {
describe("fetchzonerecords", () => {
it("should make the correct request", async () => {
const fetch = mockFetch((url, request) => {
expect(url).toBe(
"https://example.com:2000/json-api/cpanel?customonly=0&domain=a.b&cpanel_jsonapi_user=boy&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=fetchzone_records&cpanel_jsonapi_apiversion=2",
);
expect(request).toEqual({
headers: {
Authorization: "cpanel boy:boybyebye",
},
rejectUnauthorized: false,
});
});
const cpanel = CpanelClient({
host: "example.com",
port: 2000,
username: "boy",
apiKey: "boybyebye",
domain: "a.b",
dependencies: { fetch },
});
await cpanel.zone.fetch();
});
it("should make the correct request with query", async () => {
const fetch = mockFetch((url, request) => {
expect(url).toBe(
"https://example.com:2000/json-api/cpanel?customonly=0&domain=foobar.boeey&cpanel_jsonapi_user=boy&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=fetchzone_records&cpanel_jsonapi_apiversion=2",
);
expect(request).toEqual({
headers: {
Authorization: "cpanel boy:boybyebye",
},
rejectUnauthorized: false,
});
});
const cpanel = CpanelClient({
host: "example.com",
port: 2000,
username: "boy",
apiKey: "boybyebye",
domain: "a.b",
dependencies: { fetch },
});
await cpanel.zone.fetch({ domain: "foobar.boeey" });
});
});
describe("addzonerecord", () => {
it("should make the correct request", async () => {
const fetch = mockFetch((url, request) => {
expect(url).toBe(
"https://example.com:2000/json-api/cpanel?domain=a.b&name=googo&type=CNAME&cname=beey&ttl=2020&cpanel_jsonapi_user=boy&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=add_zone_record&cpanel_jsonapi_apiversion=2",
);
expect(request).toEqual({
headers: {
Authorization: "cpanel boy:boybyebye",
},
rejectUnauthorized: false,
});
});
const cpanel = CpanelClient({
host: "example.com",
port: 2000,
username: "boy",
apiKey: "boybyebye",
domain: "a.b",
dependencies: { fetch },
});
await cpanel.zone.add({
name: "googo",
type: "boyee",
cname: "beey",
type: "CNAME",
ttl: 2020,
});
});
});
describe("addzonerecord", () => {
it("should make the correct request", async () => {
const fetch = mockFetch((url, request) => {
expect(url).toBe(
"https://example.com:2000/json-api/cpanel?domain=a.b&line=500&cpanel_jsonapi_user=boy&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=remove_zone_record&cpanel_jsonapi_apiversion=2",
);
expect(request).toEqual({
headers: {
Authorization: "cpanel boy:boybyebye",
},
rejectUnauthorized: false,
});
});
const cpanel = CpanelClient({
host: "example.com",
port: 2000,
username: "boy",
apiKey: "boybyebye",
domain: "a.b",
dependencies: { fetch },
});
await cpanel.zone.remove({
line: 500,
});
});
});
describe("fetchredirections", () => {
it("should make the correct request", async () => {
const fetch = mockFetch((url, request) => {
expect(url).toBe(
"https://example.com:2000/execute/Mime/list_redirects?cpanel_jsonapi_user=boy&cpanel_jsonapi_module=Mime&cpanel_jsonapi_func=list_redirects&cpanel_jsonapi_apiversion=2",
);
expect(request).toEqual({
headers: {
Authorization: "cpanel boy:boybyebye",
},
rejectUnauthorized: false,
});
});
const cpanel = CpanelClient({
host: "example.com",
port: 2000,
username: "boy",
apiKey: "boybyebye",
domain: "a.b",
dependencies: { fetch },
});
await cpanel.redirection.fetch();
});
});
describe("addredirection", () => {
it("should make the correct request", async () => {
const fetch = mockFetch((url, request) => {
expect(url).toBe(
"https://example.com:2000/execute/Mime/add_redirect?domain=googo&destination=https%3A%2F%2Foodf.com&cpanel_jsonapi_user=boy&cpanel_jsonapi_module=Mime&cpanel_jsonapi_func=add_redirect&cpanel_jsonapi_apiversion=2",
);
expect(request).toEqual({
headers: {
Authorization: "cpanel boy:boybyebye",
},
rejectUnauthorized: false,
});
});
const cpanel = CpanelClient({
host: "example.com",
port: 2000,
username: "boy",
apiKey: "boybyebye",
domain: "a.b",
dependencies: { fetch },
});
await cpanel.redirection.add({
domain: "googo",
destination: "https://oodf.com",
});
});
});
describe("deleteredirection", () => {
it("should make the correct request", async () => {
const fetch = mockFetch((url, request) => {
expect(url).toBe(
"https://example.com:2000/execute/Mime/delete_redirect?domain=googo&cpanel_jsonapi_user=boy&cpanel_jsonapi_module=Mime&cpanel_jsonapi_func=delete_redirect&cpanel_jsonapi_apiversion=2",
);
expect(request).toEqual({
headers: {
Authorization: "cpanel boy:boybyebye",
},
rejectUnauthorized: false,
});
});
const cpanel = CpanelClient({
host: "example.com",
port: 2000,
username: "boy",
apiKey: "boybyebye",
domain: "a.b",
dependencies: { fetch },
});
await cpanel.redirection.remove({ domain: "googo" });
});
});
});
-421
View File
@@ -1,421 +0,0 @@
const R = require("ramda");
const { getDomainService, diffRecords } = require("../utils/domain-service");
const { DOMAIN_DOMAIN } = require("../utils/constants");
const getCpanel = ({
zone,
addZone,
removeZone,
redir,
addRedir,
removeRedir,
addEmail,
removeEmail,
} = {}) => ({
zone: {
fetch: (_) => zone(),
add: (rec) => addZone(rec),
remove: (rec) => removeZone(rec),
},
redirection: {
fetch: (_) => redir(),
add: (rec) => addRedir(rec),
remove: (rec) => removeRedir(rec),
},
email: {
add: (rec) => addEmail(rec),
remove: (rec) => removeEmail(rec),
},
});
describe("diffRecords", () => {
it("should show added record", () => {
const oldRecords = [
{ name: "xx", type: "CNAME", address: "fck.com." },
{ name: "xa", type: "A", address: "111.1.1212.1" },
];
const newRecords = [
{ name: "xx", type: "CNAME", address: "fck.com." },
{ name: "xa", type: "A", address: "111.1.1212.1" },
{ name: "boo", type: "CNAME", address: "x.com" },
];
const result = diffRecords(oldRecords, newRecords);
expect(result).toEqual({
remove: [],
add: [{ name: "boo", type: "CNAME", address: "x.com" }],
});
});
it("should show edited records", () => {
const oldRecords = [
{ name: "xx", type: "CNAME", address: "fck.com." },
{ name: "xa", type: "A", address: "111.1.1212.1" },
];
const newRecords = [
{ name: "xx", type: "CNAME", address: "fck.com." },
{ name: "xa", type: "A", address: "69.69.69.69" },
];
const result = diffRecords(oldRecords, newRecords);
expect(result).toEqual({
remove: [{ name: "xa", type: "A", address: "111.1.1212.1" }],
add: [{ name: "xa", type: "A", address: "69.69.69.69" }],
});
});
it("should show added records with the same name and record type", () => {
const oldRecords = [
{ name: "xx", type: "CNAME", address: "fck.com." },
{ name: "xa", type: "A", address: "69.69.69.69" },
];
const newRecords = [
{ name: "xx", type: "CNAME", address: "fck.com." },
{ name: "xa", type: "A", address: "69.69.69.69" },
{ name: "xa", type: "A", address: "69.69.4.20" },
];
const result = diffRecords(oldRecords, newRecords);
expect(result).toEqual({
remove: [],
add: [{ name: "xa", type: "A", address: "69.69.4.20" }],
});
});
it("should diff complex changes", () => {
const oldRecords = [
{ name: "a", type: "CNAME", address: "fck.com." },
{ name: "b", type: "A", address: "69.69.69.69" },
{ name: "111", type: "CNAME", address: "x" },
{ name: "d", type: "A", address: "69.69.4.20" },
];
const newRecords = [
{ name: "111", type: "CNAME", address: "x" },
{ name: "d", type: "CNAME", address: "duck.com" },
{ name: "a", type: "CNAME", address: "og.com" },
{ name: "b", type: "A", address: "69.69.69.69" },
{ name: "b", type: "A", address: "69.69.4.20" },
{ name: "c", type: "CNAME", address: "ccc.cc" },
];
const result = diffRecords(oldRecords, newRecords);
expect(result).toEqual({
remove: [
{ name: "a", type: "CNAME", address: "fck.com." },
{ name: "d", type: "A", address: "69.69.4.20" },
],
add: [
{ name: "d", type: "CNAME", address: "duck.com" },
{ name: "a", type: "CNAME", address: "og.com" },
{ name: "b", type: "A", address: "69.69.4.20" },
{ name: "c", type: "CNAME", address: "ccc.cc" },
],
});
});
});
describe("Domain service", () => {
const addZone = jest.fn(async () => ({}));
const removeZone = jest.fn(async () => ({}));
const addRedir = jest.fn(async () => ({}));
const removeRedir = jest.fn(async () => ({}));
const addEmail = jest.fn(async () => ({}));
const removeEmail = jest.fn(async () => ({}));
const mockDS = ({ zones, redirections }) =>
getDomainService({
cpanel: getCpanel({
zone: async () => zones,
redir: async () => redirections,
addZone,
addEmail,
addRedir,
removeZone,
removeRedir,
removeEmail,
}),
});
const getRecordCalls = (recfn) =>
recfn.mock.calls
.map(R.head)
.map(
R.pick([
"name",
"type",
"address",
"redirect",
"domain",
"line",
"priority",
"exchanger",
]),
);
beforeEach(() => {
addZone.mockClear();
removeZone.mockClear();
addRedir.mockClear();
removeRedir.mockClear();
addEmail.mockClear();
removeEmail.mockClear();
});
describe("getHosts", () => {
it("should resolve with a list of hosts", async () => {
const zones = [
{ name: "xx", type: "CNAME", address: "fck.com." },
{ name: "xx", type: "A", address: "111.1.1212.1" },
];
const redirections = [];
const zone = async () => zones;
const redir = async () => redirections;
const mockDomainService = getDomainService({
cpanel: getCpanel({ zone, redir }),
});
const list = await mockDomainService.getHosts();
expect(list).toEqual([
{ name: "xx", type: "CNAME", address: "fck.com" },
{ name: "xx", type: "A", address: "111.1.1212.1" },
]);
});
it("should resolve with a redirections", async () => {
const zones = [
{ line: "111", name: "xx", type: "CNAME", address: "fck.com." },
{ line: "112", name: "xx", type: "A", address: "111.1.1212.1" },
];
const redirections = [
{ domain: "foo.booboo.xyz", destination: "https://google.com" },
{ domain: "foo1.booboo.xyz", destination: "https://duck.com" },
];
const zone = async () => zones;
const redir = async () => redirections;
const mockDomainService = getDomainService({
cpanel: getCpanel({ zone, redir }),
});
const list = await mockDomainService.getHosts();
expect(list).toEqual([
{ id: "111", name: "xx", type: "CNAME", address: "fck.com" },
{ id: "112", name: "xx", type: "A", address: "111.1.1212.1" },
{
id: `foo.${DOMAIN_DOMAIN}`,
name: "foo",
type: "URL",
address: "https://google.com",
},
{
id: `foo1.${DOMAIN_DOMAIN}`,
name: "foo1",
type: "URL",
address: "https://duck.com",
},
]);
});
});
describe("updateHosts", () => {
it("should append new hosts with existing ones and set it", async () => {
const zones = [
{ line: 1, name: "a", type: "CNAME", address: "boo" },
{ line: 2, name: "b", type: "CNAME", address: "goo" },
];
const redirections = [];
const mockDomainService = mockDS({ zones, redirections });
await mockDomainService.updateHosts([
{ name: "a", type: "CNAME", address: "boo" },
{ name: "b", type: "CNAME", address: "goo" },
{ name: "c", type: "A", address: "12.131321.213" },
{ name: "c", type: "MX", address: "foobar.com", priority: 2 },
]);
expect(addZone).toHaveBeenCalledTimes(1);
expect(getRecordCalls(addZone)).toEqual([
{ name: "c", type: "A", address: "12.131321.213" },
]);
expect(addEmail).toHaveBeenCalledTimes(1);
expect(getRecordCalls(addEmail)).toEqual([
{ domain: "c.is-a.dev", exchanger: "foobar.com", priority: 2 },
]);
expect(removeZone).toHaveBeenCalledTimes(0);
expect(removeEmail).toHaveBeenCalledTimes(0);
});
it("should update matching host and set it", async () => {
const zones = [
{ line: 1, name: "a", type: "CNAME", address: "boo" },
{ line: 2, name: "b", type: "CNAME", address: "goo" },
];
const redirections = [];
const mockDomainService = mockDS({ zones, redirections });
await mockDomainService.updateHosts([
{ name: "a", type: "CNAME", address: "boo" },
{ name: "b", type: "CNAME", address: "googoogaga" },
]);
expect(addZone).toHaveBeenCalledTimes(1);
expect(getRecordCalls(addZone)).toEqual([
{ name: "b", type: "CNAME", address: "googoogaga" },
]);
expect(removeZone).toHaveBeenCalledTimes(1);
expect(getRecordCalls(removeZone)).toEqual([{ line: 2 }]);
});
it("should update matching host and set it", async () => {
const zones = [
{ line: 1, name: "a", type: "CNAME", address: "boo" },
{ line: 2, name: "b", type: "CNAME", address: "goo" },
{ line: 3, name: "b", type: "CNAME", address: "xaa" },
];
const redirections = [];
const mockDomainService = mockDS({ zones, redirections });
await mockDomainService.updateHosts([
{ name: "a", type: "CNAME", address: "boo" },
{ name: "b", type: "CNAME", address: "googoogaga" },
{ name: "b", type: "CNAME", address: "farboo" },
]);
expect(addZone).toHaveBeenCalledTimes(2);
expect(getRecordCalls(addZone)).toEqual([
{ name: "b", type: "CNAME", address: "googoogaga" },
{ name: "b", type: "CNAME", address: "farboo" },
]);
expect(removeZone).toHaveBeenCalledTimes(2);
expect(getRecordCalls(removeZone)).toEqual([
{ line: 3 },
{ line: 2 },
]);
});
it("should workout this complex example", async () => {
const zones = [
{ line: 1, name: "a", type: "CNAME", address: "world" },
{ line: 2, name: "b", type: "A", address: "1" },
{ line: 3, name: "b", type: "A", address: "2" },
{ line: 4, name: "c", type: "CNAME", address: "hello.com" },
{
line: 5,
name: "c",
type: "MX",
address: "mx1.hello.com",
priority: 20,
},
{
line: 6,
name: "c",
type: "MX",
address: "mx2.hello.com",
priority: 21,
},
{
line: 7,
name: "b",
type: "MX",
address: "foo.bar",
priority: 20,
},
{ line: 101, name: "x", type: "A", address: "1" },
{ line: 99, name: "y", type: "A", address: "2" },
{ line: 100, name: "z", type: "A", address: "3" },
];
const redirections = [
{
domain: `b.${DOMAIN_DOMAIN}`,
destination: "https://foobar.com",
},
{
domain: `c.${DOMAIN_DOMAIN}`,
destination: "https://goobar.com",
},
{
domain: `x.${DOMAIN_DOMAIN}`,
destination: "https://example.com",
},
];
const mockDomainService = mockDS({ zones, redirections });
await mockDomainService.updateHosts([
{ name: "a", type: "CNAME", address: "boo" },
{ name: "b", type: "A", address: "1" },
{ name: "b", type: "A", address: "2" },
{ name: "b", type: "A", address: "3" },
{ name: "b", type: "URL", address: "https://wowow.com" },
{ name: "c", type: "CNAME", address: "hello.com" },
{ name: "c", type: "URL", address: "https://goobar.com" },
{ name: "d", type: "CNAME", address: "helo.com" },
{ name: "d", type: "URL", address: "https://hhh.com" },
{ name: "x", type: "URL", address: "https://example69.com" },
{
name: "c",
type: "MX",
address: "mx2.hello.com",
priority: 21,
},
{ name: "a", type: "MX", address: "example.com", priority: 20 },
]);
expect(addZone).toHaveBeenCalledTimes(3);
expect(getRecordCalls(addZone)).toEqual([
{ name: "a", type: "CNAME", address: "boo" },
{ name: "b", type: "A", address: "3" },
{ name: "d", type: "CNAME", address: "helo.com" },
]);
expect(removeZone).toHaveBeenCalledTimes(4);
expect(getRecordCalls(removeZone)).toEqual([
{ line: 101 },
{ line: 100 },
{ line: 99 },
{ line: 1 },
]);
expect(addEmail).toHaveBeenCalledTimes(1);
expect(getRecordCalls(addEmail)).toEqual([
{
domain: "a.is-a.dev",
exchanger: "example.com",
priority: 20,
},
]);
expect(removeEmail).toHaveBeenCalledTimes(2);
expect(getRecordCalls(removeEmail)).toEqual([
{ domain: "b.is-a.dev", exchanger: "foo.bar", priority: 20 },
{
domain: "c.is-a.dev",
exchanger: "mx1.hello.com",
priority: 20,
},
]);
expect(addRedir).toHaveBeenCalledTimes(3);
expect(getRecordCalls(addRedir)).toEqual([
{
domain: `b.${DOMAIN_DOMAIN}`,
type: "permanent",
redirect: "https://wowow.com",
},
{
domain: `d.${DOMAIN_DOMAIN}`,
type: "permanent",
redirect: "https://hhh.com",
},
{
domain: `x.${DOMAIN_DOMAIN}`,
type: "permanent",
redirect: "https://example69.com",
},
]);
expect(removeRedir).toHaveBeenCalledTimes(2);
expect(getRecordCalls(removeRedir)).toEqual([
{ domain: `b.${DOMAIN_DOMAIN}` },
{ domain: `x.${DOMAIN_DOMAIN}` },
]);
});
});
});
-8
View File
@@ -1,8 +0,0 @@
const { getDomains } = require("../utils/get-domain");
describe("getDomains", () => {
it("should resolve with the list of domains", async () => {
const list = await getDomains();
expect(Array.isArray(list)).toBe(true);
});
});
-37
View File
@@ -1,37 +0,0 @@
const R = require("ramda");
const fs = require("fs");
const { getDomains } = require("../utils/get-domain");
const { validateDomainData } = require("../utils/validations");
const { DOMAINS_PATH } = require("../utils/constants");
describe("Domains", () => {
it("should all be json", async () => {
const files = await fs.promises.readdir(DOMAINS_PATH, {});
expect(files.filter((f) => !/\.json$/g.test(f)).length).toBe(0);
});
it("should be valid", (done) => {
getDomains()
.then(R.reject(R.propEq("name", "_psl")))
.then(
R.map((data) => {
const { errors } = validateDomainData(data);
if (errors.length) {
const message = errors
.map(
([key, { reason }]) =>
`[${data.name}.${key}]: ${reason}`,
)
.join("\n");
return `\nValidation errors in ${data.name}.json: \n${message}`;
}
return "";
}),
)
.then(R.filter(R.complement(R.isEmpty)))
.then((messages) =>
messages.length ? done(messages.join("\n")) : done(),
)
.catch(done);
});
});
-242
View File
@@ -1,242 +0,0 @@
const R = require("ramda");
const { toHostList, registerDomains } = require("../scripts/register-domains");
const { TTL, DOMAIN_DOMAIN } = require("../utils/constants");
const { getDomainService } = require("../utils/domain-service");
const getCpanel = ({
zone,
addZone,
removeZone,
redir,
addRedir,
removeRedir,
addEmail,
removeEmail,
} = {}) => ({
zone: {
fetch: (_) => zone(),
add: (rec) => addZone(rec),
remove: (rec) => removeZone(rec),
},
redirection: {
fetch: (_) => redir(),
add: (rec) => addRedir(rec),
remove: (rec) => removeRedir(rec),
},
email: {
add: (rec) => addEmail(rec),
remove: (rec) => removeEmail(rec),
},
});
describe("toHostList", () => {
it("should flatten domain data to list of hosts (without https)", () => {
const res = toHostList([
{ name: "akshay", record: { CNAME: "phenax.github.io" } },
{ name: "foobar", record: { CNAME: "v.io" } },
{ name: "xx", record: { A: ["1.2.3.4", "5.6.3.2", "1.2.31.1"] } },
{
name: "xx",
record: { CNAME: "foobar.com", MX: ["as.com", "f.com"] },
},
]);
expect(res).toEqual([
{
name: "akshay",
type: "CNAME",
address: "phenax.github.io",
ttl: TTL,
},
{ name: "foobar", type: "CNAME", address: "v.io", ttl: TTL },
{ name: "xx", type: "A", address: "1.2.3.4", ttl: TTL },
{ name: "xx", type: "A", address: "5.6.3.2", ttl: TTL },
{ name: "xx", type: "A", address: "1.2.31.1", ttl: TTL },
{ name: "xx", type: "CNAME", address: "foobar.com", ttl: TTL },
{
name: "xx",
type: "MX",
address: "as.com",
priority: 20,
ttl: TTL,
},
{
name: "xx",
type: "MX",
address: "f.com",
priority: 21,
ttl: TTL,
},
]);
});
});
describe("registerDomains", () => {
const addZone = jest.fn(async () => ({}));
const removeZone = jest.fn(async () => ({}));
const addRedir = jest.fn(async () => ({}));
const removeRedir = jest.fn(async () => ({}));
const addEmail = jest.fn(async () => ({}));
const removeEmail = jest.fn(async () => ({}));
const mockDS = ({ zones, redirections }) =>
getDomainService({
cpanel: getCpanel({
zone: async () => zones,
redir: async () => redirections,
addZone,
addEmail,
addRedir,
removeZone,
removeRedir,
removeEmail,
}),
});
beforeEach(() => {
addZone.mockClear();
removeZone.mockClear();
addRedir.mockClear();
removeRedir.mockClear();
addEmail.mockClear();
removeEmail.mockClear();
});
it("should register the new set of hosts generated from domains list", async () => {
const localHosts = [
{ name: "a", record: { CNAME: "hello" } },
{ name: "b", record: { CNAME: "xaa" } },
];
const remoteHosts = [
{ line: 1, name: "a", type: "CNAME", address: "hello" },
{ line: 2, name: "b", type: "CNAME", address: "goo" },
{ line: 3, name: "b", type: "CNAME", address: "xaa" },
];
const remoteRedirections = [];
const domainService = mockDS({
zones: remoteHosts,
redirections: remoteRedirections,
});
await registerDomains({
getDomains: async () => localHosts,
domainService,
});
expect(addZone).toHaveBeenCalledTimes(0);
expect(removeZone).toHaveBeenCalledTimes(1);
expect(addRedir).toHaveBeenCalledTimes(0);
expect(removeRedir).toHaveBeenCalledTimes(0);
});
it("should add the new set hosts", async () => {
const localHosts = [
{ name: "a", record: { CNAME: "boo" } },
{
name: "b",
record: {
A: ["1.1.1.1", "1.1.1.2"],
MX: "somemx",
TXT: "some txt",
},
},
{ name: "c", record: { URL: "https://google.com" } },
{ name: "d", record: { CNAME: "foobar" } },
{
name: "e",
record: { A: ["2.2.2.2"], TXT: ["some", "extra", "txt"] },
},
];
const remoteHosts = [
{ line: 1, name: "a", type: "CNAME", address: "boo" },
{ line: 2, name: "b", type: "MX", address: "othermx" },
{ line: 3, name: "d", type: "CNAME", address: "foobaz" },
];
const remoteRedirections = [
{ domain: `b.${DOMAIN_DOMAIN}`, destination: "x" },
{ domain: `a.${DOMAIN_DOMAIN}`, destination: "y" },
];
const domainService = mockDS({
zones: remoteHosts,
redirections: remoteRedirections,
});
await registerDomains({
getDomains: async () => localHosts,
domainService,
});
expect(addZone).toHaveBeenCalledTimes(8);
expect(addZone.mock.calls).toEqual([
[{ name: "b", type: "A", address: "1.1.1.1", line: undefined }],
[{ name: "b", type: "A", address: "1.1.1.2", line: undefined }],
[
{
name: "b",
type: "TXT",
address: "some txt",
txtdata: "some txt",
line: undefined,
},
],
[
{
name: "d",
type: "CNAME",
cname: "foobar",
address: "foobar",
line: undefined,
},
],
[{ name: "e", type: "A", address: "2.2.2.2", line: undefined }],
[
{
name: "e",
type: "TXT",
address: "some",
txtdata: "some",
line: undefined,
},
],
[
{
name: "e",
type: "TXT",
address: "extra",
txtdata: "extra",
line: undefined,
},
],
[
{
name: "e",
type: "TXT",
address: "txt",
txtdata: "txt",
line: undefined,
},
],
]);
expect(removeZone).toHaveBeenCalledTimes(1);
expect(removeZone.mock.calls).toEqual([[{ line: 3 }]]);
expect(addRedir).toHaveBeenCalledTimes(1);
expect(addRedir.mock.calls).toEqual([
[
{
domain: "c.booboo.xyz",
redirect: "https://google.com",
redirect_wildcard: 1,
redirect_www: 1,
type: "permanent",
},
],
]);
expect(addEmail).toHaveBeenCalledTimes(1);
expect(addEmail.mock.calls).toEqual([
[{ domain: "b.is-a.dev", exchanger: "somemx", priority: 20 }],
]);
});
});
-167
View File
@@ -1,167 +0,0 @@
const { validateDomainData, isValidDomain } = require("../utils/validations");
const INVALID_NAMES = require("../utils/invalid-domains.json");
const defaultDomain = {
name: "aaa",
record: {
A: ["121.121.121.121"],
},
owner: {
username: "betsy",
email: "betsyfuckyoassup@foobar.com",
},
};
const getstroflen = (len) => Array(len).fill("a").join("");
describe("isValidMX", () => {
it("should be valid mx record", () => {
const cases = [
{ mx: "foobar.com", result: true },
{ mx: "as.as", result: true },
{ mx: "ASPMX.L.GOOGLE.COM", result: true },
{ mx: "ALT4.ASPMX.L.GOOGLE.COM", result: true },
{ mx: "hello", result: false },
{ mx: "helalsds-asd5sjdsd.com", result: true },
{ mx: "helalsds?asd5sjdsd.com", result: false },
{ mx: "helalsds_asd5sjdsd.com", result: false },
];
cases.forEach(({ mx, result }) => {
expect(isValidDomain(mx)).toBe(result);
});
});
});
describe("validateDomainData", () => {
const invalidCases = [
{},
{ name: "helo" },
{ name: "wwow", record: { A: ["12312"] } },
...[
"",
" ",
undefined,
"hlo wld",
"g32++13",
"ajsdD_123yq",
"khsda%",
"122*dsd",
getstroflen(101),
].map((name) => ({
...defaultDomain,
name,
})),
{ ...defaultDomain, record: { CNAME: "sd", A: ["121,3213"] } },
{ ...defaultDomain, record: { A: ["121", "12"], FOOBAR: ["sd"] } },
{ ...defaultDomain, record: { A: [] } },
{ ...defaultDomain, owner: {} },
{ ...defaultDomain, owner: { username: "hwelo" } },
{ ...defaultDomain, owner: { email: "hwelo" } },
{ ...defaultDomain, record: { CNAME: "http://foobar.com" } },
{ ...defaultDomain, record: { CNAME: "https://foobar.com" } },
{ ...defaultDomain, record: { URL: "foobar.com" } },
{
...defaultDomain,
record: { CNAME: "foobar.com", A: ["11.22.22.33"] },
},
{
...defaultDomain,
record: { CNAME: "foobar.com", MX: ["ALT4.ASPMX.L.GOOGLE.COM"] },
},
...INVALID_NAMES.map((name) => ({ ...defaultDomain, name })).slice(
0,
1,
),
{ ...defaultDomain, name: "ww2.baa" },
{ ...defaultDomain, name: "help.baa" },
{ ...defaultDomain, name: "_github-pages-challenge-is-a-dev" },
{ ...defaultDomain, name: "_github-challenge-is-a-dev" },
{ ...defaultDomain, record: { AAAA: [] } },
{ ...defaultDomain, record: { AAAA: ["182.22.222.22", "::1"] } },
{ ...defaultDomain, record: { AAAA: "182.22.222.22" } },
{ ...defaultDomain, record: { A: "::1" } },
{ ...defaultDomain, name: "_discord" },
{ ...defaultDomain, name: "_gitlab-pages-verification-code" },
{ ...defaultDomain, name: "_acme-challenge" },
{ ...defaultDomain, name: "_dmarc" },
{ ...defaultDomain, name: "_gh-is-a-dev" },
{ ...defaultDomain, name: "_domainkey" },
{ ...defaultDomain, name: "_improvmx" },
{ ...defaultDomain, name: "_vercel" },
];
const validCases = [
defaultDomain,
...[
"hello",
"hello-world",
"11111111111",
"--wow--",
"wow--",
"--wow",
].map((name) => ({
...defaultDomain,
name,
})),
{
...defaultDomain,
description: getstroflen(99),
},
{ ...defaultDomain, record: { CNAME: "aa.sd" } },
{ ...defaultDomain, record: { URL: "https://foobar.com" } },
{ ...defaultDomain, record: { URL: "http://foobar.com/foobar/" } },
{ ...defaultDomain, record: { MX: ["ALT4.ASPMX.L.GOOGLE.COM"] } },
{ ...defaultDomain, record: { TXT: "foobar wow nice!!!" } },
{
...defaultDomain,
record: { A: ["1.1.1.1"], MX: ["mx1.example.com"] },
},
{ ...defaultDomain, name: "gogo.foo.bar" },
{ ...defaultDomain, name: "ww9.baa" },
{ ...defaultDomain, name: "_github-pages-challenge-phenax.akshay" },
{ ...defaultDomain, name: "_github-pages-challenge-hello01-ga" },
{ ...defaultDomain, name: "_github-pages-challenge-hello01_ga" },
{ ...defaultDomain, name: "_github-challenge-phenax.akshay" },
{ ...defaultDomain, name: "_github-challenge-hello01-ga" },
{ ...defaultDomain, name: "_github-challenge-hello01_ga" },
{
...defaultDomain,
record: { TXT: ["foobar wow nice!!!", "more text"] },
},
{
...defaultDomain,
record: { AAAA: ["::1", "2001:db8:3333:4444:5555:6666:7777:8888"] },
},
{ ...defaultDomain, record: { A: ["122.222.222.222"] } },
{ ...defaultDomain, name: "_discord.subdomain" },
{ ...defaultDomain, name: "_gitlab-pages-verification-code.subdomain" },
{ ...defaultDomain, name: "_acme-challenge.subdomain" },
{ ...defaultDomain, name: "_dmarc.subdomain" },
{ ...defaultDomain, name: "_gh-phenax.akshay" },
{ ...defaultDomain, name: "_gh-hello01-ga" },
{ ...defaultDomain, name: "_gh-hello01_ga" },
{ ...defaultDomain, name: "_domainkey.subdomain" },
{ ...defaultDomain, name: "mx._domainkey.subdomain" },
{ ...defaultDomain, name: '_improvmx.subdomain' },
{ ...defaultDomain, name: '_vercel.subdomain' },
{ ...defaultDomain, name: "a.b" },
];
it("should return false for invalid data", () => {
invalidCases.forEach((data) => {
const { valid, errors } = validateDomainData(data);
expect(valid).toBe(false);
expect(errors.length).toBeGreaterThan(0);
});
});
it("should return true if the name is valid", () => {
validCases.forEach((data) => {
const { valid, errors } = validateDomainData(data);
if (!valid) console.log(JSON.stringify(errors, null, 2));
expect(valid).toBe(true);
expect(errors).toEqual([]);
});
});
});
-30
View File
@@ -1,30 +0,0 @@
const path = require("path");
const { NODE_ENV: ENV = "test" } = process.env;
const {
DOMAIN_USER,
DOMAIN_API_KEY,
DOMAIN_DOMAIN,
DOMAIN_API_HOST,
DOMAIN_API_PORT,
DOMAIN_HOST_IP,
} = process.env;
const IS_TEST = ENV === "test";
const DOMAINS_PATH = path.resolve("domains");
module.exports = {
ENV,
IS_TEST,
VALID_RECORD_TYPES: ["CNAME", "A", "URL", "MX", "TXT", "AAAA"],
DOMAIN_DOMAIN: DOMAIN_DOMAIN || "booboo.xyz",
DOMAIN_USER: IS_TEST ? "testuser" : DOMAIN_USER,
DOMAIN_API_KEY: IS_TEST ? "testkey" : DOMAIN_API_KEY,
DOMAIN_API_HOST: IS_TEST ? "example.com" : DOMAIN_API_HOST,
DOMAIN_API_PORT: IS_TEST ? 6969 : DOMAIN_API_PORT,
DOMAIN_HOST_IP,
DOMAINS_PATH,
TTL: 5 * 60 * 60,
};
-206
View File
@@ -1,206 +0,0 @@
const R = require("ramda");
const { cpanel } = require("./lib/cpanel");
const { DOMAIN_DOMAIN, VALID_RECORD_TYPES } = require("./constants");
const { then, log, print, lazyTask, batchLazyTasks } = require("./helpers");
const BATCH_SIZE = 1;
const recordToRedirection = ({ name, address }) => ({
domain: name === "@" ? DOMAIN_DOMAIN : `${name}.${DOMAIN_DOMAIN}`,
redirect: address,
type: "permanent",
redirect_wildcard: 1,
redirect_www: 1,
});
const recordToZone = ({ name, type, address, id, priority }) => ({
line: id,
name: name === "@" ? `${DOMAIN_DOMAIN}.` : name,
type,
address,
...(type === "MX" ? { priority } : {}),
...(type === "CNAME" ? { cname: address } : {}),
...(type === "TXT" ? { txtdata: address } : {}),
});
const cleanName = (name) =>
[DOMAIN_DOMAIN, `${DOMAIN_DOMAIN}.`].includes(name)
? "@"
: `${name}`
.replace(new RegExp(`\\.${DOMAIN_DOMAIN}\\.?$`), "")
.toLowerCase();
const zoneToRecord = ({
name,
type,
cname,
address,
priority,
preference,
exchange,
record,
line: id,
}) => ({
id,
name: cleanName(name),
type: `${type}`,
address: `${exchange || cname || address || record}`
.replace(/\.$/g, "")
.toLowerCase(),
priority: priority || preference,
});
const redirectionToRecord = ({ domain, destination }) => ({
id: domain,
name: cleanName(domain),
type: "URL",
address: `${destination}`.replace(/\/$/g, ""),
});
const recordToEmailMx = ({ name, address, priority }) => ({
domain: `${name}.is-a.dev`,
exchanger: address,
priority,
});
const getHostKey = (host) =>
`${host.name.toLowerCase()}##${host.type.toLowerCase()}##${host.address.toLowerCase()}`;
const isReserved = (domain) =>
domain.name.startsWith("*") || !VALID_RECORD_TYPES.includes(domain.type);
const diffRecords = (oldRecords, newRecords) => {
const isMatchingRecord = (a, b) => getHostKey(a) === getHostKey(b);
const remove = R.differenceWith(isMatchingRecord, oldRecords, newRecords);
const add = R.differenceWith(isMatchingRecord, newRecords, oldRecords);
return { add, remove };
};
const executeBatch = (batches) =>
batches.reduce((promise, batch, index) => {
return promise.then(async () => {
log(
">>> Running batch number:",
index + 1,
`(size: ${batch.length})`,
);
const values = await Promise.all(
batch.map((fn) => fn().catch((e) => console.error(e))),
);
const results = values.map((data) =>
R.pathOr({ result: data }, ["cpanelresult", "data", 0], data),
);
const failed = results.filter((x) => (x.result || {}).status != 1);
log(`${values.length - failed.length}/${values.length}`);
failed.length && log(JSON.stringify(failed, null, 2));
return null;
});
}, Promise.resolve());
const getDomainService = ({ cpanel }) => {
const fetchZoneRecords = R.compose(
then(R.reject(isReserved)),
then(R.map(zoneToRecord)),
cpanel.zone.fetch,
);
const fetchRedirections = R.compose(
then(R.map(redirectionToRecord)),
cpanel.redirection.fetch,
);
const addZoneRecord = lazyTask(
R.compose(
R.ifElse(
R.propEq("type", "MX"),
R.compose(cpanel.email.add, recordToEmailMx),
cpanel.zone.add,
),
recordToZone,
print(
(r) => `Adding zone for ${r.name}: (${r.type} ${r.address})...`,
),
),
);
const removeZoneRecord = lazyTask(
R.compose(
R.ifElse(
R.propEq("type", "MX"),
R.compose(cpanel.email.remove, recordToEmailMx),
R.compose(cpanel.zone.remove, R.pick(["line"])),
),
recordToZone,
print(
(r) =>
`Deleting zone for ${r.name}: (${r.type} ${r.address})...`,
),
),
);
const addRedirection = lazyTask(
R.compose(
cpanel.redirection.add,
recordToRedirection,
print(({ name }) => `Adding redirection for ${name}`),
),
);
const removeRedirection = lazyTask(
R.compose(
cpanel.redirection.remove,
R.pick(["domain"]),
recordToRedirection,
print(({ name }) => `Deleting redirection for ${name}`),
),
);
const getHosts = () =>
Promise.all([fetchZoneRecords(), fetchRedirections()]).then(R.flatten);
const addRecords = R.compose(
batchLazyTasks(BATCH_SIZE),
R.filter(Boolean),
R.map(
R.cond([
[R.propEq("type", "URL"), addRedirection],
[R.T, addZoneRecord],
]),
),
);
const removeRecords = R.compose(
batchLazyTasks(BATCH_SIZE),
R.map(
R.cond([
[R.propEq("type", "URL"), removeRedirection],
[R.T, removeZoneRecord],
]),
),
R.sort((a, b) => b.id - a.id),
);
const updateHosts = async (hosts) => {
const remoteHostList = await getHosts();
const { add, remove } = diffRecords(remoteHostList, hosts);
console.log(`Adding ${add.length}; Removing ${remove.length}`);
await executeBatch([...removeRecords(remove), ...addRecords(add)]);
return { added: add.length, removed: remove.length };
};
return {
getHosts,
get: cpanel.zone.fetch,
addZoneRecord,
removeZoneRecord,
updateHosts,
};
};
const domainService = getDomainService({ cpanel });
module.exports = {
getDomainService,
domainService,
diffRecords,
};
-27
View File
@@ -1,27 +0,0 @@
const fs = require("fs");
const path = require("path");
const R = require("ramda");
const { DOMAINS_PATH } = require("./constants");
const toDomain = (str) => path.join(DOMAINS_PATH, str);
const parseDomain = (name) => (str) => {
try {
return JSON.parse(str);
} catch (e) {
throw new Error(`Error: Could not parse ${name} => ${str}`);
}
};
const toDomainData = (name) =>
R.compose(parseDomain(name), R.toString, fs.readFileSync, toDomain)(name);
const getDomains = () =>
fs.promises.readdir(DOMAINS_PATH, {}).then(
R.map((name) => ({
...toDomainData(name),
name: name.replace(/\.json$/, ""),
})),
);
module.exports = { getDomains };
-54
View File
@@ -1,54 +0,0 @@
const R = require("ramda");
const { IS_TEST } = require("./constants");
const log = IS_TEST ? () => {} : console.log;
const print = (fn) => (x) => log(fn(x)) || x;
const between = (min, max) => (num) => num >= min && num <= max;
const testRegex = (regex) => (str) => !!(str && str.match(regex));
const validate = (pattern) => (data) =>
R.compose(
(invalidPairs) =>
invalidPairs.length
? { errors: invalidPairs, valid: false }
: { errors: [], valid: true },
R.filter(([key, { fn }]) => (fn ? !fn(data[key]) : false)),
R.toPairs,
)(pattern);
const or = R.anyPass;
const and = R.allPass;
const then = (fn) => (p) => p.then(fn);
const lazyTask = (fn) => (data) => () => fn(data);
const batchLazyTasks = (count) => (tasks) =>
tasks.reduce((batches, task) => {
if (batches.length === 0) return [[task]];
const full = R.init(batches);
const last = R.last(batches);
if (last.length >= count) return [...batches, [task]];
return [...full, [...last, task]];
}, []);
const withLengthGte = (n) => R.compose(R.gte(R.__, n), R.length);
const withLengthEq = (n) => R.compose(R.equals(n), R.length);
module.exports = {
or,
and,
validate,
between,
testRegex,
log,
print,
then,
lazyTask,
batchLazyTasks,
withLengthEq,
withLengthGte,
};
-27
View File
@@ -1,27 +0,0 @@
[
"0",
"_acme-challenge",
"_discord",
"_dmarc",
"_domainkey",
"_gh-is-a-dev",
"_github-challenge-is-a-dev",
"_github-pages-challenge-is-a-dev",
"_gitlab-pages-verification-code",
"_improvmx",
"_vercel",
"con",
"help",
"no-reply",
"noreply",
"notification",
"notifications",
"support",
"ww",
"ww1",
"ww2",
"ww3",
"ww4",
"wwww",
"your-domain-name"
]
-123
View File
@@ -1,123 +0,0 @@
const R = require("ramda");
const qs = require("querystring");
const {
DOMAIN_API_HOST,
DOMAIN_API_PORT,
DOMAIN_USER,
DOMAIN_API_KEY,
DOMAIN_DOMAIN,
} = require("../constants");
const CpanelClient = (options) => {
const api =
({ basePath = "", action = "" }) =>
(module, func, defaultQuery = {}) =>
(q = {}) => {
const query = {
...defaultQuery,
...q,
cpanel_jsonapi_user: options.username,
cpanel_jsonapi_module: module,
cpanel_jsonapi_func: func,
cpanel_jsonapi_apiversion: 2,
};
const request = {
headers: {
Authorization: `cpanel ${options.username}:${options.apiKey}`,
},
rejectUnauthorized: false,
};
const path = `${basePath}/${action}?${qs.stringify(query)}`;
const reqUrl = `https://${options.host}:${options.port}/${path}`;
const { fetch } = options.dependencies;
return fetch(reqUrl, request).then((res) => res.json());
};
const api2 = api({ basePath: "json-api", action: "cpanel" });
const uapi = (module, func, defaultQuery) =>
api({ basePath: "execute", action: `${module}/${func}` })(
module,
func,
defaultQuery,
);
return {
zone: {
// { customonly, domain }
// -> [{ class, ttl, name, line, Line, cname, type, record }]
fetch: R.compose(
(p) => p.then(R.pathOr([], ["cpanelresult", "data"])),
api2("ZoneEdit", "fetchzone_records", {
customonly: 0,
domain: options.domain,
}),
),
// { name, type(A|CNAME), cname, address, ttl }
// -> {}
add: api2("ZoneEdit", "add_zone_record", {
domain: options.domain,
}),
// { line }
// -> {}
remove: api2("ZoneEdit", "remove_zone_record", {
domain: options.domain,
}),
},
redirection: {
// {}
// -> [{ domain, destination }]
fetch: R.compose(
(p) => p.then(R.pathOr([], ["data"])),
uapi("Mime", "list_redirects"),
),
// { domain, redirect, type(permanent|tmp), redirect_wildcard(0|1), redirect(0|1|2) }
// -> {}
add: uapi("Mime", "add_redirect"),
// { domain }
// -> {}
remove: uapi("Mime", "delete_redirect"),
},
file: {
write: uapi("Fileman", "save_file_content", {
from_charset: "UTF-8",
to_charset: "UTF-8",
fallback: 1,
}),
},
email: {
// { domain, exchanger, priority }
// -> {}
add: uapi("Email", "add_mx", { alwaysaccept: "auto" }),
// { domain, exchanger, priority }
// -> {}
remove: uapi("Email", "delete_mx", { alwaysaccept: "auto" }),
},
};
};
if (!DOMAIN_API_KEY) {
console.error("Api key cannot be empty");
process.exit(1);
}
const cpanel = CpanelClient({
host: DOMAIN_API_HOST,
port: DOMAIN_API_PORT,
username: DOMAIN_USER,
apiKey: DOMAIN_API_KEY,
domain: DOMAIN_DOMAIN,
dependencies: { fetch },
});
module.exports = {
cpanel,
CpanelClient,
};
-129
View File
@@ -1,129 +0,0 @@
const R = require("ramda");
const { VALID_RECORD_TYPES } = require("./constants");
const {
or,
and,
validate,
between,
testRegex,
withLengthEq,
withLengthGte,
} = require("./helpers");
const INVALID_NAMES = require("./invalid-domains.json");
const ipRegex_ = require("ip-regex");
const ipRegex = ipRegex_.default ?? ipRegex_;
const isValidURL = and([R.is(String), testRegex(/^https?:\/\//gi)]);
const isValidDomain = and([
R.is(String),
testRegex(/^(([a-z0-9-_]+)\.)*(([a-z0-9-]+)\.)+[a-z]+$/gi),
]);
const validateCnameRecord = (type) =>
and([
R.propIs(String, type),
R.compose(withLengthEq(1), R.keys), // CNAME cannot be used with any other record
R.propSatisfies(withLengthGte(4), type),
R.propSatisfies(isValidDomain, type),
]);
const validateARecord = (type) =>
and([
R.propIs(Array, type),
R.propSatisfies(withLengthGte(1), type),
R.all(testRegex(ipRegex.v4({ exact: true }))),
]);
const validateMXRecord = (type) =>
and([
R.propIs(Array, type),
R.propSatisfies(withLengthGte(1), type),
R.propSatisfies(R.all(isValidDomain), type),
]);
const validateAAAARecord = R.propSatisfies(
and([
R.is(Array),
withLengthGte(1),
R.all(testRegex(ipRegex.v6({ exact: true }))),
]),
);
const checkRestrictedNames = R.complement(R.includes(R.__, INVALID_NAMES));
const extraSupportedNames = [
testRegex(/^_github(-pages)?-challenge-[a-z0-9-_]+$/i),
R.equals("_discord"),
R.equals("_gitlab-pages-verification-code"),
R.equals("_acme-challenge"),
R.equals("_dmarc"),
R.equals("_domainkey"),
R.equals("_improvmx"),
R.equals("_vercel"),
testRegex(/^_gh-[a-z0-9-_]+$/i),
];
const validateDomainData = validate({
name: {
reason: "The name of the file is invalid. It must be lowercased, alphanumeric and each component must be between 1-100 characters long.",
fn: or([
R.equals("@"),
and([
R.is(String),
checkRestrictedNames,
R.compose(
R.all(
or([
and([
R.compose(between(1, 100), R.length),
testRegex(/^[a-z0-9-]+$/g),
checkRestrictedNames,
]),
...extraSupportedNames,
]),
),
R.split("."),
),
]),
]),
},
description: { reason: "", fn: R.T },
repo: { reason: "", fn: R.T },
owner: {
reason: "`owner` key needs valid username and email properties.",
fn: and([
R.is(Object),
R.complement(R.isEmpty),
R.where({
username: and([R.is(String), withLengthGte(1)]),
email: R.is(String),
}),
]),
},
record: {
reason: "Invalid record(s) found. Please check the record types and values.",
fn: and([
R.is(Object),
R.compose(
R.isEmpty,
R.difference(R.__, VALID_RECORD_TYPES),
R.keys,
),
R.cond([
[R.has("CNAME"), validateCnameRecord("CNAME")],
[R.has("A"), validateARecord("A")],
[R.has("URL"), R.propSatisfies(isValidURL, "URL")],
[R.has("MX"), validateMXRecord("MX")],
[
R.has("TXT"),
R.propSatisfies(or([R.is(String), R.is(Array)]), "TXT"),
],
[R.has("AAAA"), validateAAAARecord("AAAA")],
[R.T, R.T],
]),
]),
},
});
module.exports = { validateDomainData, isValidDomain };