ffsend_api/action/
version.rs

1use thiserror::Error;
2use url::Url;
3
4use crate::api;
5use crate::api::request::ensure_success;
6use crate::client::Client;
7#[cfg(feature = "send3")]
8use crate::config;
9
10/// The Firefox Send version data endpoint.
11const VERSION_ENDPOINT: &str = "__version__";
12
13/// The endpoint we can probe to determine if the server is running Firefox Send v2.
14#[cfg(feature = "send2")]
15const V2_PROBE_ENDPOINT: &str = "jsconfig.js";
16
17/// The endpoint we can probe to determine if the server is running Firefox Send v3.
18#[cfg(feature = "send3")]
19const V3_PROBE_ENDPOINT: &str = "app.webmanifest";
20
21/// An action to attempt to find the API version of a Send server.
22///
23/// This returns a `Version` as probed, and will return an error if failed to properly determine
24/// the server API version.
25///
26/// This API specification for this action is compatible with both Firefox Send v2 and v3.
27pub struct Version {
28    /// The server host.
29    host: Url,
30}
31
32impl Version {
33    /// Construct a new version action.
34    pub fn new(host: Url) -> Self {
35        Self { host }
36    }
37
38    /// Invoke the version action.
39    pub fn invoke(self, client: &Client) -> Result<api::Version, Error> {
40        // Attempt to fetch the version from the version endpoint
41        let mut result = self.fetch_version(client);
42
43        // Probe some server URLs to determine the API version if still unknown
44        if let Err(Error::Unknown) = result {
45            result = self.probe(client);
46        }
47
48        result
49    }
50
51    /// Fetch the version from the endpoint defined as `VERSION_ENDPOINT`.
52    ///
53    /// The internal request might fail, or the response provided by the server might hold an
54    /// unsupported version. In these situations a corresponding `Error` is returned.
55    ///
56    /// This method does not work for development versions of Firefox Send, as the version
57    /// configuration is not included with it.
58    /// For those instances, use the `probe` method instead.
59    fn fetch_version(&self, client: &Client) -> Result<api::Version, Error> {
60        // Build the version URL, request the version
61        let version_url = self.host.join(VERSION_ENDPOINT).expect("invalid host");
62        let response = client.get(version_url).send().map_err(|_| Error::Request)?;
63
64        // Endpoint is removed since ~2020-07, assume V3
65        #[cfg(feature = "send3")]
66        {
67            if response.status() == config::HTTP_STATUS_EXPIRED {
68                return Ok(api::Version::V3);
69            }
70        }
71
72        // Ensure the status code is successful
73        match ensure_success(&response) {
74            Ok(_) => {}
75            Err(_) => return Err(Error::Unknown),
76        }
77
78        // Parse the response and attempt to determine the version number
79        let response = response
80            .json::<VersionResponse>()
81            .map_err(|_| Error::Unknown)?;
82        response.determine_version()
83    }
84
85    /// Attempt to determine the server version by probing some known URLs.
86    ///
87    /// This method is not super reliable, but good enough for guessing the server version.
88    /// `Error::Unknown` will be returned if the version could not be probed.
89    ///
90    /// Internally, this method will make some requests which might fail.
91    /// These are cached and not immediately returned.
92    ///
93    /// # Panics
94    ///
95    /// This method panics if the host was invalid.
96    fn probe(&self, client: &Client) -> Result<api::Version, Error> {
97        // Probe Firefox Send v3
98        #[cfg(feature = "send3")]
99        {
100            if self.exists(client, V3_PROBE_ENDPOINT) {
101                return Ok(api::Version::V3);
102            }
103        }
104
105        // Probe Firefox Send v2
106        #[cfg(feature = "send2")]
107        {
108            if self.exists(client, V2_PROBE_ENDPOINT) {
109                return Ok(api::Version::V2);
110            }
111        }
112
113        // Unknown or unsupported version
114        Err(Error::Unknown)
115    }
116
117    /// Check if a host endpoint exists.
118    ///
119    /// A GET request to the URL will be made, if the server responds with success or with a
120    /// redirection (see [`StatusCode::is_success`](StatusCode::is_success) and
121    /// [`StatusCode::is_redirection`](StatusCode::is_redirection)) true is returned,
122    /// false otherwise.
123    ///
124    /// # Panics
125    ///
126    /// This method panics if the host was invalid.
127    fn exists(&self, client: &Client, endpoint: &str) -> bool {
128        let url = self.host.join(endpoint).expect("invalid host");
129        client
130            .get(url)
131            .send()
132            .map(|r| r.status())
133            .map(|s| s.is_success() || s.is_redirection())
134            .unwrap_or(false)
135    }
136}
137
138/// The version response.
139///
140/// The server responds with this JSON object when accessing `/__version__`.
141/// Unused fields are omitted.
142#[derive(Debug, Deserialize)]
143pub struct VersionResponse {
144    /// The version string.
145    version: String,
146}
147
148impl VersionResponse {
149    /// Attempt to determine the API version for this response.
150    ///
151    /// If the API version is unsupported (or not compiled due to a missing compiler feature) an
152    /// `Error::Unsupported` is returned holding the version number string.
153    pub fn determine_version<'a>(&'a self) -> Result<api::Version, Error> {
154        api::Version::parse(&self.version).map_err(|v| Error::Unsupported(v.into()))
155    }
156}
157
158#[derive(Error, Debug)]
159pub enum Error {
160    /// Sending the request to check whether the file exists failed.
161    #[error("failed to send request to fetch server version")]
162    Request,
163
164    /// The server was not able to respond with any version identifiable information, the server
165    /// version is unknown.
166    #[error("failed to determine server version")]
167    Unknown,
168
169    /// The server responded with the given version string that is currently not supported and is
170    /// unknown.
171    /// This might be the result of a missing compiler feature for a given Firefox Send version.
172    #[error("failed to determine server version, unsupported version")]
173    Unsupported(String),
174}