From 2d072de515b55d068e5cb4eaad04454ff5c5579b Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Fri, 26 Mar 2021 00:06:26 -0400 Subject: [PATCH] HTTP digest auth for service API, and remove unnecessary imports in Rust code. --- service/src/commands/identity.rs | 2 - service/src/httpclient.rs | 11 +++- service/src/httplistener.rs | 110 +++++++++++++++++++++++++------ service/src/service.rs | 7 +- service/src/utils.rs | 22 ++++--- service/src/vnic/common.rs | 2 - service/src/vnic/mod.rs | 2 - 7 files changed, 117 insertions(+), 39 deletions(-) diff --git a/service/src/commands/identity.rs b/service/src/commands/identity.rs index e79ad458c..e9a637f7e 100644 --- a/service/src/commands/identity.rs +++ b/service/src/commands/identity.rs @@ -15,8 +15,6 @@ use clap::ArgMatches; use zerotier_core::{Identity, IdentityType}; -use crate::store::Store; - fn new_(cli_args: &ArgMatches) -> i32 { let id_type = cli_args.value_of("type").map_or(IdentityType::Curve25519, |idt| { match idt { diff --git a/service/src/httpclient.rs b/service/src/httpclient.rs index 07c08396d..91a7e5dde 100644 --- a/service/src/httpclient.rs +++ b/service/src/httpclient.rs @@ -123,13 +123,20 @@ pub(crate) async fn request(client: &HttpClient, method: Method, uri: Uri, data: if auth.is_err() { return Err(Box::new(auth.err().unwrap())); } - let ac = digest_auth::AuthContext::new("zerotier", auth_token, uri.to_string()); + let ac = digest_auth::AuthContext::new_with_method("", auth_token, uri.to_string(), None::<&[u8]>, match method { + Method::GET => digest_auth::HttpMethod::GET, + Method::POST => digest_auth::HttpMethod::POST, + Method::HEAD => digest_auth::HttpMethod::HEAD, + Method::PUT => digest_auth::HttpMethod::OTHER("PUT"), + Method::DELETE => digest_auth::HttpMethod::OTHER("DELETE"), + _ => digest_auth::HttpMethod::OTHER(""), + }); let auth = auth.unwrap().respond(&ac); if auth.is_err() { return Err(Box::new(auth.err().unwrap())); } - let req = Request::builder().method(&method).version(hyper::Version::HTTP_11).uri(&uri).header(hyper::header::WWW_AUTHENTICATE, auth.unwrap().to_header_string()).body(Body::from(body)); + let req = Request::builder().method(&method).version(hyper::Version::HTTP_11).uri(&uri).header(hyper::header::AUTHORIZATION, auth.unwrap().to_header_string()).body(Body::from(body)); if req.is_err() { return Err(Box::new(req.err().unwrap())); } diff --git a/service/src/httplistener.rs b/service/src/httplistener.rs index 2fe88a55a..e0a2ab051 100644 --- a/service/src/httplistener.rs +++ b/service/src/httplistener.rs @@ -15,16 +15,20 @@ use std::cell::RefCell; use std::convert::Infallible; use std::net::SocketAddr; -use hyper::{Body, Request, Response, StatusCode}; +use hyper::{Body, Request, Response, StatusCode, Method}; use hyper::server::Server; use hyper::service::{make_service_fn, service_fn}; use tokio::task::JoinHandle; use crate::service::Service; use crate::api; +use crate::utils::{decrypt_http_auth_nonce, ms_since_epoch, create_http_auth_nonce}; #[cfg(target_os = "linux")] use std::os::unix::io::AsRawFd; +use digest_auth::{AuthContext, AuthorizationHeader, Charset, WwwAuthenticateHeader}; + +const HTTP_MAX_NONCE_AGE_MS: i64 = 30000; /// Listener for http connections to the API or for TCP P2P. /// Dropping a listener initiates shutdown of the background hyper Server instance, @@ -35,6 +39,91 @@ pub(crate) struct HttpListener { server: JoinHandle>, } +async fn http_handler(service: Service, req: Request) -> Result, Infallible> { + let mut authorized = false; + let mut stale = false; + + let auth_token = service.store().auth_token(false); + if auth_token.is_err() { + return Ok::, Infallible>(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Body::from("authorization token unreadable")).unwrap()); + } + let auth_context = AuthContext::new_with_method("", auth_token.unwrap(), req.uri().to_string(), None::<&[u8]>, match *req.method() { + Method::GET => digest_auth::HttpMethod::GET, + Method::POST => digest_auth::HttpMethod::POST, + Method::HEAD => digest_auth::HttpMethod::HEAD, + Method::PUT => digest_auth::HttpMethod::OTHER("PUT"), + Method::DELETE => digest_auth::HttpMethod::OTHER("DELETE"), + _ => digest_auth::HttpMethod::OTHER(""), + }); + + let auth_header = req.headers().get(hyper::header::AUTHORIZATION); + if auth_header.is_some() { + let auth_header = AuthorizationHeader::parse(auth_header.unwrap().to_str().unwrap_or("")); + if auth_header.is_err() { + return Ok::, Infallible>(Response::builder().status(StatusCode::BAD_REQUEST).body(Body::from(format!("invalid authorization header: {}", auth_header.err().unwrap().to_string()))).unwrap()); + } + let auth_header = auth_header.unwrap(); + + let mut expected = AuthorizationHeader { + realm: "zerotier-service-api".to_owned(), + nonce: auth_header.nonce.clone(), + opaque: None, + userhash: false, + algorithm: digest_auth::Algorithm::new(digest_auth::AlgorithmType::SHA2_512_256, false), + response: String::new(), + username: String::new(), + uri: req.uri().to_string(), + qop: Some(digest_auth::Qop::AUTH), + cnonce: auth_header.cnonce.clone(), + nc: auth_header.nc, + }; + expected.digest(&auth_context); + if auth_header.response == expected.response { + if (ms_since_epoch() - decrypt_http_auth_nonce(auth_header.nonce.as_str())) <= HTTP_MAX_NONCE_AGE_MS { + authorized = true; + } else { + stale = true; + } + } + } + + if authorized { + let req_path = req.uri().path(); + let (status, body) = + if req_path == "/_zt" { + (StatusCode::NOT_IMPLEMENTED, Body::from("not implemented yet")) + } else if req_path == "/status" { + api::status(service, req) + } else if req_path == "/config" { + api::config(service, req) + } else if req_path.starts_with("/peer") { + api::peer(service, req) + } else if req_path.starts_with("/network") { + api::network(service, req) + } else if req_path.starts_with("/controller") { + (StatusCode::NOT_IMPLEMENTED, Body::from("not implemented yet")) + } else if req_path == "/teapot" { + (StatusCode::IM_A_TEAPOT, Body::from("I'm a little teapot short and stout!")) + } else { + (StatusCode::NOT_FOUND, Body::from("not found")) + }; + Ok::, Infallible>(Response::builder().header("Content-Type", "application/json").status(status).body(body).unwrap()) + } else { + Ok::, Infallible>(Response::builder().header(hyper::header::WWW_AUTHENTICATE, WwwAuthenticateHeader { + domain: None, + realm: "zerotier-service-api".to_owned(), + nonce: create_http_auth_nonce(ms_since_epoch()), + opaque: None, + stale, + algorithm: digest_auth::Algorithm::new(digest_auth::AlgorithmType::SHA2_512_256, false), + qop: Some(vec![digest_auth::Qop::AUTH]), + userhash: false, + charset: Charset::ASCII, + nc: 0, + }.to_string()).status(StatusCode::UNAUTHORIZED).body(Body::empty()).unwrap()) + } +} + impl HttpListener { /// Create a new "background" TCP WebListener using the current tokio reactor async runtime. pub async fn new(_device_name: &str, address: SocketAddr, service: &Service) -> Result> { @@ -93,24 +182,7 @@ impl HttpListener { let server = tokio::task::spawn(builder.serve(make_service_fn(move |_| { let service = service.clone(); async move { - Ok::<_, Infallible>(service_fn(move |req: Request| { - let service = service.clone(); - async move { - let req_path = req.uri().path(); - let (status, body) = if req_path == "/status" { - api::status(service, req) - } else if req_path.starts_with("/config") { - api::config(service, req) - } else if req_path.starts_with("/peer") { - api::peer(service, req) - } else if req_path.starts_with("/network") { - api::network(service, req) - } else { - (StatusCode::NOT_FOUND, Body::from("not found")) - }; - Ok::, Infallible>(Response::builder().header("Content-Type", "application/json").status(status).body(body).unwrap()) - } - })) + Ok::<_, Infallible>(service_fn(move |req: Request| http_handler(service.clone(), req))) } })).with_graceful_shutdown(async { let _ = shutdown_rx.await; })); diff --git a/service/src/service.rs b/service/src/service.rs index dee882b19..eecff69a8 100644 --- a/service/src/service.rs +++ b/service/src/service.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; use std::net::{SocketAddr, Ipv4Addr, IpAddr, Ipv6Addr}; use std::sync::{Arc, Mutex, Weak}; -use std::sync::atomic::{AtomicBool, Ordering, AtomicPtr}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use zerotier_core::*; @@ -200,8 +200,9 @@ impl Service { self._node.upgrade() } - pub fn store(&self) -> Arc { - self.intl.store.clone() + #[inline(always)] + pub fn store(&self) -> &Arc { + &self.intl.store } pub fn online(&self) -> bool { diff --git a/service/src/utils.rs b/service/src/utils.rs index e523cf277..1a349ea33 100644 --- a/service/src/utils.rs +++ b/service/src/utils.rs @@ -14,8 +14,6 @@ use std::borrow::Borrow; use std::fs::File; use std::io::Read; -use std::mem::MaybeUninit; -use std::os::raw::c_uint; use std::path::Path; use zerotier_core::{Identity, Locator}; @@ -99,8 +97,8 @@ pub(crate) fn read_locator(input: &str) -> Result { /// The key used to encrypt the current time is random and is re-created for /// each execution of the process. By decrypting this nonce when it is returned, /// the client and server may check the age of a digest auth exchange. -pub(crate) fn create_http_auth_nonce(timestamp: u64) -> String { - let mut nonce_plaintext: [u64; 2] = [timestamp, 12345]; // the second u64 is arbitrary and unused +pub(crate) fn create_http_auth_nonce(timestamp: i64) -> String { + let mut nonce_plaintext: [u64; 2] = [timestamp as u64, 12345]; // the second u64 is arbitrary and unused unsafe { osdep::encryptHttpAuthNonce(nonce_plaintext.as_mut_ptr().cast()); hex::encode(*nonce_plaintext.as_ptr().cast::<[u8; 16]>()) @@ -109,7 +107,7 @@ pub(crate) fn create_http_auth_nonce(timestamp: u64) -> String { /// Decrypt HTTP auth nonce encrypted by this process and return the timestamp. /// This returns zero if the input was not valid. -pub(crate) fn decrypt_http_auth_nonce(nonce: &str) -> u64 { +pub(crate) fn decrypt_http_auth_nonce(nonce: &str) -> i64 { let nonce = hex::decode(nonce.trim()); if nonce.is_err() { return 0; @@ -121,7 +119,7 @@ pub(crate) fn decrypt_http_auth_nonce(nonce: &str) -> u64 { unsafe { osdep::decryptHttpAuthNonce(nonce.as_mut_ptr().cast()); let nonce = *nonce.as_ptr().cast::<[u64; 2]>(); - nonce[0] + nonce[0] as i64 } } @@ -134,16 +132,22 @@ pub(crate) fn decrypt_http_auth_nonce(nonce: &str) -> u64 { pub(crate) fn json_patch(target: &mut serde_json::value::Value, source: &serde_json::value::Value, depth_limit: usize) { if target.is_object() { if source.is_object() { + let mut target = target.as_object_mut().unwrap(); let source = source.as_object().unwrap(); - for kv in target.as_object_mut().unwrap() { + for kv in target.iter_mut() { let _ = source.get(kv.0).map(|new_value| { if depth_limit > 0 { json_patch(kv.1, new_value, depth_limit - 1) } }); } + for kv in source.iter() { + if !target.contains_key(kv.0) && !kv.1.is_null() { + target.insert(kv.0.clone(), kv.1.clone()); + } + } } - } else { + } else if *target != *source { *target = source.clone(); } } @@ -156,7 +160,7 @@ pub(crate) fn json_patch_object(obj: O, pa serde_json::value::to_value(obj.borrow()).map_or_else(|e| Err(e), |mut obj_value| { json_patch(&mut obj_value, &patch, depth_limit); serde_json::value::from_value::(obj_value).map_or_else(|e| Err(e), |obj_merged| { - if obj.eq(&obj_merged) { + if obj == obj_merged { Ok(None) } else { Ok(Some(obj_merged)) diff --git a/service/src/vnic/common.rs b/service/src/vnic/common.rs index 78da34f54..d81cb5567 100644 --- a/service/src/vnic/common.rs +++ b/service/src/vnic/common.rs @@ -19,8 +19,6 @@ use zerotier_core::{MAC, MulticastGroup}; #[allow(unused_imports)] use num_traits::AsPrimitive; -use crate::osdep as osdep; - /// BSD based OSes support getifmaddrs(). #[cfg(any(target_os = "macos", target_os = "ios", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly", target_os = "freebsd", target_os = "darwin"))] pub(crate) fn get_l2_multicast_subscriptions(dev: &str) -> BTreeSet { diff --git a/service/src/vnic/mod.rs b/service/src/vnic/mod.rs index 8602c74f1..7a0d395f2 100644 --- a/service/src/vnic/mod.rs +++ b/service/src/vnic/mod.rs @@ -16,5 +16,3 @@ mod common; #[cfg(target_os = "macos")] mod mac_feth_tap; - -pub(crate) use vnic::VNIC;