Plugins/Combell.ps1
function Get-CurrentPluginType { 'dns-01' } function Add-DnsTxt { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$RecordName, [Parameter(Mandatory, Position = 1)] [string]$TxtValue, [Parameter(Mandatory, Position = 2)] [SecureString]$CombellApiKey, [Parameter(Mandatory, Position = 3)] [SecureString]$CombellApiSecret, [Parameter(ValueFromRemainingArguments)] $ExtraParams ) # Convert the SecureString parameters to type String. $ApiKey = [pscredential]::new('a', $CombellApiKey).GetNetworkCredential().Password $ApiSecret = [pscredential]::new('a', $CombellApiSecret).GetNetworkCredential().Password $cmdletName = "Add-DnsTxt" $zoneName = Find-CombellZone $RecordName $ApiKey $ApiSecret Write-Verbose "${cmdletName}: Find domain '$zoneName' for record '$RecordName' - OK" $relativeRecordName = ($RecordName -ireplace [regex]::Escape($zoneName), [string]::Empty).TrimEnd('.') $txtRecords = Get-CombellTxtRecords $zoneName $relativeRecordName $TxtValue $ApiKey $ApiSecret $numberOfTxtRecords = $txtRecords.Length if ($numberOfTxtRecords -gt 0) { Write-Verbose "${cmdletName}: Domain '$zoneName' contains $numberOfTxtRecords TXT record$(if ($numberOfTxtRecords -gt 1) { "s" }) that match$(if ($numberOfTxtRecords -eq 1) { "es" }) record name ""$relativeRecordName"" and content ""$TxtValue""; abort." return } Write-Verbose "${cmdletName}: Domain '$zoneName' contains 0 TXT records that match record name ""$relativeRecordName"" and content ""$TxtValue""; add TXT record { ""record_name"": ""$relativeRecordName"", ""content"": ""$TxtValue"" }." $requestBody = @{ type = "TXT" record_name = $relativeRecordName ttl = 60 content = $TxtValue } | ConvertTo-Json -Compress Send-CombellHttpRequest POST "dns/$zoneName/records" $ApiKey $ApiSecret $requestBody | Out-Null <# .SYNOPSIS Add a DNS TXT record via the Combell API. .DESCRIPTION Add a DNS TXT record via the Combell API. .PARAMETER RecordName The fully qualified name of the TXT record. .PARAMETER TxtValue The value of the TXT record. .PARAMETER CombellApiKey The Combell API key associated with your account. .PARAMETER CombellApiSecret The Combell API secret associated with your account. .PARAMETER ExtraParams This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. .EXAMPLE $combellApiKey = Read-Host "Combell API key" -AsSecureString $combellApiSecret = Read-Host "Combell API secret" -AsSecureString Add-DnsTxt '_acme-challenge.example.com' 'txt-value' $combellApiKey $combellApiSecret Adds a TXT record for the specified site with the specified value. #> } function Remove-DnsTxt { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$RecordName, [Parameter(Mandatory, Position = 1)] [string]$TxtValue, [Parameter(Mandatory, Position = 2)] [SecureString]$CombellApiKey, [Parameter(Mandatory, Position = 3)] [SecureString]$CombellApiSecret, [Parameter(ValueFromRemainingArguments)] $ExtraParams ) # Convert the SecureString parameters to type String. $ApiKey = [pscredential]::new('a', $CombellApiKey).GetNetworkCredential().Password $ApiSecret = [pscredential]::new('a', $CombellApiSecret).GetNetworkCredential().Password $cmdletName = "Remove-DnsTxt" $zoneName = Find-CombellZone $RecordName $ApiKey $ApiSecret Write-Verbose "${cmdletName}: Find domain '$zoneName' for record '$RecordName' - OK" $relativeRecordName = ($RecordName -ireplace [regex]::Escape($zoneName), [string]::Empty).TrimEnd('.') $txtRecords = Get-CombellTxtRecords $zoneName $relativeRecordName $TxtValue $ApiKey $ApiSecret $numberOfTxtRecords = $txtRecords.Length if ($numberOfTxtRecords -eq 0) { Write-Verbose "${cmdletName}: Domain '$zoneName' contains 0 TXT records that match record name '$relativeRecordName' and content ""$TxtValue""; abort." return } Write-Verbose "${cmdletName}: Domain '$zoneName' contains $numberOfTxtRecords TXT record$(if ($numberOfTxtRecords -gt 1) { "s" }) that match$(if ($numberOfTxtRecords -eq 1) { "es" }) record name '$relativeRecordName' and content ""$TxtValue""; delete $numberOfTxtRecords record$(if ($numberOfTxtRecords -gt 1) { "s" })." foreach ($txtRecord in $txtRecords) { Write-Verbose "${cmdletName}: Delete TXT record $txtRecord" Send-CombellHttpRequest DELETE "dns/$zoneName/records/$($txtRecord.id)" $ApiKey $ApiSecret | Out-Null } <# .SYNOPSIS Remove a DNS TXT record via the Combell API. .DESCRIPTION Remove a DNS TXT record via the Combell API. .PARAMETER RecordName The fully qualified name of the TXT record. .PARAMETER TxtValue The value of the TXT record. .PARAMETER CombellApiKey The Combell API key associated with your account. .PARAMETER CombellApiSecret The Combell API secret associated with your account. .PARAMETER ExtraParams This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. .EXAMPLE $combellApiKey = Read-Host "Combell API key" -AsSecureString $combellApiSecret = Read-Host "Combell API secret" -AsSecureString Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' $combellApiKey $combellApiSecret Removes a TXT record for the specified site with the specified value. If multiple records exist with the same record name and the same content, this cmdlet deletes all of them. #> } function Save-DnsTxt { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments)] $ExtraParams ) <# .SYNOPSIS Not required. .DESCRIPTION This provider does not require calling this function to commit changes to 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 ############################ function Find-CombellZone { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$RecordName, [Parameter(Mandatory, Position = 1)] [string]$ApiKey, [Parameter(Mandatory, Position = 2)] [string]$ApiSecret ) # Setup a module variable to cache the record to zone mapping, so it's quicker to find later. if (!$script:CombellRecordZones) { $script:CombellRecordZones = @{} } # If the cache contains $RecordName, return it. if ($script:CombellRecordZones.ContainsKey($RecordName)) { return $script:CombellRecordZones.$RecordName } # Not specifying the 'take' query parameter defaults the result set to a maximum 25 items (situation on 30 # September 2021). It is not clear from the documentation how you can get all domains in a single request, so I'm # defaulting here to a 'take' parameter value of '1000'. # If you find a better solution, feel free to submit an issue. # See https://api.combell.com/v2/documentation#operation/Domains for more information. # - Steven Volckaert, 30 September 2021. # TODO Although undocumented, it appears it might be possible to retrieve the total number of domains from some # custom HTTP response headers. So: Consider removing the 'take' query parameter, which will default back to # maximum 25 items per response, and sending addtional HTTP requests if the HTTP header(s) indicate that more # domains exist. # Implementing this requires further investigation though (start by reading # https://api.combell.com/v2/documentation#section/Conventions/Pagination), so if you need this, feel free to # submit a pull request or an issue - Steven Volckaert, 5 October 2021. $zones = Send-CombellHttpRequest GET "domains?take=1000" $ApiKey $ApiSecret; # We need to find the deepest sub-zone that can hold the record and add it there, except if there is only the apex # zone. So for a $RecordName like _acme-challenge.site1.sub1.sub2.example.com, we need to search the zone from # longest to shortest set of FQDNs contained in $zones, i.e. in the following order: # - site1.sub1.sub2.example.com # - sub1.sub2.example.com # - sub2.example.com # - example.com # See https://poshac.me/docs/v4/Plugins/Plugin-Development-Guide/#zone-matching for more information. # - Steven Volckaert, 30 September 2021. $pieces = $RecordName.Split('.') for ($i = 0; $i -lt ($pieces.Count - 1); $i++) { $zoneTest = $pieces[$i..($pieces.Count - 1)] -join '.' Write-Verbose "Find domain '$zoneTest' in $($zones.Length) Combell domains" if ($zoneTest -in $zones.domain_name) { $zone = $zones | Where-Object { $_.domain_name -eq $zoneTest } $script:CombellRecordZones.$RecordName = $zone.domain_name return $zone.domain_name } } throw "FATAL: No domain zone found for record '$RecordName'." } function Get-CombellAuthorizationHeaderValue { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$ApiKey, [Parameter(Mandatory, Position = 1)] [string]$ApiSecret, [Parameter(Mandatory, Position = 2)] [string]$Method, [Parameter(Mandatory, Position = 3)] [string]$Path, [Parameter(Position = 4)] [string]$Body ) $urlEncodedPath = [System.Net.WebUtility]::UrlEncode("/v2/$Path") $unixTimestamp = [System.DateTimeOffset]::Now.ToUnixTimeSeconds().ToString() $nonce = (New-Guid).ToString() $hmacInputValue = "${ApiKey}$($Method.ToLowerInvariant())${urlEncodedPath}${unixTimestamp}${nonce}" if ($Body) { $md5Algorithm = New-Object System.Security.Cryptography.MD5CryptoServiceProvider $bodyAsByteArray = [Text.Encoding]::UTF8.GetBytes($Body) $bodyAsHashedBase64String = [Convert]::ToBase64String($md5Algorithm.ComputeHash($bodyAsByteArray)) $hmacInputValue += $bodyAsHashedBase64String } $hmacAlgorithm = New-Object System.Security.Cryptography.HMACSHA256 $hmacAlgorithm.Key = [Text.Encoding]::UTF8.GetBytes($ApiSecret) $hmacInputValueAsByteArray = [Text.Encoding]::UTF8.GetBytes($hmacInputValue) $hmacSignature = [Convert]::ToBase64String($hmacAlgorithm.ComputeHash($hmacInputValueAsByteArray)) return "hmac ${ApiKey}:${hmacSignature}:${nonce}:${unixTimestamp}" <# .SYNOPSIS Gets the value of the Authorization header. See https://api.combell.com/v2/documentation#section/Authentication for more information. #> } function Get-CombellTxtRecords { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$DomainName, [Parameter(Mandatory, Position = 1)] [string]$RelativeRecordName, [Parameter(Mandatory, Position = 2)] [string]$TxtValue, [Parameter(Mandatory, Position = 3)] [string]$ApiKey, [Parameter(Mandatory, Position = 4)] [string]$ApiSecret ) $txtRecords = Send-CombellHttpRequest ` GET "dns/$DomainName/records?type=TXT&record_name=$RelativeRecordName" $ApiKey $ApiSecret return @($txtRecords | Where-Object { $_.record_name -eq $RelativeRecordName -and $_.content -ceq $TxtValue }) } function Send-CombellHttpRequest { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [ValidateSet('GET', 'PUT', 'POST', 'DELETE')] [string]$Method, [Parameter(Mandatory, Position = 1)] [string]$Path, [Parameter(Mandatory, Position = 2)] [string]$ApiKey, [Parameter(Mandatory, Position = 3)] [string]$ApiSecret, [Parameter(Position = 4)] [string]$Body ) $uri = [uri]"https://api.combell.com/v2/$Path" $authorizationHeaderValue = Get-CombellAuthorizationHeaderValue $ApiKey $ApiSecret $Method $Path $Body $headers = @{ Accept = 'application/json' Authorization = $authorizationHeaderValue } $invokeRestMesthodParameters = @{ Method = $Method Uri = $uri Headers = $headers ContentType = 'application/json' MaximumRedirection = 0 ErrorAction = 'Stop' } if ($Body) { $invokeRestMesthodParameters.Body = $Body } try { $Stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch $Stopwatch.Start() Invoke-RestMethod @invokeRestMesthodParameters @script:UseBasic $Stopwatch.Stop() Write-Verbose "$Method $uri - OK ($($Stopwatch.ElapsedMilliseconds) ms)" } catch { $Stopwatch.Stop() Write-Error "$Method $uri - FAILED ($($Stopwatch.ElapsedMilliseconds) ms) - $($_)" throw } } |