Get URLs from .well-known (#451)
Closes #449. + `Instance::from_root_url(root_url: &str, limited: bool)`: 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?)` + `UrlBundle::from_root_url(url: &str)`: Performs a few HTTP requests to try and retrieve a UrlBundle from an instance's 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 instance's 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. + Add clippy warnings for: `clippy::todo, clippy::unimplemented, clippy::dbg_macro, clippy::print_stdout, clippy::print_stderr`
This commit is contained in:
commit
2c44232b05
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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)]
|
||||
|
|
70
src/lib.rs
70
src/lib.rs
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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)]
|
||||
|
|
|
@ -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,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;
|
||||
|
|
|
@ -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…
Reference in New Issue