Plugins/Netcup.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)]
        [int]$NetcupCustNumber,
        [Parameter(Mandatory)]
        [pscredential]$NetcupAPICredential,
        [string]$NetcupEndpoint='https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON',
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    $script:NetcupEndpoint = $NetcupEndpoint

    $zone,$rec = Get-NetcupTxtRecord @PSBoundParameters

    if ($rec) {
        Write-Verbose "Record $RecordName already contains $TxtValue. Nothing to do."
        return
    }

    $recShort = $RecordName -ireplace "\.?$([regex]::Escape($zone.TrimEnd('.')))$",''
    if (-not $recShort) { $recShort = '@' }

    $queryParams = @{
        NetcupCustNumber = $NetcupCustNumber
        NetcupAPICredential = $NetcupAPICredential
        Request = @{
            action = 'updateDnsRecords'
            param = @{
                domainname = $zone
                dnsrecordset = @{
                    dnsrecords = @(@{
                        hostname = $recShort
                        type = 'TXT'
                        destination = $TxtValue
                        deleterecord = $false
                    })
                }
            }
        }
    }
    Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue"
    $resp = Invoke-NetcupRequest @queryParams
    if ($resp -and $resp.dnsrecords) {
        $rec = $resp.dnsrecords | Where-Object {
            $_.hostname -eq $recShort -and $_.destination -eq $TxtValue
        }
        Write-Debug "New record ID $($rec.id)"
    }

    <#
    .SYNOPSIS
        Add a DNS TXT record to Netcup
 
    .DESCRIPTION
        Description for Netcup
 
    .PARAMETER RecordName
        The fully qualified name of the TXT record.
 
    .PARAMETER TxtValue
        The value of the TXT record.
 
    .PARAMETER NetcupCustNumber
        The customer number of your Netcup account. This is also the username you use to login to the portal with.
 
    .PARAMETER NetcupAPICredential
        The Netcup API Key and Password you have configured in the portal as a PSCredential object. The Key should be the username.
 
    .PARAMETER NetcupEndpoint
        The URI of the Netcup REST API endpoint. The default should work unless Netcup changes it.
 
    .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' -NetcupCustNumber 123456 -NetcupAPICredential (Get-Credential)
 
        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)]
        [int]$NetcupCustNumber,
        [Parameter(Mandatory)]
        [pscredential]$NetcupAPICredential,
        [string]$NetcupEndpoint='https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON',
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    $script:NetcupEndpoint = $NetcupEndpoint

    $zone,$rec = Get-NetcupTxtRecord @PSBoundParameters

    if (-not $rec) {
        Write-Debug "Record $RecordName with value $TxtValue doesn't exist. Nothing to do."
        return
    }

    $queryParams = @{
        NetcupCustNumber = $NetcupCustNumber
        NetcupAPICredential = $NetcupAPICredential
        Request = @{
            action = 'updateDnsRecords'
            param = @{
                domainname = $zone
                dnsrecordset = @{
                    dnsrecords = @(@{
                        id = $rec.id
                        hostname = $rec.hostname
                        type = 'TXT'
                        destination = $TxtValue
                        deleterecord = $true
                    })
                }
            }
        }
    }
    Write-Verbose "Deleting TXT record $($rec.id) for $RecordName with value $TxtValue"
    $resp = Invoke-NetcupRequest @queryParams
    if ($resp -and $resp.dnsrecords) {
        $rec = $resp.dnsrecords | Where-Object {
            $_.hostname -eq $recShort -and $_.destination -eq $TxtValue
        }
        if (-not $rec) {
            Write-Debug "Deleted successfully"
        }
    }

    <#
    .SYNOPSIS
        Remove a DNS TXT record from Netcup
 
    .DESCRIPTION
        Description for Netcup
 
    .PARAMETER RecordName
        The fully qualified name of the TXT record.
 
    .PARAMETER TxtValue
        The value of the TXT record.
 
    .PARAMETER NetcupCustNumber
        The customer number of your Netcup account. This is also the username you use to login to the portal with.
 
    .PARAMETER NetcupAPICredential
        The Netcup API Key and Password you have configured in the portal as a PSCredential object. The Key should be the username.
 
    .PARAMETER NetcupEndpoint
        The URI of the Netcup REST API endpoint. The default should work unless Netcup changes it.
 
    .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' -NetcupCustNumber 123456 -NetcupAPICredential (Get-Credential)
 
        Removes a TXT record for the specified site with the specified value.
    #>

}

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

# https://helpcenter.netcup.com/en/wiki/general/our-api

function New-NetcupSession {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [int]$NetcupCustNumber,
        [Parameter(Mandatory,Position=1)]
        [pscredential]$NetcupAPICredential
    )

    $queryParams = @{
        Uri = $script:NetcupEndpoint
        Method = 'POST'
        Body = @{
            action = 'login'
            param = @{
                customernumber = $NetcupCustNumber
                apikey = $NetcupAPICredential.UserName
                apipassword = $NetcupAPICredential.GetNetworkCredential().Password
            }
        } | ConvertTo-Json -Compress
        ContentType = 'application/json'
        Verbose = $false
        ErrorAction = 'Stop'
    }

    Write-Debug "Logging in as customer $NetcupCustNumber."
    $resp = Invoke-RestMethod @queryParams @script:UseBasic
    if ($resp.status -eq 'success') {
        $script:NetcupSession = @{
            customernumber = $NetcupCustNumber
            apikey = $NetcupAPICredential.UserName
            apisessionid = $resp.responsedata.apisessionid
        }
    } else {
        try { throw "Netcup error $($resp.statuscode): $($resp.longmessage)" }
        catch { $PSCmdlet.ThrowTerminatingError($_) }
    }
}

function Invoke-NetcupRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [int]$NetcupCustNumber,
        [Parameter(Mandatory,Position=1)]
        [pscredential]$NetcupAPICredential,
        [Parameter(Mandatory,Position=2)]
        [hashtable]$Request
    )

    # Netcup seems to be having some sort of API issue that times out session tokens
    # very quickly. Their docs claim the session is supposed to last 15 minutes, but
    # after what feels like 15 seconds, the API starts returning errors such as
    # "The session id is not in a valid format."
    # So we're going to implement a retry mechanism to get a new session if it this
    # function gets that specific error code.

    if (-not $script:NetcupEndpoint) {
        $script:NetcupEndpoint = 'https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON'
    }
    if (-not $script:NetcupSession) {
        New-NetcupSession $NetcupCustNumber $NetcupAPICredential
    }

    $tries = 0
    while ($tries -lt 2) {
        $tries++

        # inject the current session into the request
        $req = @{
            action = $Request.action
            param = ($Request.param + $script:NetcupSession)
        }
        $queryParams = @{
            Uri = $script:NetcupEndpoint
            Method = 'POST'
            Body = $req | ConvertTo-Json -Compress -Depth 10
            ContentType = 'application/json'
            Verbose = $false
            ErrorAction = 'Stop'
        }
        Write-Debug "POST $($queryParams.Uri)`n$($Request|ConvertTo-Json -Depth 10)"

        $resp = Invoke-RestMethod @queryParams @script:UseBasic
        if ($resp.status -eq 'success') {
            return $resp.responsedata
        } else {
            if ($resp.statuscode -eq 4001) {
                Write-Debug "Netcup error $($resp.statuscode): $($resp.longmessage)"
                New-NetcupSession $NetcupCustNumber $NetcupAPICredential
                continue
            } elseif ($resp.statuscode -in 5029,4013) {
                Write-Debug "Netcup error $($resp.statuscode): $($resp.longmessage)"
                # 5029 = "Domain not found" for infoDnsRecords
                # 4013 = "Invalid domain name" for infoDnsRecords
                return $null
            } else {
                try { throw "Netcup error $($resp.statuscode): $($resp.longmessage)" }
                catch { $PSCmdlet.ThrowTerminatingError($_) }
            }
        }
    }

    # We should only get here if we ran out of retries getting a working session ID
    # which means logging in was successful but the API is not accepting the session
    # ID value it gave us.
    try { throw "Unable to obtain a valid Netcup apisessionid." }
    catch { $PSCmdlet.ThrowTerminatingError($_) }
}

function Get-NetcupTxtRecord {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [string]$RecordName,
        [Parameter(Mandatory,Position=1)]
        [string]$TxtValue,
        [Parameter(Mandatory)]
        [int]$NetcupCustNumber,
        [Parameter(Mandatory)]
        [pscredential]$NetcupAPICredential,
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams2
    )

    $zone = $null
    $allrecs = $null

    # setup a module variable to cache the record to zone mapping
    if (-not $script:NetcupRecordZones) { $script:NetcupRecordZones = @{} }

    # check for the record in the cache
    if ($script:NetcupRecordZones.ContainsKey($RecordName)) {
        $zone = $script:NetcupRecordZones.$RecordName
    }

    if (-not $zone) {
        # For whatever reason, the 'listallDomains' action is only available for resellers.
        # So we're just going to try 'infoDnsRecords' for various portions of the
        # RecordName until we find them or run out of options.
        $pieces = $RecordName.Split('.')
        for ($i=0; $i -lt ($pieces.Count-1); $i++) {
            $zoneTest = $pieces[$i..($pieces.Count-1)] -join '.'
            Write-Debug "Checking $zoneTest"

            $queryParams = @{
                NetcupCustNumber = $NetcupCustNumber
                NetcupAPICredential = $NetcupAPICredential
                Request = @{
                    action = 'infoDnsRecords'
                    param = @{ domainname = $zoneTest }
                }
            }
            try {
                # a non-null result means records were returned and we found
                # the matching zone
                if ($resp = Invoke-NetcupRequest @queryParams) {
                    Write-Debug "Found matching zone $zoneTest"
                    $zone = $zoneTest
                    $script:NetcupRecordZones.$RecordName = $zoneTest
                    $allrecs = $resp.dnsrecords
                    Write-Debug "Found $($allrecs.Count) existing records"
                }
            } catch { throw }
        }
    }

    if (-not $allrecs) {
        # We already have the zone from a previous call, so re-grab the current
        # record list.
        $queryParams = @{
            NetcupCustNumber = $NetcupCustNumber
            NetcupAPICredential = $NetcupAPICredential
            Request = @{
                action = 'infoDnsRecords'
                param = @{ domainname = $zone }
            }
        }
        $resp = Invoke-NetcupRequest @queryParams
        $allrecs = $resp.dnsrecords
        Write-Debug "Found $($allrecs.Count) existing records"
    }

    $recShort = $RecordName -ireplace "\.?$([regex]::Escape($zone.TrimEnd('.')))$",''
    if (-not $recShort) { $recShort = '@' }

    $rec = $allrecs | Where-Object {
        $_.type -eq 'TXT' -and
        $_.hostname -eq $recShort -and
        $_.destination -eq $TxtValue
    }
    return $zone,$rec
}