Skip to content

Commit

Permalink
Merge branch 'main' into spofford/rsq
Browse files Browse the repository at this point in the history
  • Loading branch information
adamspofford-dfinity committed Nov 6, 2023
2 parents e6dc699 + 65b2708 commit 96fc7a7
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 89 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Added node signature certification to query calls, for protection against rogue boundary nodes. This can be disabled with `with_verify_query_signatures`.
* Added `with_nonce_generation` to `QueryBuilder` for precise cache control.
* feat: An instance of the `Agent` can now dispatch subsequent requests to URLs, which are generated dynamically, thanks to the new `RouteProvider` trait, which is added to the `HyperTransport` and `ReqwestTransport`. Also a simple `RoundRobinRouteProvider` implementation of the `RouteProvider` trait is provided. This provider generates routing URLs from an input list in a simple, fair and predictable way.
* Added `read_subnet_state_raw` to `Agent` and `read_subnet_state` to `Transport` for looking up raw state by subnet ID instead of canister ID.
* Added `read_state_subnet_metrics` to `Agent` to access subnet metrics, such as total spent cycles.
* Types passed to the `to_request_id` function can now contain nested structs, signed integers, and externally tagged enums.
Expand Down
4 changes: 4 additions & 0 deletions ic-agent/src/agent/agent_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ pub enum AgentError {
/// The rejected call had an invalid reject code (valid range 1..5).
#[error(transparent)]
InvalidRejectCode(#[from] InvalidRejectCodeError),

/// Route provider failed to generate a url for some reason.
#[error("Route provider failed to generate url: {0}")]
RouteProviderError(String),
}

impl PartialEq for AgentError {
Expand Down
128 changes: 56 additions & 72 deletions ic-agent/src/agent/http_transport/hyper_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@ pub use hyper;

use std::{any, error::Error, future::Future, marker::PhantomData, sync::atomic::AtomicPtr};

use http::uri::{Authority, PathAndQuery};
use http_body::{LengthLimitError, Limited};
use hyper::{
body::{Bytes, HttpBody},
client::HttpConnector,
header::CONTENT_TYPE,
service::Service,
Client, Method, Request, Response, Uri,
body::HttpBody, client::HttpConnector, header::CONTENT_TYPE, service::Service, Client, Method,
Request, Response,
};
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};

use crate::{
agent::{
agent_error::HttpErrorPayload,
http_transport::{IC0_DOMAIN, IC0_SUB_DOMAIN},
http_transport::route_provider::{RoundRobinRouteProvider, RouteProvider},
AgentFuture, Transport,
},
export::Principal,
Expand All @@ -28,7 +24,7 @@ use crate::{
#[derive(Debug)]
pub struct HyperTransport<B1, S = Client<HttpsConnector<HttpConnector>, B1>> {
_marker: PhantomData<AtomicPtr<B1>>,
url: Uri,
route_provider: Box<dyn RouteProvider>,
max_response_body_size: Option<usize>,
service: S,
}
Expand Down Expand Up @@ -87,7 +83,7 @@ where

impl<B1: HyperBody> HyperTransport<B1> {
/// Creates a replica transport from a HTTP URL.
pub fn create<U: Into<Uri>>(url: U) -> Result<Self, AgentError> {
pub fn create<U: Into<String>>(url: U) -> Result<Self, AgentError> {
let connector = HttpsConnectorBuilder::new()
.with_webpki_roots()
.https_or_http()
Expand All @@ -104,49 +100,19 @@ where
S: HyperService<B1>,
{
/// Creates a replica transport from a HTTP URL and a [`HyperService`].
pub fn create_with_service<U: Into<Uri>>(url: U, service: S) -> Result<Self, AgentError> {
// Parse the url
let url = url.into();
let mut parts = url.clone().into_parts();
parts.authority = parts
.authority
.map(|v| {
let host = v.host();
let host = match host.len().checked_sub(IC0_SUB_DOMAIN.len()) {
None => host,
Some(start) if host[start..].eq_ignore_ascii_case(IC0_SUB_DOMAIN) => IC0_DOMAIN,
Some(_) => host,
};
let port = v.port();
let (colon, port) = match port.as_ref() {
Some(v) => (":", v.as_str()),
None => ("", ""),
};
Authority::from_maybe_shared(Bytes::from(format!("{host}{colon}{port}")))
})
.transpose()
.map_err(|_| AgentError::InvalidReplicaUrl(format!("{url}")))?;
parts.path_and_query = Some(
parts
.path_and_query
.map_or(Ok(PathAndQuery::from_static("/api/v2")), |v| {
let mut found = false;
fn replace<T>(a: T, b: &mut T) -> T {
std::mem::replace(b, a)
}
let v = v
.path()
.trim_end_matches(|c| !replace(found || c == '/', &mut found));
PathAndQuery::from_maybe_shared(Bytes::from(format!("{v}/api/v2")))
})
.map_err(|_| AgentError::InvalidReplicaUrl(format!("{url}")))?,
);
let url =
Uri::from_parts(parts).map_err(|_| AgentError::InvalidReplicaUrl(format!("{url}")))?;
pub fn create_with_service<U: Into<String>>(url: U, service: S) -> Result<Self, AgentError> {
let route_provider = Box::new(RoundRobinRouteProvider::new(vec![url.into()])?);
Self::create_with_service_route(route_provider, service)
}

/// Creates a replica transport from a [`RouteProvider`] and a [`HyperService`].
pub fn create_with_service_route(
route_provider: Box<dyn RouteProvider>,
service: S,
) -> Result<Self, AgentError> {
Ok(Self {
_marker: PhantomData,
url,
route_provider,
service,
max_response_body_size: None,
})
Expand Down Expand Up @@ -243,7 +209,10 @@ where
_request_id: RequestId,
) -> AgentFuture<()> {
Box::pin(async move {
let url = format!("{}/canister/{effective_canister_id}/call", self.url);
let url = format!(
"{}/canister/{effective_canister_id}/call",
self.route_provider.route()?
);
self.request(Method::POST, url, Some(envelope)).await?;
Ok(())
})
Expand All @@ -255,28 +224,37 @@ where
envelope: Vec<u8>,
) -> AgentFuture<Vec<u8>> {
Box::pin(async move {
let url = format!("{}/canister/{effective_canister_id}/read_state", self.url);
let url = format!(
"{}/canister/{effective_canister_id}/read_state",
self.route_provider.route()?
);
self.request(Method::POST, url, Some(envelope)).await
})
}

fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>> {
Box::pin(async move {
let url = format!("{}/subnet/{subnet_id}/read_state", self.url);
let url = format!(
"{}/subnet/{subnet_id}/read_state",
self.route_provider.route()?
);
self.request(Method::POST, url, Some(envelope)).await
})
}

fn query(&self, effective_canister_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>> {
Box::pin(async move {
let url = format!("{}/canister/{effective_canister_id}/query", self.url);
let url = format!(
"{}/canister/{effective_canister_id}/query",
self.route_provider.route()?
);
self.request(Method::POST, url, Some(envelope)).await
})
}

fn status(&self) -> AgentFuture<Vec<u8>> {
Box::pin(async move {
let url = format!("{}/status", self.url);
let url = format!("{}/status", self.route_provider.route()?);
self.request(Method::GET, url, None).await
})
}
Expand All @@ -285,32 +263,38 @@ where
#[cfg(test)]
mod test {
use super::HyperTransport;
use hyper::{Client, Uri};
use hyper::Client;
use url::Url;

#[test]
fn redirect() {
fn test(base: &str, result: &str) {
let client: Client<_> = Client::builder().build_http();
let uri: Uri = base.parse().unwrap();
let t = HyperTransport::create_with_service(uri, client).unwrap();
assert_eq!(t.url, result, "{}", base);
let url: Url = base.parse().unwrap();
let t = HyperTransport::create_with_service(url, client).unwrap();
assert_eq!(
t.route_provider.route().unwrap().as_str(),
result,
"{}",
base
);
}

test("https://ic0.app", "https://ic0.app/api/v2");
test("https://IC0.app", "https://ic0.app/api/v2");
test("https://foo.ic0.app", "https://ic0.app/api/v2");
test("https://foo.IC0.app", "https://ic0.app/api/v2");
test("https://foo.Ic0.app", "https://ic0.app/api/v2");
test("https://foo.iC0.app", "https://ic0.app/api/v2");
test("https://foo.bar.ic0.app", "https://ic0.app/api/v2");
test("https://ic0.app/foo/", "https://ic0.app/foo/api/v2");
test("https://foo.ic0.app/foo/", "https://ic0.app/foo/api/v2");
test("https://ic0.app", "https://ic0.app/api/v2/");
test("https://IC0.app", "https://ic0.app/api/v2/");
test("https://foo.ic0.app", "https://ic0.app/api/v2/");
test("https://foo.IC0.app", "https://ic0.app/api/v2/");
test("https://foo.Ic0.app", "https://ic0.app/api/v2/");
test("https://foo.iC0.app", "https://ic0.app/api/v2/");
test("https://foo.bar.ic0.app", "https://ic0.app/api/v2/");
test("https://ic0.app/foo/", "https://ic0.app/foo/api/v2/");
test("https://foo.ic0.app/foo/", "https://ic0.app/foo/api/v2/");

test("https://ic1.app", "https://ic1.app/api/v2");
test("https://foo.ic1.app", "https://foo.ic1.app/api/v2");
test("https://ic0.app.ic1.app", "https://ic0.app.ic1.app/api/v2");
test("https://ic1.app", "https://ic1.app/api/v2/");
test("https://foo.ic1.app", "https://foo.ic1.app/api/v2/");
test("https://ic0.app.ic1.app", "https://ic0.app.ic1.app/api/v2/");

test("https://fooic0.app", "https://fooic0.app/api/v2");
test("https://fooic0.app.ic0.app", "https://ic0.app/api/v2");
test("https://fooic0.app", "https://fooic0.app/api/v2/");
test("https://fooic0.app.ic0.app", "https://ic0.app/api/v2/");
}
}
13 changes: 13 additions & 0 deletions ic-agent/src/agent/http_transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,17 @@ pub use hyper_transport::*; // remove after 0.25
#[allow(dead_code)]
const IC0_DOMAIN: &str = "ic0.app";
#[allow(dead_code)]
const ICP0_DOMAIN: &str = "icp0.io";
#[allow(dead_code)]
const ICP_API_DOMAIN: &str = "icp-api.io";
#[allow(dead_code)]
const LOCALHOST_DOMAIN: &str = "localhost";
#[allow(dead_code)]
const IC0_SUB_DOMAIN: &str = ".ic0.app";
#[allow(dead_code)]
const ICP0_SUB_DOMAIN: &str = ".icp0.io";
#[allow(dead_code)]
const ICP_API_SUB_DOMAIN: &str = ".icp-api.io";
#[allow(dead_code)]
const LOCALHOST_SUB_DOMAIN: &str = ".localhost";
pub mod route_provider;
61 changes: 44 additions & 17 deletions ic-agent/src/agent/http_transport/reqwest_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ pub use reqwest;
use futures_util::StreamExt;
use reqwest::{
header::{HeaderMap, CONTENT_TYPE},
Body, Client, Method, Request, StatusCode, Url,
Body, Client, Method, Request, StatusCode,
};

use crate::{
agent::{
agent_error::HttpErrorPayload,
http_transport::{IC0_DOMAIN, IC0_SUB_DOMAIN},
http_transport::route_provider::{RoundRobinRouteProvider, RouteProvider},
AgentFuture, Transport,
},
export::Principal,
Expand All @@ -23,7 +23,7 @@ use crate::{
/// A [`Transport`] using [`reqwest`] to make HTTP calls to the Internet Computer.
#[derive(Debug)]
pub struct ReqwestTransport {
url: Url,
route_provider: Box<dyn RouteProvider>,
client: Client,
max_response_body_size: Option<usize>,
}
Expand Down Expand Up @@ -52,19 +52,17 @@ impl ReqwestTransport {

/// Creates a replica transport from a HTTP URL and a [`reqwest::Client`].
pub fn create_with_client<U: Into<String>>(url: U, client: Client) -> Result<Self, AgentError> {
let url = url.into();
let route_provider = Box::new(RoundRobinRouteProvider::new(vec![url.into()])?);
Self::create_with_client_route(route_provider, client)
}

/// Creates a replica transport from a [`RouteProvider`] and a [`reqwest::Client`].
pub fn create_with_client_route(
route_provider: Box<dyn RouteProvider>,
client: Client,
) -> Result<Self, AgentError> {
Ok(Self {
url: Url::parse(&url)
.and_then(|mut url| {
// rewrite *.ic0.app to ic0.app
if let Some(domain) = url.domain() {
if domain.ends_with(IC0_SUB_DOMAIN) {
url.set_host(Some(IC0_DOMAIN))?;
}
}
url.join("api/v2/")
})
.map_err(|_| AgentError::InvalidReplicaUrl(url.clone()))?,
route_provider,
client,
max_response_body_size: None,
})
Expand Down Expand Up @@ -127,7 +125,7 @@ impl ReqwestTransport {
endpoint: &str,
body: Option<Vec<u8>>,
) -> Result<Vec<u8>, AgentError> {
let url = self.url.join(endpoint)?;
let url = self.route_provider.route()?.join(endpoint)?;
let mut http_request = Request::new(method, url);
http_request
.headers_mut()
Expand Down Expand Up @@ -226,7 +224,12 @@ mod test {
fn redirect() {
fn test(base: &str, result: &str) {
let t = ReqwestTransport::create(base).unwrap();
assert_eq!(t.url.as_str(), result, "{}", base);
assert_eq!(
t.route_provider.route().unwrap().as_str(),
result,
"{}",
base
);
}

test("https://ic0.app", "https://ic0.app/api/v2/");
Expand All @@ -238,12 +241,36 @@ mod test {
test("https://foo.bar.ic0.app", "https://ic0.app/api/v2/");
test("https://ic0.app/foo/", "https://ic0.app/foo/api/v2/");
test("https://foo.ic0.app/foo/", "https://ic0.app/foo/api/v2/");
test(
"https://ryjl3-tyaaa-aaaaa-aaaba-cai.ic0.app",
"https://ic0.app/api/v2/",
);

test("https://ic1.app", "https://ic1.app/api/v2/");
test("https://foo.ic1.app", "https://foo.ic1.app/api/v2/");
test("https://ic0.app.ic1.app", "https://ic0.app.ic1.app/api/v2/");

test("https://fooic0.app", "https://fooic0.app/api/v2/");
test("https://fooic0.app.ic0.app", "https://ic0.app/api/v2/");

test("https://icp0.io", "https://icp0.io/api/v2/");
test(
"https://ryjl3-tyaaa-aaaaa-aaaba-cai.icp0.io",
"https://icp0.io/api/v2/",
);
test("https://ic0.app.icp0.io", "https://icp0.io/api/v2/");

test("https://icp-api.io", "https://icp-api.io/api/v2/");
test(
"https://ryjl3-tyaaa-aaaaa-aaaba-cai.icp-api.io",
"https://icp-api.io/api/v2/",
);
test("https://icp0.io.icp-api.io", "https://icp-api.io/api/v2/");

test("http://localhost:4943", "http://localhost:4943/api/v2/");
test(
"http://ryjl3-tyaaa-aaaaa-aaaba-cai.localhost:4943",
"http://localhost:4943/api/v2/",
);
}
}
Loading

0 comments on commit 96fc7a7

Please sign in to comment.