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}