Skip to content

Commit a943a68

Browse files
committed
HttpsFS: Add authentification.
1 parent 5033a60 commit a943a68

File tree

4 files changed

+201
-8
lines changed

4 files changed

+201
-8
lines changed

examples/https_fs.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,16 @@ fn main() -> vfs::VfsResult<()> {
3030
// If the server uses a certificate issued by a official
3131
// certificate authority, than we don't need to add an additional
3232
// certificate.
33-
.add_root_certificate(cert);
33+
.add_root_certificate(cert)
34+
// if the server requests a authentification, than this method is called to
35+
// get the credentials for the authentification
36+
.set_credential_provider(|server_msg| {
37+
println!(
38+
"Server request authentification with message \"{}\".",
39+
server_msg
40+
);
41+
(String::from("user"), String::from("pass"))
42+
});
3443
let root: VfsPath = builder.build().unwrap().into();
3544
let root = root.join("example.txt")?;
3645

examples/https_fs_server.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@ fn main() {
2020
// different port.
2121
let port = 8443;
2222

23+
// The server will use this method to validate, whether the login credentials
24+
// are valide or not. In this example, only the username 'user' and the password
25+
// 'pass' is accepted.
26+
// As authentication process, 'Basic' method as defined by the
27+
// [RFC7617](https://tools.ietf.org/html/rfc7617) is used.
28+
let credential_validator =
29+
|username: &str, password: &str| username == "user" && password == "pass";
30+
2331
// Initiate the server object
24-
let mut server = HttpsFSServer::new(port, cert, private_key, fs);
32+
let mut server = HttpsFSServer::new(port, cert, private_key, fs, credential_validator);
2533

2634
// Start the server.
2735
server.run().unwrap();

src/impls/https.rs

Lines changed: 158 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
//! For an example see example directory.
2525
//!
2626
//! TODO:
27-
//! - Implement authentication process
2827
//! - Implement a [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface)
2928
//! version of the HttpsFSServer.
3029
//! * This would allow a user to use any webserver provided by its
@@ -53,7 +52,7 @@ use async_stream::stream;
5352
use chrono::prelude::*;
5453
use core::task::{Context, Poll};
5554
use futures_util::stream::Stream;
56-
use hyper::header::{COOKIE, SET_COOKIE};
55+
use hyper::header::{AUTHORIZATION, COOKIE, SET_COOKIE, WWW_AUTHENTICATE};
5756
use hyper::service::{make_service_fn, service_fn};
5857
use hyper::{Body, Method, Request, Response, Server, StatusCode};
5958
use rand::prelude::*;
@@ -70,19 +69,24 @@ use tokio_rustls::server::TlsStream;
7069
use tokio_rustls::TlsAcceptor;
7170

7271
mod httpsfserror;
72+
use httpsfserror::AuthError;
7373
use httpsfserror::HttpsFSError;
7474

7575
/// A file system exposed over https
7676
pub struct HttpsFS {
7777
addr: String,
7878
client: std::sync::Arc<reqwest::blocking::Client>,
79+
/// Will be called to get login credentials for the authentication process.
80+
/// Return value is a tuple: The first part is the user name, the second part the password.
81+
credentials: Option<fn(realm: &str) -> (String, String)>,
7982
}
8083

8184
/// Helper structure for building HttpsFS structs
8285
pub struct HttpsFSBuilder {
8386
port: u16,
8487
domain: String,
8588
root_certs: Vec<reqwest::Certificate>,
89+
credentials: Option<fn(realm: &str) -> (String, String)>,
8690
}
8791

8892
/// A https server providing a interface for HttpsFS
@@ -92,11 +96,13 @@ pub struct HttpsFSServer<T: FileSystem> {
9296
private_key: rustls::PrivateKey,
9397
file_system: std::sync::Arc<std::sync::Mutex<T>>,
9498
client_data: std::sync::Arc<std::sync::Mutex<HashMap<String, HttpsFSServerClientData>>>,
99+
credential_validator: fn(user: &str, password: &str) -> bool,
95100
}
96101

97102
#[derive(Debug)]
98103
struct HttpsFSServerClientData {
99104
last_use: DateTime<Local>,
105+
authorized: bool,
100106
}
101107

102108
struct WritableFile {
@@ -326,11 +332,50 @@ impl HttpsFS {
326332

327333
fn exec_command(&self, cmd: &Command) -> Result<CommandResponse, HttpsFSError> {
328334
let req = serde_json::to_string(&cmd)?;
329-
let result = self.client.post(&self.addr).body(req).send()?;
335+
let mut result = self.client.post(&self.addr).body(req).send()?;
336+
if result.status() == StatusCode::UNAUTHORIZED {
337+
let req = serde_json::to_string(&cmd)?;
338+
result = self
339+
.authorize(&result, self.client.post(&self.addr).body(req))?
340+
.send()?;
341+
if result.status() != StatusCode::OK {
342+
return Err(HttpsFSError::Auth(AuthError::Failed));
343+
}
344+
}
330345
let result = result.text()?;
331346
let result: CommandResponse = serde_json::from_str(&result)?;
332347
Ok(result)
333348
}
349+
350+
fn authorize(
351+
&self,
352+
prev_response: &reqwest::blocking::Response,
353+
new_request: reqwest::blocking::RequestBuilder,
354+
) -> Result<reqwest::blocking::RequestBuilder, HttpsFSError> {
355+
if self.credentials.is_none() {
356+
return Err(HttpsFSError::Auth(AuthError::NoCredentialSource));
357+
}
358+
let prev_headers = prev_response.headers();
359+
let auth_method = prev_headers
360+
.get(WWW_AUTHENTICATE)
361+
.ok_or(HttpsFSError::Auth(AuthError::NoMethodSpecified))?;
362+
let auth_method = String::from(
363+
auth_method
364+
.to_str()
365+
.map_err(|_| HttpsFSError::InvalidHeader(WWW_AUTHENTICATE.to_string()))?,
366+
);
367+
// TODO: this is a fix hack since we currently only support one method. If we start to
368+
// support more than one authentication method, we have to properly parse this header.
369+
// Furthermore, currently only the 'PME'-Realm is supported.
370+
let start_with = "Basic realm=\"PME\"";
371+
if !auth_method.starts_with(start_with) {
372+
return Err(HttpsFSError::Auth(AuthError::MethodNotSupported));
373+
}
374+
let get_cred = self.credentials.unwrap();
375+
let (username, password) = get_cred(&"PME");
376+
let new_request = new_request.basic_auth(username, Some(password));
377+
Ok(new_request)
378+
}
334379
}
335380

336381
impl HttpsFSBuilder {
@@ -339,6 +384,7 @@ impl HttpsFSBuilder {
339384
port: 443,
340385
domain: String::from(domain),
341386
root_certs: Vec::new(),
387+
credentials: None,
342388
}
343389
}
344390

@@ -357,7 +403,20 @@ impl HttpsFSBuilder {
357403
self
358404
}
359405

406+
pub fn set_credential_provider(
407+
mut self,
408+
c_provider: fn(realm: &str) -> (String, String),
409+
) -> Self {
410+
self.credentials = Some(c_provider);
411+
self
412+
}
413+
360414
pub fn build(self) -> VfsResult<HttpsFS> {
415+
if self.credentials.is_none() {
416+
return Err(VfsError::Other {
417+
message: format!("HttpsFSBuilder: No credential provider set."),
418+
});
419+
}
361420
let mut client = Client::builder().https_only(true).cookie_store(true);
362421
for cert in self.root_certs {
363422
client = client.add_root_certificate(cert);
@@ -367,6 +426,7 @@ impl HttpsFSBuilder {
367426
Ok(HttpsFS {
368427
client: std::sync::Arc::new(client),
369428
addr: format!("https://{}:{}/", self.domain, self.port),
429+
credentials: self.credentials,
370430
})
371431
}
372432
}
@@ -399,6 +459,7 @@ impl HttpsFSServerClientData {
399459
fn new() -> Self {
400460
HttpsFSServerClientData {
401461
last_use: Local::now(),
462+
authorized: false,
402463
}
403464
}
404465
}
@@ -409,6 +470,7 @@ impl<T: FileSystem> HttpsFSServer<T> {
409470
certs: Vec<rustls::Certificate>,
410471
private_key: rustls::PrivateKey,
411472
file_system: T,
473+
credential_validator: fn(user: &str, password: &str) -> bool,
412474
) -> Self {
413475
// Initially i tried to store a hyper::server::Server object in HttpsFSServer.
414476
// I failed, since this type is a very complicated generic and i could
@@ -435,6 +497,7 @@ impl<T: FileSystem> HttpsFSServer<T> {
435497
private_key,
436498
file_system: std::sync::Arc::new(std::sync::Mutex::new(file_system)),
437499
client_data: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
500+
credential_validator,
438501
}
439502
}
440503

@@ -444,6 +507,7 @@ impl<T: FileSystem> HttpsFSServer<T> {
444507
let addr = format!("127.0.0.1:{}", self.port);
445508
let fs = self.file_system.clone();
446509
let cd = self.client_data.clone();
510+
let cv = self.credential_validator.clone();
447511

448512
let mut cfg = rustls::ServerConfig::new(rustls::NoClientAuth::new());
449513
cfg.set_single_cert(self.certs.clone(), self.private_key.clone())
@@ -507,7 +571,7 @@ impl<T: FileSystem> HttpsFSServer<T> {
507571
move |request| {
508572
let fs = fs.clone();
509573
let cd = cd.clone();
510-
HttpsFSServer::https_fs_service(fs, cd, request)
574+
HttpsFSServer::https_fs_service(fs, cd, cv, request)
511575
},
512576
))
513577
}
@@ -529,12 +593,32 @@ impl<T: FileSystem> HttpsFSServer<T> {
529593
async fn https_fs_service(
530594
file_system: std::sync::Arc<std::sync::Mutex<T>>,
531595
client_data: std::sync::Arc<std::sync::Mutex<HashMap<String, HttpsFSServerClientData>>>,
596+
credential_validator: fn(user: &str, pass: &str) -> bool,
532597
req: Request<Body>,
533598
) -> Result<Response<Body>, hyper::Error> {
599+
// TODO: Separate Session, authorization and content handling in different methods.
534600
let mut response = Response::new(Body::empty());
535601

536602
HttpsFSServer::<T>::clean_up_client_data(&client_data);
537603
let sess_id = HttpsFSServer::<T>::get_session_id(&client_data, &req, &mut response);
604+
let auth_res =
605+
HttpsFSServer::<T>::try_auth(&client_data, &sess_id, &credential_validator, &req);
606+
match auth_res {
607+
Err(()) => {
608+
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
609+
return Ok(response);
610+
}
611+
Ok(value) => {
612+
if !value {
613+
*response.status_mut() = StatusCode::UNAUTHORIZED;
614+
response.headers_mut().insert(
615+
WWW_AUTHENTICATE,
616+
"Basic realm=\"PME\", charset=\"UTF-8\"".parse().unwrap(),
617+
);
618+
return Ok(response);
619+
}
620+
}
621+
}
538622

539623
match (req.method(), req.uri().path()) {
540624
(&Method::POST, "/") => {
@@ -728,6 +812,69 @@ impl<T: FileSystem> HttpsFSServer<T> {
728812

729813
return sess_id;
730814
}
815+
816+
fn try_auth(
817+
client_data: &std::sync::Arc<std::sync::Mutex<HashMap<String, HttpsFSServerClientData>>>,
818+
sess_id: &str,
819+
credential_validator: &fn(user: &str, pass: &str) -> bool,
820+
request: &Request<Body>,
821+
) -> Result<bool, ()> {
822+
let mut client_data = client_data.lock().unwrap();
823+
let sess_data = client_data.get_mut(sess_id);
824+
if let None = sess_data {
825+
return Err(());
826+
}
827+
let sess_data = sess_data.unwrap();
828+
829+
// try to authenticate client
830+
if !sess_data.authorized {
831+
let headers = request.headers();
832+
let auth = headers.get(AUTHORIZATION);
833+
if let None = auth {
834+
return Ok(false);
835+
}
836+
let auth = auth.unwrap().to_str();
837+
if let Err(_) = auth {
838+
return Ok(false);
839+
}
840+
let auth = auth.unwrap();
841+
let starts = "Basic ";
842+
if !auth.starts_with(starts) {
843+
return Ok(false);
844+
}
845+
let auth = base64::decode(&auth[starts.len()..]);
846+
if let Err(_) = auth {
847+
return Ok(false);
848+
}
849+
let auth = auth.unwrap();
850+
let auth = String::from_utf8(auth);
851+
if let Err(_) = auth {
852+
return Ok(false);
853+
}
854+
let auth = auth.unwrap();
855+
let mut auth_it = auth.split(":");
856+
let username = auth_it.next();
857+
if let None = username {
858+
return Ok(false);
859+
}
860+
let username = username.unwrap();
861+
let pass = auth_it.next();
862+
if let None = pass {
863+
return Ok(false);
864+
}
865+
let pass = pass.unwrap();
866+
if credential_validator(username, pass) {
867+
sess_data.authorized = true;
868+
}
869+
}
870+
871+
// if not authenticated, than inform client about it.
872+
if sess_data.authorized {
873+
return Ok(true);
874+
}
875+
876+
return Ok(false);
877+
}
731878
}
732879

733880
/// Load public certificate from file
@@ -903,6 +1050,7 @@ impl Seek for ReadableFile {
9031050
let fs = HttpsFS {
9041051
addr: self.addr.clone(),
9051052
client: self.client.clone(),
1053+
credentials: None,
9061054
};
9071055
let meta = fs.metadata(&self.file_name);
9081056
if let Err(e) = meta {
@@ -1038,7 +1186,7 @@ impl FileSystem for HttpsFS {
10381186
});
10391187
let result = self.exec_command(&req);
10401188
if let Err(e) = result {
1041-
println!("Error: {:?}", e);
1189+
println!("Error: {}", e);
10421190
return false;
10431191
}
10441192
match result.unwrap() {
@@ -1124,7 +1272,10 @@ mod tests {
11241272
let fs = MemoryFS::new();
11251273
let cert = load_certs("examples/cert/cert.crt").unwrap();
11261274
let private_key = load_private_key("examples/cert/private-key.key").unwrap();
1127-
let mut server = HttpsFSServer::new(server_port, cert, private_key, fs);
1275+
let credential_validator =
1276+
|username: &str, password: &str| username == "user" && password == "pass";
1277+
let mut server =
1278+
HttpsFSServer::new(server_port, cert, private_key, fs, credential_validator);
11281279
let result = server.run();
11291280
if let Err(e) = result {
11301281
println!("WARNING: {:?}", e);
@@ -1142,6 +1293,7 @@ mod tests {
11421293
HttpsFS::builder("localhost")
11431294
.set_port(server_port)
11441295
.add_root_certificate(cert)
1296+
.set_credential_provider(|_| (String::from("user"), String::from("pass")))
11451297
.build()
11461298
.unwrap()
11471299
});

src/impls/https/httpsfserror.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@ pub enum HttpsFSError {
88

99
#[error("Network error: {0}")]
1010
Network(reqwest::Error),
11+
12+
#[error("Authentification Error: {0}")]
13+
Auth(AuthError),
14+
15+
#[error("Error while parsing a http header: {0}")]
16+
InvalidHeader(String),
17+
}
18+
19+
#[derive(Error, Debug)]
20+
pub enum AuthError {
21+
#[error("Server didn't specified a authentification method.")]
22+
NoMethodSpecified,
23+
#[error("Authentification method, requested by server, is not supported.")]
24+
MethodNotSupported,
25+
#[error("No credential source set. (Use HttpsFS::builder().set_credential_provider()).")]
26+
NoCredentialSource,
27+
#[error("Faild. (Password or username wrong?)")]
28+
Failed,
1129
}
1230

1331
impl From<serde_json::Error> for HttpsFSError {
@@ -31,6 +49,12 @@ impl From<HttpsFSError> for VfsError {
3149
HttpsFSError::Network(_) => VfsError::Other {
3250
message: format!("{}", error),
3351
},
52+
HttpsFSError::Auth(_) => VfsError::Other {
53+
message: format!("{}", error),
54+
},
55+
HttpsFSError::InvalidHeader(_) => VfsError::Other {
56+
message: format!("{}", error),
57+
},
3458
});
3559
VfsError::WithContext {
3660
context: String::from("HttpsFS"),

0 commit comments

Comments
 (0)