1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use std::time::Duration;

use reqwest::{
    self,
    blocking::{
        Client as ReqwestClient, ClientBuilder as ReqwestClientBuilder, Request as ReqwestRequest,
        RequestBuilder as ReqwestRequestBuilder, Response as ReqwestResponse,
    },
    IntoUrl,
};
#[cfg(feature = "send3")]
use websocket::{
    self, client::sync::Client as WsClient, result::WebSocketResult as WsResult,
    stream::sync::NetworkStream as WsNetworkStream,
};

/// The protocol to report to the server when uploading through a websocket.
#[cfg(feature = "send3")]
const WEBSOCKET_PROTOCOL: &str = "ffsend";

/// A networking client for ffsend actions.
pub struct Client {
    /// The client configuration to use for built reqwest clients.
    config: ClientConfig,

    /// The inner reqwest client.
    reqwest: ReqwestClient,
}

impl Client {
    /// Construct the ffsend client with the given `config`.
    ///
    /// If this client is used for transfering files, `transfer` should be true.
    // TODO: properly handle errors, do not unwrap
    pub fn new(config: ClientConfig, transfer: bool) -> Self {
        // Set up the reqwest client
        let mut builder = ReqwestClientBuilder::new();
        match config.timeout {
            Some(timeout) if !transfer => builder = builder.timeout(timeout),
            _ => {}
        }
        match config.transfer_timeout {
            Some(timeout) if transfer => builder = builder.timeout(timeout),
            _ => {}
        }
        let reqwest = builder.build().expect("failed to build reqwest client");

        // Build the client
        Self { config, reqwest }
    }

    /// Create a HTTP GET request through this client, returning a `RequestBuilder`.
    pub fn get<U: IntoUrl>(&self, url: U) -> ReqwestRequestBuilder {
        self.configure(self.reqwest.get(url))
    }

    /// Create a HTTP GET request through this client, returning a `RequestBuilder`.
    pub fn post<U: IntoUrl>(&self, url: U) -> ReqwestRequestBuilder {
        self.configure(self.reqwest.post(url))
    }

    /// Execute the given reqwest request through the internal reqwest client.
    // TODO: remove this, as the timeout can't be configured?
    pub fn execute(&self, request: ReqwestRequest) -> reqwest::Result<ReqwestResponse> {
        self.reqwest.execute(request)
    }

    /// Construct a websocket client connected to the given `url` using the wrapped configuration.
    #[cfg(feature = "send3")]
    pub fn websocket(&self, url: &str) -> WsResult<WsClient<Box<dyn WsNetworkStream + Send>>> {
        // Build the websocket client
        let mut builder = websocket::ClientBuilder::new(url)
            .expect("failed to set up websocket builder")
            .add_protocol(WEBSOCKET_PROTOCOL);

        // Configure basic HTTP authentication
        if let Some((user, password)) = &self.config.basic_auth {
            let mut headers = websocket::header::Headers::new();
            headers.set(websocket::header::Authorization(websocket::header::Basic {
                username: user.to_owned(),
                password: password.to_owned(),
            }));
            builder = builder.custom_headers(&headers);
        }

        // Build and connect the client
        builder.connect(None)
    }

    /// Configure the given reqwest client to match the configuration.
    fn configure(&self, mut client: ReqwestRequestBuilder) -> ReqwestRequestBuilder {
        // Configure basic HTTP authentication
        if let Some((user, password)) = &self.config.basic_auth {
            client = client.basic_auth(user, password.to_owned());
        }

        client
    }
}

/// Configurable properties for networking client used for ffsend actions.
#[derive(Clone, Debug, Builder)]
pub struct ClientConfig {
    /// Connection timeout for control requests.
    #[builder(default = "Some(Duration::from_secs(30))")]
    timeout: Option<Duration>,

    /// Connection timeout specific to file transfers.
    #[builder(default = "Some(Duration::from_secs(86400))")]
    transfer_timeout: Option<Duration>,

    /// Basic HTTP authentication credentials.
    ///
    /// Consists of a username, and an optional password.
    #[builder(default)]
    basic_auth: Option<(String, Option<String>)>,
    // TODO: proxy settings
}

impl ClientConfig {
    /// Construct a ffsend client based on this configuration.
    ///
    /// If this client is used for transfering files, `transfer` should be true.
    pub fn client(self, transfer: bool) -> Client {
        Client::new(self, transfer)
    }
}

impl Default for ClientConfig {
    fn default() -> Self {
        Self {
            timeout: None,
            transfer_timeout: None,
            basic_auth: None,
        }
    }
}