Plugins/All-Inkl.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
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]$KasUsername,
        [Parameter(ParameterSetName='plain',Mandatory,Position=2)]
        [securestring]$KasPwd,
        [Parameter(ParameterSetName='sha1',Mandatory,Position=2)]
        [securestring]$KasPwdHash,
        [Parameter(ParameterSetName='session',Mandatory,Position=2)]
        [securestring]$KasSession,
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    # get effective KAS login data from parameter sets
    $loginData = Get-KasLoginDataFromParameters $PSCmdlet.ParameterSetName $KasUsername $KasPwd $KasPwdHash $KasSession

    # load current DNS settings from KAS API
    $settingsObj = Get-KASDNSSettings $loginData $RecordName

    # remove zone from record name
    $recNameWithoutZone = ($RecordName -ireplace [regex]::Escape($settingsObj.zone), [string]::Empty).TrimEnd('.')

    # search for existing DNS settings for the record
    $existingSettingsItem = Find-KASDNSSettingsItemInList $settingsObj.dnsSettings $recNameWithoutZone $TxtValue
    if ($existingSettingsItem) {
        Write-Verbose "Record $RecordName already contains $TxtValue. Nothing to do."
        return
    }

    # get zone_host for adding
    if ($settingsObj.zone.EndsWith(".")) {
        $zoneHost = $settingsObj.zone
    } else {
        $zoneHost = $settingsObj.zone + "."
    }

    # create DNS settings entry
    $addDnsSettingsParameters = @{
        'zone_host'=$zoneHost
        'record_type'='TXT'
        'record_name'=$recNameWithoutZone
        'record_data'=$TxtValue
        'record_aux'='0'
    }
    $kasAPIResponse = Invoke-KasApiAction $loginData 'add_dns_settings' $addDnsSettingsParameters

    Write-Debug $kasAPIResponse.OuterXml

    <#
    .SYNOPSIS
        Add a DNS TXT record to All-Inkl.com KAS

    .DESCRIPTION
        Uses the All-Inkl.com KAS API to add a DNS TXT record.

    .PARAMETER RecordName
        The fully qualified name of the TXT record.

    .PARAMETER TxtValue
        The value of the TXT record.

    .PARAMETER KasUsername
        The KAS authentication user

    .PARAMETER KasPwd
        The password for your All-Inkl KAS account.

    .PARAMETER KasPwdHash
        The sha1 hash of the password for your All-Inkl KAS account.

    .PARAMETER KasSession
        The session id of an open session on the KAS API. Use this parameter if you want to handle authentication on your own.

    .PARAMETER ExtraParams
        This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.

    .EXAMPLE
        $pass = Read-Host "Password" -AsSecureString
        Add-DnsTxt '_acme-challenge.example.com' 'txt-value' -KasUsername 'userName' -KasPwd $pass

        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,Position=2)]
        [string]$KasUsername,
        [Parameter(ParameterSetName='plain',Mandatory,Position=2)]
        [securestring]$KasPwd,
        [Parameter(ParameterSetName='sha1',Mandatory,Position=2)]
        [securestring]$KasPwdHash,
        [Parameter(ParameterSetName='session',Mandatory,Position=2)]
        [securestring]$KasSession,
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    # get effective KAS login data from parameter sets
    $loginData = Get-KasLoginDataFromParameters $PSCmdlet.ParameterSetName $KasUsername $KasPwd $KasPwdHash $KasSession

    # load current DNS settings from KAS API
    $settingsObj = Get-KASDNSSettings $loginData $RecordName

    # remove zone from record name
    $recNameWithoutZone = ($RecordName -ireplace [regex]::Escape($settingsObj.zone), [string]::Empty).TrimEnd('.')

    # search for existing DNS settings for the record
    $existingSettingsItem = Find-KASDNSSettingsItemInList $settingsObj.dnsSettings $recNameWithoutZone $TxtValue
    if ((-not $existingSettingsItem) -or (-not $existingSettingsItem.Node)) {
        Write-Verbose "Record $RecordName with value $TxtValue not found. Nothing to do."
        return
    }

    $recordIdElement = Select-XmlFromKASResult $existingSettingsItem.Node ".//item[contains(key, 'record_id')]/value"
    if ((-not $recordIdElement) -or (-not $recordIdElement.Node)) {
        throw "Couldn't read record id for $RecordName"
    }

    # create DNS settings entry
    $removeDnsSettingsParameters = @{
        'record_id'=$recordIdElement.Node.InnerText
    }
    $kasAPIResponse = Invoke-KasApiAction $loginData 'delete_dns_settings' $removeDnsSettingsParameters

    Write-Debug $kasAPIResponse.OuterXml

    <#
    .SYNOPSIS
        Remove a DNS TXT record from All-Inkl.com KAS

    .DESCRIPTION
        Uses the All-Inkl.com KAS API to remove the DNS TXT record.

    .PARAMETER RecordName
        The fully qualified name of the TXT record.

    .PARAMETER TxtValue
        The value of the TXT record.

    .PARAMETER KasUsername
        The KAS authentication user

    .PARAMETER KasPwd
        The password for your All-Inkl KAS account.

    .PARAMETER KasPwdHash
        The sha1 hash of the password for your All-Inkl KAS account.

    .PARAMETER KasSession
        The session id of an open session on the KAS API. Use this parameter if you want to handle authentication on your own.

    .PARAMETER ExtraParams
        This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.

    .EXAMPLE
        $pass = Read-Host "Password" -AsSecureString
        Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' -KasUsername 'userName' -KasPwd $pass

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

