Skip to content

Commit

Permalink
🚧 WIP: :arrow: Update to cloudflare 3
Browse files Browse the repository at this point in the history
  • Loading branch information
SteffenKn committed Mar 25, 2024
1 parent 787ca06 commit 60f6bc0
Show file tree
Hide file tree
Showing 15 changed files with 71 additions and 175 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cloudflare-ddns-sync",
"version": "3.0.2",
"version": "4.0.0-beta.1",
"description": "A simple module to update DNS records on Cloudflare whenever you want",
"main": "dist/index.js",
"author": "Steffen Knaup <SteffenKnaup@hotmail.de>",
Expand Down Expand Up @@ -29,7 +29,7 @@
"test:coverage-report": "c8 report"
},
"dependencies": {
"cloudflare": "3.0.0-beta.11",
"cloudflare": "3.0.0-beta.12",
"node-cron": "3.0.3",
"parse-domain": "8.0.2",
"public-ip": "6.0.2"
Expand Down
40 changes: 19 additions & 21 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {ScheduledTask} from 'node-cron';

import CloudflareClient from './lib/cloudflare-client.js';
import Cron from './lib/cron.js';
import ipUtils from './lib/ip-utils.js';

import {Auth, DomainRecordList, MultiSyncCallback, Record, RecordData} from './types/index.js';
import {Auth, MultiSyncCallback, Record} from './types/index.js';

export default class CloudflareDDNSSync {
private cloudflareClient: CloudflareClient;
Expand All @@ -13,68 +11,68 @@ export default class CloudflareDDNSSync {
this.cloudflareClient = new CloudflareClient(auth);
}

public getIp(): Promise<string> {
public getIp() {
return ipUtils.getIpv4();
}

public getIpv6(): Promise<string> {
public getIpv6() {
return ipUtils.getIpv6();
}

public getRecordDataForDomain(domain: string): Promise<Array<RecordData>> {
public getRecordDataForDomain(domain: string) {
return this.cloudflareClient.getRecordsByDomain(domain);
}

public getRecordDataForDomains(domains: Array<string>): Promise<DomainRecordList> {
public getRecordDataForDomains(domains: Array<string>) {
return this.cloudflareClient.getRecordsByDomains(domains);
}

public getRecordDataForRecord(record: Record): Promise<RecordData> {
public getRecordDataForRecord(record: Record) {
return this.cloudflareClient.getRecordDataForRecord(record);
}

public getRecordDataForRecords(records: Array<Record>): Promise<Array<RecordData>> {
public getRecordDataForRecords(records: Array<Record>) {
return this.cloudflareClient.getRecordDataForRecords(records);
}

public removeRecord(recordName: string, recordType?: string): Promise<void> {
public removeRecord(recordName: string, recordType?: string) {
return this.cloudflareClient.removeRecordByNameAndType(recordName, recordType);
}

public stopSyncOnIpChange(changeListenerId: string): void {
public stopSyncOnIpChange(changeListenerId: string) {
ipUtils.removeIpChangeListener(changeListenerId);
}

public syncByCronTime(cronExpression: string, records: Array<Record>, callback: MultiSyncCallback, ip?: string): ScheduledTask {
return Cron.createCronJob(cronExpression, async (): Promise<void> => {
const result: Array<RecordData> = await this.syncRecords(records, ip);
public syncByCronTime(cronExpression: string, records: Array<Record>, callback: MultiSyncCallback, ip?: string) {
return Cron.createCronJob(cronExpression, async () => {
const result = await this.syncRecords(records, ip);

callback(result);
});
}

public async syncOnIpChange(records: Array<Record>, callback: MultiSyncCallback): Promise<string> {
const changeListenerId = await ipUtils.addIpChangeListener(async (ip: string): Promise<void> => {
const result: Array<RecordData> = await this.syncRecords(records, ip);
public async syncOnIpChange(records: Array<Record>, callback: MultiSyncCallback) {
const changeListenerId = await ipUtils.addIpChangeListener(async (ip: string) => {
const result = await this.syncRecords(records, ip);

callback(result);
});

// Sync records to make sure the current ip is already set.
const currentIp = await ipUtils.getIpv4();
this.syncRecords(records, currentIp).then((syncedRecords: Array<RecordData>): void => {
this.syncRecords(records, currentIp).then((syncedRecords) => {
callback(syncedRecords);
});

return changeListenerId;
}

public syncRecord(record: Record, ip?: string): Promise<RecordData> {
public syncRecord(record: Record, ip?: string) {
return this.cloudflareClient.syncRecord(record, ip) as any;
}

public syncRecords(records: Array<Record>, ip?: string): Promise<Array<RecordData>> {
return this.cloudflareClient.syncRecords(records, ip) as any;
public syncRecords(records: Array<Record>, ip?: string) {
return this.cloudflareClient.syncRecords(records, ip);
}
}

Expand Down
42 changes: 14 additions & 28 deletions src/lib/cloudflare-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {ParseResultType, fromUrl, parseDomain} from 'parse-domain';
import Cloudflare, {ClientOptions as CloudflareOptions} from 'cloudflare';

import {DomainRecordList, Record, RecordData, ZoneMap} from '../types/index.js';
import {Record, ZoneMap} from '../types/index.js';
import IPUtils from './ip-utils.js';

const ipv4Regex = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/u;
Expand Down Expand Up @@ -104,7 +104,7 @@ export default class CloudflareClient {

const recordDataForDomains = await Promise.all(recordDataPromises);

const recordData: DomainRecordList = {};
const recordData = {};
recordDataForDomains.forEach((recordDataForDomain, index) => {
recordData[domains[index]] = recordDataForDomain;
});
Expand All @@ -115,34 +115,20 @@ export default class CloudflareClient {
public async getRecordsByDomain(domain: string) {
const zoneId = await this.getZoneIdByDomain(domain);

const records: Array<RecordData> = [];

let pageIndex = 1;
let allRecordsFound = false;
const recordsPerPage = 5000;

while (!allRecordsFound) {
const response = (await (this.cloudflare as any).dnsRecords.browse(zoneId, {
page: pageIndex,
per_page: recordsPerPage,
})) as {result: Array<RecordData>};

records.push(...response.result);

allRecordsFound = response.result.length < recordsPerPage;

pageIndex++;
const allRecords: Cloudflare.DNS.Records.DNSRecord[] = [];
for await (const recordListResponse of this.cloudflare.dns.records.list({zone_id: zoneId})) {
allRecords.push(recordListResponse);
}

return records;
return allRecords;
}

private async createRecord(zoneId: string, recordData: Record, ip?: string) {
const dnsRecordToCreate = {
const dnsRecordToCreate: Cloudflare.DNS.RecordCreateParams = {
...recordData,
name: recordData.name.toLowerCase(),
content: recordData.content ? recordData.content : ip,
type: recordData.type ? recordData.type : 'A',
content: (recordData.content ? recordData.content : ip) as string,
type: (recordData.type ? recordData.type : 'A') as any,
ttl: recordData.ttl ? recordData.ttl : 1,
zone_id: zoneId,
};
Expand All @@ -160,7 +146,7 @@ export default class CloudflareClient {
throw Error(`Could not create Record "${dnsRecordToCreate.name}": '${dnsRecordToCreate.content}' is not a valid ipv6!`);
}
} else if (dnsRecordToCreate.type === 'CNAME') {
const parsedDomain = parseDomain(fromUrl(dnsRecordToCreate.content));
const parsedDomain = parseDomain(fromUrl(dnsRecordToCreate.content as string));
if (parsedDomain.type !== ParseResultType.Listed || !parsedDomain.domain) {
throw Error(`Could not create Record "${dnsRecordToCreate.name}": '${dnsRecordToCreate.content}' is not a valid domain name!`);
}
Expand All @@ -170,11 +156,11 @@ export default class CloudflareClient {
}

private async updateRecord(zoneId: string, recordId: string, recordData: Record, ip?: string) {
const updatedDnsRecord = {
const updatedDnsRecord: Cloudflare.DNS.RecordCreateParams = {
...recordData,
name: recordData.name.toLowerCase(),
content: recordData.content ? recordData.content : ip,
type: recordData.type ? recordData.type : 'A',
content: (recordData.content ? recordData.content : ip) as string,
type: (recordData.type ? recordData.type : 'A') as any,
ttl: recordData.ttl ? recordData.ttl : 1,
zone_id: zoneId,
};
Expand All @@ -192,7 +178,7 @@ export default class CloudflareClient {
throw Error(`Could not update Record "${updatedDnsRecord.name}": '${updatedDnsRecord.content}' is not a valid ipv6!`);
}
} else if (updatedDnsRecord.type === 'CNAME') {
const parsedDomain = parseDomain(fromUrl(updatedDnsRecord.content));
const parsedDomain = parseDomain(fromUrl(updatedDnsRecord.content as string));
if (parsedDomain.type !== ParseResultType.Listed || !parsedDomain.domain) {
throw Error(`Could not update Record "${updatedDnsRecord.name}": '${updatedDnsRecord.content}' is not a valid domain name!`);
}
Expand Down
20 changes: 10 additions & 10 deletions src/tests/cloudflare-client-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import CloudflareClient from '../lib/cloudflare-client.js';
import IPUtils from '../lib/ip-utils.js';
import TestService from './test-service/test-service.js';

import {Record, RecordData} from '../types/index.js';
import {Record} from '../types/index.js';

const cloudflareClient: CloudflareClient = new CloudflareClient({
email: TestService.getTestData().auth.email,
key: TestService.getTestData().auth.key,
token: TestService.getTestData().auth.token,
apiEmail: TestService.getTestData().auth.apiEmail,
apiKey: TestService.getTestData().auth.apiKey,
apiToken: TestService.getTestData().auth.apiToken,
});

const recordsToCleanUp: Array<Record> = [];
Expand Down Expand Up @@ -67,7 +67,7 @@ describe('Cloudflare Client', (): void => {

const recordData = await cloudflareClient.getRecordDataForRecords(records);

const recordDataNames = recordData.map((recordDataEntry: RecordData): string => recordDataEntry.name.toLowerCase());
const recordDataNames = recordData.map((recordDataEntry): string => recordDataEntry.name.toLowerCase());

expect(recordData.length).to.equal(records.length);

Expand Down Expand Up @@ -102,7 +102,7 @@ describe('Cloudflare Client', (): void => {

const recordData = await cloudflareClient.syncRecords(records);

const recordDataNames = recordData.map((singleRecordData: RecordData): string => singleRecordData.name.toLowerCase());
const recordDataNames = recordData.map((singleRecordData): string => singleRecordData.name.toLowerCase());

for (const record of records) {
expect(recordDataNames).to.contain(record.name.toLowerCase());
Expand Down Expand Up @@ -171,9 +171,9 @@ describe('Cloudflare Client', (): void => {
await cloudflareClient.syncRecords(records, '1.2.3.4');
// Prepare END

const recordData = await cloudflareClient.getRecordDataForDomain(domain);
const recordData = await cloudflareClient.getRecordsByDomain(domain);

const recordDataNames = recordData.map((recordDataEntry: RecordData): string => recordDataEntry.name.toLowerCase());
const recordDataNames = recordData.map((recordDataEntry): string => recordDataEntry.name.toLowerCase());

// At least the data of the synced records should be existing
expect(recordData.length).to.be.greaterThan(records.length - 1);
Expand All @@ -194,11 +194,11 @@ describe('Cloudflare Client', (): void => {
await cloudflareClient.syncRecords(records, '1.2.3.4');
// Prepare END

const domainRecordList = await cloudflareClient.getRecordDataForDomains([domain]);
const domainRecordList = await cloudflareClient.getRecordsByDomains([domain]);

expect(Object.keys(domainRecordList)).to.contain(domain);

const recordDataNames = domainRecordList[domain].map((recordDataEntry: RecordData): string => recordDataEntry.name.toLowerCase());
const recordDataNames = domainRecordList[domain].map((recordDataEntry): string => recordDataEntry.name.toLowerCase());

// At least the data of the synced records should be existing
expect(domainRecordList[domain].length).to.be.greaterThan(records.length - 1);
Expand Down
37 changes: 16 additions & 21 deletions src/tests/test-service/test-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,13 @@ export default class TestService {
public static getTestData(): TestData {
const args: ParsedArgs = minimist(process.argv.slice(2));

const email = args.email ? args.email : testConfig.auth.email;
const key = args.key ? args.key : testConfig.auth.key;
const token = args.token ? args.token : testConfig.auth.token;
const cloudflareEmail = args.email ? args.email : testConfig.auth.email;
const cloudflareApiKey = args.key ? args.key : testConfig.auth.key;
const cloudflareApiToken = args.token ? args.token : testConfig.auth.token;
const domain = args.domain ? args.domain : testConfig.domain;

const testData: TestData = {
auth: {
email: email,
key: key,
token: token,
},
domain: domain,
records: this.getRandomRecords(5, domain),
};

const testDataNotProvided =
!testData.auth.email ||
testData.auth.email === 'your@email.com' ||
!testData.auth.key ||
testData.auth.key === 'your_cloudflare_api_key' ||
!testData.domain ||
testData.domain === 'yourdomain.com';
!cloudflareEmail || cloudflareEmail === 'your@email.com' || !cloudflareApiKey || cloudflareApiKey === 'your_cloudflare_api_key' || !domain || domain === 'yourdomain.com';

if (testDataNotProvided) {
console.error(
Expand All @@ -40,14 +25,24 @@ export default class TestService {
process.exit();
}

const testData: TestData = {
auth: {
apiEmail: cloudflareEmail,
apiKey: cloudflareApiKey,
apiToken: cloudflareApiToken,
},
domain: domain,
records: this.getRandomRecords(5, domain),
};

return testData;
}

private static getRandomRecords(amount: number, domain: string): Array<Record> {
const records: Array<Record> = [];
const records = [];

for (let index = 0; index < amount; index++) {
const record: Record = {
const record = {
name: `cddnss-test-${this.getRandomSubdomain()}.${domain}`,
};

Expand Down
3 changes: 0 additions & 3 deletions src/types/Auth.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/types/Callbacks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {RecordData} from './index.js';
import Cloudflare from 'cloudflare';

export type SingleSyncCallback = (syncResult: RecordData) => void;
export type MultiSyncCallback = (syncResult: Array<RecordData>) => void;
export type MultiSyncCallback = (syncResult: Cloudflare.DNS.DNSRecord[]) => void;
3 changes: 3 additions & 0 deletions src/types/CloudflareOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {ClientOptions} from 'cloudflare';

export type Auth = ClientOptions;
3 changes: 0 additions & 3 deletions src/types/DomainRecordList.ts

This file was deleted.

13 changes: 2 additions & 11 deletions src/types/Record.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
import {Record as CloudflareRecordTypes} from 'cloudflare';
import {Cloudflare} from 'cloudflare';

export type RecordTypes = Exclude<CloudflareRecordTypes, 'MX' | 'SRV' | 'URI'>;

export type Record = {
name: string;
type?: RecordTypes;
proxied?: boolean;
ttl?: number;
priority?: number;
content?: string;
};
export type Record = Cloudflare.DNS.DNSRecord;
23 changes: 0 additions & 23 deletions src/types/cloudflare/RecordData.ts

This file was deleted.

Loading

0 comments on commit 60f6bc0

Please sign in to comment.