Last active
March 4, 2021 05:20
-
-
Save xorxornop/9ce915e5475438e83f7142727eea8981 to your computer and use it in GitHub Desktop.
Update Dynamic DNS record at Namecheap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.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 # | |
# === # |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
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'