1use crate::{
19 DnsError,
20 parse::{TagParser, TxtRecordParser, V},
21};
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, PartialEq, Eq)]
29pub struct MtaStsRecord {
30 pub id: String,
33}
34
35#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct TlsRptRecord {
42 pub rua: Vec<ReportUri>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub enum ReportUri {
50 Mail(String),
52 Http(String),
54}
55
56const ID: u64 = (b'i' as u64) | ((b'd' as u64) << 8);
57const RUA: u64 = (b'r' as u64) | ((b'u' as u64) << 8) | ((b'a' as u64) << 16);
58
59const MAILTO: u64 = (b'm' as u64)
60 | ((b'a' as u64) << 8)
61 | ((b'i' as u64) << 16)
62 | ((b'l' as u64) << 24)
63 | ((b't' as u64) << 32)
64 | ((b'o' as u64) << 40);
65const HTTPS: u64 = (b'h' as u64)
66 | ((b't' as u64) << 8)
67 | ((b't' as u64) << 16)
68 | ((b'p' as u64) << 24)
69 | ((b's' as u64) << 32);
70
71impl TxtRecordParser for MtaStsRecord {
72 #[allow(clippy::while_let_on_iterator)]
73 fn parse(record: &[u8]) -> crate::Result<Self> {
74 let mut record = record.iter();
75 let mut id = None;
76 let mut has_version = false;
77
78 while let Some(key) = record.key() {
79 match key {
80 V => {
81 if !record.match_bytes(b"STSv1") || !record.seek_tag_end() {
82 return Err(crate::Error::Dns(DnsError::InvalidRecordType));
83 }
84 has_version = true;
85 }
86 ID => {
87 id = record.text(false).into();
88 }
89 _ => {
90 record.ignore();
91 }
92 }
93 }
94
95 if let Some(id) = id
96 && has_version
97 {
98 return Ok(MtaStsRecord { id });
99 }
100 Err(crate::Error::Dns(DnsError::InvalidRecordType))
101 }
102}
103
104impl TxtRecordParser for TlsRptRecord {
105 #[allow(clippy::while_let_on_iterator)]
106 fn parse(record: &[u8]) -> crate::Result<Self> {
107 let mut record = record.iter();
108
109 if record.key().unwrap_or(0) != V
110 || !record.match_bytes(b"TLSRPTv1")
111 || !record.seek_tag_end()
112 {
113 return Err(crate::Error::Dns(DnsError::InvalidRecordType));
114 }
115
116 let mut rua = Vec::new();
117
118 while let Some(key) = record.key() {
119 match key {
120 RUA => loop {
121 match record.flag_value() {
122 (MAILTO, b':') => {
123 let mail_to = record.uri(Vec::new());
124 if !mail_to.is_empty() {
125 rua.push(ReportUri::Mail(mail_to));
126 }
127 }
128 (HTTPS, b':') => {
129 let mut url = Vec::with_capacity(20);
130 url.extend_from_slice(b"https:");
131 let url = record.uri(url);
132 if !url.is_empty() {
133 rua.push(ReportUri::Http(url));
134 }
135 }
136 _ => {
137 record.ignore();
138 break;
139 }
140 }
141 },
142 _ => {
143 record.ignore();
144 }
145 }
146 }
147
148 if !rua.is_empty() {
149 Ok(TlsRptRecord { rua })
150 } else {
151 Err(crate::Error::Dns(DnsError::InvalidRecordType))
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use crate::{
159 mta_sts::{MtaStsRecord, ReportUri, TlsRptRecord},
160 parse::TxtRecordParser,
161 };
162
163 #[test]
164 fn mta_sts_record_parse() {
165 for (mta_sts, expected_mta_sts) in [
166 (
167 "v=STSv1; id=20160831085700Z;",
168 MtaStsRecord {
169 id: "20160831085700Z".to_string(),
170 },
171 ),
172 (
173 "v=STSv1; id=20190429T010101",
174 MtaStsRecord {
175 id: "20190429T010101".to_string(),
176 },
177 ),
178 ] {
179 assert_eq!(
180 MtaStsRecord::parse(mta_sts.as_bytes()).unwrap(),
181 expected_mta_sts
182 );
183 }
184 }
185
186 #[test]
187 fn tlsrpt_parse() {
188 for (tls_rpt, expected_tls_rpt) in [
189 (
190 "v=TLSRPTv1;rua=mailto:reports@example.com",
191 TlsRptRecord {
192 rua: vec![ReportUri::Mail("reports@example.com".to_string())],
193 },
194 ),
195 (
196 "v=TLSRPTv1; rua=https://reporting.example.com/v1/tlsrpt",
197 TlsRptRecord {
198 rua: vec![ReportUri::Http(
199 "https://reporting.example.com/v1/tlsrpt".to_string(),
200 )],
201 },
202 ),
203 (
204 "v=TLSRPTv1; rua=mailto:tlsrpt@mydomain.com,https://tlsrpt.mydomain.com/v1",
205 TlsRptRecord {
206 rua: vec![
207 ReportUri::Mail("tlsrpt@mydomain.com".to_string()),
208 ReportUri::Http("https://tlsrpt.mydomain.com/v1".to_string()),
209 ],
210 },
211 ),
212 (
213 "v=TLSRPTv1; rua=https://reporting.example.com/v1/tlsrpt?id=1",
214 TlsRptRecord {
215 rua: vec![ReportUri::Http(
216 "https://reporting.example.com/v1/tlsrpt?id=1".to_string(),
217 )],
218 },
219 ),
220 (
221 "v=TLSRPTv1; rua=https://r.example.com/tlsrpt?id=abc&token=xyz",
222 TlsRptRecord {
223 rua: vec![ReportUri::Http(
224 "https://r.example.com/tlsrpt?id=abc&token=xyz".to_string(),
225 )],
226 },
227 ),
228 (
229 "v=TLSRPTv1; rua=mailto:a@example.com , mailto:b=c@example.com ;",
230 TlsRptRecord {
231 rua: vec![
232 ReportUri::Mail("a@example.com".to_string()),
233 ReportUri::Mail("b=c@example.com".to_string()),
234 ],
235 },
236 ),
237 ] {
238 assert_eq!(
239 TlsRptRecord::parse(tls_rpt.as_bytes()).unwrap(),
240 expected_tls_rpt
241 );
242 }
243 }
244}