feat: add basic auth for HTTP proxy (#785)
Signed-off-by: Gaius <gaius.qi@gmail.com>
This commit is contained in:
parent
f73239f00a
commit
e396706adf
|
|
@ -985,6 +985,7 @@ dependencies = [
|
||||||
name = "dragonfly-client-core"
|
name = "dragonfly-client-core"
|
||||||
version = "0.1.112"
|
version = "0.1.112"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"headers 0.4.0",
|
||||||
"hyper 1.4.1",
|
"hyper 1.4.1",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"libloading",
|
"libloading",
|
||||||
|
|
@ -1043,11 +1044,13 @@ name = "dragonfly-client-util"
|
||||||
version = "0.1.112"
|
version = "0.1.112"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
"base64 0.22.1",
|
||||||
"blake3",
|
"blake3",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"dragonfly-api",
|
"dragonfly-api",
|
||||||
"dragonfly-client-core",
|
"dragonfly-client-core",
|
||||||
"hex",
|
"hex",
|
||||||
|
"http 1.1.0",
|
||||||
"http-range-header",
|
"http-range-header",
|
||||||
"hyper 1.4.1",
|
"hyper 1.4.1",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
|
@ -1405,13 +1408,28 @@ checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers-core",
|
"headers-core 0.2.0",
|
||||||
"http 0.2.11",
|
"http 0.2.11",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"mime",
|
"mime",
|
||||||
"sha1",
|
"sha1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bytes",
|
||||||
|
"headers-core 0.3.0",
|
||||||
|
"http 1.1.0",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"sha1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "headers-core"
|
name = "headers-core"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -1421,6 +1439,15 @@ dependencies = [
|
||||||
"http 0.2.11",
|
"http 0.2.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers-core"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||||
|
dependencies = [
|
||||||
|
"http 1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -4724,7 +4751,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
"headers 0.3.9",
|
||||||
"http 0.2.11",
|
"http 0.2.11",
|
||||||
"hyper 0.14.28",
|
"hyper 0.14.28",
|
||||||
"log",
|
"log",
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@ use dragonfly_client_core::{
|
||||||
error::{ErrorType, OrErr},
|
error::{ErrorType, OrErr},
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
use dragonfly_client_util::tls::{generate_ca_cert_from_pem, generate_cert_from_pem};
|
use dragonfly_client_util::{
|
||||||
|
http::basic_auth,
|
||||||
|
tls::{generate_ca_cert_from_pem, generate_cert_from_pem},
|
||||||
|
};
|
||||||
use local_ip_address::{local_ip, local_ipv6};
|
use local_ip_address::{local_ip, local_ipv6};
|
||||||
use rcgen::Certificate;
|
use rcgen::Certificate;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
@ -871,6 +874,26 @@ impl Default for GC {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// BasicAuth is the basic auth configuration for HTTP proxy in dfdaemon.
|
||||||
|
#[derive(Default, Debug, Clone, Validate, Deserialize)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
pub struct BasicAuth {
|
||||||
|
/// username is the username of the basic auth.
|
||||||
|
#[validate(length(min = 1, max = 20))]
|
||||||
|
pub username: String,
|
||||||
|
|
||||||
|
/// passwork is the passwork of the basic auth.
|
||||||
|
#[validate(length(min = 1, max = 20))]
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BasicAuth {
|
||||||
|
/// credentials loads the credentials.
|
||||||
|
pub fn credentials(&self) -> basic_auth::Credentials {
|
||||||
|
basic_auth::Credentials::new(&self.username, &self.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ProxyServer is the proxy server configuration for dfdaemon.
|
/// ProxyServer is the proxy server configuration for dfdaemon.
|
||||||
#[derive(Debug, Clone, Validate, Deserialize)]
|
#[derive(Debug, Clone, Validate, Deserialize)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
|
@ -907,6 +930,12 @@ pub struct ProxyServer {
|
||||||
/// and key, and signs the server cert with the root CA cert. When client requests via the proxy,
|
/// and key, and signs the server cert with the root CA cert. When client requests via the proxy,
|
||||||
/// the proxy can intercept the request by the server cert.
|
/// the proxy can intercept the request by the server cert.
|
||||||
pub ca_key: Option<PathBuf>,
|
pub ca_key: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// basic_auth is the basic auth configuration for HTTP proxy in dfdaemon. If basic_auth is not
|
||||||
|
/// empty, the proxy will use the basic auth to authenticate the client by Authorization
|
||||||
|
/// header. The value of the Authorization header is "Basic base64(username:password)", refer
|
||||||
|
/// to https://en.wikipedia.org/wiki/Basic_access_authentication.
|
||||||
|
pub basic_auth: Option<BasicAuth>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ProxyServer implements Default.
|
/// ProxyServer implements Default.
|
||||||
|
|
@ -917,6 +946,7 @@ impl Default for ProxyServer {
|
||||||
port: default_proxy_server_port(),
|
port: default_proxy_server_port(),
|
||||||
ca_cert: None,
|
ca_cert: None,
|
||||||
ca_key: None,
|
ca_key: None,
|
||||||
|
basic_auth: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,5 @@ hyper.workspace = true
|
||||||
hyper-util.workspace = true
|
hyper-util.workspace = true
|
||||||
opendal.workspace = true
|
opendal.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
headers.workspace = true
|
||||||
libloading = "0.8.5"
|
libloading = "0.8.5"
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,10 @@ pub enum DFError {
|
||||||
#[error{"RangeUnsatisfiable: Failed to parse range fallback error, please file an issue"}]
|
#[error{"RangeUnsatisfiable: Failed to parse range fallback error, please file an issue"}]
|
||||||
EmptyHTTPRangeError,
|
EmptyHTTPRangeError,
|
||||||
|
|
||||||
|
/// Unauthorized is the error for unauthorized.
|
||||||
|
#[error{"unauthorized"}]
|
||||||
|
Unauthorized,
|
||||||
|
|
||||||
/// TonicStatus is the error for tonic status.
|
/// TonicStatus is the error for tonic status.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
TonicStatus(#[from] tonic::Status),
|
TonicStatus(#[from] tonic::Status),
|
||||||
|
|
@ -153,6 +157,10 @@ pub enum DFError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
TokioStreamElapsed(#[from] tokio_stream::Elapsed),
|
TokioStreamElapsed(#[from] tokio_stream::Elapsed),
|
||||||
|
|
||||||
|
/// HeadersError is the error for headers.
|
||||||
|
#[error(transparent)]
|
||||||
|
HeadersError(#[from] headers::Error),
|
||||||
|
|
||||||
/// URLParseError is the error for url parse.
|
/// URLParseError is the error for url parse.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
URLParseError(#[from] url::ParseError),
|
URLParseError(#[from] url::ParseError),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ dragonfly-api.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
hyper.workspace = true
|
hyper.workspace = true
|
||||||
http-range-header.workspace = true
|
http-range-header.workspace = true
|
||||||
|
http.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
rcgen.workspace = true
|
rcgen.workspace = true
|
||||||
|
|
@ -28,6 +29,7 @@ openssl.workspace = true
|
||||||
blake3.workspace = true
|
blake3.workspace = true
|
||||||
crc32fast.workspace = true
|
crc32fast.workspace = true
|
||||||
base16ct.workspace = true
|
base16ct.workspace = true
|
||||||
|
base64 = "0.22.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 The Dragonfly Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use base64::prelude::*;
|
||||||
|
use dragonfly_client_core::{
|
||||||
|
error::{ErrorType, OrErr},
|
||||||
|
Error, Result,
|
||||||
|
};
|
||||||
|
use http::header::{self, HeaderMap};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
/// Credentials is the credentials for the basic auth.
|
||||||
|
pub struct Credentials {
|
||||||
|
/// username is the username.
|
||||||
|
pub username: String,
|
||||||
|
|
||||||
|
/// password is the password.
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Credentials is the basic auth.
|
||||||
|
impl Credentials {
|
||||||
|
/// new returns a new Credentials.
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub fn new(username: &str, password: &str) -> Credentials {
|
||||||
|
Self {
|
||||||
|
username: username.to_string(),
|
||||||
|
password: password.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// verify verifies the basic auth with the header.
|
||||||
|
pub fn verify(&self, header: &HeaderMap) -> Result<()> {
|
||||||
|
let Some(auth_header) = header.get(header::AUTHORIZATION) else {
|
||||||
|
return Err(Error::Unauthorized);
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((typ, payload)) = auth_header
|
||||||
|
.to_str()
|
||||||
|
.or_err(ErrorType::ParseError)?
|
||||||
|
.to_string()
|
||||||
|
.split_once(' ')
|
||||||
|
{
|
||||||
|
if typ.to_lowercase() != "basic" {
|
||||||
|
return Err(Error::Unauthorized);
|
||||||
|
};
|
||||||
|
|
||||||
|
let decoded = String::from_utf8(
|
||||||
|
BASE64_STANDARD
|
||||||
|
.decode(payload)
|
||||||
|
.or_err(ErrorType::ParseError)?,
|
||||||
|
)
|
||||||
|
.or_err(ErrorType::ParseError)?;
|
||||||
|
|
||||||
|
let Some((username, password)) = decoded.split_once(':') else {
|
||||||
|
return Err(Error::Unauthorized);
|
||||||
|
};
|
||||||
|
|
||||||
|
if username != self.username || password != self.password {
|
||||||
|
return Err(Error::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,8 @@ use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
|
pub mod basic_auth;
|
||||||
|
|
||||||
/// reqwest_headermap_to_hashmap converts a reqwest headermap to a hashmap.
|
/// reqwest_headermap_to_hashmap converts a reqwest headermap to a hashmap.
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub fn reqwest_headermap_to_hashmap(header: &HeaderMap<HeaderValue>) -> HashMap<String, String> {
|
pub fn reqwest_headermap_to_hashmap(header: &HeaderMap<HeaderValue>) -> HashMap<String, String> {
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ use dragonfly_client_util::{
|
||||||
use futures_util::TryStreamExt;
|
use futures_util::TryStreamExt;
|
||||||
use http_body_util::{combinators::BoxBody, BodyExt, Empty, StreamBody};
|
use http_body_util::{combinators::BoxBody, BodyExt, Empty, StreamBody};
|
||||||
use hyper::body::Frame;
|
use hyper::body::Frame;
|
||||||
use hyper::client::conn::http1::Builder;
|
use hyper::client::conn::http1::Builder as ClientBuilder;
|
||||||
use hyper::server::conn::http1;
|
use hyper::server::conn::http1::Builder as ServerBuilder;
|
||||||
use hyper::service::service_fn;
|
use hyper::service::service_fn;
|
||||||
use hyper::upgrade::Upgraded;
|
use hyper::upgrade::Upgraded;
|
||||||
use hyper::{Method, Request};
|
use hyper::{Method, Request};
|
||||||
|
|
@ -170,7 +170,7 @@ impl Proxy {
|
||||||
let registry_cert = self.registry_cert.clone();
|
let registry_cert = self.registry_cert.clone();
|
||||||
let server_ca_cert = self.server_ca_cert.clone();
|
let server_ca_cert = self.server_ca_cert.clone();
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
if let Err(err) = http1::Builder::new()
|
if let Err(err) = ServerBuilder::new()
|
||||||
.keep_alive(true)
|
.keep_alive(true)
|
||||||
.preserve_header_case(true)
|
.preserve_header_case(true)
|
||||||
.title_case_headers(true)
|
.title_case_headers(true)
|
||||||
|
|
@ -315,6 +315,21 @@ pub async fn http_handler(
|
||||||
) -> ClientResult<Response> {
|
) -> ClientResult<Response> {
|
||||||
info!("handle HTTP request: {:?}", request);
|
info!("handle HTTP request: {:?}", request);
|
||||||
|
|
||||||
|
// Authenticate the request with the basic auth.
|
||||||
|
if let Some(basic_auth) = config.proxy.server.basic_auth.as_ref() {
|
||||||
|
match basic_auth.credentials().verify(request.headers()) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(ClientError::Unauthorized) => {
|
||||||
|
error!("basic auth failed");
|
||||||
|
return Ok(make_error_response(http::StatusCode::UNAUTHORIZED, None));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("verify basic auth failed: {}", err);
|
||||||
|
return Ok(make_error_response(http::StatusCode::BAD_REQUEST, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If find the matching rule, proxy the request via the dfdaemon.
|
// If find the matching rule, proxy the request via the dfdaemon.
|
||||||
let request_uri = request.uri();
|
let request_uri = request.uri();
|
||||||
if let Some(rule) =
|
if let Some(rule) =
|
||||||
|
|
@ -425,9 +440,6 @@ async fn upgraded_tunnel(
|
||||||
registry_cert: Arc<Option<Vec<CertificateDer<'static>>>>,
|
registry_cert: Arc<Option<Vec<CertificateDer<'static>>>>,
|
||||||
server_ca_cert: Arc<Option<Certificate>>,
|
server_ca_cert: Arc<Option<Certificate>>,
|
||||||
) -> ClientResult<()> {
|
) -> ClientResult<()> {
|
||||||
// Initialize the tcp stream to the remote server.
|
|
||||||
let upgraded = TokioIo::new(upgraded);
|
|
||||||
|
|
||||||
// Generate the self-signed certificate by the given host. If the ca_cert
|
// Generate the self-signed certificate by the given host. If the ca_cert
|
||||||
// is not set, use the self-signed certificate. Otherwise, use the CA
|
// is not set, use the self-signed certificate. Otherwise, use the CA
|
||||||
// certificate to sign the self-signed certificate.
|
// certificate to sign the self-signed certificate.
|
||||||
|
|
@ -449,8 +461,9 @@ async fn upgraded_tunnel(
|
||||||
.with_single_cert(server_certs, server_key)
|
.with_single_cert(server_certs, server_key)
|
||||||
.or_err(ErrorType::TLSConfigError)?;
|
.or_err(ErrorType::TLSConfigError)?;
|
||||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||||
|
|
||||||
let tls_acceptor = TlsAcceptor::from(Arc::new(server_config));
|
let tls_acceptor = TlsAcceptor::from(Arc::new(server_config));
|
||||||
let tls_stream = tls_acceptor.accept(upgraded).await?;
|
let tls_stream = tls_acceptor.accept(TokioIo::new(upgraded)).await?;
|
||||||
|
|
||||||
// Serve the connection with the TLS stream.
|
// Serve the connection with the TLS stream.
|
||||||
if let Err(err) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
|
if let Err(err) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
|
||||||
|
|
@ -490,6 +503,20 @@ pub async fn upgraded_handler(
|
||||||
Span::current().record("uri", request.uri().to_string().as_str());
|
Span::current().record("uri", request.uri().to_string().as_str());
|
||||||
Span::current().record("method", request.method().as_str());
|
Span::current().record("method", request.method().as_str());
|
||||||
|
|
||||||
|
// Authenticate the request with the basic auth.
|
||||||
|
if let Some(basic_auth) = config.proxy.server.basic_auth.as_ref() {
|
||||||
|
match basic_auth.credentials().verify(request.headers()) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(ClientError::Unauthorized) => {
|
||||||
|
return Ok(make_error_response(http::StatusCode::UNAUTHORIZED, None));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("verify basic auth failed: {}", err);
|
||||||
|
return Ok(make_error_response(http::StatusCode::BAD_REQUEST, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If the scheme is not set, set the scheme to https.
|
// If the scheme is not set, set the scheme to https.
|
||||||
if request.uri().scheme().is_none() {
|
if request.uri().scheme().is_none() {
|
||||||
*request.uri_mut() = format!("https://{}{}", host, request.uri())
|
*request.uri_mut() = format!("https://{}{}", host, request.uri())
|
||||||
|
|
@ -827,7 +854,7 @@ async fn proxy_http(request: Request<hyper::body::Incoming>) -> ClientResult<Res
|
||||||
|
|
||||||
let stream = TcpStream::connect((host, port)).await?;
|
let stream = TcpStream::connect((host, port)).await?;
|
||||||
let io = TokioIo::new(stream);
|
let io = TokioIo::new(stream);
|
||||||
let (mut client, conn) = Builder::new()
|
let (mut client, conn) = ClientBuilder::new()
|
||||||
.preserve_header_case(true)
|
.preserve_header_case(true)
|
||||||
.title_case_headers(true)
|
.title_case_headers(true)
|
||||||
.handshake(io)
|
.handshake(io)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue