Skip to content

Commit eec3aff

Browse files
authored
Support APEXCNAME custom RR type (#38)
Fixes #30
2 parents cd5b1be + bf0e204 commit eec3aff

File tree

4 files changed

+314
-148
lines changed

4 files changed

+314
-148
lines changed

domainconnectzone/DomainConnectImpl.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ def process_custom_record(template_record, zone_records):
554554
'AAAA': ['A', 'AAAA', 'CNAME', 'REDIR301', 'REDIR302'],
555555
'MX': ['MX', 'CNAME'],
556556
'CNAME': ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'REDIR301', 'REDIR302'],
557+
'APEXCNAME': ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'REDIR301', 'REDIR302'],
557558
'REDIR301': ['A', 'AAAA', 'CNAME'],
558559
'REDIR302': ['A', 'AAAA', 'CNAME'],
559560
}
@@ -604,13 +605,43 @@ def process_other_record(template_record, zone_records):
604605
return new_record
605606

606607

608+
def process_apexcname_record(template_record, zone_records):
609+
"""
610+
Process an APEXCNAME record from a template. APEXCNAME always targets the
611+
zone apex (@) and behaves like a CNAME for conflict resolution purposes.
612+
613+
:param template_record: The record from the template to process.
614+
:type template_record: dict
615+
- keys: 'type', 'pointsTo', 'ttl'; optional 'host' (must be '@' if present)
616+
617+
:param zone_records: A list of all records in the current zone.
618+
:type zone_records: list
619+
620+
:return: The new APEXCNAME record.
621+
:rtype: dict
622+
"""
623+
new_record = {'type': 'APEXCNAME',
624+
'name': '@',
625+
'data': template_record['pointsTo'],
626+
'ttl': int(template_record['ttl'])}
627+
628+
for zone_record in zone_records:
629+
zone_record_type = zone_record['type'].upper()
630+
if zone_record_type in _delete_map['APEXCNAME'] and \
631+
zone_record['name'] == '@' and \
632+
'_replace' not in zone_record:
633+
zone_record['_delete'] = 1
634+
635+
return new_record
636+
637+
607638
def check_conflict_with_self(new_record, new_records):
608-
# Mark records that conflict with self (affects only CNAME and NS)
639+
# Mark records that conflict with self (affects only CNAME, APEXCNAME and NS)
609640
for zone_record in new_records:
610641
zone_record_type = zone_record['type'].upper()
611642

612643
error = False
613-
if (new_record['type'] == 'CNAME' or zone_record['type'] == 'CNAME') \
644+
if (new_record['type'] in ('CNAME', 'APEXCNAME') or zone_record['type'] in ('CNAME', 'APEXCNAME')) \
614645
and zone_record['name'] == new_record['name']:
615646
error = True
616647

@@ -629,7 +660,7 @@ def check_conflict_with_self(new_record, new_records):
629660
if error:
630661
raise InvalidData(f"Template record {new_record['type']} {new_record['name']} conflicts with other tempate record {zone_record['type']} {zone_record['name']}")
631662