# KAS API documentation
# https://kasapi.kasserver.com/dokumentation/

function Get-KasLoginDataFromParameters {
    [CmdletBinding()]
    param(
        [string]$paramSetName,
        [string]$KasUsername,
        [securestring]$KasPwd,
        [securestring]$KasPwdHash,
        [securestring]$KasSession
    )

    # check which parameter to use and set KAS auth type accordingly
    if ('plain' -eq $paramSetName) {
        $secureAuthData = $KasPwd
        $kasAuthType = 'sha1'
    }
    elseif ('sha1' -eq $paramSetName) {
        $secureAuthData = $KasPwdHash
        $kasAuthType = 'sha1'
    }
    elseif ('session' -eq $paramSetName) {
        $secureAuthData = $KasSession
        $kasAuthType = 'session'
    }

    # get plaintext from securestring
    $kasAuthData = (New-Object PSCredential "user", $secureAuthData).GetNetworkCredential().Password

    # when user provided plaintext password, compute sha1 of the password
    if ('plain' -eq $paramSetName) {
        $kasAuthData = [System.BitConverter]::ToString($(New-Object System.Security.Cryptography.SHA1CryptoServiceProvider).ComputeHash($([system.Text.Encoding]::UTF8).GetBytes($kasAuthData))).Replace("-", "")
    }

    # return the effective authentication data
    return @{ kas_login=$KasUsername; kas_auth_type=$kasAuthType; kas_auth_data=$kasAuthData }

    <#
    .SYNOPSIS
        Internal helper function that checks all input parameter sets/combinations and returns the effective login data

    .DESCRIPTION
        KAS API supports three different login types: plain/sha1/session
        see https://kasapi.kasserver.com/dokumentation/phpdoc/

        This plugin uses the types 'sha1' & 'session'.
        This method accepts the parameters for type 'plain', but the data is converted to 'sha1' before being sent to the API.

        By using the type 'session' it is possible to reuse existing sessions when Posh-ACME is used as a part of a larger script.

    .PARAMETER paramSetName
        The name of the paramSet that was detected by the public methods of this plugin.

    .PARAMETER KasUsername
        The username for the KAS API. The username is required for all login types.

    .PARAMETER KasPwd
        The plain password as securestring.

    .PARAMETER KasPwdHash
        The sha1 hash of the password as securestring.

    .PARAMETER KasSession
        The session id of an existing/open session in KAS as securestring.
    #>

}

