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, 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}