Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get URLs from .well-known #451

Merged
merged 15 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 28 additions & 28 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,34 +45,34 @@ jobs:
cargo build --verbose --all-features
cargo test --verbose --all-features
fi
wasm-safari:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Clone spacebar server
run: |
git clone https://github.com/bitfl0wer/server.git
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
cache-dependency-path: server/package-lock.json
- name: Prepare and start Spacebar server
run: |
npm install
npm run setup
npm run start &
working-directory: ./server
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
prefix-key: "macos"
- name: Run WASM tests with Safari, Firefox, Chrome
run: |
rustup target add wasm32-unknown-unknown
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force
SAFARIDRIVER=$(which safaridriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt"
# wasm-safari:
# runs-on: macos-latest
# steps:
# - uses: actions/checkout@v4
# - name: Clone spacebar server
# run: |
# git clone https://github.com/bitfl0wer/server.git
# - uses: actions/setup-node@v3
# with:
# node-version: 18
# cache: 'npm'
# cache-dependency-path: server/package-lock.json
# - name: Prepare and start Spacebar server
# run: |
# npm install
# npm run setup
# npm run start &
# working-directory: ./server
# - uses: Swatinem/rust-cache@v2
# with:
# cache-all-crates: "true"
# prefix-key: "macos-safari"
# - name: Run WASM tests with Safari, Firefox, Chrome
# run: |
# rustup target add wasm32-unknown-unknown
# curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
# cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force
# SAFARIDRIVER=$(which safaridriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt" --no-fail-fast
wasm-gecko:
runs-on: macos-latest
steps:
Expand Down
12 changes: 12 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ custom_error! {
InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}"
}

impl From<reqwest::Error> for ChorusError {
fn from(value: reqwest::Error) -> Self {
ChorusError::RequestFailed {
url: match value.url() {
Some(url) => url.to_string(),
None => "None".to_string(),
},
error: value.to_string(),
}
}
}

custom_error! {
#[derive(PartialEq, Eq)]
pub ObserverError
Expand Down
12 changes: 11 additions & 1 deletion src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl PartialEq for LimitsInformation {
}

impl Instance {
/// Creates a new [`Instance`] from the [relevant instance urls](UrlBundle), where `limited` is whether or not to automatically use rate limits.
/// Creates a new [`Instance`] from the [relevant instance urls](UrlBundle), where `limited` is whether Chorus will track and enforce rate limits for this instance.
pub async fn new(urls: UrlBundle, limited: bool) -> ChorusResult<Instance> {
let limits_information;
if limited {
Expand Down Expand Up @@ -99,12 +99,22 @@ impl Instance {
};
Ok(instance)
}

pub(crate) fn clone_limits_if_some(&self) -> Option<HashMap<LimitType, Limit>> {
if self.limits_information.is_some() {
return Some(self.limits_information.as_ref().unwrap().ratelimits.clone());
}
None
}

/// Creates a new [`Instance`] by trying to get the [relevant instance urls](UrlBundle) from a root url.
/// Shorthand for `Instance::new(UrlBundle::from_root_domain(root_domain).await?)`.
///
/// If `limited` is `true`, then Chorus will track and enforce rate limits for this instance.
pub async fn from_root_url(root_url: &str, limited: bool) -> ChorusResult<Instance> {
let urls = UrlBundle::from_root_url(root_url).await?;
Instance::new(urls, limited).await
}
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
Expand Down
70 changes: 69 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,23 @@ This crate uses Semantic Versioning 2.0.0 as its versioning scheme. You can read
clippy::new_without_default,
clippy::useless_conversion
)]
#![warn(
clippy::todo,
clippy::unimplemented,
clippy::dbg_macro,
clippy::print_stdout,
clippy::print_stderr
)]
#[cfg(all(feature = "rt", feature = "rt_multi_thread"))]
compile_error!("feature \"rt\" and feature \"rt_multi_thread\" cannot be enabled at the same time");

use errors::ChorusResult;
use serde::{Deserialize, Serialize};
use types::types::domains_configuration::WellKnownResponse;
use url::{ParseError, Url};

use crate::errors::ChorusError;

#[cfg(feature = "client")]
pub mod api;
pub mod errors;
Expand Down Expand Up @@ -168,7 +179,7 @@ impl UrlBundle {
let url_fmt = format!("http://{}", url);
return UrlBundle::parse_url(url_fmt);
}
Err(_) => panic!("Invalid URL"),
Err(_) => panic!("Invalid URL"), // TODO: should not panic here
};
// if the last character of the string is a slash, remove it.
let mut url_string = url.to_string();
Expand All @@ -177,6 +188,63 @@ impl UrlBundle {
}
url_string
}