function Get-KASDNSSettings {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [hashtable]$loginData,
        [Parameter(Mandatory,Position=1)]
        [string]$RecordName
    )

    # setup a module variable to cache the record to zone_host mapping
    if (!$script:KASRecordZones) { $script:KASRecordZones = @{} }

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

        $kasAPIResponse = Invoke-KASAPIGetDNSSettings $loginData $kasZoneHost

        return @{
            zone = $kasZoneHost
            dnsSettings = $kasAPIResponse
        }
    } else {

        # 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 zone $zoneTest"

            # skip calling KAS API for _acme-challenge.*
            # The API would return an error zone_syntax_incorrect anyway
            if ($zoneTest.StartsWith("_acme-challenge.")) {
                continue;
            }

            try {
                $kasAPIResponse = Invoke-KASAPIGetDNSSettings $loginData $zoneTest

                # check for results
                if (-not $kasAPIResponse) {
                    continue;
                }

                # since the current $zoneTest returned a result, cache it for future calls
                $script:KASRecordZones.$RecordName = $zoneTest

                return @{
                    zone = $zoneTest
                    dnsSettings = $kasAPIResponse
                }

            }
            catch {
                # Ignore "zone_not_found" and try the next set of FQDN pieces. Throw all other errors
                if (!$_.Exception.Message.Contains("zone_not_found")) {
                    throw
                }
            }
        }

        throw "No zone_host found for $RecordName"
    }

    <#
    .SYNOPSIS
        loads the DNS settings from the KAS API

    .DESCRIPTION
        tries to find the correct zone for the given record name and loads the
        corresponding DNS settings

    .PARAMETER loginData
        a hashtable containing the login data

    .PARAMETER RecordName
        the record name the settings should be loaded for
    #>

}

function Invoke-KASAPIGetDNSSettings {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [hashtable]$loginData,
        [Parameter(Mandatory,Position=1)]
        [string]$zoneHost
    )

    # prepare API params
    $getDnsSettingsParameters = @{
        'zone_host'=$zoneHost
    }

    # call API
    $responseDocument = Invoke-KasApiAction $loginData 'get_dns_settings' $getDnsSettingsParameters

    # check for results
    if (-not $responseDocument) {
        return $null;
    }

    # find ReturnInfo in result
    $returnInfo = Select-XmlFromKASResult $responseDocument "./return/item[contains(key, 'Response')]/value/item[contains(key,'ReturnInfo')]/value"

    if ($returnInfo -and $returnInfo.Node) {
        return $returnInfo.Node
    } else {
        return $null
    }

    <#
    .SYNOPSIS
        invokes the action 'get_dns_settings' on the KAS API

    .DESCRIPTION
        calls the KAS SOAP API, invokes the 'get_dns_settings' action and returns the result

    .PARAMETER loginData
        a hashtable containing the login data

    .PARAMETER zoneHost
        the zone_host parameter that is provided to the KAS API
    #>

}

function Invoke-KasApiAction {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [hashtable]$loginData,
        [Parameter(Mandatory,Position=1)]
        [string]$kasAPIAction,
        [Parameter(Mandatory,Position=2)]
        [hashtable]$kasParameters
    )

    # wsdl for the api service: https://kasapi.kasserver.com/soap/wsdl/KasApi.wsdl
    # wsdl for the auth service: https://kasapi.kasserver.com/soap/wsdl/KasAuth.wsdl
    $kasAPIUri = "https://kasapi.kasserver.com/soap/KasApi.php"

    # init KAS flood delay
    # The KAS API documentation can be read as if the delay is only relevant when calling
    # the same API action multiple times. But tests have shown that it is also enforced when
    # calling different API methods. So we use one generic delay for all calls.
    if (!$script:KASNextRequestTime) { $script:KASNextRequestTime = Get-Date }

    # create parameter hastable
    $paramData = @{
        'kas_action' = $kasAPIAction
        'KasRequestParams' = $kasParameters
    }

    # copy login data to parameters
    foreach($key in $loginData.keys) {
        $paramData[$key] = $loginData[$key]
    }

    # convert parameters to JSON
    $paramJson = $paramData | ConvertTo-Json -Depth 2

    # put parameters in SOAP envelope
    $envelopeXml=@'
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
                xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
                xmlns:tns="https://kasserver.com/"
                xmlns:types="https://kasserver.com/encodedTypes"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
        <q1:KasApi xmlns:q1="urn:xmethodsKasApi">
            <Params xsi:type="xsd:string">
