mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-06-11 23:13:44 +02:00
HTTP digest auth for service API, and remove unnecessary imports in Rust code.
This commit is contained in:
parent
50675004ce
commit
2d072de515
7 changed files with 117 additions and 39 deletions
|
@ -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 {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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<hyper::Result<()>>,
|
||||
}
|
||||
|
||||
async fn http_handler(service: Service, req: Request<Body>) -> Result<Response<Body>, Infallible> {
|
||||
let mut authorized = false;
|
||||
let mut stale = false;
|
||||
|
||||
let auth_token = service.store().auth_token(false);
|
||||
if auth_token.is_err() {
|
||||
return Ok::<Response<Body>, 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::<Response<Body>, 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::<Response<Body>, Infallible>(Response::builder().header("Content-Type", "application/json").status(status).body(body).unwrap())
|
||||
} else {
|
||||
Ok::<Response<Body>, 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<HttpListener, Box<dyn std::error::Error>> {
|
||||
|
@ -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<Body>| {
|
||||
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::<Response<Body>, Infallible>(Response::builder().header("Content-Type", "application/json").status(status).body(body).unwrap())
|
||||
}
|
||||
}))
|
||||
Ok::<_, Infallible>(service_fn(move |req: Request<Body>| http_handler(service.clone(), req)))
|
||||
}
|
||||
})).with_graceful_shutdown(async { let _ = shutdown_rx.await; }));
|
||||
|
||||
|
|
|
@ -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<Store> {
|
||||
self.intl.store.clone()
|
||||
#[inline(always)]
|
||||
pub fn store(&self) -> &Arc<Store> {
|
||||
&self.intl.store
|
||||
}
|
||||
|
||||
pub fn online(&self) -> bool {
|
||||
|
|
|
@ -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<Locator, String> {
|
|||
/// 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<O: Serialize + DeserializeOwned + Eq>(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::<O>(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))
|
||||
|
|
|
@ -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<MulticastGroup> {
|
||||
|
|
|
@ -16,5 +16,3 @@ mod common;
|
|||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac_feth_tap;
|
||||
|
||||
pub(crate) use vnic::VNIC;
|
||||
|
|
Loading…
Add table
Reference in a new issue