@linkedmink/node-route53-dynamic-dns
Version:
Background process that updates AWS Route 53 DNS address records whenever the public IP of the hosting environment changes
93 lines (80 loc) • 3.05 kB
text/typescript
import EventEmitter from "node:events";
import { setInterval } from "node:timers/promises";
import { IpNotFoundError, publicIpv4, publicIpv6 } from "public-ip";
import {
MINIMUM_IP_RETRIEVE_TIMEOUT_MS,
MINIMUM_IP_RETRIEVE_UPDATE_DIFF_MS,
} from "../constants/behavior.mjs";
import { PublicIpEvent } from "../constants/events.mjs";
import { loggerForModuleUrl } from "../environment/logger.mjs";
import { formatError } from "../functions/format.mjs";
import { PublicIpEventEmitter, PublicIpState } from "../types/public-ip-events.mjs";
export class PublicIpClient extends EventEmitter implements PublicIpEventEmitter {
private readonly logger = loggerForModuleUrl(import.meta.url);
constructor(
readonly updateIntervalMs: number,
readonly ipRetrieveTimeout: number,
readonly isIpV6Enabled: boolean
) {
super();
if (this.ipRetrieveTimeout < MINIMUM_IP_RETRIEVE_TIMEOUT_MS) {
throw new Error(`ipRetrieveTimeout must be at least: ${MINIMUM_IP_RETRIEVE_TIMEOUT_MS}ms`);
}
// Don't allow getting the IP again while a retrieval is possibly in progress
const updateIntervalMinimumMs =
MINIMUM_IP_RETRIEVE_TIMEOUT_MS + MINIMUM_IP_RETRIEVE_UPDATE_DIFF_MS;
if (this.updateIntervalMs < updateIntervalMinimumMs) {
throw new Error(
`updateIntervalMs must be at least: ${MINIMUM_IP_RETRIEVE_UPDATE_DIFF_MS}ms more than ipRetrieveTimeout(${MINIMUM_IP_RETRIEVE_TIMEOUT_MS}ms)`
);
}
}
start = async (): Promise<void> => {
await this.updatePublicIpState();
for await (const _ of setInterval(this.updateIntervalMs)) {
await this.updatePublicIpState();
}
};
private updatePublicIpState = async (): Promise<PublicIpState> => {
this.logger.debug("Getting latest public IP");
const [ipV4, ipV6] = await Promise.all([this.getPublicIpV4(), this.getPublicIpV6()]);
const state: PublicIpState = {
publicIpAddresses: {
v4: ipV4,
v6: ipV6,
},
lastPublicIpDateTime: new Date(),
};
this.logger.info(`Emitting public IPs to check: v4=${ipV4}, v6=${ipV6}`);
this.emit(PublicIpEvent.Retrieved, state);
return state;
};
private getPublicIpV4 = async (): Promise<string | null> => {
try {
return await publicIpv4({ timeout: this.ipRetrieveTimeout });
} catch (error: unknown) {
this.logIpError(error, "V4");
return null;
}
};
private getPublicIpV6 = async (): Promise<string | null> => {
if (!this.isIpV6Enabled) {
return null;
}
try {
return await publicIpv6({ timeout: this.ipRetrieveTimeout });
} catch (error: unknown) {
this.logIpError(error, "V6");
return null;
}
};
private logIpError = (error: unknown, version: string): void => {
if (error instanceof IpNotFoundError) {
this.logger.verbose(
`Could not determine IP ${version}. It may not be assigned, or this may indicate another problem (no internet access, etc.)`
);
} else {
this.logger.error(formatError(error));
}
};
}