'@
 + $paramJson + @'
            </Params>
        </q1:KasApi>
    </soap:Body>
</soap:Envelope>
'@


    try {
        # wait for KAS flood delay if necessary
        $slpFor = (New-TimeSpan -End $script:KASNextRequestTime).TotalMilliseconds
        if ($slpFor -gt 0) {
            Write-Verbose "Waiting ${slpFor}ms for KAS API flood delay..."
            Start-Sleep -Milliseconds $slpFor
        }

        # invoke KAS API
        $response = Invoke-WebRequest "$kasAPIUri" -Body "$envelopeXml" -contentType "text/xml; charset=utf-8" -method POST @Script:UseBasic

        # parse result content as xml
        [xml]$xmlDocument = $response.Content

        # find body element
        $bdy = Select-XmlFromKASResult $xmlDocument '/envelopeNS:Envelope/envelopeNS:Body'

        if ((-not $bdy) -or (-not $bdy.Node)) {
            throw "No body element in KAS API response found."
        }

        # check for a 'Fault' element and throw an error if necessary
        $faultElement = Select-XmlFromKASResult $bdy.Node './envelopeNS:Fault'
        if ($faultElement) {

            if (-not $faultElement.Node) {
                throw "Unexpected error: Fault elementin KAS API response found but Node property was empty."
            }

            $faultString = Select-XmlFromKASResult $faultElement.Node './faultstring'
            $faultDetail = Select-XmlFromKASResult $faultElement.Node './detail'

            if ($faultString -and $faultString.Node) {
                if ($faultDetail -and $faultDetail.Node) {
                    $errorMsg = "KAS API error: " + $faultString.Node.InnerText + " (" + $faultDetail.Node.InnerText + ")"
                } else {
                    $errorMsg = "KAS API error: " + $faultString.Node.InnerText
                }
            } else {
                if ($faultDetail -and $faultDetail.Node) {
                    $errorMsg = "KAS API error: " + $faultDetail.Node.InnerText
                } else {
                    $errorMsg = "KAS API error: unknown error"
                }
            }

            Set-KASFloodDelay $faultElement.Node

            throw $errorMsg
        }

        # check for a 'KasApiResponse' element
        $result = Select-XmlFromKASResult $bdy.Node './resultNS:KasApiResponse'
        if ($result -and $result.Node) {
            Set-KASFloodDelay $result.Node

            return $result.Node
        } else {
            throw "KAS API error: 'KasApiResponse' not found"
        }
    }
    catch {
        Set-KASFloodDelay

        throw "An error occured: " + $_.Exception.Message
    }

    <#
    .SYNOPSIS
        invokes an action on the KAS API

    .DESCRIPTION
        calls the KAS SOAP API, invokes an action and returns the result

    .PARAMETER loginData
        a hashtable containing the login data

    .PARAMETER kasAPIAction
        the action that should be invoked on the KAS API
        available actions: https://kasapi.kasserver.com/dokumentation/phpdoc/packages/API%20Funktionen.html

    .PARAMETER kasParameters
        a hashtable that contains the parameters for the called API action
    #>

}

