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:
Flori 2023-12-03 16:05:58 +01:00 committed by GitHub
commit 2c44232b05
7 changed files with 176 additions and 30 deletions

View File

@ -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:

View File

@ -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

View File

@ -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)]

View File

@ -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)]

View File

@ -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
)
}
}

View File

@ -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;

26
tests/urlbundle.rs Normal file
View File

@ -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();
}