From a325e1d4d6e574654ad237f8937c38c6e431c084 Mon Sep 17 00:00:00 2001 From: Dino Bilanovic <39695124+zoryatix@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:35:05 +0100 Subject: [PATCH] Adding plugin support for EuroDNSReseller (#590) --- Posh-ACME/Plugins/EuroDNSReseller.ps1 | 440 ++++++++++++++++++++++ Posh-ACME/Private/Import-PluginDetail.ps1 | 1 + docs/Plugins/EuroDNSReseller.md | 37 ++ docs/Plugins/index.md | 1 + 4 files changed, 479 insertions(+) create mode 100644 Posh-ACME/Plugins/EuroDNSReseller.ps1 create mode 100644 docs/Plugins/EuroDNSReseller.md diff --git a/Posh-ACME/Plugins/EuroDNSReseller.ps1 b/Posh-ACME/Plugins/EuroDNSReseller.ps1 new file mode 100644 index 00000000..1aaa0f9d --- /dev/null +++ b/Posh-ACME/Plugins/EuroDNSReseller.ps1 @@ -0,0 +1,440 @@ +function Get-CurrentPluginType { 'dns-01' } + +function Add-DnsTxt { + [CmdletBinding()] + param( + [Parameter(Mandatory,Position=0)] + [string]$RecordName, + [Parameter(Mandatory,Position=1)] + [string]$TxtValue, + [Parameter(Mandatory)] + [pscredential] + $EuroDNSReseller_Creds, + [Parameter(ValueFromRemainingArguments)] + $ExtraParams + ) + + Write-Verbose "Looking for Zonename in $($RecordName)..." + $EuroDNSResellerZone = Find-EuroDNSResellerZone -EuroDNSReseller_zone $RecordName -EuroDNSReseller_Creds $EuroDNSReseller_Creds + + If ($EuroDNSResellerZone) { + + # Getting DNS records + try { + + # Checking to see if there is any data to work with. Don't want to overwrite in case we make multiple changes + Write-Debug "Checking if EuroDNsResellerObject has data..." + If ( -not ($script:EuroDNSResellerObject)) { + + Write-Verbose "Trying to get data from EuroDNSReseller..." + $script:EuroDNSResellerObject = Get-EuroDNSResellerZone -EuroDNSReseller_Domain $EuroDNSResellerZone -EuroDNSReseller_Creds $EuroDNSReseller_Creds -ErrorAction Stop | ConvertFrom-Json + } + + Write-Verbose "Data Found. Number of Records: $($script:EuroDNSResellerObject.records.count)" + + # assumes $EuroDNSResellerZone contains the zone name containing the record + $recShort = $RecordName -ireplace "\.?$([regex]::Escape($EuroDNSResellerZone.TrimEnd('.')))$",'' + + if ($recShort -eq [string]::Empty) { + $recShort = '@' + } + + Write-debug "recShort is: $($recShort)" + + # Don't want to add an identical record/value. Check for existing records: + If (($recShort -in $script:EuroDNSResellerObject.records.host) -and ($TxtValue -in $script:EuroDNSResellerObject.records.rdata)){ + + Write-Verbose "Record exists already in EuroDNSReseller - Skipping..." + Write-Verbose "Records in object: $($script:EuroDNSResellerObject.records.host -join ",")" + Write-Verbose "In Zone: $($script:EuroDNSResellerObject)" + + } else { + + # Create the new record (No ID needed) + # For new records It expects all of these fields + $EuroDNSResellernewRecord = [pscustomobject]@{ + type = "TXT" + host = $recShort + ttl = 3600 + rdata = $TxtValue + updated = $false + locked = $false + isDynDNS = $null + proxy = $null + } + + # recreate the object with the new record + $script:EuroDNSResellerObject.records += @($EuroDNSResellernewRecord) + Write-Verbose "New object added - Number of Records now: $($script:EuroDNSResellerObject.records.count)" + + } + + } + catch { + throw + } + + } else { + Write-Verbose "...No available domain zone found..." + } + + + <# + .SYNOPSIS + Add a DNS TXT record to EuroDNSReseller + + .DESCRIPTION + Description for EuroDNSReseller + + .PARAMETER RecordName + The fully qualified name of the TXT record. + + .PARAMETER TxtValue + The value of the TXT record. + + .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' + + 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)] + [pscredential] + $EuroDNSReseller_Creds, + [Parameter(ValueFromRemainingArguments)] + $ExtraParams + ) + + # Checking for zonename + Write-Verbose "Looking for Zonename in $($RecordName)..." + $EuroDNSResellerZone = Find-EuroDNSResellerZone -EuroDNSReseller_zone $RecordName -EuroDNSReseller_Creds $EuroDNSReseller_Creds + If ($EuroDNSResellerZone) { + + # Getting DNS records + try { + + # Checking to see if there is any data to work with. Don't want to overwrite in case we make multiple changes + If ( -not ($script:EuroDNSResellerObject)) { + + Write-Verbose "Trying to get data from EuroDNSReseller..." + $script:EuroDNSResellerObject = Get-EuroDNSResellerZone -EuroDNSReseller_Domain $EuroDNSResellerZone -EuroDNSReseller_Creds $EuroDNSReseller_Creds -ErrorAction Stop | ConvertFrom-Json + } + + Write-Verbose "Data Found. Number of Records: $($script:EuroDNSResellerObject.records.count)" + + # assumes $EuroDNSResellerZone contains the zone name containing the record + $recShort = $RecordName -ireplace "\.?$([regex]::Escape($EuroDNSResellerZone.TrimEnd('.')))$",'' + + if ($recShort -eq [string]::Empty) { + $recShort = '@' + } + + # Doing a check to make sure there is something to remove + Write-Verbose "Searching for records to remove..." + If($script:EuroDNSResellerObject.records | Where-Object {($_.host -eq $recShort -and $_.rdata -eq $TxtValue)}) { + + # Seaching for record to remove - Doing a not search since we only want to remove a specific record and keep all else. + $script:EuroDNSResellerObject.records = $script:EuroDNSResellerObject.records | Where-Object {!($_.host -eq $recShort -and $_.rdata -eq $TxtValue)} + + } else { + + Write-Verbose "Could not find any records matching. Nothing will be removed." + + } + + Write-Verbose "Number of Records now: $($script:EuroDNSResellerObject.records.count)" + + } + catch { + throw + } + + } else { + Write-Verbose "...No available domain zone found..." + } + + + + <# + .SYNOPSIS + Remove a DNS TXT record from EuroDNS + + .DESCRIPTION + Description for EuroDNSReseller + + .PARAMETER RecordName + The fully qualified name of the TXT record. + + .PARAMETER TxtValue + The value of the TXT record. + + .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' + + Removes a TXT record for the specified site with the specified value. + #> +} + +function Save-DnsTxt { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [pscredential] + $EuroDNSReseller_Creds, + [Parameter(ValueFromRemainingArguments)] + $ExtraParams + ) + + # We want to confirm our changes before saving them + # Checking to see if we get a pass from EuroDNSReseller with our new records. + # If our object is not exactly in the format expected, then it will fail validation + # so we should skip the saving. + $EuroDNSReseller_Confirm = $script:EuroDNSResellerObject | Confirm-EuroDNSReseller -EuroDNSReseller_Creds $EuroDNSReseller_Creds + IF ($EuroDNSReseller_Confirm.report.isValid -eq $true) { + + Write-Verbose "EuroDNSReseller Validation completed succesfully.. Sending data to EuroDNSReseller" + $script:EuroDNSResellerObject | Save-EuroDNSReseller -EuroDNSReseller_Creds $EuroDNSReseller_Creds + Write-Debug "Cleaning EURODNSResellerObject.. " + $script:EuroDNSResellerObject = $null + + } else { + + Write-Verbose "EuroDNSReseller does NOT accept our object - Something is wrong with our data and it's not passing the confirm check. Try debug" + Write-Debug "Records in Data we tried to send: $($script:EuroDNSResellerObject.records)" + throw "Validation failed - The data sent to EuroDNSReseller was not approved" + } + + <# + .SYNOPSIS + Commits changes for pending DNS TXT record modifications to EuroDNSReseller + + .DESCRIPTION + Description for EuroDNSReseller + + .PARAMETER ExtraParams + This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. + + .EXAMPLE + Save-DnsTxt + + Commits changes for pending DNS TXT record modifications. + #> +} + +############################ +# Helper Functions +############################ +# API documentation: https://docapi.EuroDNS.com/ + +# Creates the header with our API ID/Key +function Connect-EuroDNSReseller { + [CmdletBinding()] + param ( + + [Parameter(Mandatory)] + [pscredential] + $EuroDNSReseller_Creds + + ) + + process { + + $EuroDNSReseller_headers = @{ + 'Content-Type' = 'application/json' + 'X-APP-ID' = $EuroDNSReseller_Creds.UserName + 'X-API-KEY' = $EuroDNSReseller_Creds.GetNetworkCredential().Password + } + + $EuroDNSReseller_headers + } +} + +# Returns all records for a specific zone +function Get-EuroDNSResellerZone { + param ( + [Parameter(Mandatory)] + [string] + $EuroDNSReseller_Domain, + + [Parameter(Mandatory)] + [pscredential] + $EuroDNSReseller_Creds + ) + + Process { + $URL = 'https://rest-api.EuroDNS.com/dns-zones/' + $EuroDNSReseller_Domain + + try { + + $(Invoke-WebRequest -uri $url -headers $(Connect-EuroDNSReseller $EuroDNSReseller_Creds) -erroraction Stop @script:UseBasic).content + + } + catch { + throw + } + + } + +} + +# We need to validate data before saving it. +function Confirm-EuroDNSReseller { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline)] + [psobject] + $EuroDNSReseller_Data, + + [Parameter(Mandatory)] + [pscredential] + $EuroDNSReseller_Creds + ) + process { + $domain = $($EuroDNSReseller_Data.name) + $URL = "https://rest-api.EuroDNS.com/dns-zones/$($domain)/check" + + try { + $(Invoke-WebRequest $url -headers $(Connect-EuroDNSReseller $EuroDNSReseller_Creds) -Body ($EuroDNSReseller_Data | ConvertTo-Json -Depth 10) -Method Post -erroraction Stop @script:UseBasic).content | ConvertFrom-Json -Depth 10 + } + catch { + + throw + } + } +} +# Saves the changes to EuroDNS +function Save-EuroDNSReseller { + [CmdletBinding()] + param ( + + [Parameter(ValueFromPipeline)] + [psobject] + $EuroDNSReseller_Data, + + [Parameter(Mandatory)] + [pscredential] + $EuroDNSReseller_Creds + + ) + + + process { + + $domain = $($EuroDNSReseller_Data.name) + $URL = "https://rest-api.EuroDNS.com/dns-zones/$($domain)" + + try { + + Invoke-WebRequest $url -headers $(Connect-EuroDNSReseller $EuroDNSReseller_Creds) -Body ($EuroDNSReseller_Data | ConvertTo-Json -Depth 10) -Method Put -erroraction Stop @script:UseBasic | Out-Null + + } + catch { + throw + } + + } + +} + +# Looking through each subdomain until we find one that works. +function Find-EuroDNSResellerZone { + param ( + [Parameter(Mandatory)] + [string] + $EuroDNSReseller_zone, + + [Parameter(Mandatory)] + [pscredential] + $EuroDNSReseller_Creds + + + ) + + Process { + + + $zonestring = $EuroDNSReseller_zone + # Creating a counter based on amount of subdomains. + $mycount = ($EuroDNSReseller_zone).split(".") + + # Testing the zone string from left to right, removing a subdomain for each failed call. Added a few checks in case of + ## - Domain doesn't exists + ## - We hit a Top-Level domain (like com or uk.org) (The API "Available Domains" will return 200 on this, but you can't really make changes on this level) + Write-Verbose "Number of domains to test: $($mycount.count)" + 1..$mycount.count | ForEach-Object { + + + # We want to return nothing if a useable domain cannot be found. + If ($_ -eq $mycount.Count -and $skipcounter -eq $false ) { + + $zonestring = $null + + + } else { + + + # Looking thorugh each domain starting from the left most side. + If ($call.StatusCode -eq '200') { + + Write-Verbose "Found the available domain / zonename - $zonestring" + $Getoutofloop = $true + $Call = 0 + + } else { + + If (!($Getoutofloop -eq $true)) { + try { + + $body = @{ + + domainNames = @($zonestring) + } + + Write-Verbose "$($_).. $zonestring - testing to see if this domain is available" + $call = Invoke-WebRequest 'https://rest-api.eurodns.com/das/available-domain-names' -Body $($body | ConvertTo-Json) -Headers $(Connect-EuroDNSReseller $EuroDNSReseller_Creds) -Method Post -erroraction Stop @script:UseBasic + + + } + catch { + + Write-Verbose "$zonestring - Domain not available.. Trying next" + + } + + # Want to stop looking as soons as we find the first available domain one + If ($call.StatusCode -eq '200') { + + $skipcounter = $true + + } else { + + # Removing each sub domain one at a time. + $index = $zonestring.IndexOf('.') + $zonestring = $zonestring.Substring($index + 1) + $zonestring = $zonestring + + } + } + } + } + } + + $zonestring + + } + +} diff --git a/Posh-ACME/Private/Import-PluginDetail.ps1 b/Posh-ACME/Private/Import-PluginDetail.ps1 index c39e2af4..26023c33 100644 --- a/Posh-ACME/Private/Import-PluginDetail.ps1 +++ b/Posh-ACME/Private/Import-PluginDetail.ps1 @@ -40,6 +40,7 @@ function Import-PluginDetail { 'Dynu' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'Dynu'} 'EasyDNS' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'EasyDNS'} 'Easyname' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'Easyname'} + 'EuroDNSReseller' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'EuroDNSReseller'} 'FreeDNS' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'FreeDNS'} 'Gandi' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'Gandi'} 'GCloud' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'GCloud'} diff --git a/docs/Plugins/EuroDNSReseller.md b/docs/Plugins/EuroDNSReseller.md new file mode 100644 index 00000000..820af182 --- /dev/null +++ b/docs/Plugins/EuroDNSReseller.md @@ -0,0 +1,37 @@ +title: EuroDNSReseller + +# How To Use the EuroDNSReseller DNS Plugin + +This plugin works with the [EuroDNS](https://www.eurodns.com/) DNS provider. While it is possible to purchase domains directly from EuroDNS, their APIs are currently only available to [Reseller Partners](https://www.eurodns.com/partners). Some of those partners such as [EBrand](https://ebrand.com) can provide EuroDNS API access to their customers. So in order to use this plugin, you will need to be using a reseller that will create API credentials for you. + +!!! note + As of November 2024, EuroDNS support has indicated the API (or perhaps a new API) will eventually be available for all direct customers. However, they would not provide an estimate for when that might be available. + +## Setup + +Setup involves getting API credentials from your reseller which will vary depending on the reseller. The values you need are known as `X-APP-ID` and `X-API-KEY`. + +## Using the Plugin + +The `X-APP-ID` and `X-API-KEY` will be used as the username and password in a PSCredential object called `EuroDNSReseller_Creds`. + +Here are two examples on how you can use them: + +```powershell +# Prompt for the credentials where username is X-APP-ID +# and password is X-API-KEY +$pArgs = @{EuroDNSReseller_Creds = Get-Credential} + +New-PACertificate example.com -Plugin EuroDNSReseller -PluginArgs $pArgs +``` + +For a more automated approach (This method assumes you understand the risks and methods to secure the below credentials): + +```powershell +$username = "My_X-APP-ID_Value" +$password = "My_X-API-Key_Value" | ConvertTo-SecureString -AsPlainText -Force +$cred = [pscredential]::new($username, $password) +$pArgs = @{EuroDNSReseller_Creds = $cred} + +New-PACertificate example.com -Plugin EuroDNSReseller -PluginArgs $pArgs +``` diff --git a/docs/Plugins/index.md b/docs/Plugins/index.md index 71c089f1..d7208904 100644 --- a/docs/Plugins/index.md +++ b/docs/Plugins/index.md @@ -42,6 +42,7 @@ DuckDNS | [Duck DNS](https://www.duckdns.org/) | [Usage Guide](DuckDNS.md) | :wh Dynu | [Dynu DNS](https://www.dynu.com) | [Usage Guide](Dynu.md) | :white_check_mark: EasyDNS | [EasyDNS](https://easydns.com/) | [Usage Guide](EasyDNS.md) | :white_check_mark: Easyname | [easyname.com](https://www.easyname.com/) | [Usage Guide](Easyname.md) | :white_check_mark: +EuroDNSReseller | [EuroDNS](https://www.eurodns.com/) + Resellers | [Usage Guide](EuroDNSReseller.md) | :white_check_mark: FreeDNS | [Free DNS](https://freedns.afraid.org) | [Usage Guide](FreeDNS.md) | :white_check_mark: Gandi | [Gandi LiveDNS](https://www.gandi.net) | [Usage Guide](Gandi.md) | :white_check_mark: GCloud | [Google Cloud DNS](https://cloud.google.com/dns) | [Usage Guide](GCloud.md) | :white_check_mark: