use std::{
borrow::Cow,
sync::Arc,
time::{Duration, SystemTime},
};
use anyhow::{Context, ensure};
use arc_swap::ArcSwapOption;
use async_trait::async_trait;
use lexe_api::{
auth::{self, BearerAuthenticator},
def::{
AppBackendApi, AppGatewayApi, AppNodeProvisionApi, AppNodeRunApi,
BearerAuthBackendApi,
},
error::{BackendApiError, GatewayApiError, NodeApiError, NodeErrorKind},
models::{
command::{
BackupInfo, CloseChannelRequest, CreateInvoiceRequest,
CreateInvoiceResponse, CreateOfferRequest, CreateOfferResponse,
DebugInfo, EnclavesToProvisionRequest, GetAddressResponse,
GetNewPayments, GetUpdatedPayments, HumanBitcoinAddress,
ListChannelsResponse, NodeInfo, NodeInfoV1, OpenChannelRequest,
OpenChannelResponse, PayInvoiceRequest, PayInvoiceResponse,
PayOfferRequest, PayOfferResponse, PayOnchainRequest,
PayOnchainResponse, PaymentCreatedIndexes, PaymentIdStruct,
PreflightCloseChannelRequest, PreflightCloseChannelResponse,
PreflightOpenChannelRequest, PreflightOpenChannelResponse,
PreflightPayInvoiceRequest, PreflightPayInvoiceResponse,
PreflightPayOfferRequest, PreflightPayOfferResponse,
PreflightPayOnchainRequest, PreflightPayOnchainResponse,
SetupGDrive, UpdatePaymentNote,
},
nwc::{
CreateNwcClientRequest, CreateNwcClientResponse,
ListNwcClientResponse, NostrPkStruct, UpdateNwcClientRequest,
UpdateNwcClientResponse,
},
},
rest::{POST, RequestBuilderExt, RestClient},
types::{
Empty,
payments::{MaybeBasicPaymentV2, VecBasicPaymentV1, VecBasicPaymentV2},
username::UsernameStruct,
},
};
use lexe_common::{
api::{
auth::{
BearerAuthRequestWire, BearerAuthResponse, BearerAuthToken, Scope,
TokenWithExpiration, UserSignupRequestWire,
UserSignupRequestWireV1,
},
fiat_rates::FiatRates,
models::{
SignMsgRequest, SignMsgResponse, VerifyMsgRequest,
VerifyMsgResponse,
},
provision::NodeProvisionRequest,
revocable_clients::{
CreateRevocableClientRequest, CreateRevocableClientResponse,
GetRevocableClients, RevocableClient, RevocableClients,
UpdateClientRequest, UpdateClientResponse,
},
user::UserPk,
version::{CurrentEnclaves, EnclavesToProvision, NodeEnclave},
},
byte_str::ByteStr,
constants::{self, node_provision_dns},
env::DeployEnv,
};
use lexe_crypto::{ed25519, rng::Crng};
use lexe_enclave::enclave::Measurement;
use lexe_tls::{attest_client, lexe_ca, rustls};
use reqwest::Url;
use crate::credentials::{ClientCredentials, CredentialsRef};
#[derive(Clone)]
pub struct GatewayClient {
rest: RestClient,
gateway_url: Cow<'static, str>,
}
#[derive(Clone)]
pub struct NodeClient {
inner: Arc<NodeClientInner>,
}
struct NodeClientInner {
user_pk: Option<UserPk>,
gateway_client: GatewayClient,
run_rest: ArcSwapOption<RunRestClient>,
run_url: &'static str,
use_sgx: bool,
deploy_env: DeployEnv,
authenticator: Arc<BearerAuthenticator>,
tls_config: rustls::ClientConfig,
}
struct RunRestClient {
client: RestClient,
token_expiration: Option<SystemTime>,
}
impl GatewayClient {
pub fn new(
deploy_env: DeployEnv,
gateway_url: impl Into<Cow<'static, str>>,
user_agent: impl Into<Cow<'static, str>>,
) -> anyhow::Result<Self> {
fn inner(
deploy_env: DeployEnv,
gateway_url: Cow<'static, str>,
user_agent: Cow<'static, str>,
) -> anyhow::Result<GatewayClient> {
let tls_config = lexe_ca::app_gateway_client_config(deploy_env);
let rest = RestClient::new(user_agent, "gateway", tls_config);
Ok(GatewayClient { rest, gateway_url })
}
inner(deploy_env, gateway_url.into(), user_agent.into())
}
}
impl AppBackendApi for GatewayClient {
async fn signup_v2(
&self,
signed_req: &ed25519::Signed<&UserSignupRequestWire>,
) -> Result<Empty, BackendApiError> {
let gateway_url = &self.gateway_url;
let req = self
.rest
.builder(POST, format!("{gateway_url}/app/v2/signup"))
.signed_bcs(signed_req)
.map_err(BackendApiError::bcs_serialize)?;
self.rest.send(req).await
}
async fn signup_v1(
&self,
_signed_req: &ed25519::Signed<&UserSignupRequestWireV1>,
) -> Result<Empty, BackendApiError> {
debug_assert!(false, "Use `signup_v2`");
Err(BackendApiError::not_found("Use `/app/v2/signup`"))
}
async fn enclaves_to_provision(
&self,
req: &EnclavesToProvisionRequest,
auth: BearerAuthToken,
) -> Result<EnclavesToProvision, BackendApiError> {
let gateway_url = &self.gateway_url;
let url = format!("{gateway_url}/app/v1/enclaves_to_provision");
let req = self.rest.post(url, req).bearer_auth(&auth);
self.rest.send(req).await
}
}
#[async_trait]
impl BearerAuthBackendApi for GatewayClient {
async fn bearer_auth(
&self,
signed_req: &ed25519::Signed<&BearerAuthRequestWire>,
) -> Result<BearerAuthResponse, BackendApiError> {
let gateway_url = &self.gateway_url;
let req = self
.rest
.builder(POST, format!("{gateway_url}/app/bearer_auth"))
.signed_bcs(signed_req)
.map_err(BackendApiError::bcs_serialize)?;
self.rest.send(req).await
}
}
impl AppGatewayApi for GatewayClient {
async fn get_fiat_rates(&self) -> Result<FiatRates, GatewayApiError> {
let gateway_url = &self.gateway_url;
let req = self
.rest
.get(format!("{gateway_url}/app/v1/fiat_rates"), &Empty {});
self.rest.send(req).await
}
async fn latest_release(&self) -> Result<NodeEnclave, GatewayApiError> {
let gateway_url = &self.gateway_url;
let req = self
.rest
.get(format!("{gateway_url}/app/v1/latest_release"), &Empty {});
self.rest.send(req).await
}
async fn current_releases(
&self,
) -> Result<CurrentEnclaves, GatewayApiError> {
let gateway_url = &self.gateway_url;
let req = self
.rest
.get(format!("{gateway_url}/app/v1/current_releases"), &Empty {});
self.rest.send(req).await
}
async fn current_enclaves(
&self,
) -> Result<CurrentEnclaves, GatewayApiError> {
let gateway_url = &self.gateway_url;
let req = self
.rest
.get(format!("{gateway_url}/app/v1/current_enclaves"), &Empty {});
self.rest.send(req).await
}
}
impl NodeClient {
pub fn new(
rng: &mut impl Crng,
use_sgx: bool,
deploy_env: DeployEnv,
gateway_client: GatewayClient,
credentials: CredentialsRef<'_>,
) -> anyhow::Result<Self> {
let run_url = constants::NODE_RUN_URL;
let gateway_url = &gateway_client.gateway_url;
ensure!(
gateway_url.starts_with("https://"),
"proxy connection must be https: gateway url: {gateway_url}",
);
let user_pk = credentials.user_pk();
let authenticator = credentials.bearer_authenticator();
let tls_config = credentials.tls_config(rng, deploy_env)?;
let run_rest = ArcSwapOption::from(None);
Ok(Self {
inner: Arc::new(NodeClientInner {
user_pk,
gateway_client,
run_rest,
run_url,
use_sgx,
deploy_env,
authenticator,
tls_config,
}),
})
}
pub fn user_pk(&self) -> Option<UserPk> {
self.inner.user_pk
}
async fn authed_run_rest(
&self,
) -> Result<Arc<RunRestClient>, NodeApiError> {
let now = SystemTime::now();
if let Some(run_rest) = self.maybe_authed_run_rest(now) {
return Ok(run_rest);
}
let auth_token = self.get_auth_token(now).await?;
if let Some(run_rest) = self.maybe_authed_run_rest(now) {
return Ok(run_rest);
}
let run_rest = RunRestClient::new(
&self.inner.gateway_client,
self.inner.run_url,
auth_token,
self.inner.tls_config.clone(),
)
.map_err(NodeApiError::bad_auth)?;
let run_rest = Arc::new(run_rest);
self.inner.run_rest.swap(Some(run_rest.clone()));
Ok(run_rest)
}
fn maybe_authed_run_rest(
&self,
now: SystemTime,
) -> Option<Arc<RunRestClient>> {
let maybe_run_rest = self.inner.run_rest.load_full();
if let Some(run_rest) = maybe_run_rest
&& !run_rest.token_needs_refresh(now)
{
Some(run_rest)
} else {
None
}
}
async fn get_auth_token(
&self,
now: SystemTime,
) -> Result<TokenWithExpiration, NodeApiError> {
self.inner
.authenticator
.get_token_with_exp(&self.inner.gateway_client, now)
.await
.map_err(|backend_error| {
let msg = format!("{backend_error:#}");
let BackendApiError {
data, sensitive, ..
} = backend_error;
NodeApiError {
kind: NodeErrorKind::BadAuth,
msg,
data,
sensitive,
}
})
}
fn provision_rest_client(
&self,
provision_url: &str,
auth_token: BearerAuthToken,
measurement: Measurement,
) -> anyhow::Result<RestClient> {
let proxy = static_proxy_config(
&self.inner.gateway_client.gateway_url,
provision_url,
auth_token,
)
.context("Invalid proxy config")?;
let tls_config = attest_client::app_node_provision_client_config(
self.inner.use_sgx,
self.inner.deploy_env,
measurement,
);
let user_agent = self.inner.gateway_client.rest.user_agent().clone();
let (from, to) = (user_agent, "node-provision");
let reqwest_client = RestClient::client_builder(&from)
.proxy(proxy)
.use_preconfigured_tls(tls_config)
.timeout(Duration::from_secs(30))
.build()
.context("Failed to build client")?;
let provision_rest = RestClient::from_inner(reqwest_client, from, to);
Ok(provision_rest)
}
pub async fn create_client_credentials(
&self,
req: CreateRevocableClientRequest,
) -> anyhow::Result<(RevocableClient, ClientCredentials)> {
let lexe_auth_token = self.request_long_lived_connect_token().await?;
let resp = self.create_revocable_client(req.clone()).await?;
let client = RevocableClient {
pubkey: resp.pubkey,
created_at: resp.created_at,
label: req.label,
scope: req.scope,
expires_at: req.expires_at,
is_revoked: false,
};
let client_credentials =
ClientCredentials::from_response(lexe_auth_token, resp);
Ok((client, client_credentials))
}
async fn request_long_lived_connect_token(
&self,
) -> anyhow::Result<BearerAuthToken> {
let user_key_pair = self
.inner
.authenticator
.user_key_pair()
.context("Somehow using a static bearer auth token")?;
let now = SystemTime::now();
let lifetime_secs = 10 * 365 * 24 * 60 * 60; let scope = Some(Scope::NodeConnect);
let long_lived_connect_token = lexe_api::auth::do_bearer_auth(
&self.inner.gateway_client,
now,
user_key_pair,
lifetime_secs,
scope,
)
.await
.context("Failed to get long-lived connect token")?;
Ok(long_lived_connect_token.token)
}
pub async fn request_provision_token(
&self,
) -> anyhow::Result<BearerAuthToken> {
let user_key_pair = self
.inner
.authenticator
.user_key_pair()
.context("Somehow using a static bearer auth token")?;
let now = SystemTime::now();
let lifetime_secs = 60; let scope = Some(Scope::All);
let token = lexe_api::auth::do_bearer_auth(
&self.inner.gateway_client,
now,
user_key_pair,
lifetime_secs,
scope,
)
.await
.context("Failed to get app token")?;
Ok(token.token)
}
}
impl AppNodeProvisionApi for NodeClient {
async fn provision(
&self,
measurement: Measurement,
data: NodeProvisionRequest,
) -> Result<Empty, NodeApiError> {
let now = SystemTime::now();
let mr_short = measurement.short();
let provision_dns = node_provision_dns(&mr_short);
let provision_url = format!("https://{provision_dns}");
let auth_token = self.get_auth_token(now).await?.token;
let provision_rest = self
.provision_rest_client(&provision_url, auth_token, measurement)
.context("Failed to build provision rest client")
.map_err(NodeApiError::provision)?;
let req = provision_rest
.post(format!("{provision_url}/app/provision"), &data);
provision_rest.send(req).await
}
}
impl AppNodeRunApi for NodeClient {
async fn node_info(&self) -> Result<NodeInfo, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/v2/node_info");
let req = run_rest.get(url, &Empty {});
run_rest.send(req).await
}
async fn node_info_v1(&self) -> Result<NodeInfoV1, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/node_info");
let req = run_rest.get(url, &Empty {});
run_rest.send(req).await
}
async fn debug_info(&self) -> Result<DebugInfo, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/debug_info");
let req = run_rest.get(url, &Empty {});
run_rest.send(req).await
}
async fn list_channels(
&self,
) -> Result<ListChannelsResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/list_channels");
let req = run_rest.get(url, &Empty {});
run_rest.send(req).await
}
async fn sign_message(
&self,
data: SignMsgRequest,
) -> Result<SignMsgResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/sign_message");
let req = run_rest.post(url, &data);
run_rest.send(req).await
}
async fn verify_message(
&self,
data: VerifyMsgRequest,
) -> Result<VerifyMsgResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/verify_message");
let req = run_rest.post(url, &data);
run_rest.send(req).await
}
async fn open_channel(
&self,
data: OpenChannelRequest,
) -> Result<OpenChannelResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/open_channel");
let req = run_rest.post(url, &data);
run_rest.send(req).await
}
async fn preflight_open_channel(
&self,
data: PreflightOpenChannelRequest,
) -> Result<PreflightOpenChannelResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/preflight_open_channel");
let req = run_rest.post(url, &data);
run_rest.send(req).await
}
async fn close_channel(
&self,
data: CloseChannelRequest,
) -> Result<Empty, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/close_channel");
let req = run_rest.post(url, &data);
run_rest.send(req).await
}
async fn preflight_close_channel(
&self,
data: PreflightCloseChannelRequest,
) -> Result<PreflightCloseChannelResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/preflight_close_channel");
let req = run_rest.post(url, &data);
run_rest.send(req).await
}
async fn create_invoice(
&self,
data: CreateInvoiceRequest,
) -> Result<CreateInvoiceResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/create_invoice");
let req = run_rest.post(url, &data);
run_rest.send(req).await
}
async fn pay_invoice(
&self,
req: PayInvoiceRequest,
) -> Result<PayInvoiceResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/pay_invoice");
let req = run_rest
.post(url, &req)
.timeout(constants::MAX_FLOW_TIMEOUT + Duration::from_secs(2));
run_rest.send(req).await
}
async fn preflight_pay_invoice(
&self,
req: PreflightPayInvoiceRequest,
) -> Result<PreflightPayInvoiceResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/preflight_pay_invoice");
let req = run_rest
.post(url, &req)
.timeout(constants::MAX_FLOW_TIMEOUT + Duration::from_secs(2));
run_rest.send(req).await
}
async fn pay_onchain(
&self,
req: PayOnchainRequest,
) -> Result<PayOnchainResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/pay_onchain");
let req = run_rest.post(url, &req);
run_rest.send(req).await
}
async fn preflight_pay_onchain(
&self,
req: PreflightPayOnchainRequest,
) -> Result<PreflightPayOnchainResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/preflight_pay_onchain");
let req = run_rest.post(url, &req);
run_rest.send(req).await
}
async fn create_offer(
&self,
req: CreateOfferRequest,
) -> Result<CreateOfferResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/create_offer");
let req = run_rest.post(url, &req);
run_rest.send(req).await
}
async fn pay_offer(
&self,
req: PayOfferRequest,
) -> Result<PayOfferResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/pay_offer");
let req = run_rest.post(url, &req);
run_rest.send(req).await
}
async fn preflight_pay_offer(
&self,
req: PreflightPayOfferRequest,
) -> Result<PreflightPayOfferResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/preflight_pay_offer");
let req = run_rest.post(url, &req);
run_rest.send(req).await
}
async fn get_address(&self) -> Result<GetAddressResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/get_address");
let req = run_rest.post(url, &Empty {});
run_rest.send(req).await
}
async fn get_payments_by_indexes(
&self,
_: PaymentCreatedIndexes,
) -> Result<VecBasicPaymentV1, NodeApiError> {
unimplemented!("Deprecated")
}
async fn get_new_payments(
&self,
_: GetNewPayments,
) -> Result<VecBasicPaymentV1, NodeApiError> {
unimplemented!("Deprecated")
}
async fn get_updated_payments(
&self,
req: GetUpdatedPayments,
) -> Result<VecBasicPaymentV2, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/payments/updated");
let req = run_rest.get(url, &req);
run_rest.send(req).await
}
async fn get_payment_by_id(
&self,
req: PaymentIdStruct,
) -> Result<MaybeBasicPaymentV2, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/v1/payments/id");
let req = run_rest.get(url, &req);
run_rest.send(req).await
}
async fn update_payment_note(
&self,
req: UpdatePaymentNote,
) -> Result<Empty, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/payments/note");
let req = run_rest.put(url, &req);
run_rest.send(req).await
}
async fn get_revocable_clients(
&self,
req: GetRevocableClients,
) -> Result<RevocableClients, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/clients");
let req = run_rest.get(url, &req);
run_rest.send(req).await
}
async fn create_revocable_client(
&self,
req: CreateRevocableClientRequest,
) -> Result<CreateRevocableClientResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/clients");
let req = run_rest.post(url, &req);
run_rest.send(req).await
}
async fn update_revocable_client(
&self,
req: UpdateClientRequest,
) -> Result<UpdateClientResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/clients");
let req = run_rest.put(url, &req);
run_rest.send(req).await
}
async fn list_broadcasted_txs(
&self,
) -> Result<serde_json::Value, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/list_broadcasted_txs");
let req = run_rest.get(url, &Empty {});
run_rest.send(req).await
}
async fn backup_info(&self) -> Result<BackupInfo, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/backup");
let req = run_rest.get(url, &Empty {});
run_rest.send(req).await
}
async fn setup_gdrive(
&self,
req: SetupGDrive,
) -> Result<Empty, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/backup/gdrive");
let req = run_rest.post(url, &req);
run_rest.send(req).await
}
async fn get_human_bitcoin_address(
&self,
) -> Result<HumanBitcoinAddress, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/human_bitcoin_address");
let req = run_rest.get(url, &Empty {});
run_rest.send(req).await
}
async fn update_human_bitcoin_address(
&self,
req: UsernameStruct,
) -> Result<HumanBitcoinAddress, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/human_bitcoin_address");
let req = run_rest.put(url, &req);
run_rest.send(req).await
}
#[allow(deprecated)]
async fn get_payment_address(
&self,
) -> Result<HumanBitcoinAddress, NodeApiError> {
self.get_human_bitcoin_address().await
}
#[allow(deprecated)]
async fn update_payment_address(
&self,
req: UsernameStruct,
) -> Result<HumanBitcoinAddress, NodeApiError> {
self.update_human_bitcoin_address(req).await
}
async fn list_nwc_clients(
&self,
) -> Result<ListNwcClientResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/nwc_clients");
let req = run_rest.get(url, &Empty {});
run_rest.send(req).await
}
async fn create_nwc_client(
&self,
req: CreateNwcClientRequest,
) -> Result<CreateNwcClientResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/nwc_clients");
let req = run_rest.post(url, &req);
run_rest.send(req).await
}
async fn update_nwc_client(
&self,
req: UpdateNwcClientRequest,
) -> Result<UpdateNwcClientResponse, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/nwc_clients");
let req = run_rest.put(url, &req);
run_rest.send(req).await
}
async fn delete_nwc_client(
&self,
req: NostrPkStruct,
) -> Result<Empty, NodeApiError> {
let run_rest = &self.authed_run_rest().await?.client;
let run_url = &self.inner.run_url;
let url = format!("{run_url}/app/nwc_clients");
let req = run_rest.delete(url, &req);
run_rest.send(req).await
}
}
impl RunRestClient {
fn new(
gateway_client: &GatewayClient,
run_url: &str,
auth_token: TokenWithExpiration,
tls_config: rustls::ClientConfig,
) -> anyhow::Result<Self> {
let TokenWithExpiration { expiration, token } = auth_token;
let (from, to) = (gateway_client.rest.user_agent().clone(), "node-run");
let proxy =
static_proxy_config(&gateway_client.gateway_url, run_url, token)?;
let client = RestClient::client_builder(&from)
.proxy(proxy)
.use_preconfigured_tls(tls_config.clone())
.build()
.context("Failed to build client")?;
let client = RestClient::from_inner(client, from, to);
Ok(Self {
client,
token_expiration: expiration,
})
}
fn token_needs_refresh(&self, now: SystemTime) -> bool {
auth::token_needs_refresh(now, self.token_expiration)
}
}
fn static_proxy_config(
gateway_url: &str,
node_url: &str,
auth_token: BearerAuthToken,
) -> anyhow::Result<reqwest::Proxy> {
let node_url = Url::parse(node_url).context("Invalid node url")?;
let gateway_url = gateway_url.to_owned();
let auth_header = http::HeaderValue::from_maybe_shared(ByteStr::from(
format!("Bearer {auth_token}"),
))?;
let proxy = reqwest::Proxy::custom(move |url| {
if url_base_eq(url, &node_url) {
Some(gateway_url.clone())
} else {
None
}
})
.custom_http_auth(auth_header);
Ok(proxy)
}
fn url_base_eq(u1: &Url, u2: &Url) -> bool {
u1.scheme() == u2.scheme()
&& u1.host() == u2.host()
&& u1.port_or_known_default() == u2.port_or_known_default()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_url_base_eq() {
let eq_classes = vec![
vec![
"https://hello.world",
"https://hello.world/",
"https://hello.world/my_cool_method",
"https://hello.world/my_cool_method&query=params",
"https://hello.world/&query=params",
],
vec![
"http://hello.world",
"http://hello.world/",
"http://hello.world/my_cool_method",
"http://hello.world/my_cool_method&query=params",
"http://hello.world/&query=params",
],
vec![
"https://hello.world:8080",
"https://hello.world:8080/",
"https://hello.world:8080/my_cool_method",
"https://hello.world:8080/my_cool_method&query=params",
"https://hello.world:8080/&query=params",
],
vec![
"https://127.0.0.1:8080",
"https://127.0.0.1:8080/",
"https://127.0.0.1:8080/my_cool_method",
"https://127.0.0.1:8080/my_cool_method&query=params",
"https://127.0.0.1:8080/&query=params",
],
vec![
"https://[::1]:8080",
"https://[::1]:8080/",
"https://[::1]:8080/my_cool_method",
"https://[::1]:8080/my_cool_method&query=params",
"https://[::1]:8080/&query=params",
],
];
let eq_classes = eq_classes
.into_iter()
.map(|eq_class| {
eq_class
.into_iter()
.map(|url| Url::parse(url).unwrap())
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let n_classes = eq_classes.len();
let n_urls = eq_classes[0].len();
for eq_class in &eq_classes {
for idx_u1 in 0..n_urls {
for idx_u2 in idx_u1..n_urls {
let u1 = &eq_class[idx_u1];
let u2 = &eq_class[idx_u2];
assert!(url_base_eq(u1, u2));
assert!(url_base_eq(u2, u1));
}
}
}
for idx_class1 in 0..(n_classes - 1) {
let eq_class1 = &eq_classes[idx_class1];
for eq_class2 in eq_classes.iter().skip(idx_class1 + 1) {
for u1 in eq_class1 {
for u2 in eq_class2 {
assert!(!url_base_eq(u1, u2));
assert!(!url_base_eq(u2, u1));
}
}
}
}
}
}