function Set-KASFloodDelay {
    [CmdletBinding()]
    param (
        [Parameter(Position=0)]
        [System.Xml.XmlNode]$elementToSearchIn
    )

    # The documentation of the KAS API could be read as if the delay is only enforced when an API call
    # was successfull. As expected, testing showed, that it is also enforced for failed calls.
    # But unfortunately for failed calls the repsonse doesn't include the waiting time. This is why we will
    # be waiting for the default 2 seconds that succeeded requests normally return when no value was specified.


    # when a XmlNode was specified search it for the flood delay key/value pair
    if ($elementToSearchIn) {
        $floodDelayElement = Select-XmlFromKASResult $elementToSearchIn ".//item[contains(key, 'KasFloodDelay')]/value"

        if ($floodDelayElement -and $floodDelayElement.Node -and ($floodDelayElement.Node.InnerText -gt 0)) {
            $script:KASNextRequestTime = (Get-Date).AddSeconds($floodDelayElement.Node.InnerText)
            return
        }

        $floodDelayElement = Select-XmlFromKASResult $elementToSearchIn ".//kasflooddelay"

        if ($floodDelayElement -and $floodDelayElement.Node -and ($floodDelayElement.Node.InnerText -gt 0)) {
            $script:KASNextRequestTime = (Get-Date).AddSeconds($floodDelayElement.Node.InnerText)
            return
        }
    }

    # set next request time to a default value as fallback
    if ($script:KASNextRequestTime -le (Get-Date)) {
        $script:KASNextRequestTime = (Get-Date).AddSeconds(2)
    }

    <#
    .SYNOPSIS
        searches a KAS API result XmlNode for the flood delay and stores the next request time for the KAS API

    .DESCRIPTION
        Takes a XmlNode from a KAS API result and searches for the flood delay key/value pair.
        When found it stores the value for the next API call. Otherwise it sets a default value.

    .PARAMETER elementToSearchIn
        the parent XmlNode to search in (search root)
    #>

}

function Select-XmlFromKASResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [System.Xml.XmlNode]$parentElement,
        [Parameter(Mandatory,Position=1)]
        [string]$xPathStr
    )

    $xmlNamespaces = @{
        envelopeNS = "http://schemas.xmlsoap.org/soap/envelope/"
        resultNS = "https://kasapi.kasserver.com/soap/KasApi.php"
    };

    $result = Select-Xml -Xml $parentElement -XPath $xPathStr -Namespace $xmlNamespaces

    return $result

    <#
    .SYNOPSIS
        executes a given xpath on a given XmlNode

    .DESCRIPTION
        Takes a XmlNode and executes a xpath on it.

    .PARAMETER parentElement
        the parent XmlNode to search in (search root)

    .PARAMETER xPathStr
        the xpath string to execute
    #>

}

function Find-KASDNSSettingsItemInList {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [System.Xml.XmlNode]$parentDNSSettingsElement,
        [Parameter(Mandatory,Position=1)]
        [AllowEmptyString()]
        [string]$recordNameWithoutZone,
        [Parameter(Mandatory,Position=2)]
        [string]$TxtValue
    )
    # search for existing DNS settings for the record
    return Select-XmlFromKASResult $parentDNSSettingsElement @"
./item[
    item[contains(key, 'record_name') and contains(value, '$recordNameWithoutZone')]
    and
    item[contains(key, 'record_type') and contains(value, 'TXT')]
    and
    item[contains(key, 'record_data') and contains(value, '$TxtValue')]
]
"@


    <#
    .SYNOPSIS
        searches a given XmlNode with KAS DNS Settings for matching child items

    .DESCRIPTION
        takes a XmlNode with child items and searches for the correct child by matching record_name, record_type and record_data

    .PARAMETER parentDNSSettingsElement
        the parent XmlNode to search in (search root)

    .PARAMETER recordNameWithoutZone
        the record_name to search for already truncated by the zone

    .PARAMETER TxtValue
        the value of the TXT record
    #>

}