reqsign-core 3.0.1

Signing API requests without effort.
Documentation
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use std::borrow::Cow;
use std::mem;
use std::time::Duration;

use crate::{Error, Result};
use http::HeaderMap;
use http::HeaderValue;
use http::Method;
use http::Uri;
use http::header::HeaderName;
use http::uri::Authority;
use http::uri::PathAndQuery;
use http::uri::Scheme;
use std::str::FromStr;

/// Signing context for request.
#[derive(Debug)]
pub struct SigningRequest {
    /// HTTP method.
    pub method: Method,
    /// HTTP scheme.
    pub scheme: Scheme,
    /// HTTP authority.
    pub authority: Authority,
    /// HTTP path.
    pub path: String,
    /// HTTP query parameters.
    pub query: Vec<(String, String)>,
    /// HTTP headers.
    pub headers: HeaderMap,
}

impl SigningRequest {
    /// Build a signing context from http::request::Parts.
    pub fn build(parts: &mut http::request::Parts) -> Result<Self> {
        let uri = mem::take(&mut parts.uri).into_parts();
        let paq = uri
            .path_and_query
            .unwrap_or_else(|| PathAndQuery::from_static("/"));

        Ok(SigningRequest {
            method: parts.method.clone(),
            scheme: uri.scheme.unwrap_or(Scheme::HTTP),
            authority: uri.authority.ok_or_else(|| {
                Error::request_invalid("request without authority is invalid for signing")
            })?,
            path: paq.path().to_string(),
            query: paq
                .query()
                .map(|v| {
                    form_urlencoded::parse(v.as_bytes())
                        .map(|(k, v)| (k.into_owned(), v.into_owned()))
                        .collect()
                })
                .unwrap_or_default(),

            // Take the headers out of the request to avoid copy.
            // We will return it back when apply the context.
            headers: mem::take(&mut parts.headers),
        })
    }

    /// Apply the signing context back to http::request::Parts.
    pub fn apply(mut self, parts: &mut http::request::Parts) -> Result<()> {
        let query_size = self.query_size();

        // Return headers back.
        mem::swap(&mut parts.headers, &mut self.headers);
        parts.method = self.method;
        parts.uri = {
            let mut uri_parts = mem::take(&mut parts.uri).into_parts();
            // Return scheme bakc.
            uri_parts.scheme = Some(self.scheme);
            // Return authority back.
            uri_parts.authority = Some(self.authority);
            // Build path and query.
            uri_parts.path_and_query =
                {
                    let paq = if query_size == 0 {
                        self.path
                    } else {
                        let mut s = self.path;
                        s.reserve(query_size + 1);

                        s.push('?');
                        for (i, (k, v)) in self.query.iter().enumerate() {
                            if i > 0 {
                                s.push('&');
                            }

                            s.push_str(k);
                            if !v.is_empty() {
                                s.push('=');
                                s.push_str(v);
                            }
                        }

                        s
                    };

                    Some(PathAndQuery::from_str(&paq).map_err(|e| {
                        Error::request_invalid("invalid path and query").with_source(e)
                    })?)
                };
            Uri::from_parts(uri_parts)
                .map_err(|e| Error::request_invalid("failed to build URI").with_source(e))?
        };

        Ok(())
    }

