Plugins/Porkbun.ps1

# API Documentation: https://porkbun.com/api/json/v3/documentation

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] $PorkbunAPIKey,
        [Parameter(Mandatory, Position = 3)]
        [SecureString] $PorkbunSecret,
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    );

    [string] $APIKey = [PSCredential]::new('Username', $PorkbunAPIKey).GetNetworkCredential().Password;
    [string] $APISecret = [PSCredential]::new('Username', $PorkbunSecret).GetNetworkCredential().Password;

    $DomainInfo = Get-PorkbunDomainInfo -PorkbunAPIKey $APIKey -PorkbunSecret $APISecret -LongName $RecordName;

    # Get the portion of the full name that is the domain name (e.g. 'record.name.sub.example.com' will become 'example.com')
    [string] $DomainName = $DomainInfo.Domain;
    # Get the portion of the full name that will become the record name (e.g. 'record.name.sub.example.com' will become 'record.name.sub')
    [string] $RecordNameShort = ($RecordName -ireplace [Regex]::Escape($DomainName), [string]::Empty).TrimEnd('.');

    # Get any existing TXT record(s) that already match what we want to create
    [object[]] $EqualRecords = @($DomainInfo.Records | Where-Object { ($_.type -EQ 'TXT') -AND ($_.name -eq $RecordName) -AND ($_.content -EQ $TxtValue) });

    if ($EqualRecords.Count -EQ 0)
    {
        Write-Debug 'This record does not exist yet, creating it';
        $bodyObject = @{
            name = $RecordNameShort
            type = 'TXT'
            content = $TxtValue
            apikey = $APIKey
            secretapikey = $APISecret
        }

        Write-Verbose "Creating record `"$RecordNameShort`" with value `"$TxtValue`" on domain `"$DomainName`""
        $queryParams = @{
            Uri = "https://porkbun.com/api/json/v3/dns/create/$DomainName"
            Method = 'POST'
            Body = $bodyObject | ConvertTo-Json
            ErrorAction = 'Stop'
            Verbose = $false
        }

        # sanitize credentials for logging
        $bodyObject.apikey = 'XXXXXXXX'
        $bodyObject.secretapikey = 'XXXXXXXX'
        Write-Debug "POST $($queryParams.Uri)`n$($bodyObject | ConvertTo-Json)"

        $APIResult = Invoke-WebRequest @queryParams @script:UseBasic

        if ($APIResult.StatusCode -NE 200) { throw "API returned status $($APIResult.StatusCode)"; }
        $ResultData = ConvertFrom-Json $APIResult.Content;
        if ($ResultData.status -NE 'SUCCESS') { throw "API returned result $($ResultData.status)"; }
        Write-Debug 'Successfully created record.';
    }
    else
    {
        Write-Debug 'A record already exists with this value, so no creation is necessary';
        return;
    }

    <#
    .SYNOPSIS
        Add a DNS TXT record to Porkbun.
    .DESCRIPTION
        Adds or edits a DNS TXT record using Porkbun's API.
    .PARAMETER RecordName
        The fully qualified name of the TXT record.
    .PARAMETER TxtValue
        The value of the TXT record.
    .PARAMETER PorkbunAPIKey
        The API key to use, obtained from https://porkbun.com/account/api
    .PARAMETER PorkbunSecret
        The API secret key corresponding to the API key, also obtained from https://porkbun.com/account/api (not accessible after being generated)
    .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. Does nothing if the record already exists.
    #>

}

