Skip to main content

u_sdk/esa/
dns_record.rs

1use super::Client;
2use super::utils::{
3    de_non_empty_object, de_option_empty_string_as_none, parse_json_response, se_as_json_string,
4};
5use super::{Error, OPENAPI_STYLE, OPENAPI_VERSION};
6use bon::Builder;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use time::OffsetDateTime;
10use u_sdk_common::helper::into_header_map;
11use u_sdk_common::open_api_sign::{SignParams, get_openapi_request_header};
12
13impl Client {
14    pub fn list_records(&self) -> ListRecordsBuilder<'_> {
15        ListRecords::builder(self)
16    }
17
18    pub fn get_record(&self) -> GetRecordBuilder<'_> {
19        GetRecord::builder(self)
20    }
21
22    pub fn create_record(&self) -> CreateRecordBuilder<'_> {
23        CreateRecord::builder(self)
24    }
25
26    pub fn delete_record(&self) -> DeleteRecordBuilder<'_> {
27        DeleteRecord::builder(self)
28    }
29
30    pub fn update_record(&self) -> UpdateRecordBuilder<'_> {
31        UpdateRecord::builder(self)
32    }
33}
34
35//region ListRecords request
36/// [ListRecords](https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listrecords)
37#[serde_with::skip_serializing_none]
38#[derive(Debug, Serialize, Builder)]
39#[serde(rename_all = "PascalCase")]
40pub struct ListRecords<'a> {
41    #[builder(start_fn)]
42    #[serde(skip_serializing)]
43    pub(crate) client: &'a Client,
44
45    /// 站点 ID,可通过调用 ListSites 接口获取
46    site_id: i64,
47
48    /// 记录名称,用于查询的过滤条件
49    record_name: Option<String>,
50
51    /// 记录名称的搜索匹配模式
52    /// 默认为精确匹配,取值:
53    /// prefix:前缀匹配
54    /// suffix:后缀匹配
55    /// exact:精确匹配
56    /// fuzzy:模糊匹配
57    record_match_type: Option<RecordMatchType>,
58
59    /// 页码,默认值为 1
60    page_number: Option<i32>,
61
62    /// 分页大小,默认值为 500
63    page_size: Option<i32>,
64
65    /// 记录的源站类型,用于查询的过滤条件
66    /// 仅 CNAME 记录可以通过该字段筛选
67    source_type: Option<SourceType>,
68
69    /// 记录加速时的业务场景,用于查询的过滤条件
70    /// 取值:image_video、api、web
71    biz_name: Option<BizName>,
72
73    /// 记录是否开启代理加速,用于查询的过滤条件
74    proxied: Option<bool>,
75
76    /// 记录的 DNS 记录类型,用于查询的过滤条件
77    record_type: Option<DnsRecordType>,
78}
79
80/// 记录名称的搜索匹配模式
81#[derive(Debug, Serialize)]
82#[serde(rename_all = "lowercase")]
83pub enum RecordMatchType {
84    Prefix, // 前缀匹配
85    Suffix, // 后缀匹配
86    Exact,  // 精确匹配
87    Fuzzy,  // 模糊匹配
88}
89
90/// 记录的源站类型
91#[derive(Debug, Serialize, Deserialize)]
92pub enum SourceType {
93    OSS,    // OSS 源站
94    S3,     // S3 源站
95    LB,     // 负载均衡器源站
96    OP,     // 源地址池源站
97    Domain, // 普通域名源站
98}
99
100/// 记录加速时的业务场景
101#[derive(Debug, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum BizName {
104    ImageVideo, // 视频图片
105    Api,        // API 接口
106    Web,        // Web 网页
107}
108
109/// DNS 记录类型
110#[derive(Debug, Serialize, Deserialize)]
111#[serde(rename_all = "UPPERCASE")]
112pub enum DnsRecordType {
113    #[serde(rename = "A/AAAA")]
114    AOrAAAA, // A 或 AAAA 记录,自动选择 IPv4 或 IPv6 地址
115    CNAME,  // CNAME 记录,域名的别名
116    MX,     // MX 记录,邮件交换记录
117    TXT,    // TXT 记录,文本记录
118    NS,     // NS 记录,域名服务器记录
119    SRV,    // SRV 记录,服务定位记录
120    CAA,    // CAA 记录,证书颁发机构授权记录
121    CERT,   // CERT 记录,证书记录
122    SMIMEA, // SMIMEA 记录,S/MIME 证书记录
123    SSHFP,  // SSHFP 记录,SSH 公钥指纹记录
124    TLSA,   // TLSA 记录,TLS 认证记录
125    URI,    // URI 记录,统一资源标识记录
126}
127
128/// 请求返回参数
129#[derive(Debug, Deserialize)]
130#[serde(rename_all = "PascalCase")]
131pub struct ListRecordsResponse {
132    /// 请求 ID
133    pub request_id: String,
134
135    /// 当前页码
136    pub page_number: i32,
137
138    /// 每页显示的记录条数
139    pub page_size: i32,
140
141    /// 记录总条数
142    pub total_count: i32,
143
144    /// DNS 记录信息
145    pub records: Vec<DnsRecord>,
146}
147
148/// 单个 DNS 记录信息
149#[derive(Debug, Deserialize)]
150#[serde(rename_all = "PascalCase")]
151pub struct DnsRecord {
152    /// 记录加速时的业务场景
153    #[serde(deserialize_with = "de_option_empty_string_as_none")]
154    pub biz_name: Option<BizName>,
155
156    /// 记录的具体 DNS 信息
157    pub data: DnsData,
158
159    /// 记录的创建时间
160    #[serde(with = "time::serde::rfc3339")]
161    pub create_time: OffsetDateTime,
162
163    /// 记录的更新时间
164    #[serde(with = "time::serde::rfc3339")]
165    pub update_time: OffsetDateTime,
166
167    /// 记录是否开启代理加速
168    pub proxied: bool,
169
170    /// 记录 ID
171    pub record_id: i64,
172
173    /// 记录的源站类型
174    #[serde(deserialize_with = "de_option_empty_string_as_none")]
175    pub record_source_type: Option<SourceType>,
176
177    /// 记录名称
178    pub record_name: String,
179
180    /// 记录的DNS类型
181    pub record_type: DnsRecordType,
182
183    /// 记录所属站点的 ID
184    pub site_id: i64,
185
186    /// 记录所属站点的名称
187    pub site_name: String,
188
189    /// 记录的过期时间
190    pub ttl: i32,
191
192    /// 记录的 CNAME
193    pub record_cname: String,
194
195    /// 记录的备注
196    pub comment: String,
197
198    /// CNAME 记录的源站鉴权信息
199    // 在 ListRecords 接口中,鉴权信息在没有时会返回空对象 `{}`;
200    // 在 GetRecord 接口中,鉴权信息在没有时不存在该字段。使用 `de_non_empty_object` + default 处理这两种情况
201    #[serde(default, deserialize_with = "de_non_empty_object")]
202    pub auth_conf: Option<AuthConf>,
203
204    /// 回源 HOST 策略
205    #[serde(deserialize_with = "de_option_empty_string_as_none")]
206    pub host_policy: Option<HostPolicy>,
207}
208
209/// 记录的具体 DNS 信息
210#[derive(Debug, Serialize, Deserialize)]
211#[serde(rename_all = "PascalCase")]
212pub struct DnsData {
213    /// 记录值或部分内容
214    pub value: String,
215
216    /// 记录的优先级
217    pub priority: Option<i32>,
218
219    /// 记录的标志位
220    pub flag: Option<i32>,
221
222    /// 标签
223    pub tags: Option<HashMap<String, String>>,
224
225    /// 权重
226    pub weight: Option<i32>,
227
228    /// 端口
229    pub port: Option<i32>,
230
231    /// 证书类型
232    pub r#type: Option<i32>,
233
234    /// 公钥标识
235    pub key_tag: Option<i32>,
236
237    /// 加密算法
238    pub algorithm: Option<i32>,
239
240    /// 证书信息
241    pub certificate: Option<String>,
242
243    /// 用途标识
244    pub usage: Option<i32>,
245
246    /// 证书选择器
247    pub selector: Option<i32>,
248
249    /// 证书匹配类型
250    pub matching_type: Option<i32>,
251
252    /// 公钥指纹值
253    pub fingerprint: Option<String>,
254
255    /// 标签
256    pub tag: Option<String>,
257}
258
259/// CNAME 记录的源站鉴权信息
260#[derive(Debug, Deserialize, Serialize)]
261pub struct AuthConf {
262    /// 鉴权类型
263    pub auth_type: Option<AuthType>,
264
265    /// 访问密钥
266    pub access_key: Option<String>,
267
268    /// 秘密访问密钥
269    pub secret_key: Option<String>,
270
271    /// 签名版本
272    pub version: Option<String>,
273
274    /// 区域
275    pub region: Option<String>,
276}
277
278/// 鉴权类型
279#[derive(Debug, Deserialize, Serialize)]
280#[serde(rename_all = "snake_case")]
281pub enum AuthType {
282    Public,              // 公共读
283    Private,             // 私有读
284    PrivateSameAccount,  // 私有同账号读
285    PrivateCrossAccount, // 私有跨账号读
286}
287
288/// 回源 HOST 策略
289#[derive(Debug, Deserialize, Serialize)]
290#[serde(rename_all = "snake_case")]
291pub enum HostPolicy {
292    FollowHostname,     // 跟随请求 HOST
293    FollowOriginDomain, // 跟随源站域名
294}
295
296impl ListRecords<'_> {
297    pub async fn send(&self) -> Result<ListRecordsResponse, Error> {
298        let client = self.client;
299        let creds = client.credentials_provider.load().await?;
300
301        let sign_params = SignParams {
302            req_method: "GET",
303            host: &client.host,
304            query_map: self,
305            x_acs_action: "ListRecords",
306            x_acs_version: OPENAPI_VERSION,
307            x_acs_security_token: creds.sts_security_token.as_deref(),
308            request_body: None,
309            style: &OPENAPI_STYLE,
310        };
311
312        let (common_headers, url_) =
313            get_openapi_request_header(&creds.access_key_secret, &creds.access_key_id, sign_params)
314                .map_err(|e| {
315                    Error::Common(format!("failed to get openapi request header: {}", e))
316                })?;
317        let header_map = into_header_map(common_headers);
318
319        let resp = client
320            .http_client
321            .get(url_)
322            .headers(header_map)
323            .send()
324            .await?;
325
326        let data = parse_json_response(resp).await?;
327        Ok(data)
328    }
329}
330//endregion
331
332// region GetRecord request
333/// [GetRecord](https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-getrecord)
334#[derive(Builder, Debug)]
335pub struct GetRecord<'a> {
336    #[builder(start_fn)]
337    pub(crate) client: &'a Client,
338    pub(crate) record_id: i64,
339}
340
341#[derive(Debug, Deserialize)]
342#[serde(rename_all = "PascalCase")]
343pub struct GetRecordResponse {
344    pub request_id: String,
345    pub record_model: DnsRecord,
346}
347
348impl GetRecord<'_> {
349    pub async fn send(&self) -> Result<GetRecordResponse, Error> {
350        let client = self.client;
351        let creds = client.credentials_provider.load().await?;
352
353        let sign_params = SignParams {
354            req_method: "GET",
355            host: &client.host,
356            query_map: HashMap::from([("RecordId", self.record_id)]),
357            x_acs_action: "GetRecord",
358            x_acs_version: OPENAPI_VERSION,
359            x_acs_security_token: creds.sts_security_token.as_deref(),
360            request_body: None,
361            style: &OPENAPI_STYLE,
362        };
363
364        let (common_headers, url_) =
365            get_openapi_request_header(&creds.access_key_secret, &creds.access_key_id, sign_params)
366                .map_err(|e| {
367                    Error::Common(format!("failed to get openapi request header: {}", e))
368                })?;
369        let header_map = into_header_map(common_headers);
370
371        let resp = client
372            .http_client
373            .get(url_)
374            .headers(header_map)
375            .send()
376            .await?;
377
378        let data = parse_json_response(resp).await?;
379        Ok(data)
380    }
381}
382// endregion
383
384// region CreateRecord
385/// [CreateRecord](https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord)
386#[serde_with::skip_serializing_none]
387#[derive(Builder, Debug, Serialize)]
388#[serde(rename_all = "PascalCase")]
389pub struct CreateRecord<'a> {
390    #[serde(skip_serializing)]
391    #[builder(start_fn)]
392    pub(crate) client: &'a Client,
393
394    site_id: i64,
395    record_name: &'a str,
396
397    /// 只有 CNAME 或 A/AAAA 记录可开启;true/false
398    proxied: Option<bool>,
399
400    /// DNS 记录类型:A/AAAA、CNAME、TXT 等
401    r#type: DnsRecordType,
402
403    /// 仅 CNAME 添加时要求填写;不传默认 Domain
404    source_type: Option<SourceType>,
405
406    /// proxied=true 时必填;proxied=false 时不需要
407    biz_name: Option<BizName>,
408
409    ttl: u32,
410
411    #[serde(serialize_with = "se_as_json_string")]
412    data: RecordData<'a>,
413
414    /// 备注,最大 100 字符
415    comment: Option<&'a str>,
416
417    /// CNAME 源站鉴权信息(主要用于 SourceType=OSS/S3 等场景)
418    auth_conf: Option<AuthConf>,
419
420    /// 回源 HOST 策略,仅 CNAME 生效
421    host_policy: Option<HostPolicy>,
422}
423
424#[serde_with::skip_serializing_none]
425#[derive(Debug, Serialize, Builder)]
426#[serde(rename_all = "PascalCase")]
427pub struct RecordData<'a> {
428    /// A/AAAA, CNAME, NS, MX, TXT, CAA, SRV, URI 时必填(含义随 type 变化)
429    value: Option<&'a str>,
430
431    /// MX, SRV, URI 必填;0~65535;越小优先级越高
432    priority: Option<u16>,
433
434    /// CAA 必填;0~255
435    flag: Option<u8>,
436
437    /// CAA 必填;issue/issuewild/iodef
438    tag: Option<CaaTag>,
439
440    /// SRV, URI 必填;0~65535
441    weight: Option<u16>,
442
443    /// SRV 必填;0~65535
444    port: Option<u16>,
445
446    /// CERT/SSHFP 必填(文档称 Type=integer,但示例可能是 RSA 等;这里用 String 表示更稳妥)
447    r#type: Option<&'a str>,
448
449    /// CERT 必填;0~65535
450    key_tag: Option<u16>,
451
452    /// 记录所采用的加密算法,范围为0~255。CERT、SSHFP 记录独有
453    algorithm: Option<u8>,
454
455    /// CERT/SMIMEA/TLSA 必填(base64 或证书内容)
456    certificate: Option<&'a str>,
457
458    /// SMIMEA/TLSA 必填;0~255
459    usage: Option<u8>,
460
461    /// SMIMEA/TLSA 必填;0~255
462    selector: Option<u8>,
463
464    /// SMIMEA/TLSA 必填;0~255
465    matching_type: Option<u8>,
466
467    /// SSHFP 必填
468    fingerprint: Option<&'a str>,
469}
470
471#[derive(Debug, Serialize)]
472#[serde(rename_all = "lowercase")]
473pub enum CaaTag {
474    Issue,
475    IssueWild,
476    Iodef,
477}
478
479#[derive(Debug, Deserialize)]
480#[serde(rename_all = "PascalCase")]
481pub struct CreateRecordResponse {
482    pub request_id: String,
483    pub record_id: i64,
484}
485
486impl CreateRecord<'_> {
487    pub async fn send(&self) -> Result<CreateRecordResponse, Error> {
488        let client = self.client;
489        let creds = client.credentials_provider.load().await?;
490
491        let sign_params = SignParams {
492            req_method: "POST",
493            host: &client.host,
494            query_map: self,
495            x_acs_action: "CreateRecord",
496            x_acs_version: OPENAPI_VERSION,
497            x_acs_security_token: creds.sts_security_token.as_deref(),
498            request_body: None,
499            style: &OPENAPI_STYLE,
500        };
501
502        let (common_headers, url_) =
503            get_openapi_request_header(&creds.access_key_secret, &creds.access_key_id, sign_params)
504                .map_err(|e| {
505                    Error::Common(format!("failed to get openapi request header: {}", e))
506                })?;
507        let header_map = into_header_map(common_headers);
508
509        let resp = client
510            .http_client
511            .post(url_)
512            .headers(header_map)
513            .send()
514            .await?;
515
516        let data = parse_json_response(resp).await?;
517        Ok(data)
518    }
519}
520// endregion
521
522// region DeleteRecord
523/// [DeleteRecord](https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-deleterecord)
524#[derive(Builder, Debug)]
525pub struct DeleteRecord<'a> {
526    #[builder(start_fn)]
527    pub(crate) client: &'a Client,
528    pub(crate) record_id: i64,
529}
530
531#[derive(Debug, Deserialize)]
532#[serde(rename_all = "PascalCase")]
533pub struct DeleteRecordResponse {
534    pub request_id: String,
535}
536
537impl DeleteRecord<'_> {
538    pub async fn send(&self) -> Result<DeleteRecordResponse, Error> {
539        let client = self.client;
540        let creds = client.credentials_provider.load().await?;
541
542        let sign_params = SignParams {
543            req_method: "POST",
544            host: &client.host,
545            query_map: HashMap::from([("RecordId", self.record_id)]),
546            x_acs_action: "DeleteRecord",
547            x_acs_version: OPENAPI_VERSION,
548            x_acs_security_token: creds.sts_security_token.as_deref(),
549            request_body: None,
550            style: &OPENAPI_STYLE,
551        };
552
553        let (common_headers, url_) =
554            get_openapi_request_header(&creds.access_key_secret, &creds.access_key_id, sign_params)
555                .map_err(|e| {
556                    Error::Common(format!("failed to get openapi request header: {}", e))
557                })?;
558        let header_map = into_header_map(common_headers);
559
560        let resp = client
561            .http_client
562            .post(url_)
563            .headers(header_map)
564            .send()
565            .await?;
566
567        let data = parse_json_response(resp).await?;
568        Ok(data)
569    }
570}
571// endregion
572
573// region UpdateRecord
574#[serde_with::skip_serializing_none]
575#[derive(Builder, Debug, Serialize)]
576#[serde(rename_all = "PascalCase")]
577pub struct UpdateRecord<'a> {
578    #[serde(skip_serializing)]
579    #[builder(start_fn)]
580    pub(crate) client: &'a Client,
581
582    record_id: i64,
583
584    /// 记录的过期时间,单位秒。范围为30~86400,或为 1。当取值为 1 时,表示记录的过期时间为自动。
585    ttl: Option<u32>,
586
587    /// 只有 CNAME 或 A/AAAA 记录可开启;true/false
588    proxied: Option<bool>,
589
590    /// DNS 记录类型:A/AAAA、CNAME、TXT 等
591    r#type: Option<DnsRecordType>,
592
593    /// 仅 CNAME 添加时要求填写;不传默认 Domain
594    source_type: Option<SourceType>,
595
596    /// proxied=true 时必填;proxied=false 时不需要
597    biz_name: Option<BizName>,
598
599    #[serde(serialize_with = "se_as_json_string")]
600    data: RecordData<'a>,
601
602    /// 备注,最大 100 字符
603    comment: Option<&'a str>,
604
605    /// CNAME 源站鉴权信息(主要用于 SourceType=OSS/S3 等场景)
606    auth_conf: Option<AuthConf>,
607
608    /// 回源 HOST 策略,仅 CNAME 生效
609    host_policy: Option<HostPolicy>,
610}
611
612#[derive(Debug, Deserialize)]
613#[serde(rename_all = "PascalCase")]
614pub struct UpdateRecordResponse {
615    pub request_id: String,
616}
617
618impl UpdateRecord<'_> {
619    pub async fn send(&self) -> Result<UpdateRecordResponse, Error> {
620        let client = self.client;
621        let creds = client.credentials_provider.load().await?;
622
623        let sign_params = SignParams {
624            req_method: "POST",
625            host: &client.host,
626            query_map: self,
627            x_acs_action: "UpdateRecord",
628            x_acs_version: OPENAPI_VERSION,
629            x_acs_security_token: creds.sts_security_token.as_deref(),
630            request_body: None,
631            style: &OPENAPI_STYLE,
632        };
633
634        let (common_headers, url_) =
635            get_openapi_request_header(&creds.access_key_secret, &creds.access_key_id, sign_params)
636                .map_err(|e| {
637                    Error::Common(format!("failed to get openapi request header: {}", e))
638                })?;
639        let header_map = into_header_map(common_headers);
640
641        let resp = client
642            .http_client
643            .post(url_)
644            .headers(header_map)
645            .send()
646            .await?;
647
648        let data = parse_json_response(resp).await?;
649        Ok(data)
650    }
651}
652// endregion