    /// Get the path percent decoded.
    pub fn path_percent_decoded(&self) -> Cow<'_, str> {
        percent_encoding::percent_decode_str(&self.path).decode_utf8_lossy()
    }

    /// Get query size.
    #[inline]
    pub fn query_size(&self) -> usize {
        self.query
            .iter()
            .map(|(k, v)| k.len() + v.len())
            .sum::<usize>()
    }

    /// Push a new query pair into query list.
    #[inline]
    pub fn query_push(&mut self, key: impl Into<String>, value: impl Into<String>) {
        self.query.push((key.into(), value.into()));
    }

    /// Push a query string into query list.
    #[inline]
    pub fn query_append(&mut self, query: &str) {
        self.query.push((query.to_string(), "".to_string()));
    }

    /// Get query value by filter.
    pub fn query_to_vec_with_filter(&self, filter: impl Fn(&str) -> bool) -> Vec<(String, String)> {
        self.query
            .iter()
            // Filter all queries
            .filter(|(k, _)| filter(k))
            // Clone all queries
            .map(|(k, v)| (k.to_string(), v.to_string()))
            .collect()
    }

    /// Convert sorted query to string.
    ///
    /// ```shell
    /// [(a, b), (c, d)] => "a:b\nc:d"
    /// ```
    pub fn query_to_string(mut query: Vec<(String, String)>, sep: &str, join: &str) -> String {
        let mut s = String::with_capacity(16);

        // Sort via header name.
        query.sort();

        for (idx, (k, v)) in query.into_iter().enumerate() {
            if idx != 0 {
                s.push_str(join);
            }

            s.push_str(&k);
            if !v.is_empty() {
                s.push_str(sep);
                s.push_str(&v);
            }
        }

        s
    }

    /// Convert sorted query to percent decoded string.
    ///
    /// ```shell
    /// [(a, b), (c, d)] => "a:b\nc:d"
    /// ```
    pub fn query_to_percent_decoded_string(
        mut query: Vec<(String, String)>,
        sep: &str,
        join: &str,
    ) -> String {
        let mut s = String::with_capacity(16);

        // Sort via header name.
        query.sort();

        for (idx, (k, v)) in query.into_iter().enumerate() {
            if idx != 0 {
                s.push_str(join);
            }

            s.push_str(&k);
            if !v.is_empty() {
                s.push_str(sep);
                s.push_str(&percent_encoding::percent_decode_str(&v).decode_utf8_lossy());
            }
        }

        s
    }

    /// Get header value by name.
    ///
    /// Returns empty string if header not found.
    #[inline]
    pub fn header_get_or_default(&self, key: &HeaderName) -> Result<&str> {
        match self.headers.get(key) {
            Some(v) => v
                .to_str()
                .map_err(|e| Error::request_invalid("invalid header value").with_source(e)),
            None => Ok(""),
        }
    }

    /// Normalize header value.
    pub fn header_value_normalize(v: &mut HeaderValue) {
        let bs = v.as_bytes();

        let starting_index = bs.iter().position(|b| *b != b' ').unwrap_or(0);
        let ending_offset = bs.iter().rev().position(|b| *b != b' ').unwrap_or(0);
        let ending_index = bs.len() - ending_offset;

        // This can't fail because we started with a valid HeaderValue and then only trimmed spaces
        *v = HeaderValue::from_bytes(&bs[starting_index..ending_index])
            .expect("invalid header value")
    }

    /// Get header names as sorted vector.
    pub fn header_name_to_vec_sorted(&self) -> Vec<&str> {
        let mut h = self
            .headers
            .keys()
            .map(|k| k.as_str())
            .collect::<Vec<&str>>();
        h.sort_unstable();

        h
    }

    /// Get header names with given prefix.
    pub fn header_to_vec_with_prefix(&self, prefix: &str) -> Vec<(String, String)> {
        self.headers
            .iter()
            // Filter all header that starts with prefix
            .filter(|(k, _)| k.as_str().starts_with(prefix))
            // Convert all header name to lowercase
            .map(|(k, v)| {
                (
                    k.as_str().to_lowercase(),
                    v.to_str().expect("must be valid header").to_string(),
                )
            })
            .collect()
    }

    /// Convert sorted headers to string.
    ///
    /// ```shell
    /// [(a, b), (c, d)] => "a:b\nc:d"
    /// ```
    pub fn header_to_string(mut headers: Vec<(String, String)>, sep: &str, join: &str) -> String {
        let mut s = String::with_capacity(16);

        // Sort via header name.
        headers.sort();

        for (idx, (k, v)) in headers.into_iter().enumerate() {
            if idx != 0 {
                s.push_str(join);
            }

            s.push_str(&k);
            s.push_str(sep);
            s.push_str(&v);
        }

        s
    }
}

/// SigningMethod is the method that used in signing.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum SigningMethod {
    /// Signing with header.
    Header,
    /// Signing with query.
    Query(Duration),
}