vfs_https/
httpsfs.rs

1use hyper::header::WWW_AUTHENTICATE;
2use hyper::StatusCode;
3use reqwest::blocking::Client;
4use std::fmt::{Debug, Formatter};
5use std::io::{Read, Seek, Write};
6use vfs::{FileSystem, SeekAndRead, VfsError, VfsMetadata, VfsResult};
7
8use crate::error::AuthError;
9use crate::error::HttpsFSError;
10use crate::error::HttpsFSResult;
11
12use crate::protocol::*;
13
14type CredentialProvider = Option<fn(realm: &str) -> (String, String)>;
15
16/// A file system exposed over https
17pub struct HttpsFS {
18    addr: String,
19    client: std::sync::Arc<reqwest::blocking::Client>,
20    /// Will be called to get login credentials for the authentication process.
21    /// Return value is a tuple: The first part is the user name, the second part the password.
22    credentials: CredentialProvider,
23}
24
25/// Helper struct for building [HttpsFS] structs
26pub struct HttpsFSBuilder {
27    port: u16,
28    domain: String,
29    root_certs: Vec<reqwest::Certificate>,
30    credentials: CredentialProvider,
31}
32
33struct WritableFile {
34    client: std::sync::Arc<reqwest::blocking::Client>,
35    addr: String,
36    file_name: String,
37    position: u64,
38}
39
40struct ReadableFile {
41    client: std::sync::Arc<reqwest::blocking::Client>,
42    addr: String,
43    file_name: String,
44    position: u64,
45}
46
47impl Debug for HttpsFS {
48    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
49        f.write_str("Over Https Exposed File System.")
50    }
51}
52
53impl HttpsFS {
54    /// Returns a builder to create a [HttpsFS]
55    pub fn builder(domain: &str) -> HttpsFSBuilder {
56        HttpsFSBuilder::new(domain)
57    }
58
59    fn load_certificate(filename: &str) -> HttpsFSResult<reqwest::Certificate> {
60        let mut buf = Vec::new();
61        std::fs::File::open(filename)?.read_to_end(&mut buf)?;
62        let cert = reqwest::Certificate::from_pem(&buf)?;
63        Ok(cert)
64    }
65
66    fn exec_command(&self, cmd: &Command) -> HttpsFSResult<CommandResponse> {
67        let req = serde_json::to_string(&cmd)?;
68        let mut result = self.client.post(&self.addr).body(req).send()?;
69        if result.status() == StatusCode::UNAUTHORIZED {
70            let req = serde_json::to_string(&cmd)?;
71            result = self
72                .authorize(&result, self.client.post(&self.addr).body(req))?
73                .send()?;
74            if result.status() != StatusCode::OK {
75                return Err(HttpsFSError::Auth(AuthError::Failed));
76            }
77        }
78        let result = result.text()?;
79        let result: CommandResponse = serde_json::from_str(&result)?;
80        Ok(result)
81    }
82
83    fn authorize(
84        &self,
85        prev_response: &reqwest::blocking::Response,
86        new_request: reqwest::blocking::RequestBuilder,
87    ) -> HttpsFSResult<reqwest::blocking::RequestBuilder> {
88        if self.credentials.is_none() {
89            return Err(HttpsFSError::Auth(AuthError::NoCredentialSource));
90        }
91        let prev_headers = prev_response.headers();
92        let auth_method = prev_headers
93            .get(WWW_AUTHENTICATE)
94            .ok_or(HttpsFSError::Auth(AuthError::NoMethodSpecified))?;
95        let auth_method = String::from(
96            auth_method
97                .to_str()
98                .map_err(|_| HttpsFSError::InvalidHeader(WWW_AUTHENTICATE.to_string()))?,
99        );
100        // TODO: this is a fix hack since we currently only support one method. If we start to
101        // support more than one authentication method, we have to properly parse this header.
102        // Furthermore, currently only the 'PME'-Realm is supported.
103        let start_with = "Basic realm=\"PME\"";
104        if !auth_method.starts_with(start_with) {
105            return Err(HttpsFSError::Auth(AuthError::MethodNotSupported));
106        }
107        let get_cred = self.credentials.unwrap();
108        let (username, password) = get_cred(&"PME");
109        let new_request = new_request.basic_auth(username, Some(password));
110        Ok(new_request)
111    }
112}
113
114impl HttpsFSBuilder {
115    /// Creates a new builder for a [HttpsFS].
116    ///
117    /// Takes a domain name to which the HttpsFS will connect.
118    pub fn new(domain: &str) -> Self {
119        HttpsFSBuilder {
120            port: 443,
121            domain: String::from(domain),
122            root_certs: Vec::new(),
123            credentials: None,
124        }
125    }
126
127    /// Set the port, to which the HttpsFS will connect.
128    ///
129    /// Default is 443.
130    pub fn set_port(mut self, port: u16) -> Self {
131        self.port = port;
132        self
133    }
134
135    /// Overwrites the domain name, which was set while creating the builder.
136    pub fn set_domain(mut self, domain: &str) -> Self {
137        self.domain = String::from(domain);
138        self
139    }
140
141    /// Adds an additional root certificate.
142    ///
143    /// If a self signed certificate is used during, the development,
144    /// than the certificate has to be added with this call, otherwise
145    /// the [HttpsFS] fails to connect to the [crate::HttpsFSServer].
146    pub fn add_root_certificate(mut self, cert: &str) -> Self {
147        let cert = HttpsFS::load_certificate(cert).unwrap();
148        self.root_certs.push(cert);
149        self
150    }
151
152    /// If the [crate::HttpsFSServer] request a authentication, than this function will
153    /// be called to get the credentials. The first value of the returned tuple
154    /// is the user name and the second value is the password.
155    pub fn set_credential_provider(
156        mut self,
157        c_provider: fn(realm: &str) -> (String, String),
158    ) -> Self {
159        self.credentials = Some(c_provider);
160        self
161    }
162
163    /// Generates a HttpsFS with the set configuration
164    ///
165    /// # Error
166    ///
167    /// Returns an error, if the credential provider was not set.
168    pub fn build(self) -> HttpsFSResult<HttpsFS> {
169        if self.credentials.is_none() {
170            return Err(HttpsFSError::Other {
171                message: "HttpsFSBuilder: No credential provider set.".to_string(),
172            });
173        }
174        let mut client = Client::builder().https_only(true).cookie_store(true);
175        for cert in self.root_certs {
176            client = client.add_root_certificate(cert);
177        }
178
179        let client = client.build()?;
180        Ok(HttpsFS {
181            client: std::sync::Arc::new(client),
182            addr: format!("https://{}:{}/", self.domain, self.port),
183            credentials: self.credentials,
184        })
185    }
186}
187
188impl Write for WritableFile {
189    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
190        let req = Command::Write(CommandWrite {
191            path: self.file_name.clone(),
192            pos: self.position,
193            len: buf.len() as u64,
194            data: base64::encode(buf),
195        });
196        let req = serde_json::to_string(&req)?;
197        let result = self.client.post(&self.addr).body(req).send();
198        if let Err(e) = result {
199            return Err(std::io::Error::new(
200                std::io::ErrorKind::Other,
201                format!("{:?}", e),
202            ));
203        }
204        let result = result.unwrap();
205        let result = result.text();
206        if let Err(e) = result {
207            return Err(std::io::Error::new(
208                std::io::ErrorKind::Other,
209                format!("{:?}", e),
210            ));
211        }
212        let result = result.unwrap();
213        let result: CommandResponse = serde_json::from_str(&result)?;
214        match result {
215            CommandResponse::Write(result) => match result {
216                Ok(size) => {
217                    self.position += size as u64;
218                    Ok(size)
219                }
220                Err(e) => Err(std::io::Error::new(
221                    std::io::ErrorKind::Other,
222                    format!("{:?}", e),
223                )),
224            },
225            _ => Err(std::io::Error::new(
226                std::io::ErrorKind::Other,
227                String::from("Result doesn't match the request!"),
228            )),
229        }
230    }
231
232    fn flush(&mut self) -> std::io::Result<()> {
233        todo!("flush()");
234    }
235}
236
237impl Read for ReadableFile {
238    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
239        let req = Command::Read(CommandRead {
240            path: self.file_name.clone(),
241            pos: self.position,
242            len: buf.len() as u64,
243        });
244        let req = serde_json::to_string(&req)?;
245        let result = self.client.post(&self.addr).body(req).send();
246        if let Err(e) = result {
247            return Err(std::io::Error::new(
248                std::io::ErrorKind::Other,
249                format!("{:?}", e),
250            ));
251        }
252        let result = result.unwrap();
253        let result = result.text();
254        if let Err(e) = result {
255            return Err(std::io::Error::new(
256                std::io::ErrorKind::Other,
257                format!("{:?}", e),
258            ));
259        }
260        let result = result.unwrap();
261        let result: CommandResponse = serde_json::from_str(&result)?;
262        match result {
263            CommandResponse::Read(result) => match result {
264                Ok((size, data)) => {
265                    self.position += size as u64;
266                    let decoded_data = base64::decode(data);
267                    let mut result = Err(std::io::Error::new(
268                        std::io::ErrorKind::Other,
269                        String::from("Faild to decode data"),
270                    ));
271                    if let Ok(data) = decoded_data {
272                        buf[..size].copy_from_slice(&data.as_slice()[..size]);
273                        result = Ok(size);
274                    }
275                    result
276                }
277                Err(e) => Err(std::io::Error::new(
278                    std::io::ErrorKind::Other,
279                    format!("{:?}", e),
280                )),
281            },
282            _ => Err(std::io::Error::new(
283                std::io::ErrorKind::Other,
284                String::from("Result doesn't match the request!"),
285            )),
286        }
287    }
288}
289
290impl Seek for ReadableFile {
291    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
292        match pos {
293            std::io::SeekFrom::Start(offset) => self.position = offset,
294            std::io::SeekFrom::Current(offset) => {
295                self.position = (self.position as i64 + offset) as u64
296            }
297            std::io::SeekFrom::End(offset) => {
298                let fs = HttpsFS {
299                    addr: self.addr.clone(),
300                    client: self.client.clone(),
301                    credentials: None,
302                };
303                let meta = fs.metadata(&self.file_name);
304                if let Err(e) = meta {
305                    return Err(std::io::Error::new(
306                        std::io::ErrorKind::Other,
307                        format!("{:?}", e),
308                    ));
309                }
310                let meta = meta.unwrap();
311                self.position = (meta.len as i64 + offset) as u64
312            }
313        }
314        Ok(self.position)
315    }
316}
317
318impl FileSystem for HttpsFS {
319    fn read_dir(&self, path: &str) -> VfsResult<Box<dyn Iterator<Item = String>>> {
320        let req = Command::ReadDir(CommandReadDir {
321            path: String::from(path),
322        });
323        let result = self.exec_command(&req)?;
324        let result = match result {
325            CommandResponse::ReadDir(value) => value,
326            _ => {
327                return Err(VfsError::Other {
328                    message: String::from("Result doesn't match the request!"),
329                });
330            }
331        };
332        match result.result {
333            Err(e) => Err(VfsError::Other { message: e }),
334            Ok(value) => Ok(Box::new(value.into_iter())),
335        }
336    }
337
338    fn create_dir(&self, path: &str) -> VfsResult<()> {
339        let req = Command::CreateDir(CommandCreateDir {
340            path: String::from(path),
341        });
342        let result = self.exec_command(&req)?;
343        let result = match result {
344            CommandResponse::CreateDir(value) => value,
345            _ => {
346                return Err(VfsError::Other {
347                    message: String::from("Result doesn't match the request!"),
348                });
349            }
350        };
351
352        match result {
353            CommandResponseCreateDir::Failed => Err(VfsError::Other {
354                message: String::from("Result doesn't match the request!"),
355            }),
356            CommandResponseCreateDir::Success => Ok(()),
357        }
358    }
359
360    fn open_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndRead>> {
361        if !self.exists(path)? {
362            return Err(VfsError::FileNotFound {
363                path: path.to_string(),
364            });
365        }
366
367        Ok(Box::new(ReadableFile {
368            client: self.client.clone(),
369            addr: self.addr.clone(),
370            file_name: String::from(path),
371            position: 0,
372        }))
373    }
374
375    fn create_file(&self, path: &str) -> VfsResult<Box<dyn Write>> {
376        let req = Command::CreateFile(CommandCreateFile {
377            path: String::from(path),
378        });
379        let result = self.exec_command(&req)?;
380        let result = match result {
381            CommandResponse::CreateFile(value) => value,
382            _ => {
383                return Err(VfsError::Other {
384                    message: String::from("Result doesn't match the request!"),
385                });
386            }
387        };
388
389        match result {
390            CommandResponseCreateFile::Failed => Err(VfsError::Other {
391                message: String::from("Faild to create file!"),
392            }),
393            CommandResponseCreateFile::Success => Ok(Box::new(WritableFile {
394                client: self.client.clone(),
395                addr: self.addr.clone(),
396                file_name: String::from(path),
397                position: 0,
398            })),
399        }
400    }
401
402    fn append_file(&self, path: &str) -> VfsResult<Box<dyn Write>> {
403        let meta = self.metadata(path)?;
404        Ok(Box::new(WritableFile {
405            client: self.client.clone(),
406            addr: self.addr.clone(),
407            file_name: String::from(path),
408            position: meta.len,
409        }))
410    }
411
412    fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> {
413        let req = Command::Metadata(CommandMetadata {
414            path: String::from(path),
415        });
416        let result = self.exec_command(&req)?;
417        match result {
418            CommandResponse::Metadata(value) => meta_res_convert_cmd_vfs(value),
419            _ => Err(VfsError::Other {
420                message: String::from("Result doesn't match the request!"),
421            }),
422        }
423    }
424
425    fn exists(&self, path: &str) -> VfsResult<bool> {
426        // TODO: Add more logging
427        // TODO: try to change return type to VfsResult<bool>
428        //       At the moment 'false' does not mean, that the file either does not exist
429        //       or that an error has occurred. An developer does not expect this.
430        let req = Command::Exists(CommandExists {
431            path: String::from(path),
432        });
433        let result = self.exec_command(&req)?;
434        let result = match result {
435            CommandResponse::Exists(value) => value,
436            _ => {
437                return Err(VfsError::Other {
438                    message: String::from("Result doesn't match the request!"),
439                });
440            }
441        };
442        match result {
443            Err(e) => Err(VfsError::Other {
444                message: format!("{:?}", e),
445            }),
446            Ok(val) => Ok(val),
447        }
448    }
449
450    fn remove_file(&self, path: &str) -> VfsResult<()> {
451        let req = Command::RemoveFile(CommandRemoveFile {
452            path: String::from(path),
453        });
454        let result = self.exec_command(&req)?;
455        let result = match result {
456            CommandResponse::RemoveFile(value) => value,
457            _ => {
458                return Err(VfsError::Other {
459                    message: String::from("Result doesn't match the request!"),
460                });
461            }
462        };
463
464        match result {
465            Err(e) => Err(VfsError::Other {
466                message: format!("{:?}", e),
467            }),
468            Ok(_) => Ok(()),
469        }
470    }
471
472    fn remove_dir(&self, path: &str) -> VfsResult<()> {
473        let req = Command::RemoveDir(CommandRemoveDir {
474            path: String::from(path),
475        });
476        let result = self.exec_command(&req)?;
477        let result = match result {
478            CommandResponse::RemoveDir(value) => value,
479            _ => {
480                return Err(VfsError::Other {
481                    message: String::from("Result doesn't match the request!"),
482                });
483            }
484        };
485
486        match result {
487            Err(e) => Err(VfsError::Other {
488                message: format!("{:?}", e),
489            }),
490            Ok(_) => Ok(()),
491        }
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use crate::{HttpsFS, HttpsFSServer};
498    use lazy_static::lazy_static;
499    use std::sync::{Arc, Mutex};
500    use vfs::{test_vfs, MemoryFS};
501
502    // Since we create a HttpsFSServer for each unit test, which are all executed
503    // in parallel we have to ensure, that each server is listening on a different
504    // port. This is done with this shared variable.
505    // WARNING: It will not be tested, whether a port is already used by another
506    //          program. In such a case, the corresponding unit test most likely
507    //          fails.
508    lazy_static! {
509        static ref PORT: Arc<Mutex<u16>> = Arc::new(Mutex::new(8344));
510    }
511
512    test_vfs!({
513        let server_port;
514        match PORT.lock() {
515            Ok(mut x) => {
516                println!("Number: {}", *x);
517                server_port = *x;
518                *x += 1;
519            }
520            Err(e) => panic!("Error: {:?}", e),
521        }
522        std::thread::spawn(move || {
523            let fs = MemoryFS::new();
524            let server = HttpsFSServer::builder(fs)
525                .set_port(server_port)
526                .load_certificates("examples/cert/cert.crt")
527                .load_private_key("examples/cert/private-key.key")
528                .set_credential_validator(|username: &str, password: &str| {
529                    username == "user" && password == "pass"
530                });
531            let result = server.run();
532            if let Err(e) = result {
533                println!("WARNING: {:?}", e);
534            }
535        });
536
537        // make sure, that the server is ready for the unit tests
538        let duration = std::time::Duration::from_millis(10);
539        std::thread::sleep(duration);
540
541        HttpsFS::builder("localhost")
542            .set_port(server_port)
543            // load self signed certificate
544            // WARNING: When the certificate expire, than the unit tests will frail.
545            //          In this case, a new certificate hast to be generated.
546            .add_root_certificate("examples/cert/cert.crt")
547            .set_credential_provider(|_| (String::from("user"), String::from("pass")))
548            .build()
549            .unwrap()
550    });
551}