1// Copyright (c) 2021 Maxime “pep” Buquet <pep@bouah.net>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use xso::{
8 error::{Error, FromElementError, FromEventsError},
9 exports::rxml,
10 minidom_compat, AsXml, FromXml,
11};
12
13use crate::iq::{IqGetPayload, IqResultPayload};
14use crate::ns;
15use minidom::Element;
16
17/// Requesting a slot
18#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
19#[xml(namespace = ns::HTTP_UPLOAD, name = "request")]
20pub struct SlotRequest {
21 /// The filename to be uploaded.
22 #[xml(attribute)]
23 pub filename: String,
24
25 /// Size of the file to be uploaded.
26 #[xml(attribute)]
27 pub size: u64,
28
29 /// Content-Type of the file.
30 #[xml(attribute(name = "content-type"))]
31 pub content_type: Option<String>,
32}
33
34impl IqGetPayload for SlotRequest {}
35
36/// Slot header
37#[derive(Debug, Clone, PartialEq)]
38pub enum Header {
39 /// Authorization header
40 Authorization(String),
41
42 /// Cookie header
43 Cookie(String),
44
45 /// Expires header
46 Expires(String),
47}
48
49impl TryFrom<Element> for Header {
50 type Error = FromElementError;
51 fn try_from(elem: Element) -> Result<Header, FromElementError> {
52 check_self!(elem, "header", HTTP_UPLOAD);
53 check_no_children!(elem, "header");
54 check_no_unknown_attributes!(elem, "header", ["name"]);
55 let name: String = get_attr!(elem, "name", Required);
56 let text = elem.text();
57
58 Ok(match name.to_lowercase().as_str() {
59 "authorization" => Header::Authorization(text),
60 "cookie" => Header::Cookie(text),
61 "expires" => Header::Expires(text),
62 _ => {
63 return Err(Error::Other(
64 "Header name must be either 'Authorization', 'Cookie', or 'Expires'.",
65 )
66 .into())
67 }
68 })
69 }
70}
71
72impl FromXml for Header {
73 type Builder = minidom_compat::FromEventsViaElement<Header>;
74
75 fn from_events(
76 qname: rxml::QName,
77 attrs: rxml::AttrMap,
78 ) -> Result<Self::Builder, FromEventsError> {
79 if qname.0 != ns::HTTP_UPLOAD || qname.1 != "header" {
80 return Err(FromEventsError::Mismatch { name: qname, attrs });
81 }
82 Self::Builder::new(qname, attrs)
83 }
84}
85
86impl From<Header> for Element {
87 fn from(elem: Header) -> Element {
88 let (attr, val) = match elem {
89 Header::Authorization(val) => ("Authorization", val),
90 Header::Cookie(val) => ("Cookie", val),
91 Header::Expires(val) => ("Expires", val),
92 };
93
94 Element::builder("header", ns::HTTP_UPLOAD)
95 .attr("name", attr)
96 .append(val)
97 .build()
98 }
99}
100
101impl AsXml for Header {
102 type ItemIter<'x> = minidom_compat::AsItemsViaElement<'x>;
103
104 fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, Error> {
105 minidom_compat::AsItemsViaElement::new(self.clone())
106 }
107}
108
109/// Put URL
110#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
111#[xml(namespace = ns::HTTP_UPLOAD, name = "put")]
112pub struct Put {
113 /// URL
114 #[xml(attribute)]
115 pub url: String,
116
117 /// Header list
118 #[xml(child(n = ..))]
119 pub headers: Vec<Header>,
120}
121
122/// Get URL
123#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
124#[xml(namespace = ns::HTTP_UPLOAD, name = "get")]
125pub struct Get {
126 /// URL
127 #[xml(attribute)]
128 pub url: String,
129}
130
131/// Requesting a slot
132#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
133#[xml(namespace = ns::HTTP_UPLOAD, name = "slot")]
134pub struct SlotResult {
135 /// Put URL and headers
136 #[xml(child)]
137 pub put: Put,
138
139 /// Get URL
140 #[xml(child)]
141 pub get: Get,
142}
143
144impl IqResultPayload for SlotResult {}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_slot_request() {
152 let elem: Element = "<request xmlns='urn:xmpp:http:upload:0'
153 filename='très cool.jpg'
154 size='23456'
155 content-type='image/jpeg' />"
156 .parse()
157 .unwrap();
158 let slot = SlotRequest::try_from(elem).unwrap();
159 assert_eq!(slot.filename, String::from("très cool.jpg"));
160 assert_eq!(slot.size, 23456);
161 assert_eq!(slot.content_type, Some(String::from("image/jpeg")));
162 }
163
164 #[test]
165 fn test_slot_result() {
166 let elem: Element = "<slot xmlns='urn:xmpp:http:upload:0'>
167 <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg'>
168 <header name='Authorization'>Basic Base64String==</header>
169 <header name='Cookie'>foo=bar; user=romeo</header>
170 </put>
171 <get url='https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg' />
172 </slot>"
173 .parse()
174 .unwrap();
175 let slot = SlotResult::try_from(elem).unwrap();
176 assert_eq!(slot.put.url, String::from("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg"));
177 assert_eq!(
178 slot.put.headers[0],
179 Header::Authorization(String::from("Basic Base64String=="))
180 );
181 assert_eq!(
182 slot.put.headers[1],
183 Header::Cookie(String::from("foo=bar; user=romeo"))
184 );
185 assert_eq!(slot.get.url, String::from("https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg"));
186 }
187
188 #[test]
189 fn test_result_no_header() {
190 let elem: Element = "<slot xmlns='urn:xmpp:http:upload:0'>
191 <put url='https://URL' />
192 <get url='https://URL' />
193 </slot>"
194 .parse()
195 .unwrap();
196 let slot = SlotResult::try_from(elem).unwrap();
197 assert_eq!(slot.put.url, String::from("https://URL"));
198 assert_eq!(slot.put.headers.len(), 0);
199 assert_eq!(slot.get.url, String::from("https://URL"));
200 }
201
202 #[test]
203 fn test_result_bad_header() {
204 let elem: Element = "<slot xmlns='urn:xmpp:http:upload:0'>
205 <put url='https://URL'>
206 <header name='EvilHeader'>EvilValue</header>
207 </put>
208 <get url='https://URL' />
209 </slot>"
210 .parse()
211 .unwrap();
212 SlotResult::try_from(elem).unwrap_err();
213 }
214}