Skip to content

Commit 0cc4188

Browse files
authored
Add Netcup plugin and guide (#621)
1 parent 7966748 commit 0cc4188

File tree

4 files changed

+424
-0
lines changed

4 files changed

+424
-0
lines changed

Posh-ACME/Plugins/Netcup.ps1

Lines changed: 393 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,393 @@
1+
function Get-CurrentPluginType { 'dns-01' }
2+
3+
function Add-DnsTxt {
4+
[CmdletBinding()]
5+
param(
6+
[Parameter(Mandatory,Position=0)]
7+
[string]$RecordName,
8+
[Parameter(Mandatory,Position=1)]
9+
[string]$TxtValue,
10+
[Parameter(Mandatory)]
11+
[int]$NetcupCustNumber,
12+
[Parameter(Mandatory)]
13+
[pscredential]$NetcupAPICredential,
14+
[string]$NetcupEndpoint='https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON',
15+
[Parameter(ValueFromRemainingArguments)]
16+
$ExtraParams
17+
)
18+
19+
$script:NetcupEndpoint = $NetcupEndpoint
20+
21+
$zone,$rec = Get-NetcupTxtRecord @PSBoundParameters
22+
23+
if ($rec) {
24+
Write-Verbose "Record $RecordName already contains $TxtValue. Nothing to do."
25+
return
26+
}
27+
28+
$recShort = $RecordName -ireplace "\.?$([regex]::Escape($zone.TrimEnd('.')))$",''
29+
if (-not $recShort) { $recShort = '@' }
30+
31+
$queryParams = @{
32+
NetcupCustNumber = $NetcupCustNumber
33+
NetcupAPICredential = $NetcupAPICredential
34+
Request = @{
35+
action = 'updateDnsRecords'
36+
param = @{
37+
domainname = $zone
38+
dnsrecordset = @{
39+
dnsrecords = @(@{
40+
hostname = $recShort
41+
type = 'TXT'
42+
destination = $TxtValue
43+
deleterecord = $false
44+
})
45+
}
46+
}
47+
}
48+
}
49+
Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue"
50+
$resp = Invoke-NetcupRequest @queryParams
51+
if ($resp -and $resp.dnsrecords) {
52+
$rec = $resp.dnsrecords | Where-Object {
53+
$_.hostname -eq $recShort -and $_.destination -eq $TxtValue
54+
}
55+
Write-Debug "New record ID $($rec.id)"
56+
}
57+
58+
<#
59+
.SYNOPSIS
60+
Add a DNS TXT record to Netcup
61+
62+
.DESCRIPTION
63+
Description for Netcup
64+
65+
.PARAMETER RecordName
66+
The fully qualified name of the TXT record.
67+
68+
.PARAMETER TxtValue
69+
The value of the TXT record.
70+
71+
.PARAMETER NetcupCustNumber
72+
The customer number of your Netcup account. This is also the username you use to login to the portal with.
73+
74+
.PARAMETER NetcupAPICredential
75+
The Netcup API Key and Password you have configured in the portal as a PSCredential object. The Key should be the username.
76+
77+
.PARAMETER NetcupEndpoint
78+
The URI of the Netcup REST API endpoint. The default should work unless Netcup changes it.
79+
80+
.PARAMETER ExtraParams
81+
This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.
82+
83+
.EXAMPLE
84+
Add-DnsTxt '_acme-challenge.example.com' 'txt-value' -NetcupCustNumber 123456 -NetcupAPICredential (Get-Credential)
85+
86+
Adds a TXT record for the specified site with the specified value.
87+
#>
88+
}
89+
90+
function Remove-DnsTxt {
91+
[CmdletBinding()]
92+
param(
93+
[Parameter(Mandatory,Position=0)]
94+
[string]$RecordName,
95+
[Parameter(Mandatory,Position=1)]
96+
[string]$TxtValue,
97+
[Parameter(Mandatory)]
98+
[int]$NetcupCustNumber,
99+
[Parameter(Mandatory)]
100+
[pscredential]$NetcupAPICredential,
101+
[string]$NetcupEndpoint='https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON',
102+
[Parameter(ValueFromRemainingArguments)]
103+
$ExtraParams
104+
)
105+
106+
$script:NetcupEndpoint = $NetcupEndpoint
107+
108+
$zone,$rec = Get-NetcupTxtRecord @PSBoundParameters
109+
110+
if (-not $rec) {
111+
Write-Debug "Record $RecordName with value $TxtValue doesn't exist. Nothing to do."
112+
return
113+
}
114+
115+
$queryParams = @{
116+
NetcupCustNumber = $NetcupCustNumber
117+
NetcupAPICredential = $NetcupAPICredential
118+
Request = @{
119+
action = 'updateDnsRecords'
120+
param = @{
121+
domainname = $zone
122+
dnsrecordset = @{
123+
dnsrecords = @(@{
124+
id = $rec.id
125+
hostname = $rec.hostname
126+
type = 'TXT'
127+
destination = $TxtValue
128+
deleterecord = $true
129+
})
130+
}
131+
}
132+
}
133+
}
134+
Write-Verbose "Deleting TXT record $($rec.id) for $RecordName with value $TxtValue"
135+
$resp = Invoke-NetcupRequest @queryParams
136+
if ($resp -and $resp.dnsrecords) {
137+
$rec = $resp.dnsrecords | Where-Object {
138+
$_.hostname -eq $recShort -and $_.destination -eq $TxtValue
139+
}
140+
if (-not $rec) {
141+
Write-Debug "Deleted successfully"
142+
}
143+
}
144+
145+
<#
146+
.SYNOPSIS
147+
Remove a DNS TXT record from Netcup
148+
149+
.DESCRIPTION
150+
Description for Netcup
151+
152+
.PARAMETER RecordName
153+
The fully qualified name of the TXT record.
154+
155+
.PARAMETER TxtValue
156+
The value of the TXT record.
157+
158+
.PARAMETER NetcupCustNumber
159+
The customer number of your Netcup account. This is also the username you use to login to the portal with.
160+
161+
.PARAMETER NetcupAPICredential
162+
The Netcup API Key and Password you have configured in the portal as a PSCredential object. The Key should be the username.
163+
164+
.PARAMETER NetcupEndpoint
165+
The URI of the Netcup REST API endpoint. The default should work unless Netcup changes it.
166+
167+
.PARAMETER ExtraParams
168+
This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.
169+
170+
.EXAMPLE
171+
Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' -NetcupCustNumber 123456 -NetcupAPICredential (Get-Credential)
172+
173+
Removes a TXT record for the specified site with the specified value.
174+
#>
175+
}
176+
177+
function Save-DnsTxt {
178+
[CmdletBinding()]
179+
param(
180+
[Parameter(ValueFromRemainingArguments)]
181+
$ExtraParams
182+
)
183+
<#
184+
.SYNOPSIS
185+
Not required.
186+
187+
.DESCRIPTION
188+
This provider does not require calling this function to commit changes to DNS records.
189+
190+
.PARAMETER ExtraParams
191+
This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.
192+
#>
193+
}
194+
195+
############################
196+
# Helper Functions
197+
############################
198+
199+
# https://helpcenter.netcup.com/en/wiki/general/our-api
200+
201+
function New-NetcupSession {
202+
[CmdletBinding()]
203+
param(
204+
[Parameter(Mandatory,Position=0)]
205+
[int]$NetcupCustNumber,
206+
[Parameter(Mandatory,Position=1)]
207+
[pscredential]$NetcupAPICredential
208+
)
209+
210+
$queryParams = @{
211+
Uri = $script:NetcupEndpoint
212+
Method = 'POST'
213+
Body = @{
214+
action = 'login'
215+
param = @{
216+
customernumber = $NetcupCustNumber
217+
apikey = $NetcupAPICredential.UserName
218+
apipassword = $NetcupAPICredential.GetNetworkCredential().Password
219+
}
220+
} | ConvertTo-Json -Compress
221+
ContentType = 'application/json'
222+
Verbose = $false
223+
ErrorAction = 'Stop'
224+
}
225+
226+
Write-Debug "Logging in as customer $NetcupCustNumber."
227+
$resp = Invoke-RestMethod @queryParams @script:UseBasic
228+
if ($resp.status -eq 'success') {
229+
$script:NetcupSession = @{
230+
customernumber = $NetcupCustNumber
231+
apikey = $NetcupAPICredential.UserName
232+
apisessionid = $resp.responsedata.apisessionid
233+
}
234+
} else {
235+
try { throw "Netcup error $($resp.statuscode): $($resp.longmessage)" }
236+
catch { $PSCmdlet.ThrowTerminatingError($_) }
237+
}
238+
}
239+
240+
function Invoke-NetcupRequest {
241+
[CmdletBinding()]
242+
param(
243+
[Parameter(Mandatory,Position=0)]
244+
[int]$NetcupCustNumber,
245+
[Parameter(Mandatory,Position=1)]
246+
[pscredential]$NetcupAPICredential,
247+
[Parameter(Mandatory,Position=2)]
248+
[hashtable]$Request
249+
)
250+
251+
# Netcup seems to be having some sort of API issue that times out session tokens
252+
# very quickly. Their docs claim the session is supposed to last 15 minutes, but
253+
# after what feels like 15 seconds, the API starts returning errors such as
254+
# "The session id is not in a valid format."
255+
# So we're going to implement a retry mechanism to get a new session if it this
256+
# function gets that specific error code.
257+
258+
if (-not $script:NetcupEndpoint) {
259+
$script:NetcupEndpoint = 'https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON'
260+
}
261+
if (-not $script:NetcupSession) {
262+
New-NetcupSession $NetcupCustNumber $NetcupAPICredential
263+
}
264+
265+
$tries = 0
266+
while ($tries -lt 2) {
267+
$tries++
268+
269+
# inject the current session into the request
270+
$req = @{
271+
action = $Request.action
272+
param = ($Request.param + $script:NetcupSession)
273+
}
274+
$queryParams = @{
275+
Uri = $script:NetcupEndpoint
276+
Method = 'POST'
277+
Body = $req | ConvertTo-Json -Compress -Depth 10
278+
ContentType = 'application/json'
279+
Verbose = $false
280+
ErrorAction = 'Stop'
281+
}
282+
Write-Debug "POST $($queryParams.Uri)`n$($Request|ConvertTo-Json -Depth 10)"
283+
284+
$resp = Invoke-RestMethod @queryParams @script:UseBasic
285+
if ($resp.status -eq 'success') {
286+
return $resp.responsedata
287+
} else {
288+
if ($resp.statuscode -eq 4001) {
289+
Write-Debug "Netcup error $($resp.statuscode): $($resp.longmessage)"
290+
New-NetcupSession $NetcupCustNumber $NetcupAPICredential
291+
continue
292+
} elseif ($resp.statuscode -in 5029,4013) {
293+
Write-Debug "Netcup error $($resp.statuscode): $($resp.longmessage)"
294+
# 5029 = "Domain not found" for infoDnsRecords
295+
# 4013 = "Invalid domain name" for infoDnsRecords
296+
return $null
297+
} else {
298+
try { throw "Netcup error $($resp.statuscode): $($resp.longmessage)" }
299+
catch { $PSCmdlet.ThrowTerminatingError($_) }
300+
}
301+
}
302+
}
303+
304+
# We should only get here if we ran out of retries getting a working session ID
305+
# which means logging in was successful but the API is not accepting the session
306+
# ID value it gave us.
307+
try { throw "Unable to obtain a valid Netcup apisessionid." }
308+
catch { $PSCmdlet.ThrowTerminatingError($_) }
309+
}
310+
311+
function Get-NetcupTxtRecord {
312+
[CmdletBinding()]
313+
param(
314+
[Parameter(Mandatory,Position=0)]
315+
[string]$RecordName,
316+
[Parameter(Mandatory,Position=1)]
317+
[string]$TxtValue,
318+
[Parameter(Mandatory)]
319+
[int]$NetcupCustNumber,
320+
[Parameter(Mandatory)]
321+
[pscredential]$NetcupAPICredential,
322+
[Parameter(ValueFromRemainingArguments)]
323+
$ExtraParams2
324+
)
325+
326+
$zone = $null
327+
$allrecs = $null
328+
329+
# setup a module variable to cache the record to zone mapping
330+
if (-not $script:NetcupRecordZones) { $script:NetcupRecordZones = @{} }
331+
332+
# check for the record in the cache
333+
if ($script:NetcupRecordZones.ContainsKey($RecordName)) {
334+
$zone = $script:NetcupRecordZones.$RecordName
335+
}
336+
337+
if (-not $zone) {
338+
# For whatever reason, the 'listallDomains' action is only available for resellers.
339+
# So we're just going to try 'infoDnsRecords' for various portions of the
340+
# RecordName until we find them or run out of options.
341+
$pieces = $RecordName.Split('.')
342+
for ($i=0; $i -lt ($pieces.Count-1); $i++) {
343+
$zoneTest = $pieces[$i..($pieces.Count-1)] -join '.'
344+
Write-Debug "Checking $zoneTest"
345+
346+
$queryParams = @{
347+
NetcupCustNumber = $NetcupCustNumber
348+
NetcupAPICredential = $NetcupAPICredential
349+
Request = @{
350+
action = 'infoDnsRecords'
351+
param = @{ domainname = $zoneTest }
352+
}
353+
}
354+
try {
355+
# a non-null result means records were returned and we found
356+
# the matching zone
357+
if ($resp = Invoke-NetcupRequest @queryParams) {
358+
Write-Debug "Found matching zone $zoneTest"
359+
$zone = $zoneTest
360+
$script:NetcupRecordZones.$RecordName = $zoneTest
361+
$allrecs = $resp.dnsrecords
362+
Write-Debug "Found $($allrecs.Count) existing records"
363+
}
364+
} catch { throw }
365+
}
366+
}
367+
368+
if (-not $allrecs) {
369+
# We already have the zone from a previous call, so re-grab the current
370+
# record list.
371+
$queryParams = @{
372+
NetcupCustNumber = $NetcupCustNumber
373+
NetcupAPICredential = $NetcupAPICredential
374+
Request = @{
375+
action = 'infoDnsRecords'
376+
param = @{ domainname = $zone }
377+
}
378+
}
379+
$resp = Invoke-NetcupRequest @queryParams
380+
$allrecs = $resp.dnsrecords
381+
Write-Debug "Found $($allrecs.Count) existing records"
382+
}
383+
384+
$recShort = $RecordName -ireplace "\.?$([regex]::Escape($zone.TrimEnd('.')))$",''
385+
if (-not $recShort) { $recShort = '@' }
386+
387+
$rec = $allrecs | Where-Object {
388+
$_.type -eq 'TXT' -and
389+
$_.hostname -eq $recShort -and
390+
$_.destination -eq $TxtValue
391+
}
392+
return $zone,$rec
393+
}

0 commit comments

Comments
 (0)