From: Otto Moerbeek Date: Tue, 26 Nov 2024 07:58:44 +0000 (+0100) Subject: Better routing X-Git-Tag: dnsdist-2.0.0-alpha1~95^2~28 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b518cb30f311fbc29fa67bcb7391384450becfc0;p=thirdparty%2Fpdns.git Better routing --- diff --git a/pdns/credentials.cc b/pdns/credentials.cc index ddc5add19b..062155326f 100644 --- a/pdns/credentials.cc +++ b/pdns/credentials.cc @@ -388,7 +388,9 @@ CredentialsHolder::~CredentialsHolder() bool CredentialsHolder::matches(const std::string& password) const { + cerr << "matches " << d_isHashed << ' ' << password << ' ' << d_credentials.getString() << endl; if (d_isHashed) { + cerr << "Case 1" << endl; return verifyPassword(d_credentials.getString(), d_salt, d_workFactor, d_parallelFactor, d_blockSize, password); } // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) diff --git a/pdns/recursordist/settings/rust/src/bridge.hh b/pdns/recursordist/settings/rust/src/bridge.hh index bbaf43dc9d..633035f2d6 100644 --- a/pdns/recursordist/settings/rust/src/bridge.hh +++ b/pdns/recursordist/settings/rust/src/bridge.hh @@ -22,6 +22,8 @@ #pragma once #include "rust/cxx.h" +#include "../../../credentials.hh" + namespace pdns::rust::settings::rec { @@ -32,6 +34,7 @@ void setThreadName(::rust::Str str); namespace pdns::rust::web::rec { +using CredentialsHolder = ::CredentialsHolder; struct KeyValue; struct Request; struct Response; diff --git a/pdns/recursordist/settings/rust/src/web.rs b/pdns/recursordist/settings/rust/src/web.rs index 06e3559653..17eb746ad8 100644 --- a/pdns/recursordist/settings/rust/src/web.rs +++ b/pdns/recursordist/settings/rust/src/web.rs @@ -3,14 +3,15 @@ TODO - Logging - Table based routing including OPTIONS request handling -- Requests taking e.g. an -- ACLs -- Authorization +- ACLs of webserver +- ACL handling; thread local does not work, see how domains are done +- Authorization: metrics and plain files (and more?) are not subject to password auth - Allow multipe listen addreses in settings (singlevalued right now) - TLS? - Code is now in settings dir. It's only possible to split the modules into separate Rust libs if we - use shared libs (in theory, I did not try). Currenlty all CXX using Rust cargo's must be compiled - as one and refer to a single static Rust runtime, + use shared libs (in theory, I did not try). Currently all CXX using Rust cargo's must be compiled + as one and refer to a single static Rust runtime +- Ripping out yahttp stuff, providing some basic classees only */ use std::net::SocketAddr; @@ -28,6 +29,7 @@ use std::io::ErrorKind; use std::str::FromStr; use std::sync::Arc; use tokio::sync::Mutex; +use base64::prelude::*; type GenericError = Box; type MyResult = std::result::Result; @@ -43,6 +45,51 @@ fn full>(chunk: T) -> BoxBody { type Func = fn(&rustweb::Request, &mut rustweb::Response) -> Result<(), cxx::Exception>; +fn compare_authorization(ctx: &Context, reqheaders: &header::HeaderMap) -> bool +{ + let mut auth_ok = false; + if !ctx.password_ch.is_null() { + if let Some(authorization) = reqheaders.get("authorization") { + let mut lcase = authorization.as_bytes().to_owned(); + lcase.make_ascii_lowercase(); + if lcase.starts_with(b"basic ") { + let cookie = &authorization.as_bytes()[6..]; + if let Ok(plain) = BASE64_STANDARD.decode(cookie) { + println!("plain {:?}", plain); + let mut split = plain.split(|i| *i == b':'); + println!("split {:?}", split); + if split.next().is_some() { + println!("split {:?}", split); + if let Some(split) = split.next() { + println!("split {:?}", split); + cxx::let_cxx_string!(s = &split); + auth_ok = ctx.password_ch.as_ref().unwrap().matches(&s); + println!("OK4 {}", auth_ok); + } + } + } + } + } + println!("OK5 {}", auth_ok); + } else { + auth_ok = true; + } + auth_ok +} + +fn unauthorized(response: &mut rustweb::Response, headers: &mut header::HeaderMap, auth: &str) +{ + // XXX log + let status = StatusCode::UNAUTHORIZED; + response.status = status.as_u16(); + let val = format!("{} realm=\"PowerDNS\"", auth); + headers.insert( + header::WWW_AUTHENTICATE, + header::HeaderValue::from_str(&val).unwrap(), + ); + response.body = status.canonical_reason().unwrap().as_bytes().to_vec(); +} + fn api_wrapper( ctx: &Context, handler: Func, @@ -50,56 +97,47 @@ fn api_wrapper( response: &mut rustweb::Response, reqheaders: &header::HeaderMap, headers: &mut header::HeaderMap, + allow_password: bool ) { + // security headers headers.insert( header::ACCESS_CONTROL_ALLOW_ORIGIN, header::HeaderValue::from_static("*"), ); - if ctx.api_key.is_empty() { - // XXX log - // Www-Authenticate: X-API-Key realm="PowerDNS" - let status = StatusCode::UNAUTHORIZED; - response.status = status.as_u16(); - headers.insert( - header::WWW_AUTHENTICATE, - header::HeaderValue::from_static("X-API-Key ream=\"PowerDNS\""), - ); - response.body = status.canonical_reason().unwrap().as_bytes().to_vec(); + if ctx.api_ch.is_null() { + unauthorized(response, headers, "X-API-Key"); return; } - // XXX encrypted credentials handling, password handling! - let allow_password = false; + // XXX AUDIT! let mut auth_ok = false; + println!("OK0 {}", auth_ok); if let Some(api) = reqheaders.get("x-api-key") { - auth_ok = api.as_bytes() == ctx.api_key.as_bytes(); - println!("OK {}", auth_ok); + cxx::let_cxx_string!(s = &api.as_bytes()); + auth_ok = ctx.api_ch.as_ref().unwrap().matches(&s); + println!("OK1 {}", auth_ok); } if !auth_ok { for kv in &request.vars { - if kv.key == "x-api-key" && kv.value == ctx.api_key { + cxx::let_cxx_string!(s = &kv.value); + if kv.key == "x-api-key" && ctx.api_ch.as_ref().unwrap().matches(&s) { auth_ok = true; + println!("OK2 {}", auth_ok); break; } } } + println!("OK3 {}", auth_ok); if !auth_ok && allow_password { - if !ctx.webserver_password.is_empty() { - //auth_ok = req->compareAuthorization(*d_webserverPassword); XXX - } else { - auth_ok = true; + auth_ok = compare_authorization(ctx, reqheaders); + if !auth_ok { + unauthorized(response, headers, "Basic"); + return; } } if !auth_ok { - // XXX log - let status = StatusCode::UNAUTHORIZED; - response.status = status.as_u16(); - headers.insert( - header::WWW_AUTHENTICATE, - header::HeaderValue::from_static("X-API-Key ream=\"PowerDNS\""), - ); - response.body = status.canonical_reason().unwrap().as_bytes().to_vec(); + unauthorized(response, headers, "X-API-Key"); return; } response.status = StatusCode::OK.as_u16(); // 200; @@ -137,8 +175,8 @@ fn api_wrapper( struct Context { urls: Vec, - api_key: String, - webserver_password: String, + password_ch: cxx::UniquePtr, + api_ch: cxx::UniquePtr, counter: Mutex, } @@ -150,7 +188,6 @@ async fn hello( let mut counter = ctx.counter.lock().await; *counter += 1; } - let mut rust_response = Response::builder(); let mut vars: Vec = vec![]; if let Some(query) = rust_request.uri().query() { for (k, v) in form_urlencoded::parse(query.as_bytes()) { @@ -169,73 +206,89 @@ async fn hello( body: vec![], uri: rust_request.uri().to_string(), vars, + parameters: vec![], }; let mut response = rustweb::Response { status: 0, body: vec![], headers: vec![], }; - let headers = rust_response.headers_mut().expect("no headers?"); let mut apifunc: Option = None; let method = rust_request.method().to_owned(); - match (&method, rust_request.uri().path()) { - (&Method::GET, "/jsonstat") => - apifunc = Some(rustweb::jsonstat), - (&Method::PUT, "/api/v1/servers/localhost/cache/flush") => + let path: Vec<_> = rust_request.uri().path().split('/').skip(1).collect(); + let mut allow_password = false; + match (&method, &*path) { + (&Method::GET, ["jsonstat"]) => { + allow_password = true; + apifunc = Some(rustweb::jsonstat); + } + (&Method::PUT, ["api", "v1", "servers", "localhost", "cache", "flush"]) => apifunc = Some(rustweb::apiServerCacheFlush), - (&Method::PUT, "/api/v1/servers/localhost/config/allow-from") => + (&Method::PUT, ["api", "v1", "servers", "localhost", "config", "allow-from"]) => apifunc = Some(rustweb::apiServerConfigAllowFromPUT), - (&Method::GET, "/api/v1/servers/localhost/config/allow-from") => + (&Method::GET, ["api", "v1", "servers", "localhost", "config", "allow-from"]) => apifunc = Some(rustweb::apiServerConfigAllowFromGET), - (&Method::PUT, "/api/v1/servers/localhost/config/allow-notify-from") => + (&Method::PUT, ["api", "v1", "servers", "localhost", "config", "allow-notify-from"]) => apifunc = Some(rustweb::apiServerConfigAllowNotifyFromPUT), - (&Method::GET, "/api/v1/servers/localhost/config/allow-notify-from") => + (&Method::GET, ["api", "v1", "servers", "localhost", "config", "allow-notify-from"]) => apifunc = Some(rustweb::apiServerConfigAllowNotifyFromGET), - (&Method::GET, "/api/v1/servers/localhost/config") => + (&Method::GET, ["api", "v1", "servers", "localhost", "config"]) => apifunc = Some(rustweb::apiServerConfig), - (&Method::GET, "/api/v1/servers/localhost/rpzstatistics") => + (&Method::GET, ["api", "v1", "servers", "localhost", "rpzstatistics"]) => apifunc = Some(rustweb::apiServerRPZStats), - (&Method::GET, "/api/v1/servers/localhost/search-data") => + (&Method::GET, ["api", "v1", "servers", "localhost", "search-data"]) => apifunc = Some(rustweb::apiServerSearchData), - (&Method::GET, "/api/v1/servers/localhost/zones/") => - apifunc = Some(rustweb::apiServerZoneDetailGET), - (&Method::PUT, "/api/v1/servers/localhost/zones/") => - apifunc = Some(rustweb::apiServerZoneDetailPUT), - (&Method::DELETE, "/api/v1/servers/localhost/zones/") => - apifunc = Some(rustweb::apiServerZoneDetailDELETE), - (&Method::GET, "/api/v1/servers/localhost/statistics") => - apifunc = Some(rustweb::apiServerStatistics), - (&Method::GET, "/api/v1/servers/localhost/zones") => + (&Method::GET, ["api", "v1", "servers", "localhost", "zones", id]) => { + request.parameters.push(rustweb::KeyValue{key: String::from("id"), value: String::from(*id)}); + apifunc = Some(rustweb::apiServerZoneDetailGET); + } + (&Method::PUT, ["api", "v1", "servers", "localhost", "zones", id]) => { + request.parameters.push(rustweb::KeyValue{key: String::from("id"), value: String::from(*id)}); + apifunc = Some(rustweb::apiServerZoneDetailPUT); + } + (&Method::DELETE, ["api", "v1", "servers", "localhost", "zones", id]) => { + request.parameters.push(rustweb::KeyValue{key: String::from("id"), value: String::from(*id)}); + apifunc = Some(rustweb::apiServerZoneDetailDELETE); + } + (&Method::GET, ["api", "v1", "servers", "localhost", "statistics"]) => { + allow_password = true; + apifunc = Some(rustweb::apiServerStatistics); + } + (&Method::GET, ["api", "v1", "servers", "localhost", "zones"]) => apifunc = Some(rustweb::apiServerZonesGET), - (&Method::POST, "/api/v1/servers/localhost/zones") => + (&Method::POST, ["api", "v1", "servers", "localhost", "zones"]) => apifunc = Some(rustweb::apiServerZonesPOST), - (&Method::GET, "/api/v1/servers/localhost") => - apifunc = Some(rustweb::apiServerDetail), - (&Method::GET, "/api/v1/servers") => + (&Method::GET, ["api", "v1", "servers", "localhost"]) => { + allow_password = true; + apifunc = Some(rustweb::apiServerDetail); + } + (&Method::GET, ["api", "v1", "servers"]) => apifunc = Some(rustweb::apiServer), - (&Method::GET, "/api/v1") => + (&Method::GET, ["api", "v1"]) => apifunc = Some(rustweb::apiDiscoveryV1), - (&Method::GET, "/api") => + (&Method::GET, ["api"]) => apifunc = Some(rustweb::apiDiscovery), - (&Method::GET, "/metrics") => + (&Method::GET, ["metrics"]) => rustweb::prometheusMetrics(&request, &mut response).unwrap(), _ => { - let mut path = rust_request.uri().path(); - if path == "/" { - path = "/index.html"; + let mut uripath = rust_request.uri().path(); + if uripath == "/" { + uripath = "/index.html"; } - let pos = ctx.urls.iter().position(|x| String::from("/") + x == path); + let pos = ctx.urls.iter().position(|x| String::from("/") + x == uripath); if pos.is_none() { - eprintln!("{} {} not found", rust_request.method(), path); + eprintln!("{} {} not found", rust_request.method(), uripath); } if rustweb::serveStuff(&request, &mut response).is_err() { // Return 404 not found response. response.status = StatusCode::NOT_FOUND.as_u16(); response.body = NOTFOUND.to_vec(); - eprintln!("{} {} not found case 2", rust_request.method(), path); + eprintln!("{} {} not found case 2", rust_request.method(), uripath); } } } + let mut rust_response = Response::builder(); + if let Some(func) = apifunc { let reqheaders = rust_request.headers().clone(); if rust_request.method()== Method::POST || rust_request.method() == Method::PUT { @@ -247,7 +300,8 @@ async fn hello( &request, &mut response, &reqheaders, - headers, + rust_response.headers_mut().expect("no headers?"), + allow_password, ); } @@ -302,12 +356,12 @@ async fn serveweb_async(listener: TcpListener, ctx: Arc) -> MyResult<() } } -pub fn serveweb(addresses: &Vec, urls: &[String], api_key: String, webserver_password: String) -> Result<(), std::io::Error> { +pub fn serveweb(addresses: &Vec, urls: &[String], password_ch: cxx::UniquePtr, api_ch: cxx::UniquePtr) -> Result<(), std::io::Error> { // Context (R/O for now) let ctx = Arc::new(Context { urls: urls.to_vec(), - api_key, - webserver_password, + password_ch, + api_ch, counter: Mutex::new(0), }); @@ -355,14 +409,20 @@ pub fn serveweb(addresses: &Vec, urls: &[String], api_key: String, webse Ok(()) } +unsafe impl Send for rustweb::CredentialsHolder {} +unsafe impl Sync for rustweb::CredentialsHolder {} + #[cxx::bridge(namespace = "pdns::rust::web::rec")] mod rustweb { + extern "C++" { + type CredentialsHolder; + } /* * Functions callable from C++ */ extern "Rust" { - fn serveweb(addreses: &Vec, urls: &[String], apikey: String, password: String) -> Result<()>; + fn serveweb(addreses: &Vec, urls: &[String], pwch: UniquePtr, apikeych: UniquePtr) -> Result<()>; } struct KeyValue { @@ -374,6 +434,7 @@ mod rustweb { body: Vec, uri: String, vars: Vec, + parameters: Vec, } struct Response { @@ -408,5 +469,7 @@ mod rustweb { fn jsonstat(request: &Request, response: &mut Response) -> Result<()>; fn prometheusMetrics(request: &Request, response: &mut Response) -> Result<()>; fn serveStuff(request: &Request, response: &mut Response) -> Result<()>; + + fn matches(self: &CredentialsHolder, str: &CxxString) -> bool; } } diff --git a/pdns/recursordist/ws-recursor.cc b/pdns/recursordist/ws-recursor.cc index d6d8b681ac..74b9fec810 100644 --- a/pdns/recursordist/ws-recursor.cc +++ b/pdns/recursordist/ws-recursor.cc @@ -185,8 +185,9 @@ static void apiServerConfigAllowNotifyFromPUT(HttpRequest* req, HttpResponse* re static void fillZone(const DNSName& zonename, HttpResponse* resp) { - auto iter = SyncRes::t_sstorage.domainmap->find(zonename); - if (iter == SyncRes::t_sstorage.domainmap->end()) { + auto lock = g_initialDomainMap.lock(); + auto iter = (*lock)->find(zonename); + if (iter == (*lock)->end()) { throw ApiException("Could not find domain '" + zonename.toLogString() + "'"); } @@ -215,7 +216,7 @@ static void fillZone(const DNSName& zonename, HttpResponse* resp) {"kind", zone.d_servers.empty() ? "Native" : "Forwarded"}, {"servers", servers}, {"recursion_desired", zone.d_servers.empty() ? false : zone.d_rdForward}, - {"notify_allowed", isAllowNotifyForZone(zonename)}, + //{"notify_allowed", isAllowNotifyForZone(zonename)}, {"records", records}}; resp->setJsonBody(doc); @@ -398,7 +399,8 @@ static void apiServerZonesGET(HttpRequest* /* req */, HttpResponse* resp) static inline DNSName findZoneById(HttpRequest* req) { auto zonename = apiZoneIdToName(req->parameters["id"]); - if (SyncRes::t_sstorage.domainmap->find(zonename) == SyncRes::t_sstorage.domainmap->end()) { + auto lock = g_initialDomainMap.lock(); + if ((*lock)->find(zonename) == (*lock)->end()) { throw ApiException("Could not find domain '" + zonename.toLogString() + "'"); } return zonename; @@ -961,7 +963,18 @@ void serveRustWeb() urls.emplace_back(url); } auto address = ComboAddress(arg()["webserver-address"], arg().asNum("webserver-port")); - pdns::rust::web::rec::serveweb({::rust::String(address.toStringWithPort())}, ::rust::Slice{urls.data(), urls.size()}, arg()["api-key"], arg()["webserver-password"]); + + auto passwordString = arg()["webserver-password"]; + std::unique_ptr password; + if (!passwordString.empty()) { + password = std::make_unique(std::move(passwordString), arg().mustDo("webserver-hash-plaintext-credentials")); + } + auto apikeyString = arg()["api-key"]; + std::unique_ptr apikey; + if (!apikeyString.empty()) { + apikey = std::make_unique(std::move(apikeyString), arg().mustDo("webserver-hash-plaintext-credentials")); + } + pdns::rust::web::rec::serveweb({::rust::String(address.toStringWithPort())}, ::rust::Slice{urls.data(), urls.size()}, std::move(password), std::move(apikey)); } static void fromCxxToRust(const HttpResponse& cxxresp, pdns::rust::web::rec::Response& rustResponse) @@ -986,6 +999,9 @@ static void rustWrapper(const std::function& for (const auto& [key, value] : rustRequest.vars) { request.getvars[std::string(key)] = std::string(value); } + for (const auto& [key, value] : rustRequest.parameters) { + request.parameters[std::string(key)] = std::string(value); + } request.d_slog = g_slog; // XXX response.d_slog = g_slog; // XXX try {