Plugins/NS1.ps1

function Get-CurrentPluginType { 'dns-01' }

function Add-DnsTxt {
    [CmdletBinding(DefaultParameterSetName='Secure')]
    param(
        [Parameter(Mandatory,Position=0)]
        [string]$RecordName,
        [Parameter(Mandatory,Position=1)]
        [string]$TxtValue,
        [Parameter(ParameterSetName='Secure',Mandatory,Position=2)]
        [securestring]$NS1Key,
        [Parameter(ParameterSetName='DeprecatedInsecure',Mandatory,Position=2)]
        [string]$NS1KeyInsecure,
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    # grab the cleartext key if the secure version was used
    if ('Secure' -eq $PSCmdlet.ParameterSetName) {
        $NS1KeyInsecure = [pscredential]::new('a',$NS1Key).GetNetworkCredential().Password
    }

    $apiRoot = 'https://api.nsone.net/v1'
    $restParams = @{
        Headers = @{
            Accept = 'application/json'
            'X-NSONE-Key'=$NS1KeyInsecure
        }
        ContentType = 'application/json'
    }

    # get the zone name for our record
    $zoneName = Find-NS1Zone $RecordName $restParams
    Write-Debug "Found zone $zoneName"

    # Search for the record we care about, but ignore errors
    # because the record not existing generates an exception
    # and that's ok
    try {
        $rec = Invoke-RestMethod "$apiRoot/zones/$zoneName/$RecordName/TXT" @restParams @script:UseBasic
    } catch {}

    if (-not $rec) {
        # add new record
        Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue"
        $bodyJson = @{zone=$zoneName;type='TXT';domain=$RecordName;ttl=10;answers=@(@{answer=@($TxtValue)})} |
            ConvertTo-Json -Compress -Depth 5
        Invoke-RestMethod "$apiRoot/zones/$zoneName/$RecordName/TXT" -Method Put -Body $bodyJson `
            @restParams @script:UseBasic | Out-Null
    } else {
        if ($TxtValue -in $rec.answers.answer) {
            Write-Debug "Record $RecordName already contains $TxtValue. Nothing to do."
        } else {
            # add a new answer
            $rec.answers += @{answer=@($TxtValue)}
            $bodyJson = @{answers=$rec.answers} | ConvertTo-Json -Compress -Depth 5
            Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue"
            Invoke-RestMethod "$apiRoot/zones/$zoneName/$RecordName/TXT" -Method Post -Body $bodyJson `
                @restParams @script:UseBasic | Out-Null
        }
    }

    <#
    .SYNOPSIS
        Add a DNS TXT record to NS1.
 
    .DESCRIPTION
        Add a DNS TXT record to NS1.
 
    .PARAMETER RecordName
        The fully qualified name of the TXT record.
 
    .PARAMETER TxtValue
        The value of the TXT record.
 
    .PARAMETER NS1Key
        The API key with DNS permissions on your NS1 account.
 
    .PARAMETER NS1KeyInsecure
        (DEPRECATED) The API key with DNS permissions on your NS1 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
        $key = Read-Host -Prompt "NS1 Key" -AsSecureString
        PS C:\>Add-DnsTxt '_acme-challenge.example.com' 'txt-value' $key
 
        Adds a TXT record for the specified site with the specified value from Windows.
    #>

}

function Remove-DnsTxt {
    [CmdletBinding(DefaultParameterSetName='Secure')]
    param(
        [Parameter(Mandatory,Position=0)]
        [string]$RecordName,
        [Parameter(Mandatory,Position=1)]
        [string]$TxtValue,
        [Parameter(ParameterSetName='Secure',Mandatory,Position=2)]
        [securestring]$NS1Key,
        [Parameter(ParameterSetName='DeprecatedInsecure',Mandatory,Position=2)]
        [string]$NS1KeyInsecure,
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    # grab the cleartext key if the secure version was used
    if ('Secure' -eq $PSCmdlet.ParameterSetName) {
        $NS1KeyInsecure = [pscredential]::new('a',$NS1Key).GetNetworkCredential().Password
    }

    $apiRoot = 'https://api.nsone.net/v1'
    $restParams = @{
        Headers = @{
            Accept = 'application/json'
            'X-NSONE-Key'=$NS1KeyInsecure
        }
        ContentType = 'application/json'
    }

    # get the zone name for our record
    $zoneName = Find-NS1Zone $RecordName $restParams
    Write-Debug "Found zone $zoneName"

    # Search for the record we care about, but ignore errors
    # because the record not existing generates an exception
    # and that's ok
    try {
        $rec = Invoke-RestMethod "$apiRoot/zones/$zoneName/$RecordName/TXT" @restParams @script:UseBasic
    } catch {}

    if (-not $rec) {
        Write-Debug "Record $RecordName with value $TxtValue doesn't exist. Nothing to do."
    } else {
        if ($TxtValue -in $rec.answers.answer) {
            if ($rec.answers.Count -eq 1) {
                # last answer, so delete the record
                Write-Verbose "Deleting TXT record for $RecordName"
                Invoke-RestMethod "$apiRoot/zones/$zoneName/$RecordName/TXT" -Method Delete `
                    @restParams @script:UseBasic | Out-Null
            } else {
                # just remove the answer from the list
                $rec.answers = @($rec.answers | Where-Object { $TxtValue -notin $_.answer })
                $bodyJson = @{answers=$rec.answers} | ConvertTo-Json -Compress -Depth 5
                Write-Verbose "Removing a TXT record for $RecordName with value $TxtValue"
                Invoke-RestMethod "$apiRoot/zones/$zoneName/$RecordName/TXT" -Method Post -Body $bodyJson `
                    @restParams @script:UseBasic | Out-Null
            }
        } else {
            Write-Debug "Record $RecordName with value $TxtValue doesn't exist. Nothing to do."
        }
    }

    <#
    .SYNOPSIS
        Remove a DNS TXT record from NS1.
 
    .DESCRIPTION
        Remove a DNS TXT record from NS1.
 
    .PARAMETER RecordName
        The fully qualified name of the TXT record.
 
    .PARAMETER TxtValue
        The value of the TXT record.
 
    .PARAMETER NS1Key
        The API key with DNS permissions on your NS1 account.
 
    .PARAMETER NS1KeyInsecure
        (DEPRECATED) The API key with DNS permissions on your NS1 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
        $key = Read-Host "NS1 Key" -AsSecureString
        PS C:\>Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' $key
 
        Removes a TXT record for the specified site with the specified value from Windows.
    #>

}

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
############################

# API Docs
# https://ns1.com/api

function Find-NS1Zone {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [string]$RecordName,
        [Parameter(Mandatory,Position=1)]
        [hashtable]$RestParams
    )

    # setup a module variable to cache the record to zone mapping
    # so it's quicker to find later
    if (!$script:NS1RecordZones) { $script:NS1RecordZones = @{} }

    # check for the record in the cache
    if ($script:NS1RecordZones.ContainsKey($RecordName)) {
        return $script:NS1RecordZones.$RecordName
    }

    $apiRoot = 'https://api.nsone.net/v1'

    # Since the provider 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

    # get the list of zones
    try {
        $zones = Invoke-RestMethod "$apiRoot/zones" @RestParams @script:UseBasic
    } catch { throw }

    $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.zone) {
            $zoneName = ($zones | Where-Object { $_.zone -eq $zoneTest }).zone
            $script:NS1RecordZones.$RecordName = $zoneName
            return $zoneName
        }
    }

    return $null

}