Plugins/Windows.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)] [string]$WinServer, [Parameter(Position=3)] [pscredential]$WinCred, [switch]$WinUseSSL, [string]$WinZoneScope, [Parameter(ValueFromRemainingArguments)] $ExtraParams ) $cim = Connect-WinDns @PSBoundParameters Write-Verbose "Connected to $WinServer" $dnsParams = @{ ComputerName=$WinServer; CimSession=$cim } Write-Debug "Attempting to find zone for $RecordName" if (!($zoneName = Find-WinZone $RecordName $dnsParams)) { throw "Unable to find zone for $RecordName" } Write-Verbose "Found $zoneName" $zone = Get-DnsServerZone $zoneName @dnsParams -EA Stop # separate the portion of the name that doesn't contain the zone name $recShort = ($RecordName -ireplace [regex]::Escape($zoneName), [string]::Empty).TrimEnd('.') Write-Debug "Record short name: $recShort" # check for zone scope usage $zoneScope = @{} if (-not [String]::IsNullOrWhiteSpace($WinZoneScope)) { if ('ZoneScope' -notin (Get-Command Get-DnsServerResourceRecord).Parameters.Keys) { throw "ZoneScope is not supported in the version of the DnsServer module currently installed." } else { # In some configurations, not all zones that need to be modified have the specified # scope and will throw errors if you try to access them with a specified scope. So # we're going to make sure the specified scope exists before trying to use it. $scopes = $zone | Get-DnsServerZoneScope @dnsParams if ($WinZoneScope -in $scopes.ZoneScope) { $zoneScope.ZoneScope = $WinZoneScope } } } $recs = @($zone | Get-DnsServerResourceRecord -Name $recShort -RRType Txt @dnsParams @zoneScope -EA Ignore) if ($recs.Count -eq 0 -or $TxtValue -notin $recs.RecordData.DescriptiveText) { # create new Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue" $zone | Add-DnsServerResourceRecord -Txt -Name $recShort -DescriptiveText $TxtValue -TimeToLive 00:00:10 @dnsParams @zoneScope } else { # nothing to do Write-Debug "Record $RecordName already contains $TxtValue. Nothing to do." } <# .SYNOPSIS Add a DNS TXT record to a Windows DNS server. .DESCRIPTION This plugin requires the "DnsServer" PowerShell module to be installed. On Windows Server OSes, you can install it with "Install-WindowsFeature RSAT-DNS-Server". On Windows client OSes, you will need to download and install the RSAT tools for your OS. .PARAMETER RecordName The fully qualified name of the TXT record. .PARAMETER TxtValue The value of the TXT record. .PARAMETER WinServer The hostname or IP address of the Windows DNS server. .PARAMETER WinCred Credentials with permissions to modify TXT records in the specified zone. This is optional if the current user has the proper permissions already. .PARAMETER WinUseSSL Forces the PowerShell remoting session to run over HTTPS. Requires the server have a valid certificate that is installed and trusted by the client or added to the client's TrustedHosts list. This is primarily used when connecting to a non-domain joined DNS server. .PARAMETER WinZoneScope The name of the zone scope to modify. This is generally only necessary in split-brain DNS configurations where the default scope is not external facing. .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' -WinServer 'dns1.example.com' Adds a TXT record using the credentials of the calling process. .EXAMPLE Add-DnsTxt '_acme-challenge.example.com' 'txt-value' -WinServer 'dns1.example.com' -WinCred (Get-Credential) -WinUseSSL Adds a TXT record using explicit credentials and connecting over HTTPS. #> } function Remove-DnsTxt { [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue, [Parameter(Mandatory,Position=2)] [string]$WinServer, [Parameter(Position=3)] [pscredential]$WinCred, [switch]$WinUseSSL, [string]$WinZoneScope, [Parameter(ValueFromRemainingArguments)] $ExtraParams ) $cim = Connect-WinDns @PSBoundParameters Write-Verbose "Connected to $WinServer" $dnsParams = @{ ComputerName=$WinServer; CimSession=$cim } Write-Debug "Attempting to find zone for $RecordName" if (!($zoneName = Find-WinZone $RecordName $dnsParams)) { throw "Unable to find zone for $RecordName" } Write-Verbose "Found $zoneName" $zone = Get-DnsServerZone $zoneName @dnsParams -EA Stop # separate the portion of the name that doesn't contain the zone name $recShort = ($RecordName -ireplace [regex]::Escape($zoneName), [string]::Empty).TrimEnd('.') Write-Debug "Record short name: $recShort" # check for zone scope usage $zoneScope = @{} if (-not [String]::IsNullOrWhiteSpace($WinZoneScope)) { if ('ZoneScope' -notin (Get-Command Get-DnsServerResourceRecord).Parameters.Keys) { throw "ZoneScope is not supported in the version of the DnsServer module currently installed." } else { # In some configurations, not all zones that need to be modified have the specified # scope and will throw errors if you try to access them with a specified scope. So # we're going to make sure the specified scope exists before trying to use it. $scopes = $zone | Get-DnsServerZoneScope @dnsParams if ($WinZoneScope -in $scopes.ZoneScope) { $zoneScope.ZoneScope = $WinZoneScope } } } $recs = @($zone | Get-DnsServerResourceRecord -Name $recShort -RRType Txt @dnsParams @zoneScope -EA Ignore) if ($recs.Count -gt 0 -and $TxtValue -in $recs.RecordData.DescriptiveText) { # remove the record that has the right value $toDelete = $recs | Where-Object { $_.RecordData.DescriptiveText -eq $TxtValue } Write-Verbose "Deleting $RecordName with value $TxtValue" $zone | Remove-DnsServerResourceRecord -InputObject $toDelete -Force @dnsParams @zoneScope } else { # nothing to do Write-Debug "Record $RecordName with value $TxtValue doesn't exist. Nothing to do." } <# .SYNOPSIS Remove a DNS TXT record from a Windows DNS server. .DESCRIPTION This plugin requires the "DnsServer" PowerShell module to be installed. On Windows Server OSes, you can install it with "Install-WindowsFeature RSAT-DNS-Server". On Windows client OSes, you will need to download and install the RSAT tools for your OS. .PARAMETER RecordName The fully qualified name of the TXT record. .PARAMETER TxtValue The value of the TXT record. .PARAMETER WinServer The hostname or IP address of the Windows DNS server. .PARAMETER WinCred Credentials with permissions to modify TXT records in the specified zone. This is optional if the current user has the proper permissions already. .PARAMETER WinUseSSL Forces the PowerShell remoting session to run over HTTPS. Requires the server have a valid certificate that is installed and trusted by the client or added to the client's TrustedHosts list. This is primarily used when connecting to a non-domain joined DNS server. .PARAMETER WinZoneScope The name of the zone scope to modify. This is generally only necessary in split-brain DNS configurations where the default scope is not external facing. .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' -WinServer 'dns1.example.com' Removes a TXT record using the credentials of the calling process. .EXAMPLE Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' -WinServer 'dns1.example.com' -WinCred (Get-Credential) -WinUseSSL Removes a TXT record using explicit credentials and connecting over HTTPS. #> } 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 Connect-WinDns { [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [string]$WinServer, [Parameter(Position=1)] [pscredential]$WinCred, [switch]$WinUseSSL, [Parameter(ValueFromRemainingArguments)] $ExtraConnectParams ) # make sure required modules are available if ($null -eq (Get-Module -ListAvailable 'DnsServer' -Verbose:$false)) { throw "DnsServer module was not found and is required to use this plugin." } else { Import-Module 'DnsServer' -Verbose:$false } if ($null -eq (Get-Module -ListAvailable 'CimCmdlets' -Verbose:$false)) { throw "CimCmdlets module was not found and is required to use this plugin." } else { Import-Module 'CimCmdlets' -Verbose:$false } # create a new CimSession if necessary if (Get-CimSession -ComputerName $WinServer -EA Ignore) { Write-Debug "Using existing CimSession for $WinServer" return ((Get-CimSession -ComputerName $WinServer)[0]) } else { Write-Debug "Connecting to $WinServer" $cimParams = @{ ComputerName=$WinServer } if ($WinCred) { $cimParams.Credential = $WinCred } if ($WinUseSSL) { $cimParams.SessionOption = (New-CimSessionOption -UseSsl) } return (New-CimSession @cimParams) } } function Find-WinZone { [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] [hashtable]$DnsParams ) # setup a module variable to cache the record to zone mapping # so it's quicker to find later if (!$script:WinRecordZones) { $script:WinRecordZones = @{} } # check for the record in the cache if ($script:WinRecordZones.ContainsKey($RecordName)) { return $script:WinRecordZones.$RecordName } # get the zone list $zones = @(Get-DnsServerZone @DnsParams -EA Stop | Where-Object { !$_.IsAutoCreated -and $_.ZoneName -ne 'TrustAnchors' }) # Since Windows could be hosting both apex and sub-zones, we need to find the closest/deepest # sub-zone that would hold the record rather than just adding it to the apex. So for something # like _acme-challenge.site1.sub1.sub2.example.com, we'd look for zone matches in the following # order: # - site1.sub1.sub2.example.com # - sub1.sub2.example.com # - sub2.example.com # - example.com $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 $zones.ZoneName) { $script:WinRecordZones.$RecordName = $zoneTest return $zoneTest } } return $null } |