Skip to content

Instantly share code, notes, and snippets.

@xorxornop
Last active March 4, 2021 05:20
Show Gist options
  • Save xorxornop/9ce915e5475438e83f7142727eea8981 to your computer and use it in GitHub Desktop.
Save xorxornop/9ce915e5475438e83f7142727eea8981 to your computer and use it in GitHub Desktop.
Update Dynamic DNS record at Namecheap
<#
.Synopsis
Updates a Dyanmic DNS record at Namecheap
.Description
Updates a Dyanmic DNS subdomain record at Namecheap, auto-detecting the external IP address of the local host for assignment (or that of a supplied IP value)
.Example
# Named arguments:
PS C:\> .\UpdateDynamicDNS-Namecheap.ps1 -HostRecord 'thehost' -Domain 'thedomain.tld' -Key 'thekey'
PS C:\> .\UpdateDynamicDNS-Namecheap.ps1 -HostRecord 'thehost" -Domain 'thedomain.tld' -Key 'thekey' -IP '127.0.0.1'
PS C:\> .\UpdateDynamicDNS-Namecheap.ps1 -HostRecord 'thehost" -Domain 'thedomain.tld' -Key 'thekey' -IP '127.0.0.1' -ForceUpdate
# Positional arguments only, where applicable:
PS C:\> .\UpdateDynamicDNS-Namecheap.ps1 'thehost' 'thedomain.tld' 'thekey'
These commands will modify the record for "thehost.thedomain.tld" to the external IP (what the rest of the Internet sees) of the computer that executes the script, with the exception of where the '-IP' argument is used, in which case it will be modified to that IP instead.
.Inputs
-HostRecord
-Domain
-Key
-IP (non-mandatory)
-ForceUpdate (non-mandatory)
-PauseBeforeExit (non-mandatory)
.Outputs
Windows Toast notification on successful DNS record update; otherwise, displayed terminal text diagnostics.
.Notes
For use ONLY with Namecheap-hosted Dyanmic DNS services
#>
Param(
[Parameter(Mandatory = $True, Position = 0,
HelpMessage = "Please enter the host record (e.g. [email protected]) to update:")]
[ValidateNotNull()]
[ValidateNotNullOrEmpty()]
[string]$HostRecord,
[Parameter(Mandatory = $True, Position = 1,
HelpMessage = "Please enter your domain (e.g. [email protected]) to which the host record is registered:")]
[ValidateNotNull()]
[ValidateNotNullOrEmpty()]
[string]$Domain,
[Parameter(Mandatory = $True, Position = 2,
HelpMessage = "Key/password is required for authentication. Please enter your key:")]
[ValidateNotNull()]
[ValidateNotNullOrEmpty()]
[string]$Key,
[Parameter(Mandatory = $False,
HelpMessage = "By default, the targeted DNS record will be set to that of the external IP of the script execution host (e.g. this computer). To force a different IP to be set instead, enter it here:")]
[AllowNull()]
[string]$IP,
[Parameter(Mandatory = $False,
HelpMessage = "Where this switch is set, the record will be updated irrespective if whether it appears to already match the intended value.")]
[switch]$ForceUpdate,
[Parameter(Mandatory = $False,
HelpMessage = "Where this switch is set, the user must press a key to continue after the operation (keeps window open for user to observe result text).")]
[switch]$PauseBeforeExit
)
# ==================== #
# SUPPORTING FUNCTIONS #
# ==================== #
function Test-IPAddress {
[CmdletBinding()]
[OutputType([ipaddress])]
Param (
# Parameter help description
[Parameter(Mandatory = $true,
ValueFromPipelineByPropertyName = $true,
Position = 0)]
[ValidateScript( {$_ -match [ipaddress]$_ })]
[string]$IPAddress
)
Begin {}
Process {
return [ipaddress]$IPAddress
}
End {}
}
function Get-ExternalIPAddress {
[CmdletBinding()]
[OutputType([ipaddress])]
Param ()
Begin {}
Process {
$externalIP = Invoke-RestMethod -Uri "http://ipecho.net/plain" -Method Get
return Test-IPAddress $externalIP
}
End {}
}
function Format-Xml {
<#
.Synopsis
Pretty-print formatted XML source
.Description
Runs an XmlDocument through an auto-indenting XmlWriter
.Example
[xml]$xml = get-content Data.xml
Format-Xml $xml
.Example
Get-Content Data.xml | Format-Xml
.Example
Format-Xml Data.xml -Indent 1 -Char `t
Shows how to convert the indentation to tabs (which can save bytes dramatically, while preserving readability)
.Example
ls *.xml | Format-Xml
#>
[CmdletBinding()]
[OutputType([string])]
param(
# The Xml Document
[Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ParameterSetName = "Document")]
$Xml,
# The path to an xml document (on disc or any other content provider).
[Parameter(Position = 0, Mandatory = $True, ValueFromPipelineByPropertyName = $True, ParameterSetName = "File")]
[Alias("PsPath")]
[string]$Path,
# The indent level (defaults to 2 spaces)
[Parameter(Mandatory = $False)]
[int]$Indent = 2,
# The indent character (defaults to a space)
[char]$Character = ' '
)
Begin {}
Process {
# Load from file, if necessary
if ($Path) { [xml]$xml = Get-Content $Path }
$StringWriter = New-Object System.IO.StringWriter
$XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter
$xmlWriter.Formatting = "indented"
$xmlWriter.Indentation = $Indent
$xmlWriter.IndentChar = $Character
$xml.WriteContentTo($XmlWriter)
$XmlWriter.Flush()
$StringWriter.Flush()
Write-Output $StringWriter.ToString()
}
End {}
}
Function Pause ([string]$Message = "Press any key to continue . . . ") {
if ((Test-Path variable:psISE) -and $psISE) {
[void](Read-Host 'Press Enter to continue . . .')
}
else {
Write-Host -NoNewline $Message
[void][System.Console]::ReadKey($true)
Write-Host
}
}
# ===== #
# START #
# ===== #
# Info:
Write-Host "`nDynamic DNS Update Utility" -BackgroundColor Black -ForegroundColor Green -NoNewline
Write-Host " v1.0"
Write-Host "Matthew Ducker 2021`n" -BackgroundColor Black -ForegroundColor Magenta
if ($HostRecord -eq "@") {
$domainRecord = $Domain
} else {
$domainRecord = "$HostRecord.$Domain"
}
# Validate provided (forced/overridden) IP:
if ($IP -ne "") {
# IP override is requested.
# Record will be set to supplied IP instead of execution host's external IP
try {
Write-Debug "Testing provided IP override '$IP' for validity by parsing. Result: '$(Test-IPAddress $IP)'"
Write-Information "Target IP validated. IP assignment override to $IP in effect for $domainRecord`n"
}
catch {
Write-Warning "IP supplied for override is malformed: Reverting to default behaviour."
$IP = ""
}
}
try {
# Get record's current value for comparison to target value
$currentRecord = Resolve-DnsName -Name $domainRecord
}
catch {
Write-Warning "Could not resolve current record for $domainRecord`n"
if ($PauseBeforeExit) { Pause }
exit 1
}
# Check whether record update is forced:
if ($ForceUpdate -eq $false) {
# Check if record is already the desired value:
if (($IP -ne "") -and $currentRecord.IPAddress -eq [ipaddress]$IP) {
# Record already set as desired, exit early
Write-Host 'Operation unnecessary: ' -NoNewline
Write-Host $domainRecord -BackgroundColor Black -ForegroundColor Green -NoNewline
Write-Host ' already set to supplied IP ' -NoNewline
Write-Host $IP -BackgroundColor Black -ForegroundColor Green
Write-Host
if ($PauseBeforeExit) { Pause }
Write-Host "`nExiting...`n"
exit 0
}
else {
$currentExternalIP = Get-ExternalIPAddress
if ($currentRecord.IPAddress -eq $currentExternalIP) {
# Record already set as desired, exit early
Write-Host 'Operation unnecessary: ' -NoNewline
Write-Host $domainRecord -BackgroundColor Black -ForegroundColor Green -NoNewline
Write-Host ' already set to external IP ' -NoNewline
Write-Host $currentExternalIP -BackgroundColor Black -ForegroundColor Green
Write-Host
if ($PauseBeforeExit) { Pause }
Write-Host "`nExiting...`n"
exit 0
}
}
}
# Construct the Dynamic DNS update service request URI:
$requestUri = "https://dynamicdns.park-your-domain.com/update?host=$HostRecord&domain=$Domain&password=$Key&ip=$IP"
try {
# Perform the HTTP request necessary for attempting the update, and read the response returned
Write-Host 'Updating Dynamic DNS record for ' -NoNewline
Write-Host $domainRecord -BackgroundColor Black -ForegroundColor Green -NoNewline
Write-Host ' ...'
Write-Debug "Performing HTTP GET request to: $requestUri"
$response = Invoke-RestMethod -Uri $requestUri -Method Get
}
catch {
Write-Warning 'Service unreachable: HTTP GET request failed.`n'
Write-Debug "Exception:`n`n$($_.Exception.Message)`n"
if ($PauseBeforeExit) { Pause }
exit 1
}
try {
# Attempt to reformat the response into well-formed XML.
$responseXmlFormatted = Format-Xml -Xml $response
}
catch {
Write-Warning 'Service response malformed: Cannot reformat response into well-formed XML.'
Write-Debug "Exception:`n`n$($_.Exception.Message)`n"
if ($PauseBeforeExit) { Pause }
exit 1
}
try {
# Extract the IP that the record was set to from the response XML.
$responseIP = Select-Xml -Content $responseXmlFormatted -XPath "/interface-response/IP"
Write-Information "Response IP (record updated to): '$responseIP'"
}
catch {
Write-Warning "Service response data malformed or unexpected: Cannot read the IP for record $domainRecord"
Write-Debug "Reformatted (prettified) response XML:`n`n$responseXmlFormatted`n"
Write-Debug "Exception:`n`n$($_.Exception.Message)`n"
if ($PauseBeforeExit) { Pause }
exit 1
}
# Display the result of the operation:
if ($IP -ne $responseIP) {
Write-Host "Successfully set external IP " -NoNewline
Write-Host $responseIP -NoNewline -BackgroundColor Black -ForegroundColor Green
Write-Host " to " -NoNewline
Write-Host $domainRecord -BackgroundColor Black -ForegroundColor Green
Write-Host
}
else {
Write-Host "Successfully set supplied IP "
Write-Host $responseIP -NoNewline -BackgroundColor Black -ForegroundColor Green
Write-Host " to " -NoNewline
Write-Host $domainRecord -BackgroundColor Black -ForegroundColor Green
Write-Host
}
$scriptName = [io.path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name)
Write-Debug "Creating new BurntToast AppId `'$scriptName`' for toast notification operations."
Write-Debug "Creating BurntToast toast notification. Will use AppId `'$scriptName`' for unique identifier for recurring/replacement notifications."
New-BurntToastNotification -Text "Dynamic DNS Record updated", "Record for $domainRecord successfully updated to $responseIP from previous $($currentRecord.IPAddress)" -Sound Default -UniqueIdentifier $scriptName -WarningAction SilentlyContinue
if ($PauseBeforeExit) { Pause }
Write-Host "`nExiting...`n"
exit 0
# === #
# FIN #
# === #
@xorxornop
Copy link
Author

xorxornop commented Aug 31, 2017

The 3rd party modules PSCX (the PowerShell Community Extensions) and BurntToast (displays notification 'Toast' popups) are required to be installed for this script to function as intended and/or at all. These can and should be installed through PowerShellGet.

Before doing that, install the latest NuGet provider by running the following in an elevated prompt:

  • Install the latest NuGet provider from an elevated console:
Install-PackageProvider Nuget -Force -Verbose
Exit
  • Update PowerShellGet (also from an elevated console):
Install-Module -Name PackageManagement -Force -Verbose
Install-Module –Name PowerShellGet -Force -Verbose
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Exit
  • Install the required modules (also from an elevated console):
Install-Module -Name Pscx -Force -Verbose
Install-Module -Name BurntToast -Force -Verbose
Exit

The script should now be ready for use. Example of execution follows (example values must be substituted for real value)s:
PS> .\UpdateDynamicDNS-Namecheap.ps1 -HostRecord 'host' -Domain 'mydomain.com' -Key 'mykey'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment