From f74e2cbee3c3d2ee7809499a8d3a7a2eb8b33823 Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Sat, 18 Mar 2023 09:41:20 +0100 Subject: [PATCH] porkbun: cleanup implementation --- src/dns/interface.js | 3 + src/dns/porkbun.js | 170 ++++++++++++++++++++++++++----------------- 2 files changed, 105 insertions(+), 68 deletions(-) diff --git a/src/dns/interface.js b/src/dns/interface.js index 3c6f473ee..6ecda3433 100644 --- a/src/dns/interface.js +++ b/src/dns/interface.js @@ -29,6 +29,7 @@ function injectPrivateFields(newConfig, currentConfig) { // in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER } +// replaces any existing records or creates new records of subdomain+type with values async function upsert(domainObject, subdomain, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); @@ -40,6 +41,7 @@ async function upsert(domainObject, subdomain, type, values) { throw new BoxError(BoxError.NOT_IMPLEMENTED, 'upsert is not implemented'); } +// NOTE: returns empty array when there are no records async function get(domainObject, subdomain, type) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); @@ -50,6 +52,7 @@ async function get(domainObject, subdomain, type) { throw new BoxError(BoxError.NOT_IMPLEMENTED, 'get is not implemented'); } +// must only delete records that match values async function del(domainObject, subdomain, type, values) { assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof subdomain, 'string'); diff --git a/src/dns/porkbun.js b/src/dns/porkbun.js index 473aed355..9239eaa77 100644 --- a/src/dns/porkbun.js +++ b/src/dns/porkbun.js @@ -35,66 +35,11 @@ function injectPrivateFields(newConfig, currentConfig) { if (newConfig.secretapikey === constants.SECRET_PLACEHOLDER) newConfig.secretapikey = currentConfig.secretapikey; } -async function upsert(domainObject, location, type, values) { - assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); +async function getDnsRecords(domainConfig, zoneName, name, type) { + assert.strictEqual(typeof domainConfig, 'object'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof type, 'string'); - assert(Array.isArray(values)); - - const domainConfig = domainObject.config, - zoneName = domainObject.zoneName, - name = dns.getName(domainObject, location, type) || ''; - - debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`); - - const records = await get(domainObject, location, type); - - const data = { - secretapikey: domainConfig.secretapikey, - apikey: domainConfig.apikey, - ttl: 10, // min seems to be 600 anyway - content: values[0] // for mx records, value is already of the ' ' format - }; - - let url; - if (records.length) { - url = `${PORKBUN_API}/editByNameType/${zoneName}/${type}/${name}`; - } else { // create - url = `${PORKBUN_API}/create/${zoneName}`; - data.type = type; - data.name = name; - } - - if (type === 'MX') { - data.prio = values[0].split(' ')[0]; // string - data.content = values[0].split(' ')[1]; - } else if (type === 'TXT') { - data.content = values[0].startsWith('"') && values[0].endsWith('"') ? values[0].slice(1, values[0].length-1) : values[0]; - } else { - data.content = values[0]; - } - - const [error, response] = await safe(superagent.post(url) - .timeout(30 * 1000) - .send(data) - .ok(() => true)); - - if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); - if (response.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - if (response.body.status !== 'SUCCESS') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid status in response: ${JSON.stringify(response.body)}`); - if (records.length === 0 && !response.body.id) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid id in response: ${JSON.stringify(response.body)}`); // only for create - - debug(`upsert: created record with id ${response.body.id}`); -} - -async function get(domainObject, location, type) { - assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof location, 'string'); - assert.strictEqual(typeof type, 'string'); - - const domainConfig = domainObject.config, - zoneName = domainObject.zoneName, - name = dns.getName(domainObject, location, type) || ''; debug(`get: ${name} in zone ${zoneName} of type ${type}`); @@ -113,7 +58,90 @@ async function get(domainObject, location, type) { if (response.body.status !== 'SUCCESS') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid status in response: ${JSON.stringify(response.body)}`); if (!Array.isArray(response.body.records)) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid records in response: ${JSON.stringify(response.body)}`); - return response.body.records.map(r => r.content); + // TXT records are returned without quoting. When no records match, it returns empty records array + return response.body.records; +} + +async function delDnsRecords(domainConfig, zoneName, name, type) { + assert.strictEqual(typeof domainConfig, 'object'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof type, 'string'); + + const data = { + secretapikey: domainConfig.secretapikey, + apikey: domainConfig.apikey, + }; + + // deletes all the records matching type+name + const [error, response] = await safe(superagent.post(`${PORKBUN_API}/deleteByNameType/${zoneName}/${type}/${name}`) + .send(data) + .timeout(30 * 1000) + .ok(() => true)); + + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 400) return; // not found, "Could not delete record." + if (response.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); + if (response.body.status !== 'SUCCESS') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid status in response: ${JSON.stringify(response.body)}`); +} + +async function upsert(domainObject, location, type, values) { + assert.strictEqual(typeof domainObject, 'object'); + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof type, 'string'); + assert(Array.isArray(values)); + + const domainConfig = domainObject.config, + zoneName = domainObject.zoneName, + name = dns.getName(domainObject, location, type) || ''; + + debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`); + + await delDnsRecords(domainConfig, zoneName, name, type); + + for (const value of values) { + const data = { + secretapikey: domainConfig.secretapikey, + apikey: domainConfig.apikey, + ttl: '10', // 600 seems to be minimum anyway + type, + name + }; + + if (type === 'MX') { + data.prio = value.split(' ')[0]; // string + data.content = value.split(' ')[1]; + } else if (type === 'TXT') { // strip quotes + data.content = value.startsWith('"') && value.endsWith('"') ? value.slice(1, value.length-1) : value; + } else { + data.content = value; + } + + const [error, response] = await safe(superagent.post(`${PORKBUN_API}/create/${zoneName}`) + .timeout(30 * 1000) + .send(data) + .ok(() => true)); + + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); + if (response.body.status !== 'SUCCESS') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid status in response: ${JSON.stringify(response.body)}`); + if (!response.body.id) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid id in response: ${JSON.stringify(response.body)}`); + + debug(`upsert: created record with id ${response.body.id}`); + } +} + +async function get(domainObject, location, type) { + assert.strictEqual(typeof domainObject, 'object'); + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof type, 'string'); + + const domainConfig = domainObject.config, + zoneName = domainObject.zoneName, + name = dns.getName(domainObject, location, type) || ''; + + const records = await getDnsRecords(domainConfig, zoneName, name, type); + return records.map(r => r.content); } async function del(domainObject, location, type, values) { @@ -133,15 +161,21 @@ async function del(domainObject, location, type, values) { apikey: domainConfig.apikey, }; - const [error, response] = await safe(superagent.post(`${PORKBUN_API}/deleteByNameType/${zoneName}/${type}/${name}`) - .send(data) - .timeout(30 * 1000) - .ok(() => true)); + // note that deleteByNameType deletes all the records matching type+name. we only want to delete records matching values + const records = await getDnsRecords(domainConfig, zoneName, name, type); + const ids = records.filter(r => values.includes(r.content)).map(r => r.id); - if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); - if (response.statusCode === 400) return; // not found! - if (response.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); - if (response.body.status !== 'SUCCESS') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid status in response: ${JSON.stringify(response.body)}`); + for (const id of ids) { + const [error, response] = await safe(superagent.post(`${PORKBUN_API}/delete/${zoneName}/${id}`) + .send(data) + .timeout(30 * 1000) + .ok(() => true)); + + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.statusCode === 400) continue; // not found! "Invalid record id." + if (response.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response)); + if (response.body.status !== 'SUCCESS') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid status in response: ${JSON.stringify(response.body)}`); + } } async function wait(domainObject, subdomain, type, value, options) {