http_upload.rs

  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},
  9    AsXml, FromXml,
 10};
 11
 12use crate::iq::{IqGetPayload, IqResultPayload};
 13use crate::ns;
 14use minidom::Element;
 15
 16/// Requesting a slot
 17#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
 18#[xml(namespace = ns::HTTP_UPLOAD, name = "request")]
 19pub struct SlotRequest {
 20    /// The filename to be uploaded.
 21    #[xml(attribute)]
 22    pub filename: String,
 23
 24    /// Size of the file to be uploaded.
 25    #[xml(attribute)]
 26    pub size: u64,
 27
 28    /// Content-Type of the file.
 29    #[xml(attribute(name = "content-type"))]
 30    pub content_type: Option<String>,
 31}
 32
 33impl IqGetPayload for SlotRequest {}
 34
 35/// Slot header
 36#[derive(Debug, Clone, PartialEq)]
 37pub enum Header {
 38    /// Authorization header
 39    Authorization(String),
 40
 41    /// Cookie header
 42    Cookie(String),
 43
 44    /// Expires header
 45    Expires(String),
 46}
 47
 48impl TryFrom<Element> for Header {
 49    type Error = FromElementError;
 50    fn try_from(elem: Element) -> Result<Header, FromElementError> {
 51        check_self!(elem, "header", HTTP_UPLOAD);
 52        check_no_children!(elem, "header");
 53        check_no_unknown_attributes!(elem, "header", ["name"]);
 54        let name: String = get_attr!(elem, "name", Required);
 55        let text = elem.text();
 56
 57        Ok(match name.to_lowercase().as_str() {
 58            "authorization" => Header::Authorization(text),
 59            "cookie" => Header::Cookie(text),
 60            "expires" => Header::Expires(text),
 61            _ => {
 62                return Err(Error::Other(
 63                    "Header name must be either 'Authorization', 'Cookie', or 'Expires'.",
 64                )
 65                .into())
 66            }
 67        })
 68    }
 69}
 70
 71impl From<Header> for Element {
 72    fn from(elem: Header) -> Element {
 73        let (attr, val) = match elem {
 74            Header::Authorization(val) => ("Authorization", val),
 75            Header::Cookie(val) => ("Cookie", val),
 76            Header::Expires(val) => ("Expires", val),
 77        };
 78
 79        Element::builder("header", ns::HTTP_UPLOAD)
 80            .attr("name", attr)
 81            .append(val)
 82            .build()
 83    }
 84}
 85
 86generate_element!(
 87    /// Put URL
 88    Put, "put", HTTP_UPLOAD,
 89    attributes: [
 90        /// URL
 91        url: Required<String> = "url",
 92    ],
 93    children: [
 94        /// Header list
 95        headers: Vec<Header> = ("header", HTTP_UPLOAD) => Header
 96    ]
 97);
 98
 99/// Get URL
100#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
101#[xml(namespace = ns::HTTP_UPLOAD, name = "get")]
102pub struct Get {
103    /// URL
104    #[xml(attribute)]
105    pub url: String,
106}
107
108/// Requesting a slot
109#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
110#[xml(namespace = ns::HTTP_UPLOAD, name = "slot")]
111pub struct SlotResult {
112    /// Put URL and headers
113    #[xml(child)]
114    pub put: Put,
115
116    /// Get URL
117    #[xml(child)]
118    pub get: Get,
119}
120
121impl IqResultPayload for SlotResult {}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_slot_request() {
129        let elem: Element = "<request xmlns='urn:xmpp:http:upload:0'
130            filename='très cool.jpg'
131            size='23456'
132            content-type='image/jpeg' />"
133            .parse()
134            .unwrap();
135        let slot = SlotRequest::try_from(elem).unwrap();
136        assert_eq!(slot.filename, String::from("très cool.jpg"));
137        assert_eq!(slot.size, 23456);
138        assert_eq!(slot.content_type, Some(String::from("image/jpeg")));
139    }
140
141    #[test]
142    fn test_slot_result() {
143        let elem: Element = "<slot xmlns='urn:xmpp:http:upload:0'>
144            <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg'>
145              <header name='Authorization'>Basic Base64String==</header>
146              <header name='Cookie'>foo=bar; user=romeo</header>
147            </put>
148            <get url='https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg' />
149          </slot>"
150            .parse()
151            .unwrap();
152        let slot = SlotResult::try_from(elem).unwrap();
153        assert_eq!(slot.put.url, String::from("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg"));
154        assert_eq!(
155            slot.put.headers[0],
156            Header::Authorization(String::from("Basic Base64String=="))
157        );
158        assert_eq!(
159            slot.put.headers[1],
160            Header::Cookie(String::from("foo=bar; user=romeo"))
161        );
162        assert_eq!(slot.get.url, String::from("https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg"));
163    }
164
165    #[test]
166    fn test_result_no_header() {
167        let elem: Element = "<slot xmlns='urn:xmpp:http:upload:0'>
168            <put url='https://URL' />
169            <get url='https://URL' />
170          </slot>"
171            .parse()
172            .unwrap();
173        let slot = SlotResult::try_from(elem).unwrap();
174        assert_eq!(slot.put.url, String::from("https://URL"));
175        assert_eq!(slot.put.headers.len(), 0);
176        assert_eq!(slot.get.url, String::from("https://URL"));
177    }
178
179    #[test]
180    fn test_result_bad_header() {
181        let elem: Element = "<slot xmlns='urn:xmpp:http:upload:0'>
182            <put url='https://URL'>
183              <header name='EvilHeader'>EvilValue</header>
184            </put>
185            <get url='https://URL' />
186          </slot>"
187            .parse()
188            .unwrap();
189        SlotResult::try_from(elem).unwrap_err();
190    }
191}