From 610f9f16ff2c0b53a382e86e9696817e95e9c428 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 14:09:57 +0300 Subject: [PATCH 01/10] Add deleteRecordsByIpAddress method --- .../src/AutoCertifierServer.ts | 1 + .../autocertifier-server/src/Route53Api.ts | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/autocertifier-server/src/AutoCertifierServer.ts b/packages/autocertifier-server/src/AutoCertifierServer.ts index 60c87829d4..6d051926dd 100644 --- a/packages/autocertifier-server/src/AutoCertifierServer.ts +++ b/packages/autocertifier-server/src/AutoCertifierServer.ts @@ -144,6 +144,7 @@ export class AutoCertifierServer implements RestInterface, ChallengeManager { const fqdn = subdomain + '.' + this.domainName if (this.route53Api !== undefined) { + await this.route53Api.deleteRecordsByIpAddress(ipAddress) await this.route53Api.upsertRecord(RRType.A, fqdn, ipAddress, 300) } diff --git a/packages/autocertifier-server/src/Route53Api.ts b/packages/autocertifier-server/src/Route53Api.ts index f431acb2e7..737df06005 100644 --- a/packages/autocertifier-server/src/Route53Api.ts +++ b/packages/autocertifier-server/src/Route53Api.ts @@ -70,4 +70,39 @@ export class Route53Api { const response = await this.client.send(command) return response } + + // Query all records that point to a specific IP address + public async getRecordsByIpAddress(ipAddress: string): Promise { + const response = await this.listRecords() + const matchingRecords: Record[] = [] + + if (response.ResourceRecordSets) { + for (const recordSet of response.ResourceRecordSets) { + // Only check A records (which contain IP addresses) + if (recordSet.Type === RRType.A && recordSet.ResourceRecords) { + for (const resourceRecord of recordSet.ResourceRecords) { + if (resourceRecord.Value === ipAddress) { + matchingRecords.push({ + fqdn: recordSet.Name || '', + value: resourceRecord.Value + }) + } + } + } + } + } + + return matchingRecords + } + + // Remove all A records that point to a specific IP address + public async deleteRecordsByIpAddress(ipAddress: string, ttl: number = 300): Promise { + const recordsToDelete = await this.getRecordsByIpAddress(ipAddress) + + if (recordsToDelete.length === 0) { + return null // No records found to delete + } + + return this.changeRecords(ChangeAction.DELETE, RRType.A, recordsToDelete, ttl) + } } From eafab48d4745735c834e269d7f7c06abdbc35656 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 14:28:12 +0300 Subject: [PATCH 02/10] do deletion in the correct place --- packages/autocertifier-server/src/AutoCertifierServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/autocertifier-server/src/AutoCertifierServer.ts b/packages/autocertifier-server/src/AutoCertifierServer.ts index 6d051926dd..727a98d375 100644 --- a/packages/autocertifier-server/src/AutoCertifierServer.ts +++ b/packages/autocertifier-server/src/AutoCertifierServer.ts @@ -116,6 +116,7 @@ export class AutoCertifierServer implements RestInterface, ChallengeManager { const fqdn = subdomain + '.' + this.domainName if (this.route53Api !== undefined) { + await this.route53Api.deleteRecordsByIpAddress(ipAddress) await this.route53Api.upsertRecord(RRType.A, fqdn, ipAddress, 300) } @@ -144,7 +145,6 @@ export class AutoCertifierServer implements RestInterface, ChallengeManager { const fqdn = subdomain + '.' + this.domainName if (this.route53Api !== undefined) { - await this.route53Api.deleteRecordsByIpAddress(ipAddress) await this.route53Api.upsertRecord(RRType.A, fqdn, ipAddress, 300) } From e84421b3dda04235a8016c8360f7c2b45158141b Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 14:32:04 +0300 Subject: [PATCH 03/10] add logging --- packages/autocertifier-server/src/Route53Api.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/autocertifier-server/src/Route53Api.ts b/packages/autocertifier-server/src/Route53Api.ts index 737df06005..e33442007b 100644 --- a/packages/autocertifier-server/src/Route53Api.ts +++ b/packages/autocertifier-server/src/Route53Api.ts @@ -7,11 +7,14 @@ import { RRType, ChangeResourceRecordSetsCommandOutput } from '@aws-sdk/client-route-53' +import { Logger } from '@streamr/utils' interface Record { fqdn: string value: string } + +const logger = new Logger(module) export class Route53Api { private hostedZoneId: string @@ -98,7 +101,7 @@ export class Route53Api { // Remove all A records that point to a specific IP address public async deleteRecordsByIpAddress(ipAddress: string, ttl: number = 300): Promise { const recordsToDelete = await this.getRecordsByIpAddress(ipAddress) - + logger.info('deleting records by ip address: ' + ipAddress, { recordsToDelete }) if (recordsToDelete.length === 0) { return null // No records found to delete } From 59c5bbf43e53879a8175801cdcce3c58bb623c20 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 14:36:41 +0300 Subject: [PATCH 04/10] pagination handling --- .../autocertifier-server/src/Route53Api.ts | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/packages/autocertifier-server/src/Route53Api.ts b/packages/autocertifier-server/src/Route53Api.ts index e33442007b..7e0f82de3e 100644 --- a/packages/autocertifier-server/src/Route53Api.ts +++ b/packages/autocertifier-server/src/Route53Api.ts @@ -74,21 +74,57 @@ export class Route53Api { return response } - // Query all records that point to a specific IP address + // List all records in a zone with pagination support + public async listAllRecords(): Promise { + const allResponses: ListResourceRecordSetsCommandOutput[] = [] + let startRecordName: string | undefined + let startRecordType: RRType | undefined + let isTruncated = true + + while (isTruncated) { + const input: any = { + HostedZoneId: this.hostedZoneId, + } + + if (startRecordName) { + input.StartRecordName = startRecordName + } + if (startRecordType) { + input.StartRecordType = startRecordType + } + + const command = new ListResourceRecordSetsCommand(input) + const response = await this.client.send(command) + + allResponses.push(response) + + isTruncated = response.IsTruncated || false + if (isTruncated) { + startRecordName = response.NextRecordName + startRecordType = response.NextRecordType as RRType + } + } + + return allResponses + } + + // Query all records that point to a specific IP address (with pagination support) public async getRecordsByIpAddress(ipAddress: string): Promise { - const response = await this.listRecords() + const allResponses = await this.listAllRecords() const matchingRecords: Record[] = [] - if (response.ResourceRecordSets) { - for (const recordSet of response.ResourceRecordSets) { - // Only check A records (which contain IP addresses) - if (recordSet.Type === RRType.A && recordSet.ResourceRecords) { - for (const resourceRecord of recordSet.ResourceRecords) { - if (resourceRecord.Value === ipAddress) { - matchingRecords.push({ - fqdn: recordSet.Name || '', - value: resourceRecord.Value - }) + for (const response of allResponses) { + if (response.ResourceRecordSets) { + for (const recordSet of response.ResourceRecordSets) { + // Only check A records (which contain IP addresses) + if (recordSet.Type === RRType.A && recordSet.ResourceRecords) { + for (const resourceRecord of recordSet.ResourceRecords) { + if (resourceRecord.Value === ipAddress) { + matchingRecords.push({ + fqdn: recordSet.Name || '', + value: resourceRecord.Value + }) + } } } } From 82a1556d0da3e731d9d9b2a9c5d23d4eabfec942 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 17:21:01 +0300 Subject: [PATCH 05/10] eslint --- packages/autocertifier-server/src/Route53Api.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/autocertifier-server/src/Route53Api.ts b/packages/autocertifier-server/src/Route53Api.ts index 7e0f82de3e..92772af1a5 100644 --- a/packages/autocertifier-server/src/Route53Api.ts +++ b/packages/autocertifier-server/src/Route53Api.ts @@ -98,10 +98,10 @@ export class Route53Api { allResponses.push(response) - isTruncated = response.IsTruncated || false + isTruncated = response.IsTruncated ?? false if (isTruncated) { startRecordName = response.NextRecordName - startRecordType = response.NextRecordType as RRType + startRecordType = response.NextRecordType } } @@ -121,7 +121,7 @@ export class Route53Api { for (const resourceRecord of recordSet.ResourceRecords) { if (resourceRecord.Value === ipAddress) { matchingRecords.push({ - fqdn: recordSet.Name || '', + fqdn: recordSet.Name ?? '', value: resourceRecord.Value }) } From ec66534c47bbe5f6540e13cd3de9ef951c41ca74 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 20:01:42 +0300 Subject: [PATCH 06/10] remove domains from route53 based on ip and port combination --- .../src/AutoCertifierServer.ts | 6 +++++- packages/autocertifier-server/src/Database.ts | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/autocertifier-server/src/AutoCertifierServer.ts b/packages/autocertifier-server/src/AutoCertifierServer.ts index 727a98d375..da64c66572 100644 --- a/packages/autocertifier-server/src/AutoCertifierServer.ts +++ b/packages/autocertifier-server/src/AutoCertifierServer.ts @@ -116,7 +116,11 @@ export class AutoCertifierServer implements RestInterface, ChallengeManager { const fqdn = subdomain + '.' + this.domainName if (this.route53Api !== undefined) { - await this.route53Api.deleteRecordsByIpAddress(ipAddress) + const subdomains = await this.database!.getSubdomainsByIpAndPort(ipAddress, streamrWebSocketPort) + logger.info('Deleting all subdomains from ip: ' + ipAddress + ' port: ' + streamrWebSocketPort) + await Promise.all(subdomains.map((subdomain) => { + this.route53Api!.deleteRecord(RRType.A, subdomain.subdomainName, ipAddress, 300) + })) await this.route53Api.upsertRecord(RRType.A, fqdn, ipAddress, 300) } diff --git a/packages/autocertifier-server/src/Database.ts b/packages/autocertifier-server/src/Database.ts index c420ae16f4..b6ab5a9385 100644 --- a/packages/autocertifier-server/src/Database.ts +++ b/packages/autocertifier-server/src/Database.ts @@ -18,6 +18,7 @@ export class Database { private updateSubdomainIpStatement?: Statement private getSubdomainAcmeChallengeStatement?: Statement private updateSubdomainAcmeChallengeStatement?: Statement + private getSubdomainsByIpAndPortStatement?: Statement private databaseFilePath: string constructor(filePath: string) { @@ -59,6 +60,16 @@ export class Database { return ret } + public async getSubdomainsByIpAndPort(ip: string, port: string): Promise { + let ret: Subdomain[] + try { + ret = await this.getSubdomainsByIpAndPortStatement!.all(ip, port) + } catch (e) { + throw new DatabaseError(`Failed to get subdomains for IP ${ip} and port ${port}`, e) + } + return ret || [] + } + private async getSubdomainWithToken(subdomain: string, token: string): Promise { let ret: Subdomain | undefined try { @@ -115,7 +126,9 @@ export class Database { this.updateSubdomainIpStatement = await this.db.prepare("UPDATE subdomains SET ip = ?, port = ? WHERE subdomainName = ? AND token = ?") this.getSubdomainAcmeChallengeStatement = await this.db.prepare("SELECT acmeChallenge FROM subdomains WHERE subdomainName = ?") this.updateSubdomainAcmeChallengeStatement = await this.db.prepare("UPDATE subdomains SET acmeChallenge = ? WHERE subdomainName = ?") - + this.getSubdomainsByIpAndPortStatement = await this.db.prepare("SELECT * FROM subdomains WHERE ip = ? AND port = ?") + + logger.info('Database is running') } @@ -141,6 +154,9 @@ export class Database { if (this.updateSubdomainAcmeChallengeStatement) { await this.updateSubdomainAcmeChallengeStatement.finalize() } + if (this.getSubdomainsByIpAndPortStatement) { + await this.getSubdomainsByIpAndPortStatement.finalize() + } if (this.db) { await this.db.close() } From a47e93fb682ba5b57967fd9f61da83c27849bb74 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 20:06:16 +0300 Subject: [PATCH 07/10] logging --- packages/autocertifier-server/src/AutoCertifierServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/autocertifier-server/src/AutoCertifierServer.ts b/packages/autocertifier-server/src/AutoCertifierServer.ts index da64c66572..f76eaf3b5a 100644 --- a/packages/autocertifier-server/src/AutoCertifierServer.ts +++ b/packages/autocertifier-server/src/AutoCertifierServer.ts @@ -117,7 +117,7 @@ export class AutoCertifierServer implements RestInterface, ChallengeManager { if (this.route53Api !== undefined) { const subdomains = await this.database!.getSubdomainsByIpAndPort(ipAddress, streamrWebSocketPort) - logger.info('Deleting all subdomains from ip: ' + ipAddress + ' port: ' + streamrWebSocketPort) + logger.info('Deleting all subdomains from ip: ' + ipAddress + ' port: ' + streamrWebSocketPort + ' number of subdomains: ' + subdomains.length, { subdomains }) await Promise.all(subdomains.map((subdomain) => { this.route53Api!.deleteRecord(RRType.A, subdomain.subdomainName, ipAddress, 300) })) From 53ecef91b8facb965417487f5e4eb27614a40895 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 20:10:47 +0300 Subject: [PATCH 08/10] fix domain name --- packages/autocertifier-server/src/AutoCertifierServer.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/autocertifier-server/src/AutoCertifierServer.ts b/packages/autocertifier-server/src/AutoCertifierServer.ts index f76eaf3b5a..c0460e0438 100644 --- a/packages/autocertifier-server/src/AutoCertifierServer.ts +++ b/packages/autocertifier-server/src/AutoCertifierServer.ts @@ -119,7 +119,12 @@ export class AutoCertifierServer implements RestInterface, ChallengeManager { const subdomains = await this.database!.getSubdomainsByIpAndPort(ipAddress, streamrWebSocketPort) logger.info('Deleting all subdomains from ip: ' + ipAddress + ' port: ' + streamrWebSocketPort + ' number of subdomains: ' + subdomains.length, { subdomains }) await Promise.all(subdomains.map((subdomain) => { - this.route53Api!.deleteRecord(RRType.A, subdomain.subdomainName, ipAddress, 300) + this.route53Api!.deleteRecord( + RRType.A, + subdomain.subdomainName + '.' + this.domainName, + ipAddress, + 300 + ) })) await this.route53Api.upsertRecord(RRType.A, fqdn, ipAddress, 300) } From 7bd7e143162419352d74fd1ea58190ba7f7a0e8f Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 20:13:54 +0300 Subject: [PATCH 09/10] Remove entries from AWS based on ip and port combination --- .../src/AutoCertifierServer.ts | 3 +- packages/autocertifier-server/src/Database.ts | 3 +- .../autocertifier-server/src/Route53Api.ts | 74 ------------------- 3 files changed, 3 insertions(+), 77 deletions(-) diff --git a/packages/autocertifier-server/src/AutoCertifierServer.ts b/packages/autocertifier-server/src/AutoCertifierServer.ts index c0460e0438..64174e425b 100644 --- a/packages/autocertifier-server/src/AutoCertifierServer.ts +++ b/packages/autocertifier-server/src/AutoCertifierServer.ts @@ -117,7 +117,8 @@ export class AutoCertifierServer implements RestInterface, ChallengeManager { if (this.route53Api !== undefined) { const subdomains = await this.database!.getSubdomainsByIpAndPort(ipAddress, streamrWebSocketPort) - logger.info('Deleting all subdomains from ip: ' + ipAddress + ' port: ' + streamrWebSocketPort + ' number of subdomains: ' + subdomains.length, { subdomains }) + logger.info('Deleting all subdomains from ip: ' + ipAddress + ' port: ' + + streamrWebSocketPort + ' number of subdomains: ' + subdomains.length, { subdomains }) await Promise.all(subdomains.map((subdomain) => { this.route53Api!.deleteRecord( RRType.A, diff --git a/packages/autocertifier-server/src/Database.ts b/packages/autocertifier-server/src/Database.ts index b6ab5a9385..49774ac11c 100644 --- a/packages/autocertifier-server/src/Database.ts +++ b/packages/autocertifier-server/src/Database.ts @@ -127,8 +127,7 @@ export class Database { this.getSubdomainAcmeChallengeStatement = await this.db.prepare("SELECT acmeChallenge FROM subdomains WHERE subdomainName = ?") this.updateSubdomainAcmeChallengeStatement = await this.db.prepare("UPDATE subdomains SET acmeChallenge = ? WHERE subdomainName = ?") this.getSubdomainsByIpAndPortStatement = await this.db.prepare("SELECT * FROM subdomains WHERE ip = ? AND port = ?") - - + logger.info('Database is running') } diff --git a/packages/autocertifier-server/src/Route53Api.ts b/packages/autocertifier-server/src/Route53Api.ts index 92772af1a5..f431acb2e7 100644 --- a/packages/autocertifier-server/src/Route53Api.ts +++ b/packages/autocertifier-server/src/Route53Api.ts @@ -7,14 +7,11 @@ import { RRType, ChangeResourceRecordSetsCommandOutput } from '@aws-sdk/client-route-53' -import { Logger } from '@streamr/utils' interface Record { fqdn: string value: string } - -const logger = new Logger(module) export class Route53Api { private hostedZoneId: string @@ -73,75 +70,4 @@ export class Route53Api { const response = await this.client.send(command) return response } - - // List all records in a zone with pagination support - public async listAllRecords(): Promise { - const allResponses: ListResourceRecordSetsCommandOutput[] = [] - let startRecordName: string | undefined - let startRecordType: RRType | undefined - let isTruncated = true - - while (isTruncated) { - const input: any = { - HostedZoneId: this.hostedZoneId, - } - - if (startRecordName) { - input.StartRecordName = startRecordName - } - if (startRecordType) { - input.StartRecordType = startRecordType - } - - const command = new ListResourceRecordSetsCommand(input) - const response = await this.client.send(command) - - allResponses.push(response) - - isTruncated = response.IsTruncated ?? false - if (isTruncated) { - startRecordName = response.NextRecordName - startRecordType = response.NextRecordType - } - } - - return allResponses - } - - // Query all records that point to a specific IP address (with pagination support) - public async getRecordsByIpAddress(ipAddress: string): Promise { - const allResponses = await this.listAllRecords() - const matchingRecords: Record[] = [] - - for (const response of allResponses) { - if (response.ResourceRecordSets) { - for (const recordSet of response.ResourceRecordSets) { - // Only check A records (which contain IP addresses) - if (recordSet.Type === RRType.A && recordSet.ResourceRecords) { - for (const resourceRecord of recordSet.ResourceRecords) { - if (resourceRecord.Value === ipAddress) { - matchingRecords.push({ - fqdn: recordSet.Name ?? '', - value: resourceRecord.Value - }) - } - } - } - } - } - } - - return matchingRecords - } - - // Remove all A records that point to a specific IP address - public async deleteRecordsByIpAddress(ipAddress: string, ttl: number = 300): Promise { - const recordsToDelete = await this.getRecordsByIpAddress(ipAddress) - logger.info('deleting records by ip address: ' + ipAddress, { recordsToDelete }) - if (recordsToDelete.length === 0) { - return null // No records found to delete - } - - return this.changeRecords(ChangeAction.DELETE, RRType.A, recordsToDelete, ttl) - } } From 3a783c97ce5f99a3960bb4224a64578ab3a496d5 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 6 Oct 2025 20:20:34 +0300 Subject: [PATCH 10/10] fix promise await --- packages/autocertifier-server/src/AutoCertifierServer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/autocertifier-server/src/AutoCertifierServer.ts b/packages/autocertifier-server/src/AutoCertifierServer.ts index 64174e425b..5fe130d74c 100644 --- a/packages/autocertifier-server/src/AutoCertifierServer.ts +++ b/packages/autocertifier-server/src/AutoCertifierServer.ts @@ -119,14 +119,14 @@ export class AutoCertifierServer implements RestInterface, ChallengeManager { const subdomains = await this.database!.getSubdomainsByIpAndPort(ipAddress, streamrWebSocketPort) logger.info('Deleting all subdomains from ip: ' + ipAddress + ' port: ' + streamrWebSocketPort + ' number of subdomains: ' + subdomains.length, { subdomains }) - await Promise.all(subdomains.map((subdomain) => { + await Promise.all(subdomains.map((subdomain) => this.route53Api!.deleteRecord( RRType.A, subdomain.subdomainName + '.' + this.domainName, ipAddress, 300 ) - })) + )) await this.route53Api.upsertRecord(RRType.A, fqdn, ipAddress, 300) }