Skip to main content

aliyun_oss/operations/
bucket_referer.rs

1//! Bucket referer (hotlink protection) operations.
2
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::bucket::BucketName;
11
12#[derive(Debug, Clone, Serialize)]
13#[serde(rename = "RefererConfiguration")]
14struct RefererConfiguration {
15    #[serde(rename = "AllowEmptyReferer")]
16    allow_empty_referer: bool,
17    #[serde(rename = "RefererList", skip_serializing_if = "Option::is_none")]
18    referer_list: Option<RefererList>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22struct RefererList {
23    #[serde(rename = "Referer")]
24    referer: Vec<String>,
25}
26
27#[derive(Debug, Clone, Deserialize)]
28#[serde(rename = "RefererConfiguration")]
29struct RefererConfigurationResponse {
30    #[serde(rename = "AllowEmptyReferer")]
31    allow_empty_referer: bool,
32    #[serde(rename = "RefererList", default)]
33    referer_list: Option<RefererList>,
34}
35
36pub struct PutBucketRefererBuilder {
37    client: Arc<OSSClientInner>,
38    bucket: BucketName,
39    allow_empty_referer: bool,
40    referers: Vec<String>,
41}
42
43impl PutBucketRefererBuilder {
44    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
45        Self {
46            client,
47            bucket,
48            allow_empty_referer: true,
49            referers: Vec::new(),
50        }
51    }
52
53    pub fn allow_empty_referer(mut self, allow: bool) -> Self {
54        self.allow_empty_referer = allow;
55        self
56    }
57
58    pub fn add_referer(mut self, referer: impl Into<String>) -> Self {
59        self.referers.push(referer.into());
60        self
61    }
62
63    pub async fn send(self) -> Result<PutBucketRefererOutput> {
64        let endpoint = self.client.endpoint.clone();
65        let uri = format!("https://{}.{}?referer", self.bucket.as_str(), endpoint);
66        let query_params: Vec<(String, String)> = vec![("referer".into(), String::new())];
67
68        let config = RefererConfiguration {
69            allow_empty_referer: self.allow_empty_referer,
70            referer_list: if self.referers.is_empty() {
71                None
72            } else {
73                Some(RefererList {
74                    referer: self.referers,
75                })
76            },
77        };
78        let body_xml = crate::util::xml::to_xml(&config)?;
79
80        let request = HttpRequest::builder()
81            .method(http::Method::PUT)
82            .uri(&uri)
83            .body(bytes::Bytes::from(body_xml))
84            .build();
85
86        let response = self
87            .client
88            .send_signed(request, Some(&self.bucket), query_params)
89            .await
90            .map_err(|e| OssError {
91                kind: OssErrorKind::TransportError,
92                context: Box::new(ErrorContext {
93                    operation: Some("PutBucketReferer".into()),
94                    bucket: Some(self.bucket.to_string()),
95                    endpoint: Some(endpoint),
96                    ..Default::default()
97                }),
98                source: Some(Box::new(e)),
99            })?;
100
101        if response.status().is_success() {
102            Ok(PutBucketRefererOutput {
103                request_id: response
104                    .headers
105                    .get("x-oss-request-id")
106                    .and_then(|v| v.to_str().ok())
107                    .unwrap_or("")
108                    .to_string(),
109            })
110        } else {
111            Err(OssError {
112                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
113                    status_code: response.status().as_u16(),
114                    code: String::new(),
115                    message: String::new(),
116                    request_id: String::new(),
117                    host_id: String::new(),
118                    resource: Some(self.bucket.to_string()),
119                    string_to_sign: None,
120                })),
121                context: Box::new(ErrorContext {
122                    operation: Some("PutBucketReferer".into()),
123                    bucket: Some(self.bucket.to_string()),
124                    ..Default::default()
125                }),
126                source: None,
127            })
128        }
129    }
130}
131
132#[derive(Debug, Clone)]
133pub struct PutBucketRefererOutput {
134    pub request_id: String,
135}
136
137pub struct GetBucketRefererBuilder {
138    client: Arc<OSSClientInner>,
139    bucket: BucketName,
140}
141
142impl GetBucketRefererBuilder {
143    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
144        Self { client, bucket }
145    }
146
147    pub async fn send(self) -> Result<GetBucketRefererOutput> {
148        let endpoint = self.client.endpoint.clone();
149        let uri = format!("https://{}.{}?referer", self.bucket.as_str(), endpoint);
150        let query_params: Vec<(String, String)> = vec![("referer".into(), String::new())];
151
152        let request = HttpRequest::builder()
153            .method(http::Method::GET)
154            .uri(&uri)
155            .build();
156
157        let response = self
158            .client
159            .send_signed(request, Some(&self.bucket), query_params)
160            .await
161            .map_err(|e| OssError {
162                kind: OssErrorKind::TransportError,
163                context: Box::new(ErrorContext {
164                    operation: Some("GetBucketReferer".into()),
165                    bucket: Some(self.bucket.to_string()),
166                    endpoint: Some(endpoint),
167                    ..Default::default()
168                }),
169                source: Some(Box::new(e)),
170            })?;
171
172        if response.is_success() {
173            let body_str = response.body_as_str().unwrap_or("");
174            let config: RefererConfigurationResponse = crate::util::xml::from_xml(body_str)
175                .map_err(|e| OssError {
176                    kind: OssErrorKind::DeserializationError,
177                    context: Box::new(ErrorContext {
178                        operation: Some("GetBucketReferer: parse XML".into()),
179                        bucket: Some(self.bucket.to_string()),
180                        ..Default::default()
181                    }),
182                    source: Some(Box::new(e)),
183                })?;
184
185            Ok(GetBucketRefererOutput {
186                allow_empty_referer: config.allow_empty_referer,
187                referers: config.referer_list.map(|rl| rl.referer).unwrap_or_default(),
188            })
189        } else {
190            Err(OssError {
191                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
192                    status_code: response.status().as_u16(),
193                    code: String::new(),
194                    message: String::new(),
195                    request_id: String::new(),
196                    host_id: String::new(),
197                    resource: Some(self.bucket.to_string()),
198                    string_to_sign: None,
199                })),
200                context: Box::new(ErrorContext {
201                    operation: Some("GetBucketReferer".into()),
202                    bucket: Some(self.bucket.to_string()),
203                    ..Default::default()
204                }),
205                source: None,
206            })
207        }
208    }
209}
210
211#[derive(Debug, Clone)]
212pub struct GetBucketRefererOutput {
213    pub allow_empty_referer: bool,
214    pub referers: Vec<String>,
215}
216
217impl BucketOperations {
218    pub fn put_referer(&self) -> PutBucketRefererBuilder {
219        PutBucketRefererBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
220    }
221
222    pub fn get_referer(&self) -> GetBucketRefererBuilder {
223        GetBucketRefererBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use std::sync::Mutex;
230
231    use crate::client::OSSClientInner;
232    use crate::config::credentials::Credentials;
233    use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
234    use crate::types::region::Region;
235
236    use super::*;
237
238    struct RecordingHttpClient {
239        requests: Arc<Mutex<Vec<HttpRequest>>>,
240        status_code: http::StatusCode,
241        response_body: bytes::Bytes,
242    }
243
244    #[async_trait::async_trait]
245    impl HttpClient for RecordingHttpClient {
246        async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
247            self.requests.lock().unwrap().push(request);
248            let mut headers = http::HeaderMap::new();
249            headers.insert(
250                "x-oss-request-id",
251                http::HeaderValue::from_static("rid-referer"),
252            );
253            Ok(HttpResponse {
254                status: self.status_code,
255                headers,
256                body: self.response_body.clone(),
257            })
258        }
259    }
260
261    fn create_test_inner_with_body(
262        status: http::StatusCode,
263        body: bytes::Bytes,
264    ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
265        let requests = Arc::new(Mutex::new(Vec::new()));
266        let http = Arc::new(RecordingHttpClient {
267            requests: requests.clone(),
268            status_code: status,
269            response_body: body,
270        });
271        let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
272            Credentials::builder()
273                .access_key_id("test-ak")
274                .access_key_secret("test-sk")
275                .build()
276                .unwrap(),
277        ));
278        let inner = Arc::new(OSSClientInner {
279            http,
280            credentials,
281            signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
282            region: Region::CnHangzhou,
283            endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
284        });
285        (inner, requests)
286    }
287
288    #[test]
289    fn referer_xml_generation() {
290        let config = RefererConfiguration {
291            allow_empty_referer: true,
292            referer_list: Some(RefererList {
293                referer: vec!["https://example.com".into(), "https://*.example.com".into()],
294            }),
295        };
296        let xml = crate::util::xml::to_xml(&config).unwrap();
297        assert!(xml.contains("<AllowEmptyReferer>true</AllowEmptyReferer>"));
298        assert!(xml.contains("<Referer>https://example.com</Referer>"));
299    }
300
301    #[tokio::test]
302    async fn put_referer_sends_request() {
303        let (inner, requests) =
304            create_test_inner_with_body(http::StatusCode::OK, bytes::Bytes::new());
305        let builder = PutBucketRefererBuilder::new(inner, BucketName::new("test-bucket").unwrap())
306            .add_referer("https://example.com");
307        builder.send().await.unwrap();
308        let captured = requests.lock().unwrap();
309        assert_eq!(captured[0].method, http::Method::PUT);
310        assert!(captured[0].uri.contains("?referer"));
311    }
312}