diff --git a/.env.example b/.env.example deleted file mode 100644 index 8022d3f4e..000000000 --- a/.env.example +++ /dev/null @@ -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 diff --git a/.envrc b/.envrc deleted file mode 100644 index 4a4726a5c..000000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use_nix diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml deleted file mode 100644 index be5f73e48..000000000 --- a/.github/workflows/checks.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/publish-records.yml b/.github/workflows/publish-records.yml deleted file mode 100644 index 40e58b3ea..000000000 --- a/.github/workflows/publish-records.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..8577075f3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -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" \ No newline at end of file diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 000000000..b49211b69 --- /dev/null +++ b/.github/workflows/validate.yml @@ -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/" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 990a6e0b2..000000000 --- a/Dockerfile +++ /dev/null @@ -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"] - diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 502212010..000000000 Binary files a/bun.lockb and /dev/null differ diff --git a/default.nix b/default.nix deleted file mode 100644 index bc0f66aa3..000000000 --- a/default.nix +++ /dev/null @@ -1,15 +0,0 @@ -{ nixpkgs ? import {} }: -let - inherit (nixpkgs) pkgs; - - nixPackages = with pkgs; [ - yarn - docker-compose - dnsutils - bun - ]; -in -pkgs.stdenv.mkDerivation { - name = "env"; - buildInputs = nixPackages; -} diff --git a/dnsconfig.js b/dnsconfig.js new file mode 100644 index 000000000..f053b8b4b --- /dev/null +++ b/dnsconfig.js @@ -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]); +} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 5b161f929..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: '3' -services: - dev: - build: - context: . - dockerfile: ./Dockerfile - volumes: - - ./:/opt/app/code diff --git a/package.json b/package.json deleted file mode 100644 index ccc8dbd41..000000000 --- a/package.json +++ /dev/null @@ -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 ", - "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" - } -} diff --git a/scripts/action-utils.js b/scripts/action-utils.js deleted file mode 100644 index 38a33a420..000000000 --- a/scripts/action-utils.js +++ /dev/null @@ -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); - }, -}; diff --git a/scripts/certbot-auth.sh b/scripts/certbot-auth.sh deleted file mode 100755 index 6b4ee8bc7..000000000 --- a/scripts/certbot-auth.sh +++ /dev/null @@ -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; - diff --git a/scripts/certbot.sh b/scripts/certbot.sh deleted file mode 100755 index ee84aa6d7..000000000 --- a/scripts/certbot.sh +++ /dev/null @@ -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 - diff --git a/scripts/register-domains.js b/scripts/register-domains.js deleted file mode 100644 index 94bc02089..000000000 --- a/scripts/register-domains.js +++ /dev/null @@ -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 }; -} diff --git a/scripts/reply.js b/scripts/reply.js deleted file mode 100644 index dd4df142d..000000000 --- a/scripts/reply.js +++ /dev/null @@ -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! - -Buy Me A Coffee Liberapay recurring donation button - -`; - -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(), - }); - }, -}; diff --git a/scripts/verify-record.sh b/scripts/verify-record.sh deleted file mode 100644 index a4fc0da6a..000000000 --- a/scripts/verify-record.sh +++ /dev/null @@ -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; - diff --git a/tests/cpanel.test.js b/tests/cpanel.test.js deleted file mode 100644 index b2415f8b4..000000000 --- a/tests/cpanel.test.js +++ /dev/null @@ -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" }); - }); - }); -}); diff --git a/tests/domain-service.test.js b/tests/domain-service.test.js deleted file mode 100644 index dfd6400ed..000000000 --- a/tests/domain-service.test.js +++ /dev/null @@ -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}` }, - ]); - }); - }); -}); diff --git a/tests/domain-utils.test.js b/tests/domain-utils.test.js deleted file mode 100644 index 896cf017d..000000000 --- a/tests/domain-utils.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/tests/domains.test.js b/tests/domains.test.js deleted file mode 100644 index 75971b00b..000000000 --- a/tests/domains.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/tests/register.test.js b/tests/register.test.js deleted file mode 100644 index 6628df211..000000000 --- a/tests/register.test.js +++ /dev/null @@ -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 }], - ]); - }); -}); diff --git a/tests/validations.test.js b/tests/validations.test.js deleted file mode 100644 index af27b68bb..000000000 --- a/tests/validations.test.js +++ /dev/null @@ -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([]); - }); - }); -}); diff --git a/utils/constants.js b/utils/constants.js deleted file mode 100644 index b0cf0079d..000000000 --- a/utils/constants.js +++ /dev/null @@ -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, -}; diff --git a/utils/domain-service.js b/utils/domain-service.js deleted file mode 100644 index b76eeac01..000000000 --- a/utils/domain-service.js +++ /dev/null @@ -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, -}; diff --git a/utils/get-domain.js b/utils/get-domain.js deleted file mode 100644 index 064240358..000000000 --- a/utils/get-domain.js +++ /dev/null @@ -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 }; diff --git a/utils/helpers.js b/utils/helpers.js deleted file mode 100644 index 03369f2b7..000000000 --- a/utils/helpers.js +++ /dev/null @@ -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, -}; diff --git a/utils/invalid-domains.json b/utils/invalid-domains.json deleted file mode 100644 index 736faa9bd..000000000 --- a/utils/invalid-domains.json +++ /dev/null @@ -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" -] diff --git a/utils/lib/cpanel.js b/utils/lib/cpanel.js deleted file mode 100644 index 573a22e5e..000000000 --- a/utils/lib/cpanel.js +++ /dev/null @@ -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, -}; diff --git a/utils/validations.js b/utils/validations.js deleted file mode 100644 index eea099cd4..000000000 --- a/utils/validations.js +++ /dev/null @@ -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 };