/// Performs a few HTTP requests to try and retrieve a `UrlBundle` from an instances' root url.
/// The method tries to retrieve the `UrlBundle` via these three strategies, in order:
/// - GET: `$url/.well-known/spacebar` -> Retrieve UrlBundle via `$wellknownurl/api/policies/instance/domains`
/// - GET: `$url/api/policies/instance/domains`
/// - GET: `$url/policies/instance/domains`
///
/// The URL stored at `.well-known/spacebar` is the instances' API endpoint. The API
/// stores the CDN and WSS URLs under the `$api/policies/instance/domains` endpoint. If all three
/// of the above approaches fail, it is very likely that the instance is misconfigured, unreachable, or that
/// a wrong URL was provided.
pub async fn from_root_url(url: &str) -> ChorusResult<UrlBundle> {
let parsed = UrlBundle::parse_url(url.to_string());
let client = reqwest::Client::new();
let request_wellknown = client
.get(format!("{}/.well-known/spacebar", &parsed))
.header(http::header::ACCEPT, "application/json")
.build()?;
let response_wellknown = client.execute(request_wellknown).await?;
if response_wellknown.status().is_success() {
let body = response_wellknown.json::<WellKnownResponse>().await?.api;
UrlBundle::from_api_url(&body).await
} else {
if let Ok(response_slash_api) =
UrlBundle::from_api_url(&format!("{}/api/policies/instance/domains", parsed)).await
{
return Ok(response_slash_api);
}
if let Ok(response_api) =
UrlBundle::from_api_url(&format!("{}/policies/instance/domains", parsed)).await
{
Ok(response_api)
} else {
Err(ChorusError::RequestFailed { url: parsed.to_string(), error: "Could not retrieve UrlBundle from url after trying 3 different approaches. Check the provided Url and make sure the instance is reachable.".to_string() } )
}
}
}

async fn from_api_url(url: &str) -> ChorusResult<UrlBundle> {
let client = reqwest::Client::new();
let request = client
.get(url)
.header(http::header::ACCEPT, "application/json")
.build()?;
let response = client.execute(request).await?;
if let Ok(body) = response
.json::<types::types::domains_configuration::Domains>()
.await
{
Ok(UrlBundle::new(body.api_endpoint, body.gateway, body.cdn))
} else {
Err(ChorusError::RequestFailed {
url: url.to_string(),
error: "Could not retrieve a UrlBundle from the given url. Check the provided url and make sure the instance is reachable.".to_string(),
})
}
}
}

#[cfg(test)]
Expand Down
29 changes: 29 additions & 0 deletions src/types/config/types/domains_configuration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Eq, PartialEq, Hash, Clone, Debug)]
/// Represents the result of the `$rooturl/.well-known/spacebar` endpoint.
///
/// See <https://docs.spacebar.chat/setup/server/wellknown/> for more information.
pub struct WellKnownResponse {
pub api: String,
}

#[derive(Deserialize, Serialize, Eq, PartialEq, Hash, Clone, Debug)]
#[serde(rename_all = "camelCase")]
/// Represents the result of the `$api/policies/instance/domains` endpoint.
pub struct Domains {
pub cdn: String,
pub gateway: String,
pub api_endpoint: String,
pub default_api_version: String,
}

impl std::fmt::Display for Domains {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{{\n\tCDN URL: {},\n\tGateway URL: {},\n\tAPI Endpoint: {},\n\tDefault API Version: {}\n}}",
self.cdn, self.gateway, self.api_endpoint, self.default_api_version
)
}
}
1 change: 1 addition & 0 deletions src/types/config/types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod api_configuration;
pub mod cdn_configuration;
pub mod defaults_configuration;
pub mod domains_configuration;
pub mod email_configuration;
pub mod endpoint_configuration;
pub mod external_tokens_configuration;
Expand Down
26 changes: 26 additions & 0 deletions tests/urlbundle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use chorus::types::types::domains_configuration::WellKnownResponse;
use chorus::UrlBundle;
use serde_json::json;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_parse_url() {
// TODO: Currently only tests two of the three branches in UrlBundle::from_root_domain.
let url = url::Url::parse("http://localhost:3001/").unwrap();
UrlBundle::from_root_url(url.as_str()).await.unwrap();
let url = url::Url::parse("http://localhost:3001/api/").unwrap();
UrlBundle::from_root_url(url.as_str()).await.unwrap();
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_parse_wellknown() {
let json = json!({
"api": "http://localhost:3001/api/v9"
});
let _well_known: WellKnownResponse = serde_json::from_value(json).unwrap();
}
Loading