Skip to content

Commit

Permalink
teliod: Add qnap authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
lcruz99 committed Jan 9, 2025
1 parent 0c1ee30 commit f4edb99
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 7 deletions.
Empty file added .unreleased/LLT-5812
Empty file.
7 changes: 1 addition & 6 deletions clis/teliod/src/cgi/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub(crate) fn handle_api(request: &CgiRequest) -> Option<Response> {
(&Method::GET, "/get-status") => Some(get_status()),
(&Method::GET, "/get-teliod-logs") => Some(get_teliod_logs()),
(&Method::GET, "/get-meshnet-logs") => Some(get_meshnet_logs()),
(_, _) => Some(text_response(StatusCode::BAD_REQUEST, "Invalid request.")),
(_, _) => Some(text_response(StatusCode::NOT_FOUND, "Inexistent endpoint")),
}
}

Expand Down Expand Up @@ -230,7 +230,6 @@ mod tests {
use reqwest::StatusCode;
use serial_test::serial;
use tracing::level_filters::LevelFilter;
use uuid::Uuid;

use super::{update_config, TeliodDaemonConfig};
use crate::{
Expand All @@ -249,7 +248,6 @@ mod tests {
name: "eth0".to_owned(),
config_provider: InterfaceConfigurationProvider::Manual,
},
app_user_uid: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(),
authentication_token:
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(),
http_certificate_file_path: Some(PathBuf::from("/http/certificate/path/")),
Expand Down Expand Up @@ -290,8 +288,6 @@ mod tests {
expected_config.log_file_path = "/new/path/to/log".to_owned();
expected_config.authentication_token =
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_owned();
expected_config.app_user_uid =
Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
expected_config.interface = InterfaceConfig {
name: "eth1".to_owned(),
config_provider: InterfaceConfigurationProvider::Ifconfig,
Expand Down Expand Up @@ -338,7 +334,6 @@ mod tests {
name: "eth0".to_owned(),
config_provider: InterfaceConfigurationProvider::Manual,
},
app_user_uid: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(),
authentication_token:
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(),
http_certificate_file_path: Some(PathBuf::from("/http/certificate/path/")),
Expand Down
7 changes: 7 additions & 0 deletions clis/teliod/src/cgi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use rust_cgi::{http::StatusCode, text_response, Request, Response};

mod api;
pub(crate) mod constants;
#[cfg(feature = "qnap")]
mod qnap;

pub struct CgiRequest {
pub inner: Request,
Expand Down Expand Up @@ -33,6 +35,11 @@ impl Deref for CgiRequest {
pub fn handle_request(request: Request) -> Response {
let request = CgiRequest::new(request);

#[cfg(feature = "qnap")]
if let Err(error) = qnap::validate_auth(&request) {
return text_response(StatusCode::UNAUTHORIZED, format!("Unauthorized: {}", error));
}

if let Some(response) = api::handle_api(&request) {
#[cfg(debug_assertions)]
let response = trace_request(&request, &response).unwrap_or(text_response(
Expand Down
165 changes: 165 additions & 0 deletions clis/teliod/src/cgi/qnap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use rust_cgi::{http::header::COOKIE, Request};
use serde::Deserialize;

use crate::TIMEOUT_SEC;

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Rewquest(#[from] reqwest::header::ToStrError),
#[error("Missing authentication token")]
MissingAuthToken,
#[error("Headers missing HTTP cookie")]
MissingHTTPCookie,
#[error("User must be authenticated")]
UserNotAuthenticated,
#[error("User must have admin rights")]
UserNotAdminGroup,
#[error(transparent)]
FailedAuthValidation(#[from] reqwest::Error),
#[error("Timed out while validating auth token")]
AuthValidationTimeOut,
#[error("Unknown status value from auth check")]
UnknownAuthStatusValue,
#[error("Unknown group admin value from auth check")]
UnknownAuthGroupAdminValue,
}

#[derive(Debug)]
enum AuthCheckStatus {
Success,
Failed,
}

impl TryFrom<u8> for AuthCheckStatus {
type Error = Error;

fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(AuthCheckStatus::Success),
3 => Ok(AuthCheckStatus::Failed),
_ => Err(Error::UnknownAuthStatusValue),
}
}
}

impl<'de> Deserialize<'de> for AuthCheckStatus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: u8 = Deserialize::deserialize(deserializer)?;
AuthCheckStatus::try_from(value).map_err(serde::de::Error::custom)
}
}

#[derive(Debug, Deserialize)]
struct AuthCheckResponse {
status: AuthCheckStatus,
admingroup: u8,
}

pub fn validate_auth(request: &Request) -> Result<(), Error> {
retrieve_sid(request).and_then(|sid| {
let auth_response = match tokio::runtime::Handle::current().block_on(tokio::time::timeout(
std::time::Duration::from_secs(TIMEOUT_SEC),
check_user_auth(sid),
)) {
Ok(Ok(resp)) => resp,
Ok(Err(error)) => return Err(error),
_ => return Err(Error::AuthValidationTimeOut),
};
match (auth_response.status, auth_response.admingroup) {
(AuthCheckStatus::Success, 1) => Ok(()),
(AuthCheckStatus::Success, 0) => Err(Error::UserNotAdminGroup),
(AuthCheckStatus::Failed, 0 | 1) => Err(Error::UserNotAuthenticated),
_ => Err(Error::UnknownAuthGroupAdminValue),
}
})
}

fn retrieve_sid(request: &Request) -> Result<String, Error> {
request
.headers()
.get(COOKIE)
.ok_or(Error::MissingHTTPCookie)?
.to_str()?
.split(';')
.filter_map(|pair| {
let mut parts = pair.trim().split('=');
match (parts.next(), parts.next()) {
(Some("NAS_SID"), Some(value)) => Some(value.to_string()),
_ => None,
}
})
.next()
.ok_or(Error::MissingAuthToken)
}

async fn check_user_auth(sid: String) -> Result<AuthCheckResponse, Error> {
let url = format!(
"http://127.0.0.1:8080/cgi-bin/filemanager/utilRequest.cgi?func=check_sid&sid={}",
sid
);
Ok(reqwest::get(&url)
.await?
.json::<AuthCheckResponse>()
.await?)
}

#[cfg(test)]
mod tests {
use super::*;

use rust_cgi::http::{header::COOKIE, Request};

#[test]
fn test_sid_retrieval_with_cookie() {
let expected_sid = "nastestsid0%&)+";
let request = Request::builder()
.uri("https://www.test.org/")
.header("User-Agent", "test-agent/1.0")
.header(
COOKIE,
format!(
"DESKTOP=1; NAS_USER=admin; home=1; NAS_SID={}; remeber=1;",
expected_sid
),
)
.body(vec![])
.unwrap();

let parsed_sid = retrieve_sid(&request).unwrap();

assert!(parsed_sid.eq(expected_sid));
}

#[test]
fn test_sid_retrieval_without_cookie() {
let request = Request::builder()
.uri("https://www.test.org/")
.header("User-Agent", "test-agent/1.0")
.body(vec![])
.unwrap();

assert!(matches!(
retrieve_sid(&request),
Err(Error::MissingHTTPCookie)
));
}

#[test]
fn test_sid_retrieval_without_authentication() {
let request = Request::builder()
.uri("https://www.test.org/")
.header("User-Agent", "test-agent/1.0")
.header(COOKIE, "DESKTOP=1; NAS_USER=admin; home=1; remeber=1;")
.body(vec![])
.unwrap();

assert!(matches!(
retrieve_sid(&request),
Err(Error::MissingAuthToken)
));
}
}
5 changes: 4 additions & 1 deletion clis/teliod/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,10 @@ async fn main() -> Result<(), TeliodError> {
}
#[cfg(feature = "cgi")]
Cmd::Cgi => {
rust_cgi::handle(cgi::handle_request);
tokio::task::spawn_blocking(|| {
rust_cgi::handle(cgi::handle_request);
})
.await?;
Ok(())
}
}
Expand Down

0 comments on commit f4edb99

Please sign in to comment.