632-
_CORE_TYPES = {'A', 'AAAA', 'CNAME', 'MX', 'NS', 'SRV', 'TXT', 'SPFM',
663+
_CORE_TYPES = {'A', 'AAAA', 'CNAME', 'APEXCNAME', 'MX', 'NS', 'SRV', 'TXT', 'SPFM',
633664
'REDIR301', 'REDIR302'}
634665

635666
# Maps RR type → list of fields whose string values must NOT be lowercased
@@ -838,7 +869,7 @@ def process_records(template_records, zone_records, domain, host, params,
838869
template_record_type = template_record['type'].upper()
839870

840871
# We can only handle certain record types
841-
supported = ['A', 'AAAA', 'MX', 'CNAME', 'TXT', 'SRV', 'SPFM', 'NS']
872+
supported = ['A', 'AAAA', 'MX', 'CNAME', 'APEXCNAME', 'TXT', 'SRV', 'SPFM', 'NS']
842873
if redirect_records is not None:
843874
supported += ['REDIR301', 'REDIR302']
844875
is_custom = not template_record_type in supported and is_custom_record_type(template_record_type)
@@ -848,7 +879,7 @@ def process_records(template_records, zone_records, domain, host, params,
848879

849880
# Deal with the variables and validation
850881

851-
# Deal with the host/name
882+
# Deal with the host/name
852883
if template_record_type == 'SRV':
853884
template_record['name'] = resolve_variables(
854885
template_record['name'], domain, host, params, 'name')
@@ -861,6 +892,14 @@ def process_records(template_records, zone_records, domain, host, params,
861892
raise InvalidData('Invalid data for SRV host: ' +
862893
srvhost)
863894

895+
elif template_record_type == 'APEXCNAME':
896+
# host is optional for APEXCNAME; if present it must be '@'
897+
apex_host = template_record.get('host', '@')
898+
if apex_host != '@':
899+
raise InvalidData('Invalid data for APEXCNAME host: ' +
900+
apex_host + ' (must be @ or omitted)')
901+
template_record['host'] = '@'
902+
864903
else:
865904
orig_host = template_record['host']
866905
template_record['host'] = resolve_variables(
@@ -889,14 +928,14 @@ def process_records(template_records, zone_records, domain, host, params,
889928
raise InvalidData(err_msg)
890929

891930
# Points To / Target
892-
if template_record_type in ['A', 'AAAA', 'MX', 'CNAME', 'NS']:
931+
if template_record_type in ['A', 'AAAA', 'MX', 'CNAME', 'APEXCNAME', 'NS']:
893932
orig_pointsto = template_record['pointsTo']
894933
if template_record_type == 'NS' and orig_pointsto == '@':
895934
raise InvalidData('Invalid data for NS pointsTo: @ would create a circular delegation')
896935
template_record['pointsTo'] = resolve_variables(
897936
template_record['pointsTo'], domain, host, params, 'pointsTo')
898937

899-
if template_record_type in ['MX', 'CNAME', 'NS']:
938+
if template_record_type in ['MX', 'CNAME', 'APEXCNAME', 'NS']:
900939
if not is_valid_pointsTo_host(
901940
template_record['pointsTo']):
902941
raise InvalidData('Invalid data for ' +
@@ -1010,6 +1049,8 @@ def process_records(template_records, zone_records, domain, host, params,
10101049
new_record = process_srv_record(template_record, zone_records)
10111050
elif template_record_type in ['REDIR301', 'REDIR302']:
10121051
new_record = process_redir_record(template_record, zone_records)
1052+
elif template_record_type == 'APEXCNAME':
1053+
new_record = process_apexcname_record(template_record, zone_records)
10131054
elif is_custom:
10141055
new_record = process_custom_record(template_record, zone_records)
10151056
else:
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
version: "1.0"
2+
suite_type: process_records
3+
description: "Domain Connect process_records compliance tests — APEXCNAME"
4+
5+
tests:
6+
7+
# ---------------------------------------------------------------------------
8+
# APEXCNAME
9+
# ---------------------------------------------------------------------------
10+
11+
- id: apexcname_basic
12+
description: "APEXCNAME creates a record at the apex (@) with no host field"
13+
input:
14+
zone_records: []
15+
template_records:
16+
- {type: APEXCNAME, pointsTo: target.example.com, ttl: 600}
17+
domain: foo.com
18+
host: ""
19+
params: {}
20+
expect:
21+
new_count: 1
22+
delete_count: 0
23+
records:
24+
- {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600}
25+
26+
- id: apexcname_host_at
27+
description: "APEXCNAME with explicit host '@' is accepted"
28+
input:
29+
zone_records: []
30+
template_records:
31+
- {type: APEXCNAME, host: "@", pointsTo: target.example.com, ttl: 600}
32+
domain: foo.com
33+
host: ""
34+
params: {}
35+
expect:
36+
new_count: 1
37+
delete_count: 0
38+
records:
39+
- {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600}
40+
41+
- id: apexcname_host_non_apex_rejected
42+
description: "APEXCNAME with host other than '@' raises InvalidData"
43+
input:
44+
zone_records: []
45+
template_records:
46+
- {type: APEXCNAME, host: "sub", pointsTo: target.example.com, ttl: 600}
47+
domain: foo.com
48+
host: ""
49+
params: {}
50+
expect:
51+
exception: InvalidData
52+
53+
- id: apexcname_delete_conflicts
54+
description: "APEXCNAME deletes conflicting records at @ (A, AAAA, CNAME, MX, TXT)"
55+
input:
56+
zone_records:
57+
- {type: A, name: "@", data: 1.2.3.4, ttl: 300}
58+
- {type: AAAA, name: "@", data: "::1", ttl: 300}
59+
- {type: CNAME, name: "@", data: old.example.com, ttl: 300}
60+
- {type: MX, name: "@", data: mail.example.com, ttl: 300, priority: 10}
61+
- {type: TXT, name: "@", data: "v=spf1 ~all", ttl: 300}
62+
- {type: NS, name: "@", data: ns1.example.com, ttl: 300}
63+
template_records:
64+
- {type: APEXCNAME, pointsTo: target.example.com, ttl: 600}
65+
domain: foo.com
66+
host: ""
67+
params: {}
68+
expect:
69+
new_count: 1
70+
delete_count: 5
71+
records:
72+
- {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600}
73+
- {type: NS, name: "@", data: ns1.example.com, ttl: 300}
74+
75+
- id: apexcname_no_delete_other_hosts
76+
description: "APEXCNAME only deletes records at @, not at other hosts"
77+
input:
78+
zone_records:
79+
- {type: A, name: "@", data: 1.2.3.4, ttl: 300}
80+
- {type: A, name: "sub", data: 5.6.7.8, ttl: 300}
81+
template_records:
82+
- {type: APEXCNAME, pointsTo: target.example.com, ttl: 600}
83+
domain: foo.com
84+
host: ""
85+
params: {}
86+
expect:
87+
new_count: 1
88+
delete_count: 1
89+
records:
90+
- {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600}
91+
- {type: A, name: "sub", data: 5.6.7.8, ttl: 300}
92+
93+
- id: apexcname_conflict_itself
94+
description: "Two APEXCNAME records in same template raise InvalidData"
95+
input:
96+
zone_records: []
97+
template_records:
98+
- {type: APEXCNAME, pointsTo: target1.example.com, ttl: 600}
99+
- {type: APEXCNAME, pointsTo: target2.example.com, ttl: 600}
100+
domain: foo.com
101+
host: ""
102+
params: {}
103+
expect:
104+
exception: InvalidData
105+
106+
- id: apexcname_variable_substitution
107+
description: "APEXCNAME supports %variable% substitution in pointsTo"
108+
input:
109+
zone_records: []
110+
template_records:
111+
- {type: APEXCNAME, pointsTo: "%target%", ttl: 600}
112+
domain: foo.com
113+
host: ""
114+
params: {target: target.example.com}
115+
expect:
116+
new_count: 1
117+
delete_count: 0
118+
records:
119+
- {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
version: "1.0"
2+
suite_type: process_records
3+
description: "Domain Connect process_records compliance tests — REDIR301/REDIR302"
4+
5+
tests:
6+
7+
# ---------------------------------------------------------------------------
8+
# REDIR
9+
# ---------------------------------------------------------------------------
10+
- id: redir301_basic
11+
description: "REDIR301 replaces A/AAAA/CNAME at host and adds redirect record"
12+
input:
13+
zone_records:
14+
- {type: A, name: bar, data: abc, ttl: 400}
15+
- {type: AAAA, name: bar, data: abc, ttl: 400}
16+
- {type: CNAME, name: bar, data: abc, ttl: 400}
17+
- {type: A, name: random.value, data: abc, ttl: 400}
18+
template_records:
19+
- {type: REDIR301, host: "@", target: "http://%target%"}
20+
domain: foo.com
21+
host: bar
22+
params: {target: example.com}
23+
redirect_records:
24+
- {type: A, pointsTo: "127.0.0.1", ttl: 600}
25+
- {type: AAAA, pointsTo: "::1", ttl: 600}
26+
expect:
27+
new_count: 3
28+
delete_count: 3
29+
records:
30+
- {type: A, name: bar, data: "127.0.0.1", ttl: 600}
31+
- {type: AAAA, name: bar, data: "::1", ttl: 600}
32+
- {type: A, name: random.value, data: abc, ttl: 400}
33+
- {type: REDIR301, name: bar, data: "http://example.com"}
34+
35+
- id: redir301_double
36+
description: "Two REDIR301 records at different hosts both get redirect records"
37+
input:
38+
zone_records: []
39+
template_records:
40+
- {type: REDIR301, host: www, target: "http://%target%"}
41+
- {type: REDIR301, host: "@", target: "http://www.%fqdn%"}
42+
domain: foo.com
43+
host: ""
44+
params: {target: example.com}
45+
redirect_records:
46+
- {type: A, pointsTo: "127.0.0.1", ttl: 600}
47+
- {type: AAAA, pointsTo: "::1", ttl: 600}
48+
expect:
49+
new_count: 6
50+
delete_count: 0
51+
records:
52+
- {type: A, name: "@", data: "127.0.0.1", ttl: 600}
53+
- {type: AAAA, name: "@", data: "::1", ttl: 600}
54+
- {type: REDIR301, name: "@", data: "http://www.foo.com"}
55+
- {type: A, name: www, data: "127.0.0.1", ttl: 600}
56+
- {type: AAAA, name: www, data: "::1", ttl: 600}
57+
- {type: REDIR301, name: www, data: "http://example.com"}
58+
59+
- id: redir301_with_groupid_filtered_out
60+
description: "REDIR301 filtered out by group_ids does nothing"
61+
input:
62+
zone_records:
63+
- {type: A, name: bar, data: abc, ttl: 400}
64+
- {type: A, name: random.value, data: abc, ttl: 400}
65+
template_records:
66+
- {type: REDIR301, host: "@", target: "http://example.com", groupId: b}
67+
domain: foo.com
68+
host: bar
69+
params: {}
70+
group_ids: [a]
71+
redirect_records:
72+
- {type: A, pointsTo: "127.0.0.1", ttl: 600}
73+
- {type: AAAA, pointsTo: "::1", ttl: 600}
74+
expect:
75+
new_count: 0
76+
delete_count: 0
77+
records:
78+
- {type: A, name: bar, data: abc, ttl: 400}
79+
- {type: A, name: random.value, data: abc, ttl: 400}
80+
81+
- id: redir302_basic
82+
description: "REDIR302 replaces A/AAAA/CNAME at host and adds redirect record"
83+
input:
84+
zone_records:
85+
- {type: A, name: bar, data: abc, ttl: 400}
86+
- {type: AAAA, name: bar, data: abc, ttl: 400}
87+
- {type: CNAME, name: bar, data: abc, ttl: 400}
88+
- {type: A, name: random.value, data: abc, ttl: 400}
89+
template_records:
90+
- {type: REDIR302, host: "@", target: "http://example.com"}
91+
domain: foo.com
92+
host: bar
93+
params: {}
94+
redirect_records:
95+
- {type: A, pointsTo: "127.0.0.1", ttl: 600}
96+
- {type: AAAA, pointsTo: "::1", ttl: 600}
97+
expect:
98+
new_count: 3
99+
delete_count: 3
100+
records:
101+
- {type: A, name: bar, data: "127.0.0.1", ttl: 600}
102+
- {type: AAAA, name: bar, data: "::1", ttl: 600}
103+
- {type: A, name: random.value, data: abc, ttl: 400}
104+
- {type: REDIR302, name: bar, data: "http://example.com"}
105+
106+
- id: exception_redir301_empty_target
107+
description: "REDIR301 with empty target raises InvalidData"
108+
input:
109+
zone_records: []
110+
template_records:
111+
- {type: REDIR301, host: "@", target: "", ttl: 600}
112+
domain: foo.com
113+
host: ""
114+
params: {}
115+
redirect_records:
116+
- {type: A, pointsTo: "127.0.0.1", ttl: 600}
117+
- {type: AAAA, pointsTo: "::1", ttl: 600}
118+
expect:
119+
exception: InvalidData
120+
121+
- id: exception_redir302_invalid_target
122+
description: "REDIR302 with an invalid URL target raises InvalidData"
123+
input:
124+
zone_records: []
125+
template_records:
126+
- {type: REDIR302, host: "@", target: "http://ijfjiör@@@a:43244434::", ttl: 600}
127+
domain: foo.com
128+
host: ""
129+
params: {}
130+
redirect_records:
131+
- {type: A, pointsTo: "127.0.0.1", ttl: 600}
132+
- {type: AAAA, pointsTo: "::1", ttl: 600}
133+
expect:
134+
exception: InvalidData
135+
136+
- id: exception_redir301_missing_redirect_records
137+
description: "REDIR301 without redirect_records raises InvalidTemplate"
138+
input:
139+
zone_records: []
140+
template_records:
141+
- {type: REDIR301, host: "@", target: "", ttl: 600}
142+
domain: foo.com
143+
host: ""
144+
params: {}
145+
expect:
146+
exception: InvalidTemplate

0 commit comments

Comments
 (0)