use super::types::*; use crate::{Error, success_or_error}; use reqwest::{ IntoUrl, header::{HeaderMap, HeaderValue}, }; use sensitive_url::SensitiveUrl; use serde::{Serialize, de::DeserializeOwned}; use std::fmt::{self, Display}; use std::fs; use std::path::Path; pub use reqwest; pub use reqwest::{Response, StatusCode, Url}; use types::graffiti::GraffitiString; use zeroize::Zeroizing; /// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a /// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`). #[derive(Clone)] pub struct ValidatorClientHttpClient { client: reqwest::Client, server: SensitiveUrl, api_token: Option>, authorization_header: AuthorizationHeader, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum AuthorizationHeader { /// Do not send any Authorization header. Omit, /// Send a `Basic` Authorization header (legacy). Basic, /// Send a `Bearer` Authorization header. Bearer, } impl Display for AuthorizationHeader { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // The `Omit` variant should never be `Display`ed, but would result in a harmless rejection. write!(f, "{:?}", self) } } impl ValidatorClientHttpClient { /// Create a new client pre-initialised with an API token. pub fn new(server: SensitiveUrl, secret: String) -> Result { Ok(Self { client: reqwest::Client::new(), server, api_token: Some(secret.into()), authorization_header: AuthorizationHeader::Bearer, }) } /// Create a client without an API token. /// /// A token can be fetched by using `self.get_auth`, and then reading the token from disk. pub fn new_unauthenticated(server: SensitiveUrl) -> Result { Ok(Self { client: reqwest::Client::new(), server, api_token: None, authorization_header: AuthorizationHeader::Omit, }) } pub fn from_components( server: SensitiveUrl, client: reqwest::Client, secret: String, ) -> Result { Ok(Self { client, server, api_token: Some(secret.into()), authorization_header: AuthorizationHeader::Bearer, }) } /// Get a reference to this client's API token, if any. pub fn api_token(&self) -> Option<&Zeroizing> { self.api_token.as_ref() } /// Read an API token from the specified `path`, stripping any trailing whitespace. pub fn load_api_token_from_file(path: &Path) -> Result, Error> { let token = fs::read_to_string(path).map_err(|e| Error::TokenReadError(path.into(), e))?; Ok(token.trim_end().to_string().into()) } /// Add an authentication token to use when making requests. pub fn add_auth_token(&mut self, token: Zeroizing) -> Result<(), Error> { self.api_token = Some(token); self.authorization_header = AuthorizationHeader::Bearer; Ok(()) } /// Set to `false` to disable sending the `Authorization` header on requests. /// /// Failing to send the `Authorization` header will cause the VC to reject requests with a 403. /// This function is intended only for testing purposes. pub fn send_authorization_header(&mut self, should_send: bool) { if should_send { self.authorization_header = AuthorizationHeader::Bearer; } else { self.authorization_header = AuthorizationHeader::Omit; } } /// Use the legacy basic auth style (bearer auth preferred by default now). pub fn use_basic_auth(&mut self) { self.authorization_header = AuthorizationHeader::Basic; } fn headers(&self) -> Result { let mut headers = HeaderMap::new(); if self.authorization_header == AuthorizationHeader::Basic || self.authorization_header == AuthorizationHeader::Bearer { let auth_header_token = self.api_token().ok_or(Error::NoToken)?; let header_value = HeaderValue::from_str(&format!( "{} {}", self.authorization_header, auth_header_token.as_str() )) .map_err(|e| { Error::InvalidSecret(format!("secret is invalid as a header value: {}", e)) })?; headers.insert("Authorization", header_value); } Ok(headers) } /// Perform a HTTP GET request, returning the `Response` for further processing. async fn get_response(&self, url: U) -> Result { let response = self .client .get(url) .headers(self.headers()?) .send() .await .map_err(Error::from)?; success_or_error(response).await } /// Perform a HTTP DELETE request, returning the `Response` for further processing. async fn delete_response(&self, url: U) -> Result { let response = self .client .delete(url) .headers(self.headers()?) .send() .await .map_err(Error::from)?; success_or_error(response).await } async fn get(&self, url: U) -> Result { let response = self.get_response(url).await?; let body = response.bytes().await.map_err(Error::from)?; serde_json::from_slice(&body).map_err(Error::InvalidJson) } async fn delete(&self, url: U) -> Result<(), Error> { let response = self.delete_response(url).await?; if response.status().is_success() { Ok(()) } else { Err(Error::StatusCode(response.status())) } } async fn get_unsigned(&self, url: U) -> Result { self.get_response(url) .await? .json() .await .map_err(Error::from) } /// Perform a HTTP GET request, returning `None` on a 404 error. async fn get_opt(&self, url: U) -> Result, Error> { match self.get_response(url).await { Ok(resp) => { let body = resp.bytes().await.map(Option::Some)?; if let Some(body) = body { serde_json::from_slice(&body).map_err(Error::InvalidJson) } else { Ok(None) } } Err(err) => { if err.status() == Some(StatusCode::NOT_FOUND) { Ok(None) } else { Err(err) } } } } /// Perform a HTTP POST request. async fn post_with_raw_response( &self, url: U, body: &T, ) -> Result { let response = self .client .post(url) .headers(self.headers()?) .json(body) .send() .await .map_err(Error::from)?; success_or_error(response).await } async fn post( &self, url: U, body: &T, ) -> Result { let response = self.post_with_raw_response(url, body).await?; let body = response.bytes().await.map_err(Error::from)?; serde_json::from_slice(&body).map_err(Error::InvalidJson) } async fn post_with_unsigned_response( &self, url: U, body: &T, ) -> Result { let response = self.post_with_raw_response(url, body).await?; Ok(response.json().await?) } /// Perform a HTTP PATCH request. async fn patch(&self, url: U, body: &T) -> Result<(), Error> { let response = self .client .patch(url) .headers(self.headers()?) .json(body) .send() .await .map_err(Error::from)?; success_or_error(response).await?; Ok(()) } /// Perform a HTTP DELETE request. async fn delete_with_raw_response( &self, url: U, body: &T, ) -> Result { let response = self .client .delete(url) .headers(self.headers()?) .json(body) .send() .await .map_err(Error::from)?; success_or_error(response).await } /// Perform a HTTP DELETE request. async fn delete_with_unsigned_response( &self, url: U, body: &T, ) -> Result { let response = self.delete_with_raw_response(url, body).await?; Ok(response.json().await?) } /// `GET lighthouse/version` pub async fn get_lighthouse_version(&self) -> Result, Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("version"); self.get(path).await } /// `GET lighthouse/health` pub async fn get_lighthouse_health(&self) -> Result, Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("health"); self.get(path).await } /// `GET lighthouse/spec` pub async fn get_lighthouse_spec( &self, ) -> Result, Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("spec"); self.get(path).await } /// `GET lighthouse/validators` pub async fn get_lighthouse_validators( &self, ) -> Result>, Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("validators"); self.get(path).await } /// `GET lighthouse/validators/{validator_pubkey}` pub async fn get_lighthouse_validators_pubkey( &self, validator_pubkey: &PublicKeyBytes, ) -> Result>, Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("validators") .push(&validator_pubkey.to_string()); self.get_opt(path).await } /// `POST lighthouse/validators` pub async fn post_lighthouse_validators( &self, validators: Vec, ) -> Result, Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("validators"); self.post(path, &validators).await } /// `POST lighthouse/validators/mnemonic` pub async fn post_lighthouse_validators_mnemonic( &self, request: &CreateValidatorsMnemonicRequest, ) -> Result>, Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("validators") .push("mnemonic"); self.post(path, &request).await } /// `POST lighthouse/validators/keystore` pub async fn post_lighthouse_validators_keystore( &self, request: &KeystoreValidatorsPostRequest, ) -> Result, Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("validators") .push("keystore"); self.post(path, &request).await } /// `POST lighthouse/validators/web3signer` pub async fn post_lighthouse_validators_web3signer( &self, request: &[Web3SignerValidatorRequest], ) -> Result<(), Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("validators") .push("web3signer"); self.post(path, &request).await } /// `PATCH lighthouse/validators/{validator_pubkey}` #[allow(clippy::too_many_arguments)] pub async fn patch_lighthouse_validators( &self, voting_pubkey: &PublicKeyBytes, enabled: Option, gas_limit: Option, builder_proposals: Option, builder_boost_factor: Option, prefer_builder_proposals: Option, graffiti: Option, ) -> Result<(), Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("validators") .push(&voting_pubkey.to_string()); self.patch( path, &ValidatorPatchRequest { enabled, gas_limit, builder_proposals, builder_boost_factor, prefer_builder_proposals, graffiti, }, ) .await } /// `DELETE eth/v1/keystores` pub async fn delete_lighthouse_keystores( &self, req: &DeleteKeystoresRequest, ) -> Result { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("keystores"); self.delete_with_unsigned_response(path, req).await } fn make_keystores_url(&self) -> Result { let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("keystores"); Ok(url) } fn make_remotekeys_url(&self) -> Result { let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("remotekeys"); Ok(url) } fn make_fee_recipient_url(&self, pubkey: &PublicKeyBytes) -> Result { let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("validator") .push(&pubkey.to_string()) .push("feerecipient"); Ok(url) } fn make_graffiti_url(&self, pubkey: &PublicKeyBytes) -> Result { let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("validator") .push(&pubkey.to_string()) .push("graffiti"); Ok(url) } fn make_gas_limit_url(&self, pubkey: &PublicKeyBytes) -> Result { let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("validator") .push(&pubkey.to_string()) .push("gas_limit"); Ok(url) } /// `GET lighthouse/auth` pub async fn get_auth(&self) -> Result { let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") .push("auth"); self.get_unsigned(url).await } /// `GET eth/v1/keystores` pub async fn get_keystores(&self) -> Result { let url = self.make_keystores_url()?; self.get_unsigned(url).await } /// `POST eth/v1/keystores` pub async fn post_keystores( &self, req: &ImportKeystoresRequest, ) -> Result { let url = self.make_keystores_url()?; self.post_with_unsigned_response(url, req).await } /// `DELETE eth/v1/keystores` pub async fn delete_keystores( &self, req: &DeleteKeystoresRequest, ) -> Result { let url = self.make_keystores_url()?; self.delete_with_unsigned_response(url, req).await } /// `GET eth/v1/remotekeys` pub async fn get_remotekeys(&self) -> Result { let url = self.make_remotekeys_url()?; self.get_unsigned(url).await } /// `POST eth/v1/remotekeys` pub async fn post_remotekeys( &self, req: &ImportRemotekeysRequest, ) -> Result { let url = self.make_remotekeys_url()?; self.post_with_unsigned_response(url, req).await } /// `DELETE eth/v1/remotekeys` pub async fn delete_remotekeys( &self, req: &DeleteRemotekeysRequest, ) -> Result { let url = self.make_remotekeys_url()?; self.delete_with_unsigned_response(url, req).await } /// `GET /eth/v1/validator/{pubkey}/feerecipient` pub async fn get_fee_recipient( &self, pubkey: &PublicKeyBytes, ) -> Result { let url = self.make_fee_recipient_url(pubkey)?; self.get(url) .await .map(|generic: GenericResponse| generic.data) } /// `POST /eth/v1/validator/{pubkey}/feerecipient` pub async fn post_fee_recipient( &self, pubkey: &PublicKeyBytes, req: &UpdateFeeRecipientRequest, ) -> Result { let url = self.make_fee_recipient_url(pubkey)?; self.post_with_raw_response(url, req).await } /// `DELETE /eth/v1/validator/{pubkey}/feerecipient` pub async fn delete_fee_recipient(&self, pubkey: &PublicKeyBytes) -> Result { let url = self.make_fee_recipient_url(pubkey)?; self.delete_with_raw_response(url, &()).await } /// `GET /eth/v1/validator/{pubkey}/gas_limit` pub async fn get_gas_limit( &self, pubkey: &PublicKeyBytes, ) -> Result { let url = self.make_gas_limit_url(pubkey)?; self.get(url) .await .map(|generic: GenericResponse| generic.data) } /// `POST /eth/v1/validator/{pubkey}/gas_limit` pub async fn post_gas_limit( &self, pubkey: &PublicKeyBytes, req: &UpdateGasLimitRequest, ) -> Result { let url = self.make_gas_limit_url(pubkey)?; self.post_with_raw_response(url, req).await } /// `DELETE /eth/v1/validator/{pubkey}/gas_limit` pub async fn delete_gas_limit(&self, pubkey: &PublicKeyBytes) -> Result { let url = self.make_gas_limit_url(pubkey)?; self.delete_with_raw_response(url, &()).await } /// `POST /eth/v1/validator/{pubkey}/voluntary_exit` pub async fn post_validator_voluntary_exit( &self, pubkey: &PublicKeyBytes, epoch: Option, ) -> Result, Error> { let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") .push("v1") .push("validator") .push(&pubkey.to_string()) .push("voluntary_exit"); if let Some(epoch) = epoch { path.query_pairs_mut() .append_pair("epoch", &epoch.to_string()); } self.post(path, &()).await } /// `GET /eth/v1/validator/{pubkey}/graffiti` pub async fn get_graffiti( &self, pubkey: &PublicKeyBytes, ) -> Result { let url = self.make_graffiti_url(pubkey)?; self.get(url) .await .map(|generic: GenericResponse| generic.data) } /// `POST /eth/v1/validator/{pubkey}/graffiti` pub async fn set_graffiti( &self, pubkey: &PublicKeyBytes, graffiti: GraffitiString, ) -> Result<(), Error> { let url = self.make_graffiti_url(pubkey)?; let set_graffiti_request = SetGraffitiRequest { graffiti }; self.post(url, &set_graffiti_request).await } /// `DELETE /eth/v1/validator/{pubkey}/graffiti` pub async fn delete_graffiti(&self, pubkey: &PublicKeyBytes) -> Result<(), Error> { let url = self.make_graffiti_url(pubkey)?; self.delete(url).await } }