Plugins/HurricaneElectric.ps1
function Get-CurrentPluginType { 'dns-01' } function Add-DnsTxt { [CmdletBinding(DefaultParameterSetName='Secure')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','')] param( [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue, [Parameter(ParameterSetName='Secure',Mandatory)] [pscredential]$HECredential, [Parameter(ParameterSetName='Insecure',Mandatory)] [string]$HEUsername, [Parameter(ParameterSetName='Insecure',Mandatory)] [string]$HEPassword, [Parameter(ValueFromRemainingArguments)] $ExtraParams ) Connect-HurricaneElectric @PSBoundParameters $zone = Find-HEZone $RecordName Write-Verbose "Found domain $($zone.domain) ($($zone.id))" $rec = Get-HETxtRecord $zone.id $RecordName $TxtValue if ($rec) { Write-Debug "Record $RecordName already contains $TxtValue. Nothing to do." } else { # add the new record Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue" # build the form body $addBody = "account=&menu=edit_zone&Type=TXT&hosted_dns_zoneid=$($zone.id)&hosted_dns_recordid=&hosted_dns_editzone=1&Priority=&Name=$RecordName&Content=$TxtValue&TTL=300&hosted_dns_editrecord=Submit" $iwrArgs = @{ Uri = 'https://dns.he.net/' Method = 'Post' Body = $addBody WebSession = $script:HESession ErrorAction = 'Stop' } try { $response = Invoke-WebRequest @iwrArgs @script:UseBasic } catch { throw } $reStatus = '"dns_status"[^>]+>(?<status>[^<]+)<' if ($response.Content -match $reStatus) { $status = $matches['status'] if ($status -notlike 'Successfully added new record*') { Write-Warning "Unexpected result status while adding record: $status" } } else { Write-Debug "No dns_status div found after add." } } <# .SYNOPSIS Add a DNS TXT record to Hurricane Electric .DESCRIPTION Add a DNS TXT record to Hurricane Electric .PARAMETER RecordName The fully qualified name of the TXT record. .PARAMETER TxtValue The value of the TXT record. .PARAMETER HECredential Username and password for Hurricane Electric. This PSCredential option can only be used from Windows or any OS running PowerShell 6.2 or later. .PARAMETER HEUsername Username for Hurricane Electric. This should be used from non-Windows OSes running PowerShell 6.0-6.1. .PARAMETER HEPassword Password for Hurricane Electric. This should be used from non-Windows OSes running PowerShell 6.0-6.1. .PARAMETER ExtraParams This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. .EXAMPLE Add-DnsTxt '_acme-challenge.example.com' 'txt-value' -HECredential (Get-Credential) Adds a TXT record using after providing credentials in a prompt. .EXAMPLE Add-DnsTxt '_acme-challenge.example.com' 'txt-value' -HEUsername 'user' -HEPassword 'pass' Adds a TXT record using plain text credentials. #> } function Remove-DnsTxt { [CmdletBinding(DefaultParameterSetName='Secure')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','')] param( [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue, [Parameter(ParameterSetName='Secure',Mandatory)] [pscredential]$HECredential, [Parameter(ParameterSetName='Insecure',Mandatory)] [string]$HEUsername, [Parameter(ParameterSetName='Insecure',Mandatory)] [string]$HEPassword, [Parameter(ValueFromRemainingArguments)] $ExtraParams ) Connect-HurricaneElectric @PSBoundParameters $zone = Find-HEZone $RecordName Write-Verbose "Found domain $($zone.domain) ($($zone.id))" $rec = Get-HETxtRecord $zone.id $RecordName $TxtValue if ($rec) { # remove the record Write-Verbose "Removing TXT record for $RecordName with value $TxtValue" # build the form body $delBody = "menu=edit_zone&hosted_dns_zoneid=$($zone.id)&hosted_dns_recordid=$($rec.id)&hosted_dns_editzone=1&hosted_dns_delrecord=1&hosted_dns_delconfirm=delete" $iwrArgs = @{ Uri = 'https://dns.he.net/' Method = 'Post' Body = $delBody WebSession = $script:HESession ErrorAction = 'Stop' } try { $response = Invoke-WebRequest @iwrArgs @script:UseBasic } catch { throw } $reStatus = '"dns_status"[^>]+>(?<status>[^<]+)<' if ($response.Content -match $reStatus) { $status = $matches['status'] if ($status -ne 'Successfully removed record.') { Write-Warning "Unexpected result status while removing record: $status" } } else { Write-Debug "No dns_status div found after add." } } else { Write-Debug "Record $RecordName with value $TxtValue doesn't exist. Nothing to do." } <# .SYNOPSIS Remove a DNS TXT record from Hurricane Electric .DESCRIPTION Remove a DNS TXT record from Hurricane Electric .PARAMETER RecordName The fully qualified name of the TXT record. .PARAMETER TxtValue The value of the TXT record. .PARAMETER HECredential Username and password for Hurricane Electric. This PSCredential option can only be used from Windows or any OS running PowerShell 6.2 or later. .PARAMETER HEUsername Username for Hurricane Electric. This should be used from non-Windows OSes running PowerShell 6.0-6.1. .PARAMETER HEPassword Password for Hurricane Electric. This should be used from non-Windows OSes running PowerShell 6.0-6.1. .PARAMETER ExtraParams This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. .EXAMPLE Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' -HECredential (Get-Credential) Removes a TXT record using after providing credentials in a prompt. .EXAMPLE Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' -HEUsername 'user' -HEPassword 'pass' Removes a TXT record using plain text credentials. #> } function Save-DnsTxt { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments)] $ExtraParams ) <# .SYNOPSIS Not required .DESCRIPTION This provider does not require calling this function to save DNS records. .PARAMETER ExtraParams This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. #> } ############################ # Helper Functions ############################ # Adapted from # https://github.com/Neilpang/acme.sh/blob/master/dnsapi/dns_he.sh # Web scraping is obviously brittle and can easily break if the site owner changes their markup. # But without a well-defined API, this is the only option for automated record manipulation. function Connect-HurricaneElectric { [CmdletBinding(DefaultParameterSetName='Secure')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','')] param( [Parameter(ParameterSetName='Secure',Mandatory)] [pscredential]$HECredential, [Parameter(ParameterSetName='Insecure',Mandatory)] [string]$HEUsername, [Parameter(ParameterSetName='Insecure',Mandatory)] [string]$HEPassword, [Parameter(ValueFromRemainingArguments)] $ExtraConnectParams ) # no need to login again if we already have an authenticated session if ($script:HESession) { Write-Debug "Using cached HE.net session" return } # get plain text versions of the pscredential we can work with if ('Secure' -eq $PSCmdlet.ParameterSetName) { $HEUsername = $HECredential.UserName $HEPassword = $HECredential.GetNetworkCredential().Password } # URI escape the credentials $userEscaped = [uri]::EscapeDataString($HEUsername) $passEscaped = [uri]::EscapeDataString($HEPassword) $siteRoot = 'https://dns.he.net/' $loginParams = @{ Uri = $siteRoot Method = 'Post' Body = "email=$userEscaped&pass=$passEscaped" ErrorAction = 'Stop' } try { # Do an initial GET to establish the session cookie Invoke-WebRequest $siteRoot -SessionVariable 'HESession' @script:UseBasic -EA Stop | Out-Null # try to login $response = Invoke-WebRequest @loginParams -WebSession $HESession @script:UseBasic } catch { throw } if ($response.Content -like '*>Incorrect<*') { throw "Invalid he.net login credentials. Please check username and password." } # save the session variable for later $script:HESession = $HESession } function Get-HEDomains { [CmdletBinding()] param() $url = 'https://dns.he.net/' $reDomains = [regex]'onclick="delete_dom\(this\);" name="(?<domain>[-_.a-z0-9]+)" value="(?<id>\d+)"' Write-Debug "Querying domains page" try { $response = Invoke-WebRequest $url -WebSession $script:HESession @script:UseBasic -EA Stop } catch { throw } $domains = $reDomains.Matches($response.Content) if ($domains.Count -eq 0) { throw "Unable to find any domains." } else { Write-Debug "$($domains.Count) domain matches found." } $domains | ForEach-Object { [pscustomobject]@{domain=$_.Groups['domain'].value; id=$_.Groups['id'].value} } } function Get-HETxtRecord { [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [string]$DomainID, [Parameter(Mandatory,Position=1)] [string]$RecordName, [Parameter(Mandatory,Position=2)] [string]$TxtValue ) $url = "https://dns.he.net/?hosted_dns_zoneid=$DomainID&menu=edit_zone&hosted_dns_editzone" $reVal = [regex]'data=""(?<val>[^&]+)"' $reName = [regex]'deleteRecord\(''(?<id>\d+)'',''(?<fqdn>[-_.a-z0-9]+)''' Write-Debug "Querying records for domain $DomainID" $response = Invoke-WebRequest $url -WebSession $script:HESession @script:UseBasic # split the content by line breaks so we can loop through it looking for the data $lines = $response.Content -split "`r?`n" for ($i=0; $i -lt $lines.Count; $i++) { if ($lines[$i] -like '*rrlabel TXT*') { # the next line should have the record value if ($lines[$i+1] -notmatch $reVal) { Write-Debug "Failed to parse TXT record value from line following 'rrlabel TXT'" continue } $recVal = $matches['val'] # the line after that should have the record name and ID if ($lines[$i+2] -notmatch $reName) { Write-Debug "Failed to parse TXT record name/ID from line following 'rrlabel TXT'" continue } $recFQDN = $matches['fqdn'] $recID = $matches['id'] # send back a match if we found it if ($recFQDN -eq $RecordName -and $recVal -eq $TxtValue) { [pscustomobject]@{ fqdn = $recFQDN id = $recID value = $recVal } return } $i += 2 } } } function Find-HEZone { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$RecordName ) # setup a module variable to cache the record to zone ID mapping # so it's quicker to find later if (!$script:HERecordZones) { $script:HERecordZones = @{} } # check for the record in the cache if ($script:HERecordZones.ContainsKey($RecordName)) { return $script:HERecordZones.$RecordName } # grab the set of owned domains for this account $domains = Get-HEDomains # Search for the zone from longest to shortest set of FQDN pieces. $pieces = $RecordName.Split('.') for ($i=0; $i -lt ($pieces.Count-1); $i++) { $zoneTest = $pieces[$i..($pieces.Count-1)] -join '.' Write-Debug "Checking $zoneTest" if ($zoneTest -in $domains.domain) { $script:HERecordZones.$RecordName = $domains | Where-Object { $zoneTest -eq $_.domain } return $script:HERecordZones.$RecordName } } throw "No zone found for $RecordName" } |