diff --git a/Posh-ACME/Plugins/TencentDNS.ps1 b/Posh-ACME/Plugins/TencentDNS.ps1 new file mode 100644 index 00000000..e241a4ed --- /dev/null +++ b/Posh-ACME/Plugins/TencentDNS.ps1 @@ -0,0 +1,404 @@ +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]$TencentKeyId, + [Parameter(Mandatory, Position = 3)] + [securestring]$TencentSecret, + [Parameter(ValueFromRemainingArguments)] + $ExtraParams + ) + + # find the zone for this record + try { $zoneName = Find-TencentZone $RecordName $TencentKeyId $TencentSecret } catch { throw } + Write-Debug "Found zone $zoneName" + + # grab the relative portion of the fqdn + $recShort = ($RecordName -ireplace [regex]::Escape($zoneName), [string]::Empty).TrimEnd('.') + if ($recShort -eq $RecordName) { $recShort = '@' } + + # query for an existing record + try { $record = Find-TencentRecord -Domain $zoneName -Subdomain $recShort -RecordType 'TXT' -RecordValue $TxtValue -TencentKeyId $TencentKeyId -TencentSecret $TencentSecret } catch { throw } + + if ($null -eq $record) { + # add the record + Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue" + $body = @{ + SubDomain = $recShort + Domain = $zoneName + RecordType = 'TXT' + Value = $TxtValue + RecordLine = '默认' # default + } + $response = Invoke-TencentRest CreateRecord $body $TencentKeyId $TencentSecret + Invoke-Response $response + } + else { + Write-Debug "Record $RecordName already contains $TxtValue. Nothing to do." + } + + <# + .SYNOPSIS + Add a DNS TXT record to Tencentyun (Tencent Cloud) + + .DESCRIPTION + Add a DNS TXT record to Tencentyun (Tencent Cloud) + + .PARAMETER RecordName + The fully qualified name of the TXT record. + + .PARAMETER TxtValue + The value of the TXT record. + + .PARAMETER TencentKeyId + The Access Key ID for your Tencentyun account. + + .PARAMETER TencentSecret + The Access Secret for your Tencentyun 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 + $secret = Read-Host "Secret" -AsSecureString + PS C:\>Add-DnsTxt '_acme-challenge.example.com' 'txt-value' 'key-id' $secret + + Adds a TXT record using a securestring object for TencentSecret. + #> +} + +function Remove-DnsTxt { + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)] + [string]$RecordName, + [Parameter(Mandatory, Position = 1)] + [string]$TxtValue, + [Parameter(Mandatory, Position = 2)] + [string]$TencentKeyId, + [Parameter(Mandatory, Position = 3)] + [securestring]$TencentSecret, + [Parameter(ValueFromRemainingArguments)] + $ExtraParams + ) + + # find the zone for this record + try { $zoneName = Find-TencentZone $RecordName $TencentKeyId $TencentSecret } catch { throw } + Write-Debug "Found zone $zoneName" + + # grab the relative portion of the fqdn + $recShort = ($RecordName -ireplace [regex]::Escape($zoneName), [string]::Empty).TrimEnd('.') + if ($recShort -eq $RecordName) { $recShort = '@' } + + # query for an existing record + try { $record = Find-TencentRecord -Domain $zoneName -Subdomain $recShort -RecordType 'TXT' -RecordValue $TxtValue -TencentKeyId $TencentKeyId -TencentSecret $TencentSecret } catch { throw } + Write-Debug "Found Record $record" + + if ($null -eq $record) { + Write-Debug "Record $RecordName with value $TxtValue doesn't exist. Nothing to do." + } + else { + # remove the record + Write-Verbose "Removing TXT record for $RecordName with RecordId $($record.RecordId)" + $body = @{ + Domain = $zoneName + RecordId = $record.RecordId + } + $response = Invoke-TencentRest DeleteRecord $body $TencentKeyId $TencentSecret + Invoke-Response $response + } + + + <# + .SYNOPSIS + Remove a DNS TXT record from Tencentyun (Tencent Cloud) + + .DESCRIPTION + Remove a DNS TXT record from Tencentyun (Tencent Cloud) + + .PARAMETER RecordName + The fully qualified name of the TXT record. + + .PARAMETER TxtValue + The value of the TXT record. + + .PARAMETER TencentKeyId + The Access Key ID for your Tencentyun account. + + .PARAMETER TencentSecret + The Access Secret for your Tencentyun 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 + $secret = Read-Host "Secret" -AsSecureString + PS C:\>Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' 'key-id' $secret + + Removes a TXT record using a securestring object for TencentSecret. + #> +} + +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://www.tencentcloud.com/document/product/228/31723 +# https://cloud.tencent.com/document/api/1427 + +function Invoke-TencentRest { + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)] + [string]$action, + [Parameter(Position = 1)] + [Object]$bodyData, + [Parameter(Mandatory, Position = 2)] + [string]$secretId, + [Parameter(Mandatory, Position = 3)] + [securestring]$AccessSecret + ) + + $secretKey = [pscredential]::new('a', $AccessSecret).GetNetworkCredential().Password + $body = $bodyData | ConvertTo-Json -Compress + # BuildRequest + $region = "" + $token = "" + $version = "2021-03-23" + $apihost = "dnspod.tencentcloudapi.com" + $contentType = "application/json;charset=utf-8" + $epochStart = Get-Date -Year 1970 -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0 + $timestamp = [Math]::Round((Get-Date).ToUniversalTime().Subtract($epochStart).TotalSeconds) + + # Signature mechanism + # https://cloud.tencent.com/document/api/1427/56189 + $auth = GetAuth -secretId $secretId -secretKey $secretKey -apihost $apihost -contentType $contentType -timestamp $timestamp -body $body -action $action + + $queryParams = @{ + Uri = "https://$apihost" + Method = 'Post' + Headers = @{ + 'Authorization' = $auth + 'User-Agent' = '' + 'Host' = $apihost + 'X-TC-Timestamp' = $timestamp + 'X-TC-Version' = $version + 'X-TC-Action' = $action + 'X-TC-Region' = $region + 'X-TC-Token' = $token + } + Body = $body + ContentType = $contentType + Verbose = $false + ErrorAction = 'Stop' + } + Write-Debug "POST $($queryParams.Uri)`n$($queryParams.Body)" + + # PowerShell 7 does header validation by default now and rejects the request + # unless you specify the SkipHeaderValidation flag. + if ('SkipHeaderValidation' -in (Get-Command Invoke-RestMethod).Parameters.Keys) { + $queryParams.SkipHeaderValidation = $true + } + + Invoke-RestMethod @queryParams +} + +function GetAuth { + [CmdletBinding()] + param( + [string]$secretId, + [string]$secretKey, + [string]$apihost, + [string]$contentType, + [long]$timestamp, + [string]$body, + [string]$action + ) + + $canonicalURI = "/" + $xtcaction = $action.ToLower() + $canonicalHeaders = "content-type:$contentType`nhost:$apihost`nx-tc-action:$xtcaction`n" + $signedHeaders = "content-type;host;x-tc-action" + $hashedRequestPayload = Sha256Hex $body + $canonicalRequest = "POST`n$canonicalURI`n`n$canonicalHeaders`n$signedHeaders`n$hashedRequestPayload" + + Write-Debug "canonicalRequest $canonicalRequest" + + $algorithm = "TC3-HMAC-SHA256" + $epochStart = Get-Date -Year 1970 -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0 + $timestampDateTime = $epochStart.AddSeconds($timestamp) + $date = $timestampDateTime.ToString("yyyy-MM-dd") + $service = $apihost.Split(".")[0] + $credentialScope = "$date/$service/tc3_request" + $hashedCanonicalRequest = Sha256Hex $canonicalRequest + $stringToSign = "$algorithm`n$timestamp`n$credentialScope`n$hashedCanonicalRequest" + + Write-Debug "stringToSign $stringToSign" + + $tc3SecretKey = [Text.Encoding]::UTF8.GetBytes("TC3" + $secretKey) + $secretDate = HmacSha256 -key $tc3SecretKey -msg ([Text.Encoding]::UTF8.GetBytes($date)) + $secretService = HmacSha256 -key $secretDate -msg ([Text.Encoding]::UTF8.GetBytes($service)) + $secretSigning = HmacSha256 -key $secretService -msg ([Text.Encoding]::UTF8.GetBytes("tc3_request")) + $signatureBytes = HmacSha256 -key $secretSigning -msg ([Text.Encoding]::UTF8.GetBytes($stringToSign)) + $signature = ($signatureBytes | ForEach-Object { $_.ToString("x2") }) -join '' + $signature = $signature.ToLower() + + $auth = "$algorithm Credential=$secretId/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature" + Write-Debug "auth $auth" + return $auth +} + +function Sha256Hex { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [String]$inputString + ) + + $sha256 = [Security.Cryptography.SHA256]::Create() + $inputBytes = [Text.Encoding]::UTF8.GetBytes($inputString) + $hashBytes = $sha256.ComputeHash($inputBytes) + $hashHexString = [BitConverter]::ToString($hashBytes) -replace "-" + + return $hashHexString.ToLower() +} + +function HmacSha256 { + [CmdletBinding()] + param( + [byte[]]$key, + [byte[]]$msg + ) + + $mac = [Security.Cryptography.HMACSHA256]::new($key) + return $mac.ComputeHash($msg) +} +function Invoke-Response { + [CmdletBinding()] + param( + [object]$response + ) + if ($response.Response.Error) { + $strRps = $response | ConvertTo-Json -Compress + Write-Warning "Response Error $strRps" + throw + } + +} + +function Find-TencentZone { + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)] + [string]$RecordName, + [Parameter(Mandatory, Position = 1)] + [string]$TencentKeyId, + [Parameter(Mandatory, Position = 2)] + [securestring]$TencentSecret + ) + + # setup a module variable to cache the record to zone ID mapping + # so it's quicker to find later + if (!$script:TencentRecordZones) { $script:TencentRecordZones = @{} } + + # check for the record in the cache + if ($script:TencentRecordZones.ContainsKey($RecordName)) { + return $script:TencentRecordZones.$RecordName + } + + # Search for the zone from longest to shortest set of FQDN pieces. + $pieces = $RecordName.Split('.') + for ($i = 0; $i -lt ($pieces.Count - 1); $i++) { + $zoneTest = $pieces[$i..($pieces.Count - 1)] -join '.' + Write-Debug "Checking $zoneTest" + try { + $body = @{ + Keyword = $zoneTest + } + $response = Invoke-TencentRest DescribeDomainList $body $TencentKeyId $TencentSecret + + # check for results + if ($response.Response.DomainCountInfo.DomainTotal -gt 0) { + $script:TencentRecordZones.$RecordName = $response.Response.DomainList[0].Name # or PunyCode? + return $script:TencentRecordZones.$RecordName + } + } + catch { throw } + } + + throw "No zone found for $RecordName" +} +function Find-TencentRecord { + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)] + [string]$Domain, + [Parameter(Mandatory, Position = 1)] + [AllowEmptyString()] + [string]$Subdomain, + [Parameter(Mandatory, Position = 2)] + [string]$RecordType, + [Parameter(Position = 3)] + [string]$RecordValue, + [Parameter(Mandatory, Position = 4)] + [string]$TencentKeyId, + [Parameter(Mandatory, Position = 5)] + [securestring]$TencentSecret + ) + + try { + $body = @{ + Domain = $Domain + Subdomain = $Subdomain + #Keyword =$TxtValue # can not add this param + RecordType = $RecordType + } + $response = Invoke-TencentRest DescribeRecordList $body $TencentKeyId $TencentSecret + } + catch { throw } + + if ($response.Response.RecordCountInfo.TotalCount -gt 0) { + + $recordList = $response.Response.RecordList + if ($RecordValue -eq $null) { + return $recordList[0] + } + # found data . then foreach + for ($i = 0; $i -lt ($recordList.Count); $i++) { + + $item = $recordList[$i] + if ($item.Value -eq $RecordValue) { + #Write-Debug "Find RecordItem $item" + return $item + } + + } + + } + Write-Debug "No Record found for $Domain Subdomain $Subdomain RecordType $RecordType RecordValue $RecordValue" +} diff --git a/Posh-ACME/Private/Import-PluginDetail.ps1 b/Posh-ACME/Private/Import-PluginDetail.ps1 index cc5d9eae..12937c60 100644 --- a/Posh-ACME/Private/Import-PluginDetail.ps1 +++ b/Posh-ACME/Private/Import-PluginDetail.ps1 @@ -76,6 +76,7 @@ function Import-PluginDetail { 'Simply' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'Simply'} 'SimplyCom' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'SimplyCom'} 'SSHProxy' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'SSHProxy'} + 'TencentDNS' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'TencentDNS'} 'TotalUptime' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'TotalUptime'} 'UKFast' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'UKFast'} 'WebRoot' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'http-01'; Path = ''; Name = 'WebRoot'} diff --git a/docs/Plugins/TencentDNS.md b/docs/Plugins/TencentDNS.md new file mode 100644 index 00000000..97b928ff --- /dev/null +++ b/docs/Plugins/TencentDNS.md @@ -0,0 +1,23 @@ +title: TencentDNS + +# How To Use the TencentDNS Plugin + +This plugin works against [DNSPod](https://dnspod.com/) which is now part of the Tencent Cloud using their v3 API. It is assumed that you have already setup an account and created the domain you will be working against. + +## Setup + +This plugin works with Tencent Cloud API Keys which is a change from the old DNSPod native plugin that used DNSPod Tokens. Despite what the console says, you cannot use old DNSPod Tokens with this plugin. You will need to generate a new Tencent Cloud API Key if you haven't already. + +Login to the console and go to the [Tencent Cloud API Keys](https://console.dnspod.com/account/token/apikey) section. Create a new key and make a note of its ID and Secret values. + +## Using the Plugin + +The Secret ID is used with the `TencentKeyId` string parameter. The Secret value is used with `TencentSecret` as a SecureString. + +```powershell +$pArgs = @{ + TencentKeyId = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + TencentSecret = (Read-Host 'Enter Secret' -AsSecureString) +} +New-PACertificate example.com -Plugin TencentDNS -PluginArgs $pArgs +``` diff --git a/docs/Plugins/index.md b/docs/Plugins/index.md index 3a8ac3e5..dcf5da4a 100644 --- a/docs/Plugins/index.md +++ b/docs/Plugins/index.md @@ -77,6 +77,7 @@ SimpleDNSPlus | [Simple DNS Plus](https://simpledns.com/) | [Usage Guide](Simple Simply | [Simply.com](https://www.simply.com/) (deprecated) | [Usage Guide](Simply.md) | :white_check_mark: SimplyCom | [Simply.com](https://www.simply.com/) (formerly UnoEuro) | [Usage Guide](SimplyCom.md) | :white_check_mark: SSHProxy | Custom SSH Script | [Usage Guide](SSHProxy.md) | :white_check_mark: +TencentDNS | [TencentDNS](https://dnspod.com/) | [Usage Guide](TencentDNS.md) | :white_check_mark: TotalUptime | [TotalUptime](https://totaluptime.com/solutions/cloud-dns-service/) | [Usage Guide](TotalUptime.md) | :white_check_mark: UKFast | [UKFast](https://ukfast.co.uk) | [Usage Guide](UKFast.md) | :white_check_mark: WebsupportSK | [Websupport.sk](https://www.websupport.sk/?ref=NTIqFFo7Rg) | [Usage Guide](WebsupportSK.md) | :white_check_mark: