merge: main
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
use arbiter_proto::{
|
||||
format_challenge,
|
||||
ClientMetadata, format_challenge,
|
||||
proto::client::{
|
||||
AuthChallengeRequest, AuthChallengeSolution, AuthResult, ClientRequest,
|
||||
client_request::Payload as ClientRequestPayload,
|
||||
AuthChallengeRequest, AuthChallengeSolution, AuthResult, ClientInfo as ProtoClientInfo,
|
||||
ClientRequest, client_request::Payload as ClientRequestPayload,
|
||||
client_response::Payload as ClientResponsePayload,
|
||||
},
|
||||
};
|
||||
@@ -14,19 +14,7 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConnectError {
|
||||
#[error("Could not establish connection")]
|
||||
Connection(#[from] tonic::transport::Error),
|
||||
|
||||
#[error("Invalid server URI")]
|
||||
InvalidUri(#[from] http::uri::InvalidUri),
|
||||
|
||||
#[error("Invalid CA certificate")]
|
||||
InvalidCaCert(#[from] webpki::Error),
|
||||
|
||||
#[error("gRPC error")]
|
||||
Grpc(#[from] tonic::Status),
|
||||
|
||||
pub enum AuthError {
|
||||
#[error("Auth challenge was not returned by server")]
|
||||
MissingAuthChallenge,
|
||||
|
||||
@@ -43,48 +31,54 @@ pub enum ConnectError {
|
||||
Storage(#[from] StorageError),
|
||||
}
|
||||
|
||||
fn map_auth_result(code: i32) -> ConnectError {
|
||||
fn map_auth_result(code: i32) -> AuthError {
|
||||
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
|
||||
AuthResult::ApprovalDenied => ConnectError::ApprovalDenied,
|
||||
AuthResult::NoUserAgentsOnline => ConnectError::NoUserAgentsOnline,
|
||||
AuthResult::ApprovalDenied => AuthError::ApprovalDenied,
|
||||
AuthResult::NoUserAgentsOnline => AuthError::NoUserAgentsOnline,
|
||||
AuthResult::Unspecified
|
||||
| AuthResult::Success
|
||||
| AuthResult::InvalidKey
|
||||
| AuthResult::InvalidSignature
|
||||
| AuthResult::Internal => ConnectError::UnexpectedAuthResponse,
|
||||
| AuthResult::Internal => AuthError::UnexpectedAuthResponse,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_auth_challenge_request(
|
||||
transport: &mut ClientTransport,
|
||||
metadata: ClientMetadata,
|
||||
key: &ed25519_dalek::SigningKey,
|
||||
) -> std::result::Result<(), ConnectError> {
|
||||
) -> std::result::Result<(), AuthError> {
|
||||
transport
|
||||
.send(ClientRequest {
|
||||
request_id: next_request_id(),
|
||||
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
||||
AuthChallengeRequest {
|
||||
pubkey: key.verifying_key().to_bytes().to_vec(),
|
||||
client_info: Some(ProtoClientInfo {
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
version: metadata.version,
|
||||
}),
|
||||
},
|
||||
)),
|
||||
})
|
||||
.await
|
||||
.map_err(|_| ConnectError::UnexpectedAuthResponse)
|
||||
.map_err(|_| AuthError::UnexpectedAuthResponse)
|
||||
}
|
||||
|
||||
async fn receive_auth_challenge(
|
||||
transport: &mut ClientTransport,
|
||||
) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, ConnectError> {
|
||||
) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, AuthError> {
|
||||
let response = transport
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|_| ConnectError::MissingAuthChallenge)?;
|
||||
.map_err(|_| AuthError::MissingAuthChallenge)?;
|
||||
|
||||
let payload = response.payload.ok_or(ConnectError::MissingAuthChallenge)?;
|
||||
let payload = response.payload.ok_or(AuthError::MissingAuthChallenge)?;
|
||||
match payload {
|
||||
ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge),
|
||||
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)),
|
||||
_ => Err(ConnectError::UnexpectedAuthResponse),
|
||||
_ => Err(AuthError::UnexpectedAuthResponse),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +86,7 @@ async fn send_auth_challenge_solution(
|
||||
transport: &mut ClientTransport,
|
||||
key: &ed25519_dalek::SigningKey,
|
||||
challenge: arbiter_proto::proto::client::AuthChallenge,
|
||||
) -> std::result::Result<(), ConnectError> {
|
||||
) -> std::result::Result<(), AuthError> {
|
||||
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
|
||||
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
|
||||
|
||||
@@ -104,20 +98,20 @@ async fn send_auth_challenge_solution(
|
||||
)),
|
||||
})
|
||||
.await
|
||||
.map_err(|_| ConnectError::UnexpectedAuthResponse)
|
||||
.map_err(|_| AuthError::UnexpectedAuthResponse)
|
||||
}
|
||||
|
||||
async fn receive_auth_confirmation(
|
||||
transport: &mut ClientTransport,
|
||||
) -> std::result::Result<(), ConnectError> {
|
||||
) -> std::result::Result<(), AuthError> {
|
||||
let response = transport
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|_| ConnectError::UnexpectedAuthResponse)?;
|
||||
.map_err(|_| AuthError::UnexpectedAuthResponse)?;
|
||||
|
||||
let payload = response
|
||||
.payload
|
||||
.ok_or(ConnectError::UnexpectedAuthResponse)?;
|
||||
.ok_or(AuthError::UnexpectedAuthResponse)?;
|
||||
match payload {
|
||||
ClientResponsePayload::AuthResult(result)
|
||||
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
|
||||
@@ -125,15 +119,16 @@ async fn receive_auth_confirmation(
|
||||
Ok(())
|
||||
}
|
||||
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)),
|
||||
_ => Err(ConnectError::UnexpectedAuthResponse),
|
||||
_ => Err(AuthError::UnexpectedAuthResponse),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn authenticate(
|
||||
transport: &mut ClientTransport,
|
||||
metadata: ClientMetadata,
|
||||
key: &ed25519_dalek::SigningKey,
|
||||
) -> std::result::Result<(), ConnectError> {
|
||||
send_auth_challenge_request(transport, key).await?;
|
||||
) -> std::result::Result<(), AuthError> {
|
||||
send_auth_challenge_request(transport, metadata, key).await?;
|
||||
let challenge = receive_auth_challenge(transport).await?;
|
||||
send_auth_challenge_solution(transport, key, challenge).await?;
|
||||
receive_auth_confirmation(transport).await
|
||||
|
||||
48
server/crates/arbiter-client/src/bin/test_connect.rs
Normal file
48
server/crates/arbiter-client/src/bin/test_connect.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
use arbiter_client::ArbiterClient;
|
||||
use arbiter_proto::{ClientMetadata, url::ArbiterUrl};
|
||||
use tonic::ConnectError;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("Testing connection to Arbiter server...");
|
||||
print!("Enter ArbiterUrl: ");
|
||||
let _ = io::stdout().flush();
|
||||
|
||||
let mut input = String::new();
|
||||
if let Err(err) = io::stdin().read_line(&mut input) {
|
||||
eprintln!("Failed to read input: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
eprintln!("ArbiterUrl cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
let url = match ArbiterUrl::try_from(input) {
|
||||
Ok(url) => url,
|
||||
Err(err) => {
|
||||
eprintln!("Invalid ArbiterUrl: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("{:#?}", url);
|
||||
|
||||
let metadata = ClientMetadata {
|
||||
name: "arbiter-client test_connect".to_string(),
|
||||
description: Some("Manual connection smoke test".to_string()),
|
||||
version: Some(env!("CARGO_PKG_VERSION").to_string()),
|
||||
};
|
||||
|
||||
match ArbiterClient::connect(url, metadata).await {
|
||||
Ok(_) => println!("Connected and authenticated successfully."),
|
||||
Err(err) => eprintln!("Failed to connect: {:#?}", err),
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,36 @@
|
||||
use arbiter_proto::{proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
|
||||
use arbiter_proto::{ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::transport::ClientTlsConfig;
|
||||
|
||||
use crate::{
|
||||
auth::{ConnectError, authenticate},
|
||||
storage::{FileSigningKeyStorage, SigningKeyStorage},
|
||||
transport::{BUFFER_LENGTH, ClientTransport},
|
||||
StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport}
|
||||
};
|
||||
|
||||
#[cfg(feature = "evm")]
|
||||
use crate::wallets::evm::ArbiterEvmWallet;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ClientError {
|
||||
pub enum Error {
|
||||
#[error("gRPC error")]
|
||||
Grpc(#[from] tonic::Status),
|
||||
|
||||
#[error("Connection closed by server")]
|
||||
ConnectionClosed,
|
||||
#[error("Could not establish connection")]
|
||||
Connection(#[from] tonic::transport::Error),
|
||||
|
||||
#[error("Invalid server URI")]
|
||||
InvalidUri(#[from] http::uri::InvalidUri),
|
||||
|
||||
#[error("Invalid CA certificate")]
|
||||
InvalidCaCert(#[from] webpki::Error),
|
||||
|
||||
#[error("Authentication error")]
|
||||
Authentication(#[from] AuthError),
|
||||
|
||||
#[error("Storage error")]
|
||||
Storage(#[from] StorageError),
|
||||
|
||||
}
|
||||
|
||||
pub struct ArbiterClient {
|
||||
@@ -28,27 +39,29 @@ pub struct ArbiterClient {
|
||||
}
|
||||
|
||||
impl ArbiterClient {
|
||||
pub async fn connect(url: ArbiterUrl) -> Result<Self, ConnectError> {
|
||||
pub async fn connect(url: ArbiterUrl, metadata: ClientMetadata) -> Result<Self, Error> {
|
||||
let storage = FileSigningKeyStorage::from_default_location()?;
|
||||
Self::connect_with_storage(url, &storage).await
|
||||
Self::connect_with_storage(url, metadata, &storage).await
|
||||
}
|
||||
|
||||
pub async fn connect_with_storage<S: SigningKeyStorage>(
|
||||
url: ArbiterUrl,
|
||||
metadata: ClientMetadata,
|
||||
storage: &S,
|
||||
) -> Result<Self, ConnectError> {
|
||||
) -> Result<Self, Error> {
|
||||
let key = storage.load_or_create()?;
|
||||
Self::connect_with_key(url, key).await
|
||||
Self::connect_with_key(url, metadata, key).await
|
||||
}
|
||||
|
||||
pub async fn connect_with_key(
|
||||
url: ArbiterUrl,
|
||||
metadata: ClientMetadata,
|
||||
key: ed25519_dalek::SigningKey,
|
||||
) -> Result<Self, ConnectError> {
|
||||
) -> Result<Self, Error> {
|
||||
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
||||
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
||||
|
||||
let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))?
|
||||
let channel = tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
|
||||
.tls_config(tls)?
|
||||
.connect()
|
||||
.await?;
|
||||
@@ -62,7 +75,7 @@ impl ArbiterClient {
|
||||
receiver: response_stream,
|
||||
};
|
||||
|
||||
authenticate(&mut transport, &key).await?;
|
||||
authenticate(&mut transport, metadata, &key).await?;
|
||||
|
||||
Ok(Self {
|
||||
transport: Arc::new(Mutex::new(transport)),
|
||||
@@ -70,7 +83,7 @@ impl ArbiterClient {
|
||||
}
|
||||
|
||||
#[cfg(feature = "evm")]
|
||||
pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, ClientError> {
|
||||
pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, Error> {
|
||||
todo!("fetch EVM wallet list from server")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ mod storage;
|
||||
mod transport;
|
||||
pub mod wallets;
|
||||
|
||||
pub use auth::ConnectError;
|
||||
pub use client::{ArbiterClient, ClientError};
|
||||
pub use auth::AuthError;
|
||||
pub use client::{ArbiterClient, Error};
|
||||
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
|
||||
|
||||
#[cfg(feature = "evm")]
|
||||
|
||||
Reference in New Issue
Block a user