From b1f52740c8103f78aa0cdfcb1589cf1d9cd08981 Mon Sep 17 00:00:00 2001 From: blu3mania Date: Mon, 1 Mar 2021 22:08:41 -0500 Subject: [PATCH] Initial code commit --- .github/workflows/codeql-analysis.yml | 34 ++++ .gitignore | 4 + .yarnclean | 45 +++++ DNS-Providers.md | 79 ++++++++ LICENSE | 17 +- README.md | 69 +++++++ package.json | 45 +++++ src/app.js | 191 ++++++++++++++++++ src/dns-provider.js | 273 ++++++++++++++++++++++++++ src/dns-resolver.js | 59 ++++++ src/dns-update-failure.png | Bin 0 -> 4793 bytes src/dns-update-scheduled.png | Bin 0 -> 3353 bytes src/dns-updated.png | Bin 0 -> 4856 bytes src/images/dns-update-failure.png | Bin 0 -> 4793 bytes src/images/dns-update-scheduled.png | Bin 0 -> 3353 bytes src/images/dns-updated.png | Bin 0 -> 4856 bytes src/images/ip-changed.png | Bin 0 -> 4385 bytes src/install-service.js | 41 ++++ src/ip-changed.png | Bin 0 -> 4385 bytes src/network-interface-monitor.js | 128 ++++++++++++ src/print.js | 47 +++++ src/settings.json | 16 ++ src/show-interfaces.js | 35 ++++ src/uninstall-service.js | 23 +++ 24 files changed, 1092 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .gitignore create mode 100644 .yarnclean create mode 100644 DNS-Providers.md create mode 100644 README.md create mode 100644 package.json create mode 100644 src/app.js create mode 100644 src/dns-provider.js create mode 100644 src/dns-resolver.js create mode 100644 src/dns-update-failure.png create mode 100644 src/dns-update-scheduled.png create mode 100644 src/dns-updated.png create mode 100644 src/images/dns-update-failure.png create mode 100644 src/images/dns-update-scheduled.png create mode 100644 src/images/dns-updated.png create mode 100644 src/images/ip-changed.png create mode 100644 src/install-service.js create mode 100644 src/ip-changed.png create mode 100644 src/network-interface-monitor.js create mode 100644 src/print.js create mode 100644 src/settings.json create mode 100644 src/show-interfaces.js create mode 100644 src/uninstall-service.js diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..a40b8ba --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,34 @@ +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + types: [ opened, synchronize ] + branches: [ main ] + schedule: + - cron: '29 1 12 * *' + +jobs: + analyze: + name: Analyze + runs-on: windows-latest + + strategy: + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: javascript + + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0681f4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +package-lock.json +yarn.lock +/src/daemon/ diff --git a/.yarnclean b/.yarnclean new file mode 100644 index 0000000..b591611 --- /dev/null +++ b/.yarnclean @@ -0,0 +1,45 @@ +# test directories +__tests__ +test +tests +powered-test + +# asset directories +docs +doc +website +images +assets + +# examples +example +examples + +# code coverage directories +coverage +.nyc_output + +# build scripts +Makefile +Gulpfile.js +Gruntfile.js + +# configs +appveyor.yml +circle.yml +codeship-services.yml +codeship-steps.yml +wercker.yml +.tern-project +.gitattributes +.editorconfig +.*ignore +.eslintrc +.jshintrc +.flowconfig +.documentup.json +.yarn-metadata.json +.travis.yml + +# misc +*.md diff --git a/DNS-Providers.md b/DNS-Providers.md new file mode 100644 index 0000000..464ee21 --- /dev/null +++ b/DNS-Providers.md @@ -0,0 +1,79 @@ +# DNS Provider Instructions + +## [Dynu](https://www.dynu.com/) + +To obtain your access key and domain ID: +1. Go to https://www.dynu.com/en-US/ControlPanel/APICredentials. +2. In "API Credentials" section, click "View" icon in Action column for API Key. +3. The key is now displayed. Use it as accessKey in settings.json. +4. In a command window, run 'curl https://api.dynu.com/v2/dns -H "API-Key:ACCESSKEY"'. If not already + installed, curl can be downloaded from https://curl.se/windows/. +5. The returned string is in JSON format. Under "domains", each domain entry comes with id, name, along with + other properties. Find the id value for the domain name you want to update. +6. Use this id as domainID in settings.json. + +API to update IP address: + +POST https://api.dynu.com/v2/dns/{DOMAINID} + +Header: "API-Key: {ACCESSKEY}" + +Body: { "name": "{DOMAINNAME}"[, "ipv4Address": "ddd.ddd.ddd.ddd"][, "ipv6Address": "xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx"] } + +## [FreeDNS](https://freedns.afraid.org/) + +To enable programmically updating DNS entry and obtain your access key: +1. Go to https://freedns.afraid.org/dynamic/v2/. +2. Select the domain(s) you want to enable. +3. Make sure you select "Enable Dynamic DNS..." in Action dropdown list. +4. Click "Apply" button. +5. These domains should now appear under "Active dynamic entries" section. +6. For the domain you want to update, grab the API key in the URL shown in the last column. It is the last + segment in the URL displayed, i.e. http://sync.afraid.org/u/{ACCESSKEY}/. +7. Use this key as accessKey in settings.json. +8. domainID in settings.json is not needed. FreeDNS assigns unique API key per domain. + +API to update IP address: + +GET http://[v6.]sync.afraid.org/u/{ACCESSKEY}/?ip=[ddd.ddd.ddd.ddd][xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx] + +## [DuckDNS](https://www.duckdns.org/) + +To obtain your access key and domain ID: +1. Go to https://www.duckdns.org/ +2. "token" displayed on this page (near the top) is the API token. Use it as accessKey in settings.json. +3. Domains are listed below. The first column is domain ID. It is essentially the subdomain name in + DOMAINID.duckdns.org. +4. Use this id as domainID in settings.json. + +API to update IP address: + +GET https://www.duckdns.org/update?domains={DOMAINID}&token={ACCESSKEY}[&ip=ddd.ddd.ddd.ddd][&ipv6=xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx] + +## [YDNS](https://ydns.io/) + +To obtain your access key: +1. Your access key is either your account username:password, or API username:password which can be found here: + https://ydns.io/user/api. + + For example, if your email is "someone@mail.com" and password is "MySuperSecretPwd" then use + "someone@mail.com:MySuperSecretPwd" as accessKey in settings.json. +2. domainID in settings.json is not needed. YDNS uses domain name to update. + +API to update IP address: + +GET https://ydns.io/api/v1/update/?host={DOMAINNAME}&ip=[ddd.ddd.ddd.ddd][xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx] + +Header: "Authorization: Basic {base64-encoded ACCESSKEY}" + +## [No-IP](https://www.noip.com/) + +To obtain your access key: +1. Your access key is username:password. See [YDNS](#ydns-httpsydnsio) section for example. +2. domainID in settings.json is not needed. NoIP uses domain name to update. + +API to update IP address: + +GET https://dynupdate.no-ip.com/nic/update?hostname={DOMAINNAME}[&myip==ddd.ddd.ddd.ddd][&myipv6=xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx] + +Header: "Authorization: Basic {base64-encoded ACCESSKEY}" diff --git a/LICENSE b/LICENSE index 261eeb9..0da6b26 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -175,24 +175,13 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright 2021 blu3mania. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/README.md b/README.md new file mode 100644 index 0000000..fedf4f9 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Update-Dynamic-DNS-with-VPN +![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-yellow) +![node.js 10+](https://img.shields.io/badge/node.js-10.16.3-blue?logo=node.js) +![Latest Release](https://img.shields.io/github/v/release/blu3mania/update-dynamic-dns-with-vpn) + +Auto update a dynamic DNS registration based on a given network interface, such as VPN. **Note**, it does +not update dynamic DNS with your public IP. If that's what you are looking for, there are already many other +apps doing just that. + +It can be run as a standalone application or as a Windows service. When running in standalone mode, it can +also be used to just monitor a network interface without auto DNS update. + +## Run these steps first: + +1. One of the packages, "ffi-napi", uses native modules and relies on "node-gyp" to build the project. As a + result, there are some prerequisites that need to be installed/configured. Please refer to [node-gyp's + instructions](https://github.com/nodejs/node-gyp#installation). +2. Run "npm run show \[addessFamily\]" or "node src/show-interfaces.js \[addessFamily\]". + + addessFamily is optional, which can be "ipv4" or "ipv6" if you only care about one type of IP address. + Shortened forms of parameter are accepted as well, which are "4", "6", "v4", "v6". + + Find the network interface you want to monitor, and note down the key to that interface, e.g. "Local Area + Connection", "Wi-Fi". +3. Edit src/settings.json. + * networkInterface is the network interface you wrote down in the previous step. + * addressFamily is the IP address family to monitor and register on dynamic DNS. + + Valid values are "IPv4" and "IPv6". + * dnsProvider is the provider of your domain. + + Supported values are: "Dynu", "FreeDNS", "DuckDNS", "YDNS", "NoIP". + + **Note**, if only using the script to monitor a network interface, leave this setting empty. + * domainName is the domain name to be updated on that provider. + * domainID is the ID assigned to the given domain by that provider. + + Not all providers support this. Some of them map access key to individual domains or simply use domain + name as id. + * accessKey is the access key or token assigned by the DNS provider that can be used to update your domain. + + How to obtain this info is provider specific. Please refer to [DNS Provider Instructions](DNS-Providers.md). + * showNotification allows showing Windows notification when an action is taken, such as domain is updated + in provider, or domain update is queued (due to update interval). + + **Note**, this only works when running in standalone mode instead of as a Windows service. + * notificationTypes is an array of string values that defines what types of notification should be shown. + + Supported values are: "DNS Registration", "Scheduled DNS Registration", "IP Changed", "IP Assigned", "IP + Removed". +4. Run "npm install". Accept UAC prompts if any (there could be up to 4). "npm link" can be used as well, + which will create a command "showip" that can be used as a shortcut to "src/show-interfaces.js". + + **Note**, this step installs the script as a Windows service. If it's not desired, run "npm run uninstall" + afterwards. + +## To run the script manually: + +Run "npm start" or "node src/app.js". + +## To install and run the script as a Windows service: + +Run "npm run install" or "node src/install-service.js". Accept UAC prompts if any (there could be up to 4). + +**Note**, if settings.json is updated when service is running, restart it in Windows Services control panel. + +## To uninstall the Windows service: + +Run "npm run uninstall" or "node src/uninstall-service.js". Accept UAC prompts if any (there could be up to 4). diff --git a/package.json b/package.json new file mode 100644 index 0000000..da6c2c3 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "update-dynamic-dns-with-vpn", + "version": "1.0.0", + "description": "Automatically update a dynamic DNS registration based on a given network interface, such as VPN", + "main": "src/app.js", + "scripts": { + "start": "node src/app.js", + "install": "node src/install-service.js", + "uninstall": "node src/uninstall-service.js", + "show": "node src/show-interfaces.js" + }, + "bin": { + "showip": "src/show-interfaces.js" + }, + "keywords": [ + "dynamic", + "dns", + "dynamic-dns", + "network-interface", + "vpn" + ], + "author": "blu3mania ", + "license": "Apache-2.0", + "homepage": "https://github.com/blu3mania/update-dynamic-dns-with-vpn#readme", + "bugs": { + "url": "https://github.com/blu3mania/update-dynamic-dns-with-vpn/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/blu3mania/update-dynamic-dns-with-vpn.git" + }, + "os": [ + "win32" + ], + "dependencies": { + "chalk": "^4.1.0", + "ffi-napi": "^3.1.0", + "node-notifier": "^9.0.0", + "node-windows": "^1.0.0-beta.5" + }, + "devDependencies": {}, + "resolutions": { + "**/minimist": "^1.2.5" + } +} diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..a82cc60 --- /dev/null +++ b/src/app.js @@ -0,0 +1,191 @@ +(function() { + 'use strict'; + + const path = require('path'); + const notifier = require('node-notifier'); + const { + error, + warning, + info, + verbose } = require('./print.js'); + const DnsResolver = require('./dns-resolver.js'); + const NetworkInterfaceMonitor = require('./network-interface-monitor.js'); + const { + DnsProvider, + Dynu, + FreeDNS, + DuckDNS, + YDNS, + NoIP } = require('./dns-provider.js'); + const settings = require('./settings.json'); + + const NotificationType = { + DnsRegistration: 'dns registration', + ScheduledDnsRegistration: 'scheduled dns registration', + IpChanged: 'ip changed', + IpAssigned: 'ip assigned', + IpRemoved: 'ip removed', + }; + + let dnsProvider = null; + let currentRegisteredIP = null; + let ipToRegister = null; + + const monitor = new NetworkInterfaceMonitor(settings.networkInterface, settings.addressFamily, (address, eventType) => { + switch (eventType) { + case NetworkInterfaceMonitor.EventType.Initial: + // Initial callback + if (address !== null) { + info(`Current ${settings.addressFamily} address: ${address[settings.addressFamily]}`); + } else { + warning(`Network interface '${settings.networkInterface}' is inactive!`); + } + break; + + case NetworkInterfaceMonitor.EventType.IPChanged: + info(`${settings.addressFamily} address changed: ${address[settings.addressFamily]}`); + if (settings.showNotification && settings.notificationTypes.find(type => type.toLowerCase() === NotificationType.IpChanged)) { + sendDesktopNotification('IP Changed', `Network interface '${settings.networkInterface}' ${settings.addressFamily} address changed: ${address[settings.addressFamily]}`, 'ip-changed.png'); + } + break; + + case NetworkInterfaceMonitor.EventType.IPAssigned: + info(`Network interface '${settings.networkInterface}' is now active.`); + info(`${settings.addressFamily} address assigned: ${address[settings.addressFamily]}`); + if (settings.showNotification && settings.notificationTypes.find(type => type.toLowerCase() === NotificationType.IpAssigned)) { + sendDesktopNotification('IP Assigned', `Network interface '${settings.networkInterface}' ${settings.addressFamily} address assigned: ${address[settings.addressFamily]}`, 'ip-changed.png'); + } + break; + + case NetworkInterfaceMonitor.EventType.IPRemoved: + warning(`Network interface '${settings.networkInterface}' is now inactive!`); + if (settings.showNotification && settings.notificationTypes.find(type => type.toLowerCase() === NotificationType.IpRemoved)) { + sendDesktopNotification('IP Removed', `Network interface '${settings.networkInterface}' is now inactive!`, 'ip-changed.png'); + } + break; + } + + if (dnsProvider !== null + && address !== null && address[settings.addressFamily] + && ( + (ipToRegister === null && currentRegisteredIP !== address[settings.addressFamily]) + || (ipToRegister !== null && ipToRegister !== address[settings.addressFamily]) + )) { + ipToRegister = address[settings.addressFamily]; + info(`Registering domain ${settings.domainName} with IP ${ipToRegister}...`); + dnsProvider.register(ipToRegister, settings.addressFamily, (data, eventType) => { + switch (eventType) { + case DnsProvider.EventType.Registered: + ipToRegister = null; + currentRegisteredIP = data; + info(`Registered ${settings.addressFamily} address ${currentRegisteredIP}`); + if (settings.showNotification && settings.notificationTypes.find(type => type.toLowerCase() === NotificationType.DnsRegistration)) { + sendDesktopNotification('DNS Registration Updated', `Updated domain ${settings.domainName} with IP ${currentRegisteredIP}`, 'dns-updated.png'); + } + break; + + case DnsProvider.EventType.RegistrationScheduled: + const waitTime = Math.round(data / 1000); + warning(`Last IP registration just happened so next one is deferred. Waiting for ${waitTime < 1 ? 'less than 1' : waitTime} second${waitTime > 1 ? 's' : ''} before calling provider...`); + if (settings.showNotification && settings.notificationTypes.find(type => type.toLowerCase() === NotificationType.ScheduledDnsRegistration)) { + sendDesktopNotification('DNS Registration Scheduled', `Will update domain ${settings.domainName} with IP ${ipToRegister} in ${waitTime} seconds.`, 'dns-update-scheduled.png'); + } + break; + + case DnsProvider.EventType.Failed: + ipToRegister = null; + error(`Registration failed due to error: ${data}`); + if (settings.showNotification && settings.notificationTypes.find(type => type.toLowerCase() === NotificationType.DnsRegistration)) { + sendDesktopNotification('DNS Registration Failure', `Failed to update domain ${settings.domainName}!`, 'dns-update-failure.png'); + } + break; + } + }); + } + }); + + main(); + + function main() { + verbose('Starting...'); + + switch (settings.dnsProvider.toLowerCase()) { + case DnsProvider.Vendor.Dynu: + dnsProvider = new Dynu(settings.domainName, settings.domainID, settings.accessKey); + break; + + case DnsProvider.Vendor.FreeDNS: + dnsProvider = new FreeDNS(settings.domainName, settings.domainID, settings.accessKey); + break; + + case DnsProvider.Vendor.DuckDNS: + dnsProvider = new DuckDNS(settings.domainName, settings.domainID, settings.accessKey); + break; + + case DnsProvider.Vendor.YDNS: + dnsProvider = new YDNS(settings.domainName, settings.domainID, settings.accessKey); + break; + + case DnsProvider.Vendor.NoIP: + dnsProvider = new NoIP(settings.domainName, settings.domainID, settings.accessKey); + break; + } + + if (dnsProvider !== null) { + verbose(`DNS provider specified: ${settings.dnsProvider}.`); + } else if (settings.dnsProvider.length > 0) { + warning(`Invalid DNS provider specified: ${settings.dnsProvider}. Running in monitor-only mode...`); + } else { + info('DNS provider not specified. Running in monitor-only mode...'); + } + + if (dnsProvider !== null && settings.domainName.length > 0) { + verbose(`Checking current registered IP for ${settings.domainName}...`); + new DnsResolver(settings.domainName).resolve(settings.addressFamily) + .then((ip) => { + currentRegisteredIP = ip; + if (ip !== null) { + info(`Current registered ${settings.addressFamily} address for ${settings.domainName} is ${ip}`); + } else { + warning(`${settings.domainName} is not registered to any IP!`); + } + startMonitoring(); + }) + .catch((err) => { + error(err); + startMonitoring(); + }); + } else { + startMonitoring(); + } + + process.on('SIGINT', () => { + warning('SIGINT received, exiting...'); + process.exit(); + }); + + // Use a no-op timer to keep the process running. + setInterval(() => {}, 60 * 60 * 1000); + } + + function startMonitoring() { + verbose(`Monitoring changes in interface '${settings.networkInterface}' for IP address family ${settings.addressFamily}...`); + if (!monitor.start()) { + error('Failed to start network interface monitor. Exiting...'); + process.exit(-1); + } + } + + function sendDesktopNotification(title, message, icon) { + notifier.notify({ + title: title, + message: message, + appID: 'Update Dynamic DNS', + icon: getImagePath(icon), + }); + } + + function getImagePath(imageFile) { + return path.join(__dirname, 'images', imageFile); + } +})(); diff --git a/src/dns-provider.js b/src/dns-provider.js new file mode 100644 index 0000000..4a019bf --- /dev/null +++ b/src/dns-provider.js @@ -0,0 +1,273 @@ +(function() { + 'use strict'; + + const http = require('http'); + const https = require('https'); + const os = require('os'); + //const print = require('./print.js'); + const packageJson = require('../package.json'); + + const Vendor = { + Dynu: 'dynu', + FreeDNS: 'freedns', + DuckDNS: 'duckdns', + YDNS: 'ydns', + NoIP: 'noip', + }; + + const EventType = { + Registered: 0, + RegistrationScheduled: 1, + Failed: 2, + }; + + class DnsProvider { + constructor(domainName, domainID, accessKey) { + this.domainName = domainName; + this.domainID = domainID; + this.accessKey = accessKey; + this.lastRegistrationTime = null; + this.registrationTimer = null; + } + + static get Vendor() { + return Vendor; + } + + static get EventType() { + return EventType; + } + + get registrationInterval() { + // By default, do not attempt another registration within 2 minutes. + return 2 * 60 * 1000; + } + + get useHttps() { + // By default, use HTTPS. + return true; + } + + get method() { + return 'GET'; + } + + get headers() { + return {}; + } + + getData(ip, addressFamily) { + return null; + } + + register(ip, addressFamily, callback) { + // Check last registration time to make sure we are not registering too often. + if (this.lastRegistrationTime === null || new Date(this.lastRegistrationTime.valueOf() + this.registrationInterval) < new Date()) { + this.performRegistration(ip, addressFamily) + .then((resp) => callback(ip, EventType.Registered)) + .catch((error) => callback(error, EventType.Failed)); + } else { + if (this.registrationTimer !== null) { + clearTimeout(this.registrationTimer); + this.registrationTimer = null; + } + + let waitTime = this.lastRegistrationTime.valueOf() + this.registrationInterval - new Date().valueOf(); + if (waitTime < 0) { + waitTime = 0; + } + callback(waitTime, EventType.RegistrationScheduled); + this.registrationTimer = setTimeout(() => { + this.performRegistration(ip, addressFamily) + .then((resp) => callback(ip, EventType.Registered)) + .catch((error) => callback(error, EventType.Failed)); + }, waitTime); + } + } + + performRegistration(ip, addressFamily) { + this.registrationTimer = null; + + // Set the registration time now to prevent registration again while waiting for response. + // It will be updated again when response is received. + this.lastRegistrationTime = new Date(); + const request = { + https: this.useHttps, + options: { + host: this.getHost(addressFamily), + path: this.getPath(ip, addressFamily), + method: this.method, + headers: this.headers, + agent: false, + }, + data: this.getData(ip, addressFamily), + }; + return this.sendRequest(request) + .then((resp) => { + this.lastRegistrationTime = new Date(); + return Promise.resolve(resp); + }) + .catch((error) => { + this.lastRegistrationTime = new Date(); + return Promise.reject(error); + }); + } + + sendRequest(request) { + return new Promise((resolve, reject) => { + // Provide User Agent if not already specified. + if (!request.options.headers['User-Agent']) { + request.options.headers['User-Agent'] = `Update-Dynamic-DNS/${packageJson.version} (${os.platform()} ${os.release()}) ${packageJson.author}`; + } + + // Make sure Content-Length is provided when sending data. + if (request.data) { + if (!request.options.headers['Content-Length']) { + request.options.headers['Content-Length'] = Buffer.byteLength(request.data); + } + } + //print(request); + + const req = (request.https ? https : http).request(request.options) + .on('timeout', () => { + req.abort(); + reject('Http request timed out'); + }) + .on('abort', () => reject('Http request aborted')) + .on('error', (error) => reject(`Http request errored - ${error.message}`)) + .on('close', () => reject('Http request closed')) + .on('response', (response) => { + const code = response.statusCode || 0; + if (code >= 200 && code < 300) { + const dataSequence = []; + response + .on('aborted', () => reject('Http request aborted')) + .on('error', (error) => reject(`Http request errored - ${error.message}`)) + .on('data', (data) => dataSequence.push(data)) + .on('end', () => resolve(Buffer.concat(dataSequence))); + } + else if (code >= 400 && code < 500) { + reject(`Http status ${code}, check if your access key and domain name/ID in settings.json are correct.`); + } + else { + reject(`Http status ${code}`); + } + }); + if (request.data) { + req.write(request.data); + } + req.end(); + }); + } + } + + class Dynu extends DnsProvider { + constructor(domainName, domainID, accessKey) { + super(domainName, domainID, accessKey); + } + + get method() { + return 'POST'; + } + + get headers() { + return { + 'Content-Type': 'application/json', + 'API-Key': this.accessKey, + }; + } + + getHost(addressFamily) { + return 'api.dynu.com'; + } + + getPath(ip, addressFamily) { + return `/v2/dns/${this.domainID}`; + } + + getData(ip, addressFamily) { + return `{ "name": "${this.domainName}", "${addressFamily === 'IPv6' ? 'ipv6' : 'ipv4'}Address": "${ip}" }`; + } + } + + class FreeDNS extends DnsProvider { + constructor(domainName, domainID, accessKey) { + super(domainName, domainID, accessKey); + } + + get useHttps() { + return false; + } + + getHost(addressFamily) { + return addressFamily === 'IPv6' ? 'v6.sync.afraid.org' : 'sync.afraid.org'; + } + + getPath(ip, addressFamily) { + return `/u/${this.accessKey}/?address=${ip}`; + } + } + + class DuckDNS extends DnsProvider { + constructor(domainName, domainID, accessKey) { + super(domainName, domainID, accessKey); + } + + getHost(addressFamily) { + return 'www.duckdns.org'; + } + + getPath(ip, addressFamily) { + return `/update?domains=${this.domainID}&token=${this.accessKey}&${addressFamily === 'IPv6' ? 'ipv6' : 'ip'}=${ip}`; + } + } + + class BasicAuthorizationDnsProvider extends DnsProvider { + constructor(domainName, domainID, accessKey) { + super(domainName, domainID, accessKey); + } + + get headers() { + return { + 'Authorization': `Basic ${Buffer.from(this.accessKey).toString('base64')}`, + }; + } + } + + class YDNS extends BasicAuthorizationDnsProvider { + constructor(domainName, domainID, accessKey) { + super(domainName, domainID, accessKey); + } + + getHost(addressFamily) { + return 'ydns.io'; + } + + getPath(ip, addressFamily) { + return `/api/v1/update/?host=${this.domainName}&ip=${ip}`; + } + } + + class NoIP extends BasicAuthorizationDnsProvider { + constructor(domainName, domainID, accessKey) { + super(domainName, domainID, accessKey); + } + + getHost(addressFamily) { + return 'dynupdate.no-ip.com'; + } + + getPath(ip, addressFamily) { + return `/nic/update?hostname=${this.domainName}&${addressFamily === 'IPv6' ? 'myipv6' : 'myip'}=${ip}`; + } + } + + module.exports = { + DnsProvider, + Dynu, + FreeDNS, + DuckDNS, + YDNS, + NoIP, + }; +})(); diff --git a/src/dns-resolver.js b/src/dns-resolver.js new file mode 100644 index 0000000..da2ce26 --- /dev/null +++ b/src/dns-resolver.js @@ -0,0 +1,59 @@ +(function() { + 'use strict'; + + const dns = require('dns'); + + // Use Google, Cloudflare, and Quad9 DNS servers + dns.setServers([ + '8.8.8.8', + '8.8.4.4', + '1.1.1.1', + '1.0.0.1', + '9.9.9.9', + ]); + + class DnsResolver { + constructor(host) { + this.host = host; + } + + resolve(addressFamily = 'IPv4', numRetries = 5, retryInterval = 10) { + return new Promise((resolve, reject) => { + let retries = 0; + const queryDns = (async () => { + let hasError = false; + let errorMessage = ''; + const records = await dns.promises.resolve(this.host, addressFamily === 'IPv6' ? 'AAAA' : 'A') + .catch((error) => { + // dns.resolve errors with ENODATA if the AAAA (for IPv6) or A (for IPv4) record does not exist. + // THough, if dns.lookup is used, it errors with ENOTFOUND when the OS does not have this info. + if (error.code === 'ENODATA' || error.code === 'ENOTFOUND') { + // Domain name not registered + resolve(null); + } else { + hasError = true; + errorMessage = error.message; + } + return []; + }); + + if (hasError) { + if (retries++ < numRetries) { + setTimeout(() => queryDns(), retryInterval * 1000); + } else { + reject(`Cannot resolve DNS for ${this.host}: ${errorMessage}`); + } + } else if (records.length == 0) { + resolve(null); + } else { + resolve(records[0]); + } + }); + + queryDns(); + }); + } + } + + module.exports = DnsResolver; +})(); diff --git a/src/dns-update-failure.png b/src/dns-update-failure.png new file mode 100644 index 0000000000000000000000000000000000000000..f7e4a91d68534696cca76684fc4287fba8236488 GIT binary patch literal 4793 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 T7g&4J00000NkvXXu0mjfX%aBh literal 0 HcmV?d00001 diff --git a/src/dns-update-scheduled.png b/src/dns-update-scheduled.png new file mode 100644 index 0000000000000000000000000000000000000000..784d5b9ba61bca0dd3c7a3c25e1915c0a4b94c1a GIT binary patch literal 3353 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 literal 0 HcmV?d00001 diff --git a/src/dns-updated.png b/src/dns-updated.png new file mode 100644 index 0000000000000000000000000000000000000000..e10970ea6f532cdb944491915e0b218fc0f0cfe7 GIT binary patch literal 4856 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 T7g&4J00000NkvXXu0mjfX%aBh literal 0 HcmV?d00001 diff --git a/src/images/dns-update-scheduled.png b/src/images/dns-update-scheduled.png new file mode 100644 index 0000000000000000000000000000000000000000..784d5b9ba61bca0dd3c7a3c25e1915c0a4b94c1a GIT binary patch literal 3353 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 literal 0 HcmV?d00001 diff --git a/src/images/dns-updated.png b/src/images/dns-updated.png new file mode 100644 index 0000000000000000000000000000000000000000..e10970ea6f532cdb944491915e0b218fc0f0cfe7 GIT binary patch literal 4856 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D5{*ejK~#8N?VW#g z6jheT->d$S1VR#sM1CkqL`i@Iq7lL{V7ikbG6S9gHOPoS7+D-wjiTeqA3H}p$Jyg7 z&gwWQq60)>Mg|y#nV^D@?m!fFApGhikO(Vbg$M!pO_J`adV6p6>ue|eqpCaIosQ&l z4s^X+mFlYdeeb?^t6o);UV7=JmtK15rI%iM>E)(_^3SM5wq)NXF*cS7BohuagE06< zNirieo-mdGho1GCj9Jpn{$uRM^p*GEMt=- zDitwC3!qd$D7}lX3L3_kgHrkxd~Rb@_&mmRXFhzAo5D^+1ZcJ9+$9ik1tqkQo1zv*$Tj%*cf+~N<#g6Y$!QHAVHZHO zjGik~iq7xLO+5rwrDOCLg4y;hH-v?-3BdN}_7Shz{|MjkOKu7ZMi_(xx*OudQA%08 zEa~fwM#oh`bXUnF+htNaN>488`ZBwiAnNqP1VYo{Z?YkrFq>R3$GuW33 zlj=5w_!2YY0&^9tMn$@qELwt5&`60IRhn0tb&+7f}8xq$PrnV=fxD( z9_E&AgIV(R4tn8PaK)?O3~iS|82e9yx#lHa>JnWRK>lh%oYZ!`681EXn^a(b(i!7R zg^7DxxFITNvy93Sh_FHH1wfg~3uE$YSMj1x?3H83Q(2k^f<4Vm9T50u&`539-qhlJ z3^{nv22;(yM4R)G7<1hrU0mzEaGcw?$uCra?bhrKM3(o0s6NlP zO9F_MqaT5tFXBa?z>u;P`nrd-)R^u>m~i}xfhAAJIL}Q!fg!B{I`Kc_C3J35V7o^R zb6B!gLOx=H@KTJOJd;^DU+qEAA*f|{_8mHr9E2}O;3luYD5nJysA&dMt%aMS9vJ&m zu`>G|Sp1dK;v3j@1LO#gIQv2A1xCqp;M5qt8qfctfh4C;bEU{l9SB+gyCv&Ws0Bae zMXx|}mM9|+L;Ty!O+5w_!C}o>1LruOn|vda_2BqvP#ODiOFJOIUySCOIlSmWP$38o zY^Iv?6)1VCCpsSiXEMV;c@YSq-f{&3S+=M15$M?e!56&}umJM5F?|`MZ}OraSeY@u zW+gB5NN84_T^}Po3?c-Tnd87ir}y8f2P}XXBR>sl593AeF!r@h?^9V3UqA(t0@e8vH))6Z z9{sooNC1bB@Hq6^e(z?%h-W2qI{SICN8_+${{%)u`#^@jL!UR0n>8Z~f~o+TM?p{% z1ksvx96X%PO&&lqJ+C*)8A`M5!Pu=?KZ5Y_TL`sgZVCcc4@qYxFZe`r3xK36-=FiH z&^LOr^YLmfX0MPtebqJRBh>Hc$3*}kNSMnPyaKiVvcX}*D~ml6sP!+w$nywg*Fq&$92eoJ%#M<{DdFs-A3Cw==6qLtrHL1h}%6uL_jUd|i6zjJ6O&NdaH- z4rBj}Ewt71QjZ5)ve9IFPoH0t4Xt7dh_MlBwdOoPE&FvxV;{i$c1VoUzwpH$6wvCK z!ED&uF7VYj8c|KY z+))*P333Ks^bVz!*i?DxrXtpK=xY#Ti6P!NOeXAcFzwfp{ko$n01Pkv3lRKBK@(L~ zRd1X=efps5--#0^EEN?MFRxg!Vl3aJ1E)`)I#^Rv^9e6{aN@*q%er;ze$In$-rAZCvbqM;f?U6Y4F1`WHZ8G@vgGl=Sp; z(|6x}cWB?fePuSAZF5so)1i!vj2Vj-EjonG9JCpT@{R8m$}W~#5R-<*_`l*TvdejL^;o5PyTeEqXmWNV*O6Cmg? ztd;IH)&n~os zcyh^-B}&E~6BDQ0P|}=zPcvbkaryFPkLSN8O`5cL%a$!A zxNV?A(5|8`044rod<*m6M+Awh18#0^_7DIaYBrmfV)$@5JT>)pr2|$r;9!VBr-Or+ zrldIIzpoD0wQWjDit@dYLRA1ra{US*QvEG_F{E@JvADnlthl&X8LMyKzP-JZ9W-c2 z&bf2vJjB|zZQFC1nVB;(Gc#sj2hGhMxQ3$MT1Yri6@U?PmM?lmphVl0^Z>yY(AL&= z2|GvphZ!+utn=p0vtsD^@WT&HuG){`3j2#QXU=#CfHgZVE-tBm{{d;P{sgzj@jvbt zL)q4?Tc1Tyaa~`Z3 zZueLpA0MBD;mBLQfM6@Y^{x&YlCew2jvd>K;b!K{nPoU!#nyqMdK>bF6V+J}c57BC zCG-!x=m8^alhITYn)|3XA?hta7X07oV&p!Yyu?er1c<8R@P>>iNoW1t6Ob9rjxisp z;l&Tyh6HFcm}V6Vc7lu$~I+#D2)F*~?*6&1wa?3};z z|L>IfKs^yd7^&b(-l1e%t0hZy8eTL~xVq?3yN(@$5WA?W&PQ+`u&*bXtgkQppy&~9 z_KBtzfD^MESM5!(V?U20v1JBR&A;(N)RVh1*N|jPeg{0Zkek8;dsj?td#19hk0{~e zIOFM0s|z!d`#QmOfH;M2Gx%=DGh`?MGxo8au#pYjO-FjU{#r_DI4{E2dp8F$)3 zkOKRVFL(u8hl9${fkii1^CRhe1V&2JZp7F(0uXbHX3`v3$VQF73PZ{Ux7J|zQvfeh*M1f!{TA2&q>g&!0q6Ipi*ybzYFw^{-J zT#$^0%*r|C%Dwae`T$VN+}H{qeiJTGS%BuY7GF!rO;G_13;!b$&X3UUFDiN&2UiReO~Bx^4Cti4J7b6f(sQ!aQ@SIb4Y34Ttx_9 zFhUjkv$!XB+DP@{Aa-VBa+j|UCF7W zbb@aYz&A03Ig}A&Kz)Ea;M{Wf<7vF;73>dSn@0?0+a_*`I4{5avQU+_rxDsdw1V*9 z?tFKSe}9U+wRbIPlkD8$24klO^i50+xCOY7bfgrMFJANt_6LlTwGQj(E z%?qdVv6h$SJ?7zjZ@v?1;jEUeCn`d&ETep z0+l3e;-h2H!5DIUam{{%NX+|HYJri0pano6Ir}U0D5~#+P(+|)6ND{Q!9opX>|Va8 zicZjx#29UpJe)OuuGgCH;y_a^dB1uik!0^L;AMmc-vV6dV)`zIJ*kfQq2tkk5Gx(l z?A3U2kDDWbrhC=`#LJSLnPkc2g(SI8e{S^-*8HdP9`#%E4>Z-0KOOiRWUs#O(J;y~ zx-0;`eoUv+O@p?sb(;DD@mkw|w^_#o{8HCofb3oMEsp5?1$RG0#t+RV1LBg%uP2s} zTlx;*Ydhi!I~mEopILW0f0NIe?}9QixXS_{!hMIDMM0Pf@wSPZRG?Xu$oud{kG%Iz zzi-^?GK~362;H6U0!4V?NAo>I(Bk}qO-D63AAzvkFC+pW_-hmp(~7i;fFTL~D-~Pg z8tm4r7rG;JhTMQGyr;N>^If0_izk2xeVng^!=Wa|d^P94f53Ol_eRJBK=8)AB+wtj z;&G=%9q1Qs1ut(9ni4Vf@RRM!9Wu4v_)6h!%zhvZr z3^SQNqW#Gw4^Sv+A99!JvV2KL!8eTX}1t#xy``T`x1? zYOG$qOqw#UkWsZde0_bZC?x6#yA5)aR3_7GMhJ?Z_iA(hgfDNzId|rV=@};Sz=*(8 zhG3nqz{sYs3E)DsWIg8m0f+ERV^?GS<#k-u19`W4JrSiK zoDVuuzu+bRbk+F?m4%A1w*YrU zOZG1yE%`l!(}?|S6@#(fywfS9S=4chI%+K;6wb%cI{D;;^su)8cl2i4NBX#yO!)HO zba=c!j8Nz}%zXoURkgpCU_1MDmz|GL!q(wb#)t}l;QN`4X4|U+zZQF z)d%V%a({f_FTp|)KIS5X%K1*{dle;HPb*7GIZD7^*^>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D5VlD~K~#8N?VJmE z6~z^R=ghs~p;kqyM6?n>iXer2609f~K`0af3G#@L$V22+ixfn#yqd3qQ2YS3f(p_? zp(t+yM9=_iMsACj%?)32|Bqp3&z#-No-=c1 zW_Rz37%^hRh!G=3j2JOu#E21Hz-%itw+>S&N{MI48i|1744EYh133QZY-R)V1e2F0px^3{(!jqz^n3v5qVCT zIAF?@DcA9K%$PCXYJtI)+mtN#k~&YS`!FCjkOL}f0pc7HfKkei?Z_w#K|q|5zh9*Q z+U{No=kJC7~wFe+l-t8yO6 zU(w~1`~v#cAFPI%Vo!;>18hSdKlasjEilxqSyRh5Z`(GrX|rZ2iHV6RjT$vdnLc@P z!l_fI4#vmFC$3nrqK{UN6l&$tFaA!(|Ai#O0^&D5pXhwxbk;wsFr3Oa zu%1cN8w^q%OtEJ~-2n*+2@mi#|H$DSEil}4(@oj|U*+eA$VZJIe`xF0tur}q>qJV! z>v6gi&`}#-;RT-(-FDBC`?&0>&8lbM93CJ3p_)%|A96FGSQd*OQsG|90pOELEN(bR zHPk2bh%)N|-xL&t^Z;(*$BrE<95Q6c4kHS}J%G1Wt5&^U3nHNL0QES9l3~HHf@0X` zbL1v28~LGXMzfuHaRR7=z++E;gyUGg`wjVx42Vt=u}~i;;99>DE1LAdi0e_3s5^iS z%?5QF+8!539@6@4?C?Ym3US({lK6y5rVE{AhZAc3FMl>yzRG1Ib5(6J?rJ~4RXhso zgd$^fo=W5Lo%7_sXmOTG;V58wLVXd3OOhv|pUW?os5*cRU8PEu>q{Ea?iJsjK3k{- zF8G+njT`qZDk>`6y?gg6Bfeyk`m6dBN>)k*l_-P=3Mtvw-wGqRY~mJmZDlDksnyr8 zdxmN0K*oOK**w*X^~hQ4DO`=Qj;C@3ZBHM@XMAZRx>(F34VB# z(J}3P}`>D#a`#0H6D9M>KX9~#?!@G9vI^T#cnY;U_<+RZ1 zYVF}^rNq747Ff3yGTv7df?$v?`X3Wl$L4wR zM>PyLQFQ>%<#`O4FkwQ%q)C&4mHzI#?@sY{+qP|g)dGTK2RT%oJb5x?y>O}A_n5j< z0qo*D8H(rl9kv;dUTtyy~S8Qpf{v_3G7Y8?lb@BSwrk$crwX_Z#tL(;{7^Qz$q0d2n^!0@>Nhlg08> zeN`;_QtxnI|7f#irkbq=1nwN2EEkzw(IQQy3JHt#4q{H;-=rQZu4oo6T=+2GTG+K~ z*GePO*mAFLH?&B@F88M+a%!2b-Z$c~Ov^#ElM#i5&ezjE_x))q*N7t`YJ)Hjq`W68NG~jfki^ps=uz zR<$RChURg>UYPyZDdwfnvL5`AjPxA!grcA$f(dkwv~gOr5g16X)(?**<0Br71M#c% zi{w{EH6o(s0KT8|_ShMQea$;beey;zP{esL=~;#aiF?Get2zjFkeroVo^nPR9S#s6|Xvr=eE@9a5b^HtS7i7BnZ@kw4MKk%!awim0xHuLRs- z#Ep9u>?WMPS5!C$xI>DY;Eg!F=MYZcD`Gvs5YEuy^t~c#V?eiVs;W}Z4gfn-ByCEO zwFWIjYnb9PVc9Y#roXk`iS<3HmL^N}cI0k-uPF5%kerPE?p<*p%^(i=L9Fx%=ton# z#BZe3-O+>})hC{+ifc2+gxZ^RENuD(VKn0WWaTnWidxG^A{zE_l52ukmw8UzI2 zx`kVeh;Qv)piaCF5!TQZeF4SdIQrp&DUJ$&V!z3XMJt_~p@Sn2r|%V|5`w6^ z&Z8J#H`sj@PNd3rDQn|1!8!%aYEf2Wfaf0V*HI;dgy0=d6!^klGUX_s?H6}XcY5W#Os7R}SnIU(2Oo0e{X08yx4u_+e!>AA)keBf>j4Gg zXWv$ZyOx@_&Sq>qD63N2qLjFa`sA3h`j>-htsZnP3vu}-*{URL*lJnd3LJE(~iUjD#&iv7!HWyx-w zpOvi!(2Qsf=cyp~*axCD-zwJoQRX)toRIQLmJ?Dw&UEB=^IF{yPTwm$?E%zHYueqF zT2k=mJ$@nXvKKj0;>%@ae8#RHCpSMeHmhL{LH1O5|{NALw2)`@H*x{$-gr9V-b6UYM7J;l?Q_WK7Z)`K$!Jioyj1OC3A#s(u-D3W@&v&MjM z`q5_pGW8^Nj;1M-1=3o#+hl4!ONVF7StGS1A%?04tmZ5DH}%7{w><(&tlAMMdfN4) z@{kv~yuwol=pQebi*dhUrenQHZru9X8D*HLCoGFYNTuFDwGm=sGPWV-JYdxii@_&? zuhV-8|839U%Gxc)-2;L~rWDcxY+Xvl=kX+U^8?XLRE2-g*fU>&XnRX{*}X;&ILq~# z{Ghs#2IE&<*YbKswmiyZUgPN)@LY08tj`#mYmEVGvK%=ok<_uHH3o#!FH5N9SC12z zN%d;31&D>zo$naCQ6AU(9^-8faD^0i4{&$&kRA|5zpQwk!cG;$kNpVEtvX?VLSwJe zGCt#}1A<+alaVoQL%Rv5A1$c0Uo)+RYWSy?YS7e{Y7^D-Js(1Ct`Cs$sZLLhd0*-~ zx}}AuA?Sq`u8>0W`jH=6Atif_Gu@;iE~&K%K_U8A5Hvm%0*FUP=2<^!Dos3Xc-+!2 z)}rm!U$O^;=wAs*VKJrGl^+^J!}yG+4&akH7h}m7_k7#mKzTa^xqE~x0U_%E zm%dwGk+^IJ%x$KU<~CKor8JsaazndChlC*7zgP%z_Y5e;Hgl3xr`MXI{snqPAz}wC zX^7YS^)9v2uV9;Bz)O^-P)f87?LM&%H72=nj}3f$971MgfnU;j7xVdzTDc}-2NYGt z1yVqlV2dx2e26EFYKWmrOhdo^;jtj;ys314>r(*boQNE-un8uTky31<(R}&~TH!9W z6V0tBe-9A+7>17jq{qpP|Gbz)3!j&rV=;@)Rjkh|pyQ&Z*utomTOwKqEJ{>E$w04g z9tHmU+;z~TUoI5s!i(^)70x<9(mLSwl<@QY#Z8H=clQk4V%`i;0CfSu?+`(tcx#!R|&A3^K|YR zbZ-51tMW=j4p>4Vh(d<>jwhZM0nw`a=kg=o`*InMp_4wZi!K4`K`+(rCiRz6vTj$E z&t4^RsJGms3jx!+?~n_1zMK-#7|>$}Y^ON?t`*mz5{;TGR^F+8#$^dgOrx?)KJoug z_(`Qo@GNCDMODO~{RsE)d1-9;_t`>NW{S!!Y=?-zVlj}6bb<#=azYINk-5IHx*B>w zpfW;lngwmQ2i}V%a?-&{=S9a`I>UeXeB{8VXz zhsahbG;=v;&4wWHT(MX{AqDu^YVif}5C}ZXF|K4H_noGda_9J7){~~ycdB;elqq^f zMS-83**9fp#kesSu>+VCiu+`=LBW5?ld2A9R)i(a7Wb{KoEb$Ad$H&p}|17==9;-;kXFTZv z3{4@gjg#UdYRz|Q0pd|I_}y*x$|JmcWD!=sr8gAlG9tUaqJBi-Yqd}2 z>%t0zXAWTAtA}-HYRNy)HlKmV%|%MI>ab6qv-T}E)K~oh;SJ7H*eRkhpcNT^i!Q8S zcoKqa+U|t|G?jeRr?A)>n4$UAAgtep8FK2WmcD?PZI$ci1_LUp@flAYz_i*6*%ZY` zSW{C@kp4kk)X#=<>#Dn`weuHKD(NYw!YRr7oDZT_L_7^aZu07=*C-SPKWb?0g { + verbose('Service installed.'); + info('Starting service, please accept UAC prompts if any...'); + svc.start(); + }); + + svc.on('start', () => { + verbose('Service started.'); + }); + + svc.on('alreadyinstalled', () => { + warning('Service is already installed!'); + info('Starting the service in case it is not running, please accept UAC prompts if any...'); + svc.start(); + }); + + info('Installing service, please accept UAC prompts if any...'); + svc.install(); +})(); diff --git a/src/ip-changed.png b/src/ip-changed.png new file mode 100644 index 0000000000000000000000000000000000000000..8e5fc109ef1fc4c45886f73f08b05dfa208d8127 GIT binary patch literal 4385 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 { + onNotifyIpInterfaceChange(callerContext, row, notificationType); + } + ); + + // callerContext is not used by this class. + this.callerContext = ref.alloc('pointer'); + + // Handle returned by NotifyIpInterfaceChange, which can be used to cancel the registration. + this.notificationHandle = ref.alloc('pointer'); + } + + static get EventType() { + return EventType; + } + + start() { + this.currentAddress = this.getIP(true); + if (this.clientCallback) { + // Send intial callback for current IP address + this.clientCallback(this.currentAddress, EventType.Initial); + } + + return (this.iphlpapi.NotifyIpInterfaceChange(AddressFamilyApiValue[this.addressFamily], this.callback, this.callerContext, 0, this.notificationHandle) === 0); + } + + stop() { + this.clientCallback = null; + return (this.iphlpapi.CancelMibChangeNotify2(this.notificationHandle.deref()) === 0); + } + + onNotifyIpInterfaceChange(callerContext, row, notificationType) { + //print(`Callback type: ${NotificationTypesApiValue[notificationType]}`); + const newAddress = this.getIP(); + let eventType = EventType.Initial; + if (newAddress !== null) { + if (this.currentAddress === null) { + eventType = EventType.IPAssigned; + } else { + for (const family in newAddress) { + if (this.currentAddress[family] !== newAddress[family]) { + eventType = EventType.IPChanged; + } + } + } + } else { + if (this.currentAddress !== null) { + eventType = EventType.IPRemoved; + } + } + + this.currentAddress = newAddress; + if (eventType !== EventType.Initial && this.clientCallback) { + this.clientCallback(this.currentAddress, eventType); + } + } + + getIP(allowLinkLocalAddress = false) { + const networkInterfaces = os.networkInterfaces(); + if (networkInterfaces && networkInterfaces[this.networkInterface]) { + const result = {}; + for (const ip of networkInterfaces[this.networkInterface]) { + if ((this.addressFamily === ip.family || this.addressFamily === 'Any') + && (allowLinkLocalAddress || !this.isLinkLocalAddress(ip.address, this.addressFamily))) { + result[ip.family] = ip.address; + } + } + + return Object.keys(result).length > 0 ? result : null; + } + + return null; + } + + isLinkLocalAddress(address, addressFamily) { + if (addressFamily === 'IPv4') { + return address && address.startsWith('169.254'); // IPv4 link-local address block is 169.254.0.0/16 + } else if (addressFamily === 'IPv6') { + return address && (address.startsWith('fe8') || address.startsWith('fe9') || address.startsWith('fea') || address.startsWith('feb')); // IPv6 link-local address block is fe80::/10 + } + return false; + } + } + + module.exports = NetworkInterfaceMonitor; +})(); diff --git a/src/print.js b/src/print.js new file mode 100644 index 0000000..7a2003c --- /dev/null +++ b/src/print.js @@ -0,0 +1,47 @@ +(function() { + 'use strict'; + + const chalk = require('chalk'); + + const dateTimeFormatOprions = { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }; + + function formatMessage(msg) { + return `[${new Intl.DateTimeFormat('en-US', dateTimeFormatOprions).format(new Date())}] ${typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2)}`; + } + + function print(msg, color = chalk.white) { + console.log(color(formatMessage(msg))); + } + + function error(msg) { + console.log(chalk.red(formatMessage(msg))); + } + + function warning(msg) { + console.log(chalk.yellow(formatMessage(msg))); + } + + function info(msg) { + console.log(chalk.cyan(formatMessage(msg))); + } + + function verbose(msg) { + console.log(chalk.green(formatMessage(msg))); + } + + module.exports = { + print, + error, + warning, + info, + verbose, + }; +})(); diff --git a/src/settings.json b/src/settings.json new file mode 100644 index 0000000..b5f8fbd --- /dev/null +++ b/src/settings.json @@ -0,0 +1,16 @@ +{ + "networkInterface": "Local Area Connection", + "addressFamily": "IPv4", + "dnsProvider": "Dynu", + "domainName": "xxx.theworkpc.com", + "domainID": "", + "accessKey": "xxxxxx", + "showNotification": false, + "notificationTypes": [ + "DNS Registration", + "Scheduled DNS Registration", + "IP Changed", + "IP Assigned", + "IP Removed" + ] +} \ No newline at end of file diff --git a/src/show-interfaces.js b/src/show-interfaces.js new file mode 100644 index 0000000..6ebdb42 --- /dev/null +++ b/src/show-interfaces.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node + +(function() { + 'use strict'; + + const os = require('os'); + + let family = null; + if (process.argv.length > 2) { + family = process.argv[2].toLowerCase(); + if (family === '4' || family === 'v4') { + family = 'ipv4'; + } else if (family === '6' || family === 'v6') { + family = 'ipv6'; + } + } + + const interfaces = os.networkInterfaces(); + for (const name in interfaces) { + interfaces[name] = interfaces[name].filter(ipAddress => { + if (family === null || ipAddress.family.toLowerCase() === family) { + for (const property in ipAddress) { + if (property !== 'address' && (property !== 'family' || family !== null)) { + delete ipAddress[property]; + } + } + return true; + } + + return false; + }); + } + + console.log(JSON.stringify(interfaces, null, 2)); +})(); diff --git a/src/uninstall-service.js b/src/uninstall-service.js new file mode 100644 index 0000000..8fcd754 --- /dev/null +++ b/src/uninstall-service.js @@ -0,0 +1,23 @@ +(function() { + 'use strict'; + + const path = require('path'); + const Service = require('node-windows').Service; + const { + info, + verbose } = require('./print.js'); + + // Create a new service object. + const svc = new Service({ + name: 'Update Dynamic DNS', + script: `${path.join(__dirname, 'app.js')}`, + }); + + // Listen for the "uninstall" event so we know when it's done. + svc.on('uninstall', () => { + verbose('Service uninstalled.'); + }); + + info('Uninstalling service, please accept UAC prompts if any...'); + svc.uninstall(); +})();