rustream/squire/
authenticator.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::sync::Arc;
4
5use actix_web::{HttpRequest, web};
6use actix_web::http::header::HeaderValue;
7use chrono::Utc;
8use fernet::Fernet;
9
10use crate::constant;
11use crate::squire;
12
13/// Represents user credentials extracted from an authorization header.
14///
15/// Contains the username, signature, and timestamp obtained by decoding and parsing the authorization header.
16struct Credentials {
17    username: String,
18    signature: String,
19    timestamp: String,
20}
21
22/// Represents the result of authentication, indicating whether it was successful or not.
23///
24/// If successful, it includes the username and a generated key for the session.
25pub struct AuthToken {
26    pub ok: bool,
27    pub detail: String,
28    pub username: String,
29    pub time_left: i64
30}
31
32
33/// Extracts credentials from the authorization header in the following steps
34///
35/// # Arguments
36///
37/// * `authorization` - An optional `HeaderValue` containing the authorization header.
38///
39/// # See Also
40/// - Decodes the base64 encoded header
41/// - Splits it into 3 parts with first one being the username followed by the signature and timestamp
42/// - Converts the username from hex into a string.
43///
44/// # Returns
45///
46/// Returns a `Result` containing the extracted `Credentials` or an error message if extraction fails.
47fn extract_credentials(authorization: &HeaderValue) -> Result<Credentials, &'static str> {
48    let header = authorization.to_str().unwrap().to_string();
49    // base64 encoded in JavaScript using inbuilt btoa function
50    let b64_decode_response = squire::secure::base64_decode(&header);
51    return match b64_decode_response {
52        Ok(decoded_auth) => {
53            if decoded_auth.is_empty() {
54                log::warn!("Authorization header was received without a value");
55                return Err("No credentials received");
56            }
57            let vector: Vec<&str> = decoded_auth.split(',').collect();
58            Ok(Credentials {
59                // Decode hex username into string to retrieve password from config file
60                username: squire::secure::hex_decode(vector.first().unwrap()),
61                signature: vector.get(1).unwrap().to_string(),
62                timestamp: vector.get(2).unwrap().to_string(),
63            })
64        }
65        Err(err) => {
66            Err(err)
67        }
68    };
69}
70
71/// Verifies user login based on extracted credentials and configuration settings.
72///
73/// # Arguments
74///
75/// * `request` - A reference to the Actix web `HttpRequest` object.
76/// * `config` - Configuration data for the application.
77/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
78///
79/// # Returns
80///
81/// Returns a `Result` containing a `HashMap` with session information if authentication is successful,
82/// otherwise returns an error message.
83pub fn verify_login(
84    request: &HttpRequest,
85    config: &web::Data<Arc<squire::settings::Config>>,
86    session: &web::Data<Arc<constant::Session>>,
87) -> Result<HashMap<&'static str, String>, String> {
88    let err_response;
89    if let Some(authorization) = request.headers().get("authorization") {
90        let extracted_credentials = extract_credentials(authorization);
91        match extracted_credentials {
92            Ok(credentials) => {
93                // Check if the username is present in HashMap as key
94                if let Some(password) = config.authorization.get(&credentials.username) {
95                    let message = format!("{}{}{}",
96                                          squire::secure::hex_encode(&credentials.username),
97                                          squire::secure::hex_encode(password),
98                                          credentials.timestamp);
99                    // Create a new signature with hex encoded username and password stored in config file as plain text
100                    let expected_signature = squire::secure::calculate_hash(message);
101                    if expected_signature == credentials.signature {
102                        let key = squire::secure::keygen();
103                        session.mapping.lock().unwrap().insert(credentials.username.to_string(), key.to_string());
104                        let mut mapped = HashMap::new();
105                        mapped.insert("username", credentials.username.to_string());
106                        mapped.insert("key", key.to_string());
107                        mapped.insert("timestamp", credentials.timestamp.to_string());
108                        return Ok(mapped);
109                    } else {
110                        log::warn!("{} entered bad credentials", credentials.username);
111                        err_response = "Incorrect username or password";
112                    }
113                } else {
114                    log::warn!("{} is not allowed", credentials.username);
115                    err_response = "Incorrect username or password";
116                }
117            }
118            Err(err) => {
119                err_response = err;
120            }
121        }
122    } else {
123        log::warn!("Authorization header was missing");
124        err_response = "No credentials received";
125    }
126    Err(err_response.to_string())
127}
128
129/// Verifies a session token extracted from an HTTP request against stored session mappings and configuration.
130///
131/// # Arguments
132///
133/// * `request` - A reference to the Actix web `HttpRequest` object.
134/// * `config` - Configuration data for the application.
135/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
136/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
137///
138/// # Returns
139///
140/// Returns an instance of the `AuthToken` struct indicating the result of the token verification.
141pub fn verify_token(
142    request: &HttpRequest,
143    config: &squire::settings::Config,
144    fernet: &Fernet,
145    session: &constant::Session,
146) -> AuthToken {
147    if session.mapping.lock().unwrap().is_empty() {
148        log::warn!("No stored sessions, no point in validating further");
149        return AuthToken {
150            ok: false,
151            detail: "Server doesn't recognize your session".to_string(),
152            username: "NA".to_string(),
153            time_left: 0
154        };
155    }
156    if let Some(cookie) = request.cookie("session_token") {
157        if let Ok(decrypted) = fernet.decrypt(cookie.value()) {
158            let payload: HashMap<String, String> = serde_json::from_str(&String::from_utf8_lossy(&decrypted)).unwrap();
159            let username = payload.get("username").unwrap().to_string();
160            let cookie_key = payload.get("key").unwrap().to_string();
161            let timestamp = payload.get("timestamp").unwrap().parse::<i64>().unwrap();
162            let stored_key = session.mapping.lock().unwrap().get(&username).unwrap().to_string();
163            let current_time = Utc::now().timestamp();
164            // Max time and expiry for session token is set in the Cookie, but this is a fallback mechanism
165            if stored_key != *cookie_key {
166                return AuthToken {
167                    ok: false,
168                    detail: "Invalid session token".to_string(),
169                    username,
170                    time_left: 0
171                };
172            }
173            if current_time - timestamp > config.session_duration {
174                return AuthToken {
175                    ok: false,
176                    detail: "Session Expired".to_string(),
177                    username,
178                    time_left: 0
179                };
180            }
181            let time_left = timestamp + config.session_duration - current_time;
182            AuthToken {
183                ok: true,
184                detail: format!("Session valid for {}s", time_left),
185                username,
186                time_left
187            }
188        } else {
189            AuthToken {
190                ok: false,
191                detail: "Invalid session token".to_string(),
192                username: "NA".to_string(),
193                time_left: 0
194            }
195        }
196    } else {
197        AuthToken {
198            ok: false,
199            detail: "Session information not found".to_string(),
200            username: "NA".to_string(),
201            time_left: 0
202        }
203    }
204}
205
206/// Verifies the secure index of the directory/file that's being accessed.
207///
208/// # Arguments
209/// * `path` - A reference to the `PathBuf` object that's being accessed.
210/// * `username` - Username of the session.
211///
212/// ## See Also
213/// **Content delivery endpoints**
214/// * `stream` - Servers the content's landing page.
215/// * `track` - Servers the content's subtitles track.
216/// * `media` - Servers the content as a streaming response.
217/// * `home` - Servers the content's listing page.
218///
219/// **Endpoints that require secure index validation**
220/// * `stream` - Handles validation for both the landing page and subdirectories.
221/// * `media` - Handles validation for streaming the requested content.
222/// * `track` - Handles validation for subtitles track file.
223///
224/// # Returns
225///
226/// Returns a boolean value to indicate if the access can be granted.
227pub fn verify_secure_index(path: &Path, username: &String) -> bool {
228    for dir in path.iter() {
229        let child = dir.to_string_lossy().to_string();
230        if child.ends_with(constant::SECURE_INDEX) && child != format!("{}_{}", username, constant::SECURE_INDEX) {
231            let user_dir = child
232                .strip_suffix(constant::SECURE_INDEX).unwrap()
233                .strip_suffix('_').unwrap();
234            log::warn!("'{}' tried to access {:?} that belongs to '{}'", username, path, user_dir);
235            return false;
236        }
237    }
238    true
239}