We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Length limit check on Route53 TXT records is two characters short
*cfn-lint version: (`cfn-lint --version`)* master

*Description of issue.*

The length limit check on TXT records takes into account the starting and ending double quote characters, but these aren't counted on the API, so cfn-lint is really restricting to 253 characters rather than 255.

```
$ cat test.yml
Resources:
  Example:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: abc123
      Name: example.com.
      Type: TXT
      TTL: '14400'
      ResourceRecords:
      # 255 "a" characters within appropriate quotes
      - '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"'
$ cfn-lint test.yml
E3020 The length of the TXT record (257) exceeds the limit (255)
test.yml:9:7
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/cfnlint/rules/resources/route53/RecordSet.py`
Content:
```
1 """
2   Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 
4   Permission is hereby granted, free of charge, to any person obtaining a copy of this
5   software and associated documentation files (the "Software"), to deal in the Software
6   without restriction, including without limitation the rights to use, copy, modify,
7   merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8   permit persons to whom the Software is furnished to do so.
9 
10   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11   INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12   PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 """
17 import re
18 from cfnlint import CloudFormationLintRule
19 from cfnlint import RuleMatch
20 
21 from cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC
22 
23 class RecordSet(CloudFormationLintRule):
24     """Check Route53 Recordset Configuration"""
25     id = 'E3020'
26     shortdesc = 'Validate Route53 RecordSets'
27     description = 'Check if all RecordSets are correctly configured'
28     source_url = 'https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html'
29     tags = ['resources', 'route53', 'record_set']
30 
31     # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html
32     VALID_RECORD_TYPES = [
33         'A',
34         'AAAA',
35         'CAA',
36         'CNAME',
37         'MX',
38         'NAPTR',
39         'NS',
40         'PTR',
41         'SOA'
42         'SPF',
43         'SRV',
44         'TXT'
45     ]
46 
47     REGEX_CNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(.)$')
48 
49     def check_a_record(self, path, recordset):
50         """Check A record Configuration"""
51         matches = []
52 
53         resource_records = recordset.get('ResourceRecords')
54         for index, record in enumerate(resource_records):
55 
56             if not isinstance(record, dict):
57                 tree = path[:] + ['ResourceRecords', index]
58 
59                 # Check if a valid IPv4 address is specified
60                 if not re.match(REGEX_IPV4, record):
61                     message = 'A record ({}) is not a valid IPv4 address'
62                     matches.append(RuleMatch(tree, message.format(record)))
63 
64         return matches
65 
66     def check_aaaa_record(self, path, recordset):
67         """Check AAAA record Configuration"""
68         matches = []
69 
70         resource_records = recordset.get('ResourceRecords')
71         for index, record in enumerate(resource_records):
72 
73             if not isinstance(record, dict):
74                 tree = path[:] + ['ResourceRecords', index]
75 
76                 # Check if a valid IPv4 address is specified
77                 if not re.match(REGEX_IPV6, record):
78                     message = 'AAAA record ({}) is not a valid IPv6 address'
79                     matches.append(RuleMatch(tree, message.format(record)))
80 
81         return matches
82 
83     def check_caa_record(self, path, recordset):
84         """Check CAA record Configuration"""
85         matches = []
86 
87         resource_records = recordset.get('ResourceRecords')
88 
89         for index, record in enumerate(resource_records):
90             tree = path[:] + ['ResourceRecords', index]
91 
92             if not isinstance(record, dict):
93                 # Split the record up to the mandatory settings (flags tag "value")
94                 items = record.split(' ', 2)
95 
96                 # Check if the 3 settings are given.
97                 if len(items) != 3:
98                     message = 'CAA record must contain 3 settings (flags tag "value"), record contains {} settings.'
99                     matches.append(RuleMatch(tree, message.format(len(items))))
100                 else:
101                     # Check the flag value
102                     if not items[0].isdigit():
103                         message = 'CAA record flag setting ({}) should be of type Integer.'
104                         matches.append(RuleMatch(tree, message.format(items[0])))
105                     else:
106                         if int(items[0]) not in [0, 128]:
107                             message = 'Invalid CAA record flag setting ({}) given, must be 0 or 128.'
108                             matches.append(RuleMatch(tree, message.format(items[0])))
109 
110                     # Check the tag value
111                     if not re.match(REGEX_ALPHANUMERIC, items[1]):
112                         message = 'Invalid CAA record tag setting {}. Value has to be alphanumeric.'
113                         matches.append(RuleMatch(tree, message.format(items[0])))
114 
115                     # Check the value
116                     if not items[2].startswith('"') or not items[2].endswith('"'):
117                         message = 'CAA record value setting has to be enclosed in double quotation marks (").'
118                         matches.append(RuleMatch(tree, message))
119 
120         return matches
121 
122     def check_cname_record(self, path, recordset):
123         """Check CNAME record Configuration"""
124         matches = []
125 
126         resource_records = recordset.get('ResourceRecords')
127         if len(resource_records) > 1:
128             message = 'A CNAME recordset can only contain 1 value'
129             matches.append(RuleMatch(path + ['ResourceRecords'], message))
130         else:
131             for index, record in enumerate(resource_records):
132                 if not isinstance(record, dict):
133                     tree = path[:] + ['ResourceRecords', index]
134                     if (not re.match(self.REGEX_CNAME, record)
135                             # ACM Route 53 validation uses invalid CNAMEs starting with `_`,
136                             # special-case them rather than complicate the regex.
137                             and not record.endswith('.acm-validations.aws.')):
138                         message = 'CNAME record ({}) does not contain a valid domain name'
139                         matches.append(RuleMatch(tree, message.format(record)))
140 
141         return matches
142 
143     def check_txt_record(self, path, recordset):
144         """Check TXT record Configuration"""
145         matches = []
146 
147         # Check quotation of the records
148         resource_records = recordset.get('ResourceRecords')
149 
150         for index, record in enumerate(resource_records):
151             tree = path[:] + ['ResourceRecords', index]
152 
153             if not isinstance(record, dict):
154                 if not record.startswith('"') or not record.endswith('"'):
155                     message = 'TXT record ({}) has to be enclosed in double quotation marks (")'
156                     matches.append(RuleMatch(tree, message.format(record)))
157                 elif len(record) > 255:
158                     message = 'The length of the TXT record ({}) exceeds the limit (255)'
159                     matches.append(RuleMatch(tree, message.format(len(record))))
160 
161         return matches
162 
163     def check_recordset(self, path, recordset):
164         """Check record configuration"""
165 
166         matches = []
167         recordset_type = recordset.get('Type')
168 
169         # Skip Intrinsic functions
170         if not isinstance(recordset_type, dict):
171             if recordset_type not in self.VALID_RECORD_TYPES:
172                 message = 'Invalid record type "{0}" specified'
173                 matches.append(RuleMatch(path + ['Type'], message.format(recordset_type)))
174             elif not recordset.get('AliasTarget'):
175                 # Record type specific checks
176                 if recordset_type == 'A':
177                     matches.extend(self.check_a_record(path, recordset))
178                 elif recordset_type == 'AAAA':
179                     matches.extend(self.check_aaaa_record(path, recordset))
180                 elif recordset_type == 'CAA':
181                     matches.extend(self.check_caa_record(path, recordset))
182                 elif recordset_type == 'CNAME':
183                     matches.extend(self.check_cname_record(path, recordset))
184                 elif recordset_type == 'TXT':
185                     matches.extend(self.check_txt_record(path, recordset))
186 
187         return matches
188 
189     def match(self, cfn):
190         """Check RecordSets and RecordSetGroups Properties"""
191 
192         matches = []
193 
194         recordsets = cfn.get_resources(['AWS::Route53::RecordSet'])
195 
196         for name, recordset in recordsets.items():
197             path = ['Resources', name, 'Properties']
198 
199             if isinstance(recordset, dict):
200                 props = recordset.get('Properties')
201                 if props:
202                     matches.extend(self.check_recordset(path, props))
203 
204         recordsetgroups = cfn.get_resource_properties(['AWS::Route53::RecordSetGroup', 'RecordSets'])
205 
206         for recordsetgroup in recordsetgroups:
207             path = recordsetgroup['Path']
208             value = recordsetgroup['Value']
209             if isinstance(value, list):
210                 for index, recordset in enumerate(value):
211                     tree = path[:] + [index]
212                     matches.extend(self.check_recordset(tree, recordset))
213 
214         return matches
215
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/cfnlint/rules/resources/route53/RecordSet.py b/src/cfnlint/rules/resources/route53/RecordSet.py
--- a/src/cfnlint/rules/resources/route53/RecordSet.py
+++ b/src/cfnlint/rules/resources/route53/RecordSet.py
@@ -154,7 +154,7 @@
                 if not record.startswith('"') or not record.endswith('"'):
                     message = 'TXT record ({}) has to be enclosed in double quotation marks (")'
                     matches.append(RuleMatch(tree, message.format(record)))
-                elif len(record) > 255:
+                elif len(record) > 257:  # 2 extra characters for start and end double quotation marks
                     message = 'The length of the TXT record ({}) exceeds the limit (255)'
                     matches.append(RuleMatch(tree, message.format(len(record))))

{"golden_diff": "diff --git a/src/cfnlint/rules/resources/route53/RecordSet.py b/src/cfnlint/rules/resources/route53/RecordSet.py\n--- a/src/cfnlint/rules/resources/route53/RecordSet.py\n+++ b/src/cfnlint/rules/resources/route53/RecordSet.py\n@@ -154,7 +154,7 @@\n                 if not record.startswith('\"') or not record.endswith('\"'):\n                     message = 'TXT record ({}) has to be enclosed in double quotation marks (\")'\n                     matches.append(RuleMatch(tree, message.format(record)))\n-                elif len(record) > 255:\n+                elif len(record) > 257:  # 2 extra characters for start and end double quotation marks\n                     message = 'The length of the TXT record ({}) exceeds the limit (255)'\n                     matches.append(RuleMatch(tree, message.format(len(record))))\n", "issue": "Length limit check on Route53 TXT records is two characters short\n*cfn-lint version: (`cfn-lint --version`)* master\r\n\r\n*Description of issue.*\r\n\r\nThe length limit check on TXT records takes into account the starting and ending double quote characters, but these aren't counted on the API, so cfn-lint is really restricting to 253 characters rather than 255.\r\n\r\n```\r\n$ cat test.yml\r\nResources:\r\n  Example:\r\n    Type: AWS::Route53::RecordSet\r\n    Properties:\r\n      HostedZoneId: abc123\r\n      Name: example.com.\r\n      Type: TXT\r\n      TTL: '14400'\r\n      ResourceRecords:\r\n      # 255 \"a\" characters within appropriate quotes\r\n      - '\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"'\r\n$ cfn-lint test.yml\r\nE3020 The length of the TXT record (257) exceeds the limit (255)\r\ntest.yml:9:7\r\n```\n", "before_files": [{"content": "\"\"\"\n  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy of this\n  software and associated documentation files (the \"Software\"), to deal in the Software\n  without restriction, including without limitation the rights to use, copy, modify,\n  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n  permit persons to whom the Software is furnished to do so.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\nimport re\nfrom cfnlint import CloudFormationLintRule\nfrom cfnlint import RuleMatch\n\nfrom cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC\n\nclass RecordSet(CloudFormationLintRule):\n    \"\"\"Check Route53 Recordset Configuration\"\"\"\n    id = 'E3020'\n    shortdesc = 'Validate Route53 RecordSets'\n    description = 'Check if all RecordSets are correctly configured'\n    source_url = 'https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html'\n    tags = ['resources', 'route53', 'record_set']\n\n    # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html\n    VALID_RECORD_TYPES = [\n        'A',\n        'AAAA',\n        'CAA',\n        'CNAME',\n        'MX',\n        'NAPTR',\n        'NS',\n        'PTR',\n        'SOA'\n        'SPF',\n        'SRV',\n        'TXT'\n    ]\n\n    REGEX_CNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(.)$')\n\n    def check_a_record(self, path, recordset):\n        \"\"\"Check A record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        for index, record in enumerate(resource_records):\n\n            if not isinstance(record, dict):\n                tree = path[:] + ['ResourceRecords', index]\n\n                # Check if a valid IPv4 address is specified\n                if not re.match(REGEX_IPV4, record):\n                    message = 'A record ({}) is not a valid IPv4 address'\n                    matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_aaaa_record(self, path, recordset):\n        \"\"\"Check AAAA record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        for index, record in enumerate(resource_records):\n\n            if not isinstance(record, dict):\n                tree = path[:] + ['ResourceRecords', index]\n\n                # Check if a valid IPv4 address is specified\n                if not re.match(REGEX_IPV6, record):\n                    message = 'AAAA record ({}) is not a valid IPv6 address'\n                    matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_caa_record(self, path, recordset):\n        \"\"\"Check CAA record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict):\n                # Split the record up to the mandatory settings (flags tag \"value\")\n                items = record.split(' ', 2)\n\n                # Check if the 3 settings are given.\n                if len(items) != 3:\n                    message = 'CAA record must contain 3 settings (flags tag \"value\"), record contains {} settings.'\n                    matches.append(RuleMatch(tree, message.format(len(items))))\n                else:\n                    # Check the flag value\n                    if not items[0].isdigit():\n                        message = 'CAA record flag setting ({}) should be of type Integer.'\n                        matches.append(RuleMatch(tree, message.format(items[0])))\n                    else:\n                        if int(items[0]) not in [0, 128]:\n                            message = 'Invalid CAA record flag setting ({}) given, must be 0 or 128.'\n                            matches.append(RuleMatch(tree, message.format(items[0])))\n\n                    # Check the tag value\n                    if not re.match(REGEX_ALPHANUMERIC, items[1]):\n                        message = 'Invalid CAA record tag setting {}. Value has to be alphanumeric.'\n                        matches.append(RuleMatch(tree, message.format(items[0])))\n\n                    # Check the value\n                    if not items[2].startswith('\"') or not items[2].endswith('\"'):\n                        message = 'CAA record value setting has to be enclosed in double quotation marks (\").'\n                        matches.append(RuleMatch(tree, message))\n\n        return matches\n\n    def check_cname_record(self, path, recordset):\n        \"\"\"Check CNAME record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        if len(resource_records) > 1:\n            message = 'A CNAME recordset can only contain 1 value'\n            matches.append(RuleMatch(path + ['ResourceRecords'], message))\n        else:\n            for index, record in enumerate(resource_records):\n                if not isinstance(record, dict):\n                    tree = path[:] + ['ResourceRecords', index]\n                    if (not re.match(self.REGEX_CNAME, record)\n                            # ACM Route 53 validation uses invalid CNAMEs starting with `_`,\n                            # special-case them rather than complicate the regex.\n                            and not record.endswith('.acm-validations.aws.')):\n                        message = 'CNAME record ({}) does not contain a valid domain name'\n                        matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_txt_record(self, path, recordset):\n        \"\"\"Check TXT record Configuration\"\"\"\n        matches = []\n\n        # Check quotation of the records\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict):\n                if not record.startswith('\"') or not record.endswith('\"'):\n                    message = 'TXT record ({}) has to be enclosed in double quotation marks (\")'\n                    matches.append(RuleMatch(tree, message.format(record)))\n                elif len(record) > 255:\n                    message = 'The length of the TXT record ({}) exceeds the limit (255)'\n                    matches.append(RuleMatch(tree, message.format(len(record))))\n\n        return matches\n\n    def check_recordset(self, path, recordset):\n        \"\"\"Check record configuration\"\"\"\n\n        matches = []\n        recordset_type = recordset.get('Type')\n\n        # Skip Intrinsic functions\n        if not isinstance(recordset_type, dict):\n            if recordset_type not in self.VALID_RECORD_TYPES:\n                message = 'Invalid record type \"{0}\" specified'\n                matches.append(RuleMatch(path + ['Type'], message.format(recordset_type)))\n            elif not recordset.get('AliasTarget'):\n                # Record type specific checks\n                if recordset_type == 'A':\n                    matches.extend(self.check_a_record(path, recordset))\n                elif recordset_type == 'AAAA':\n                    matches.extend(self.check_aaaa_record(path, recordset))\n                elif recordset_type == 'CAA':\n                    matches.extend(self.check_caa_record(path, recordset))\n                elif recordset_type == 'CNAME':\n                    matches.extend(self.check_cname_record(path, recordset))\n                elif recordset_type == 'TXT':\n                    matches.extend(self.check_txt_record(path, recordset))\n\n        return matches\n\n    def match(self, cfn):\n        \"\"\"Check RecordSets and RecordSetGroups Properties\"\"\"\n\n        matches = []\n\n        recordsets = cfn.get_resources(['AWS::Route53::RecordSet'])\n\n        for name, recordset in recordsets.items():\n            path = ['Resources', name, 'Properties']\n\n            if isinstance(recordset, dict):\n                props = recordset.get('Properties')\n                if props:\n                    matches.extend(self.check_recordset(path, props))\n\n        recordsetgroups = cfn.get_resource_properties(['AWS::Route53::RecordSetGroup', 'RecordSets'])\n\n        for recordsetgroup in recordsetgroups:\n            path = recordsetgroup['Path']\n            value = recordsetgroup['Value']\n            if isinstance(value, list):\n                for index, recordset in enumerate(value):\n                    tree = path[:] + [index]\n                    matches.extend(self.check_recordset(tree, recordset))\n\n        return matches\n", "path": "src/cfnlint/rules/resources/route53/RecordSet.py"}], "after_files": [{"content": "\"\"\"\n  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy of this\n  software and associated documentation files (the \"Software\"), to deal in the Software\n  without restriction, including without limitation the rights to use, copy, modify,\n  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n  permit persons to whom the Software is furnished to do so.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\nimport re\nfrom cfnlint import CloudFormationLintRule\nfrom cfnlint import RuleMatch\n\nfrom cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC\n\nclass RecordSet(CloudFormationLintRule):\n    \"\"\"Check Route53 Recordset Configuration\"\"\"\n    id = 'E3020'\n    shortdesc = 'Validate Route53 RecordSets'\n    description = 'Check if all RecordSets are correctly configured'\n    source_url = 'https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html'\n    tags = ['resources', 'route53', 'record_set']\n\n    # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html\n    VALID_RECORD_TYPES = [\n        'A',\n        'AAAA',\n        'CAA',\n        'CNAME',\n        'MX',\n        'NAPTR',\n        'NS',\n        'PTR',\n        'SOA'\n        'SPF',\n        'SRV',\n        'TXT'\n    ]\n\n    REGEX_CNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(.)$')\n\n    def check_a_record(self, path, recordset):\n        \"\"\"Check A record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        for index, record in enumerate(resource_records):\n\n            if not isinstance(record, dict):\n                tree = path[:] + ['ResourceRecords', index]\n\n                # Check if a valid IPv4 address is specified\n                if not re.match(REGEX_IPV4, record):\n                    message = 'A record ({}) is not a valid IPv4 address'\n                    matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_aaaa_record(self, path, recordset):\n        \"\"\"Check AAAA record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        for index, record in enumerate(resource_records):\n\n            if not isinstance(record, dict):\n                tree = path[:] + ['ResourceRecords', index]\n\n                # Check if a valid IPv4 address is specified\n                if not re.match(REGEX_IPV6, record):\n                    message = 'AAAA record ({}) is not a valid IPv6 address'\n                    matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_caa_record(self, path, recordset):\n        \"\"\"Check CAA record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict):\n                # Split the record up to the mandatory settings (flags tag \"value\")\n                items = record.split(' ', 2)\n\n                # Check if the 3 settings are given.\n                if len(items) != 3:\n                    message = 'CAA record must contain 3 settings (flags tag \"value\"), record contains {} settings.'\n                    matches.append(RuleMatch(tree, message.format(len(items))))\n                else:\n                    # Check the flag value\n                    if not items[0].isdigit():\n                        message = 'CAA record flag setting ({}) should be of type Integer.'\n                        matches.append(RuleMatch(tree, message.format(items[0])))\n                    else:\n                        if int(items[0]) not in [0, 128]:\n                            message = 'Invalid CAA record flag setting ({}) given, must be 0 or 128.'\n                            matches.append(RuleMatch(tree, message.format(items[0])))\n\n                    # Check the tag value\n                    if not re.match(REGEX_ALPHANUMERIC, items[1]):\n                        message = 'Invalid CAA record tag setting {}. Value has to be alphanumeric.'\n                        matches.append(RuleMatch(tree, message.format(items[0])))\n\n                    # Check the value\n                    if not items[2].startswith('\"') or not items[2].endswith('\"'):\n                        message = 'CAA record value setting has to be enclosed in double quotation marks (\").'\n                        matches.append(RuleMatch(tree, message))\n\n        return matches\n\n    def check_cname_record(self, path, recordset):\n        \"\"\"Check CNAME record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        if len(resource_records) > 1:\n            message = 'A CNAME recordset can only contain 1 value'\n            matches.append(RuleMatch(path + ['ResourceRecords'], message))\n        else:\n            for index, record in enumerate(resource_records):\n                if not isinstance(record, dict):\n                    tree = path[:] + ['ResourceRecords', index]\n                    if (not re.match(self.REGEX_CNAME, record)\n                            # ACM Route 53 validation uses invalid CNAMEs starting with `_`,\n                            # special-case them rather than complicate the regex.\n                            and not record.endswith('.acm-validations.aws.')):\n                        message = 'CNAME record ({}) does not contain a valid domain name'\n                        matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_txt_record(self, path, recordset):\n        \"\"\"Check TXT record Configuration\"\"\"\n        matches = []\n\n        # Check quotation of the records\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict):\n                if not record.startswith('\"') or not record.endswith('\"'):\n                    message = 'TXT record ({}) has to be enclosed in double quotation marks (\")'\n                    matches.append(RuleMatch(tree, message.format(record)))\n                elif len(record) > 257:  # 2 extra characters for start and end double quotation marks\n                    message = 'The length of the TXT record ({}) exceeds the limit (255)'\n                    matches.append(RuleMatch(tree, message.format(len(record))))\n\n        return matches\n\n    def check_recordset(self, path, recordset):\n        \"\"\"Check record configuration\"\"\"\n\n        matches = []\n        recordset_type = recordset.get('Type')\n\n        # Skip Intrinsic functions\n        if not isinstance(recordset_type, dict):\n            if recordset_type not in self.VALID_RECORD_TYPES:\n                message = 'Invalid record type \"{0}\" specified'\n                matches.append(RuleMatch(path + ['Type'], message.format(recordset_type)))\n            elif not recordset.get('AliasTarget'):\n                # Record type specific checks\n                if recordset_type == 'A':\n                    matches.extend(self.check_a_record(path, recordset))\n                elif recordset_type == 'AAAA':\n                    matches.extend(self.check_aaaa_record(path, recordset))\n                elif recordset_type == 'CAA':\n                    matches.extend(self.check_caa_record(path, recordset))\n                elif recordset_type == 'CNAME':\n                    matches.extend(self.check_cname_record(path, recordset))\n                elif recordset_type == 'TXT':\n                    matches.extend(self.check_txt_record(path, recordset))\n\n        return matches\n\n    def match(self, cfn):\n        \"\"\"Check RecordSets and RecordSetGroups Properties\"\"\"\n\n        matches = []\n\n        recordsets = cfn.get_resources(['AWS::Route53::RecordSet'])\n\n        for name, recordset in recordsets.items():\n            path = ['Resources', name, 'Properties']\n\n            if isinstance(recordset, dict):\n                props = recordset.get('Properties')\n                if props:\n                    matches.extend(self.check_recordset(path, props))\n\n        recordsetgroups = cfn.get_resource_properties(['AWS::Route53::RecordSetGroup', 'RecordSets'])\n\n        for recordsetgroup in recordsetgroups:\n            path = recordsetgroup['Path']\n            value = recordsetgroup['Value']\n            if isinstance(value, list):\n                for index, recordset in enumerate(value):\n                    tree = path[:] + [index]\n                    matches.extend(self.check_recordset(tree, recordset))\n\n        return matches\n", "path": "src/cfnlint/rules/resources/route53/RecordSet.py"}]}