use actix_web::dev::Payload;
use actix_web::web::{Bytes, BytesMut};
use actix_web::HttpRequest;
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use bsv::auth::types::{AuthMessage, MessageType};
use futures_util::StreamExt;
#[derive(Clone, Debug)]
pub struct AuthHeaders {
pub version: String,
pub identity_key: String,
pub nonce: String,
pub your_nonce: String,
pub signature: String,
pub request_id: String,
}
pub fn extract_auth_headers(req: &HttpRequest) -> Option<AuthHeaders> {
let version = req
.headers()
.get("x-bsv-auth-version")?
.to_str()
.ok()?
.to_string();
let identity_key = req
.headers()
.get("x-bsv-auth-identity-key")?
.to_str()
.ok()?
.to_string();
let nonce = req
.headers()
.get("x-bsv-auth-nonce")?
.to_str()
.ok()?
.to_string();
let your_nonce = req
.headers()
.get("x-bsv-auth-your-nonce")?
.to_str()
.ok()?
.to_string();
let signature = req
.headers()
.get("x-bsv-auth-signature")?
.to_str()
.ok()?
.to_string();
let request_id = req
.headers()
.get("x-bsv-auth-request-id")?
.to_str()
.ok()?
.to_string();
Some(AuthHeaders {
version,
identity_key,
nonce,
your_nonce,
signature,
request_id,
})
}
pub async fn read_body(mut payload: Payload) -> Result<Bytes, actix_web::Error> {
let mut body = BytesMut::new();
while let Some(chunk) = payload.next().await {
body.extend_from_slice(&chunk?);
}
Ok(body.freeze())
}
pub fn payload_from_bytes(bytes: Bytes) -> Payload {
let (_, mut h1_payload) = actix_http::h1::Payload::create(true);
h1_payload.unread_data(bytes);
Payload::from(h1_payload)
}
pub fn build_auth_message(
req: &HttpRequest,
body_bytes: &[u8],
headers: &AuthHeaders,
) -> AuthMessage {
let request_nonce_bytes = BASE64.decode(&headers.request_id).unwrap_or_default();
let payload =
crate::payload::serialize_from_http_request(&request_nonce_bytes, req, body_bytes);
tracing::debug!(
"build_auth_message: method={} path={} query={} body_len={} nonce_len={} request_id={} nonce={} your_nonce={} payload_len={}",
req.method(),
req.path(),
req.query_string(),
body_bytes.len(),
request_nonce_bytes.len(),
headers.request_id,
headers.nonce,
headers.your_nonce,
payload.len(),
);
let signature_bytes = hex::decode(&headers.signature).unwrap_or_default();
AuthMessage {
version: headers.version.clone(),
message_type: MessageType::General,
identity_key: headers.identity_key.clone(),
nonce: Some(headers.nonce.clone()),
your_nonce: Some(headers.your_nonce.clone()),
initial_nonce: None,
certificates: None,
requested_certificates: None,
payload: Some(payload),
signature: Some(signature_bytes),
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::test::TestRequest;
#[test]
fn test_extract_auth_headers_all_present() {
let req = TestRequest::default()
.insert_header(("x-bsv-auth-version", "0.1"))
.insert_header(("x-bsv-auth-identity-key", "02abc123"))
.insert_header(("x-bsv-auth-nonce", "nonce1"))
.insert_header(("x-bsv-auth-your-nonce", "nonce2"))
.insert_header(("x-bsv-auth-signature", "deadbeef"))
.insert_header(("x-bsv-auth-request-id", "AQIDBA=="))
.to_http_request();
let headers = extract_auth_headers(&req).expect("should extract all headers");
assert_eq!(headers.version, "0.1");
assert_eq!(headers.identity_key, "02abc123");
assert_eq!(headers.nonce, "nonce1");
assert_eq!(headers.your_nonce, "nonce2");
assert_eq!(headers.signature, "deadbeef");
assert_eq!(headers.request_id, "AQIDBA==");
}
#[test]
fn test_extract_auth_headers_missing_one() {
let req = TestRequest::default()
.insert_header(("x-bsv-auth-version", "0.1"))
.insert_header(("x-bsv-auth-identity-key", "02abc123"))
.insert_header(("x-bsv-auth-your-nonce", "nonce2"))
.insert_header(("x-bsv-auth-signature", "deadbeef"))
.insert_header(("x-bsv-auth-request-id", "AQIDBA=="))
.to_http_request();
assert!(extract_auth_headers(&req).is_none());
}
#[test]
fn test_extract_auth_headers_missing_all() {
let req = TestRequest::default().to_http_request();
assert!(extract_auth_headers(&req).is_none());
}
#[actix_web::test]
async fn test_payload_from_bytes_roundtrip() {
let original = Bytes::from_static(b"hello world");
let payload = payload_from_bytes(original.clone());
let recovered = read_body(payload).await.expect("should read body");
assert_eq!(recovered, original);
}
#[test]
fn test_build_auth_message_basic() {
let req = TestRequest::default()
.method(actix_web::http::Method::GET)
.uri("/test")
.to_http_request();
let headers = AuthHeaders {
version: "0.1".to_string(),
identity_key: "02abc123".to_string(),
nonce: "nonce1".to_string(),
your_nonce: "nonce2".to_string(),
signature: "deadbeef".to_string(),
request_id: "AQIDBA==".to_string(), };
let msg = build_auth_message(&req, b"", &headers);
assert_eq!(msg.version, "0.1");
assert_eq!(msg.identity_key, "02abc123");
assert_eq!(msg.nonce, Some("nonce1".to_string()));
assert_eq!(msg.your_nonce, Some("nonce2".to_string()));
assert!(matches!(msg.message_type, MessageType::General));
assert!(msg.payload.is_some());
assert_eq!(msg.signature, Some(vec![0xde, 0xad, 0xbe, 0xef]));
}
}