function Remove-DnsTxt
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, Position = 0)]
        [string] $RecordName,
        [Parameter(Mandatory, Position = 1)]
        [string] $TxtValue,
        [Parameter(Mandatory, Position = 2)]
        [SecureString] $PorkbunAPIKey,
        [Parameter(Mandatory, Position = 3)]
        [SecureString] $PorkbunSecret,
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    );

    [string] $APIKey = [PSCredential]::new('Username', $PorkbunAPIKey).GetNetworkCredential().Password;
    [string] $APISecret = [PSCredential]::new('Username', $PorkbunSecret).GetNetworkCredential().Password;

    $DomainInfo = Get-PorkbunDomainInfo -PorkbunAPIKey $APIKey -PorkbunSecret $APISecret -LongName $RecordName;

    # Get the portion of the full name that is the domain name (e.g. 'record.name.sub.example.com' will become 'example.com')
    [string] $DomainName = $DomainInfo.Domain;

    # Get any existing TXT record(s) that have matching content
    [object[]] $EqualRecords = @($DomainInfo.Records | Where-Object { ($_.type -EQ 'TXT') -AND ($_.name -eq $RecordName) -AND ($_.content -EQ $TxtValue) });

    if ($EqualRecords.Count -EQ 0)
    {
        Write-Debug 'There are no records with this content, so no deletion is necessary';
        return;
    }
    else
    {
        Write-Debug "Found $($EqualRecords.Count) record(s) to delete.";
        foreach($RecordToDelete in $EqualRecords)
        {
            $RecordID = $RecordToDelete.id;

            Write-Verbose "Deleting record ID `"$RecordID`" on domain `"$DomainName`""
            $queryParams = @{
                Uri = "https://porkbun.com/api/json/v3/dns/delete/$DomainName/$RecordID"
                Method = 'POST'
                Body = "{`"secretapikey`": `"$APISecret`", `"apikey`": `"$APIKey`"}"
                ErrorAction = 'Stop'
                Verbose = $false
            }
            Write-Debug "POST $($queryParams.Uri)"
            $APIResult = Invoke-WebRequest @queryParams @script:UseBasic
            if ($APIResult.StatusCode -NE 200) { throw "API returned status $($APIResult.StatusCode)"; }
            $ResultData = ConvertFrom-Json $APIResult.Content;
            if ($ResultData.status -NE 'SUCCESS') { throw "API returned result $($ResultData.status)"; }
            Write-Debug 'Successfully deleted record.';
        }
    }

    <#
    .SYNOPSIS
        Remove a DNS TXT record from Porkbun.
    .DESCRIPTION
        Removes a DNS TXT record using Porkbun's API.
    .PARAMETER RecordName
        The fully qualified name of the TXT record.
    .PARAMETER TxtValue
        The value of the TXT record.
    .PARAMETER PorkbunAPIKey
        The API key to use, obtained from https://porkbun.com/account/api
    .PARAMETER PorkbunSecret
        The API secret key corresponding to the API key, also obtained from https://porkbun.com/account/api (not accessible after being generated)
    .EXAMPLE
        Remove-DnsTxt '_acme-challenge.example.com' 'txt-value'

        Removes a TXT record for the specified site with the specified value. Does nothing if the record does not exist.
    #>

}

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.
    .EXAMPLE
        Save-DnsTxt

        Commits changes for pending DNS TXT record modifications. (Not required)
    #>

}

function Get-PorkbunDomainInfo
{
    [CmdletBinding()]
    param
    (
        [string] $PorkbunAPIKey,
        [string] $PorkbunSecret,
        [string] $LongName
    )

    [string] $RequestBody = "{`"secretapikey`": `"$PorkbunSecret`", `"apikey`": `"$PorkbunAPIKey`"}";

    Write-Debug "Looking for domain `"$LongName`"";
    [string[]] $Sections = $LongName.Split('.');
    [int] $MaxIndex = $Sections.Count - 1;
    for ([int]$i = 0; $i -LT $MaxIndex; $i++)
    {
        [string] $NameToCheck = [string]::Join('.', $Sections[$i .. $MaxIndex]);
        Write-Debug "Querying API for `"$NameToCheck`"";

        try {
            $queryParams = @{
                Uri = "https://porkbun.com/api/json/v3/dns/retrieve/$NameToCheck"
                Method = 'POST'
                Body = $RequestBody
                ErrorAction = 'Stop'
                Verbose = $false
            }
            Write-Debug "POST $($queryParams.Uri)"
            $APIResult = Invoke-WebRequest @queryParams @script:UseBasic
        }
        catch {
            if ($_.Exception.Response.StatusCode -eq 400) {
                Write-Debug "Could not find domain `"$NameToCheck`"."
                continue
            } else {
                Write-Debug "Something went wrong while checking domain `"$NameToCheck`""
                throw
            }
        }

        if ($APIResult.StatusCode -NE 200) { Write-Debug "API returned code $($APIResult.StatusCode) for domain `"$NameToCheck`""; continue; }
        $ResultData = ConvertFrom-Json $APIResult.Content;
        if ($ResultData.status -NE 'SUCCESS') { Write-Debug "API returned status $($ResultData.status) for domain `"$NameToCheck`""; continue; }

        Write-Debug "Found domain `"$NameToCheck`"";
        return New-Object 'PSObject' -Property @{ Domain = $NameToCheck; Records = $ResultData.records; };
    }
    throw "No matching domain could be found for `"$LongName`" on this Porkbun account. Check that the domain is correct, that your API key and secret are entered correctly, and that you've enabled API access for this domain in the settings.";

    <#
    .SYNOPSIS
        Finds the domain and existing records.
    .DESCRIPTION
        Uses the Porkbun API to find the relevant domain and existing records for this full name.
    .PARAMETER PorkbunAPIKey
        The API key to use, obtained from https://porkbun.com/account/api
    .PARAMETER PorkbunSecret
        The API secret key corresponding to the API key, also obtained from https://porkbun.com/account/api (not accessible after being generated)
    .PARAMETER LongName
        The combined record/domain name to query for.
    .OUTPUTS
        An object containing a 'Domain' property, which contains the base domain name, and a 'Records' property which contains all currently existing records for this domain, including ones for other subdomains.
    .EXAMPLE
        Get-PorkbunDomainInfo -PorkbunAPIKey (key) -PorkbunAPISecret (secret) -LongName 'long.name.for.example.com'
        Will return a Domain of 'example.com' and any records for that entire domain.
    #>

}