Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a88118349 | ||
|
|
e07bf35cd0 | ||
|
|
a1147a98d2 |
No files matched your search
Generated
+9
@@ -313,6 +313,7 @@ dependencies = [
|
||||
"actix-http",
|
||||
"actix-rt",
|
||||
"actix-web",
|
||||
"domains",
|
||||
"log",
|
||||
"server_environment",
|
||||
"speculoos",
|
||||
@@ -2688,6 +2689,13 @@ dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "domains"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
@@ -8715,6 +8723,7 @@ dependencies = [
|
||||
"datetimes",
|
||||
"derive_more 0.99.17",
|
||||
"diesel",
|
||||
"domains",
|
||||
"easyenv",
|
||||
"elasticsearch",
|
||||
"elasticsearch_schema",
|
||||
|
||||
@@ -67,6 +67,7 @@ members = [
|
||||
"crates/lib/files/zip_archives",
|
||||
"crates/lib/http_headers",
|
||||
"crates/lib/jobs_common",
|
||||
"crates/lib/domains",
|
||||
"crates/lib/jwt_light",
|
||||
"crates/lib/opaque_cursors",
|
||||
"crates/lib/jwt_signer",
|
||||
@@ -252,6 +253,7 @@ grok_api_client = { path = "crates/api_clients/grok_api_client" }
|
||||
rootly_client = { path = "crates/api_clients/rootly_client" }
|
||||
rootly_config = { path = "crates/schema/config/rootly_config" }
|
||||
shared_env_var_config = { path = "crates/schema/config/shared_env_var_config" }
|
||||
domains = { path = "crates/lib/domains" }
|
||||
http_headers = { path = "crates/lib/http_headers" }
|
||||
images = { path = "crates/lib/files/images" }
|
||||
jobs_common = { path = "crates/lib/jobs_common" }
|
||||
|
||||
@@ -20,6 +20,7 @@ normal = ["workspace-build-acceleration"]
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
domains.workspace = true
|
||||
server_environment.workspace = true
|
||||
|
||||
# External
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use actix_cors::Cors;
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::artcraft::ARTCRAFT_2D;
|
||||
use domains::storyteller::STORYTELLER_2D;
|
||||
|
||||
pub fn add_artcraft_2d(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://2d.storyteller.ai")
|
||||
.allowed_origin("https://2d.getartcraft.com")
|
||||
.allowed_origin(STORYTELLER_2D.https_link_without_path())
|
||||
.allowed_origin(ARTCRAFT_2D.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-2d.netlify.app")
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use actix_cors::Cors;
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::artcraft::ARTCRAFT_3D;
|
||||
use domains::storyteller::STORYTELLER_3D;
|
||||
|
||||
pub fn add_artcraft_3d(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://3d.storyteller.ai")
|
||||
.allowed_origin("https://3d.getartcraft.com")
|
||||
.allowed_origin(STORYTELLER_3D.https_link_without_path())
|
||||
.allowed_origin(ARTCRAFT_3D.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-3d.netlify.app")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use actix_cors::Cors;
|
||||
use domains::artcraft::ARTCRAFT_DESKTOP;
|
||||
|
||||
pub fn add_artcraft_desktop(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Stable identity sent by the Artcraft native HTTP bridge (dev and packaged apps).
|
||||
.allowed_origin("https://desktop.getartcraft.com")
|
||||
.allowed_origin(ARTCRAFT_DESKTOP.https_link_without_path())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use actix_cors::Cors;
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::artcraft::ARTCRAFT_WEBAPP;
|
||||
|
||||
pub fn add_artcraft_webapp(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Actual domains
|
||||
.allowed_origin("https://app.getartcraft.com")
|
||||
.allowed_origin(ARTCRAFT_WEBAPP.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "artcraft-webapp.netlify.app")
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use actix_cors::Cors;
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::artcraft::ARTCRAFT_AI;
|
||||
use domains::artcraft::ARTCRAFT_AI_WWW;
|
||||
use domains::artcraft::ARTCRAFT_WEBSITE;
|
||||
use domains::artcraft::ARTCRAFT_WEBSITE_WWW;
|
||||
|
||||
pub fn add_artcraft_website(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Actual domains
|
||||
.allowed_origin("https://getartcraft.com")
|
||||
.allowed_origin("https://www.getartcraft.com")
|
||||
.allowed_origin(ARTCRAFT_WEBSITE.https_link_without_path())
|
||||
.allowed_origin(ARTCRAFT_WEBSITE_WWW.https_link_without_path())
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://artcraft.ai")
|
||||
.allowed_origin("https://www.artcraft.ai")
|
||||
.allowed_origin(ARTCRAFT_AI.https_link_without_path())
|
||||
.allowed_origin(ARTCRAFT_AI_WWW.https_link_without_path())
|
||||
// Development against production
|
||||
.allowed_origin("http://localhost:4200")
|
||||
.allowed_origin("http://localhost:4201")
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
use actix_cors::Cors;
|
||||
use log::warn;
|
||||
use url::{Host, Url};
|
||||
use domains::fakeyou::FAKEYOU;
|
||||
use domains::fakeyou::FAKEYOU_API;
|
||||
use domains::fakeyou::FAKEYOU_ENGINE;
|
||||
use domains::fakeyou::FAKEYOU_STAGING;
|
||||
|
||||
pub fn add_fakeyou(cors: Cors, is_production: bool) -> Cors {
|
||||
if is_production {
|
||||
cors
|
||||
// Storyteller Engine (Production)
|
||||
.allowed_origin("https://engine.fakeyou.com")
|
||||
.allowed_origin(FAKEYOU_ENGINE.https_link_without_path())
|
||||
// FakeYou (Production)
|
||||
.allowed_origin("https://api.fakeyou.com")
|
||||
.allowed_origin("https://fakeyou.com")
|
||||
.allowed_origin(FAKEYOU_API.https_link_without_path())
|
||||
.allowed_origin(FAKEYOU.https_link_without_path())
|
||||
// FakeYou (Staging)
|
||||
.allowed_origin("https://staging.fakeyou.com")
|
||||
.allowed_origin(FAKEYOU_STAGING.https_link_without_path())
|
||||
// Allow Netlify domains within "fakeyou" project.
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
let maybe_url = origin.to_str()
|
||||
@@ -50,7 +54,7 @@ pub fn add_fakeyou(cors: Cors, is_production: bool) -> Cors {
|
||||
.allowed_origin("https://dev.fakeyou.com:7000") // Yarn default port
|
||||
.allowed_origin("https://dev.fakeyou.com:7001") // NB: Mac frontend
|
||||
// Storyteller Engine (Development)
|
||||
.allowed_origin("https://engine.fakeyou.com") // NB: We use prod for integration testing
|
||||
.allowed_origin(FAKEYOU_ENGINE.https_link_without_path()) // NB: We use prod for integration testing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
use actix_cors::Cors;
|
||||
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::storyteller::STORYTELLER;
|
||||
use domains::storyteller::STORYTELLER_API;
|
||||
use domains::storyteller::STORYTELLER_ENGINE;
|
||||
use domains::storyteller::STORYTELLER_STAGING;
|
||||
use domains::storyteller::STORYTELLER_STUDIO;
|
||||
|
||||
pub fn add_storyteller(cors: Cors, is_production: bool) -> Cors {
|
||||
if is_production {
|
||||
cors
|
||||
// Storyteller Engine (Production)
|
||||
.allowed_origin("https://engine.storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_ENGINE.https_link_without_path())
|
||||
// Storyteller Studio (Production)
|
||||
.allowed_origin("https://studio.storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
|
||||
// Storyteller.ai (Production)
|
||||
.allowed_origin("https://api.storyteller.ai")
|
||||
.allowed_origin("https://storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_API.https_link_without_path())
|
||||
.allowed_origin(STORYTELLER.https_link_without_path())
|
||||
// Storyteller.ai (Staging)
|
||||
.allowed_origin("https://staging.storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_STAGING.https_link_without_path())
|
||||
// Allow Netlify domains within "storyteller-ai" project.
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-ai.netlify.app")
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use actix_cors::Cors;
|
||||
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::storyteller::STORYTELLER_BOARD;
|
||||
|
||||
pub fn add_storyteller_board(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://memeboard.ai")
|
||||
.allowed_origin("https://dingboard.ai")
|
||||
.allowed_origin("https://board.storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_BOARD.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-board.netlify.app")
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use actix_cors::Cors;
|
||||
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::storyteller::STORYTELLER_RENDER;
|
||||
|
||||
pub fn add_storyteller_render(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://render.storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_RENDER.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-render.netlify.app")
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use actix_cors::Cors;
|
||||
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::storyteller::STORYTELLER_ANIMATE;
|
||||
use domains::storyteller::STORYTELLER_STUDIO;
|
||||
use domains::storyteller::STORYTELLER_STUDIO_STAGING;
|
||||
use domains::storyteller::STORYTELLER_STUDIO_TESTING;
|
||||
|
||||
pub fn add_storyteller_studio(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors.allowed_origin("https://studio.storyteller.ai")
|
||||
.allowed_origin("https://studio-staging.studio.storyteller.ai")
|
||||
.allowed_origin("https://studio-testing.studio.storyteller.ai")
|
||||
cors.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
|
||||
.allowed_origin(STORYTELLER_STUDIO_STAGING.https_link_without_path())
|
||||
.allowed_origin(STORYTELLER_STUDIO_TESTING.https_link_without_path())
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storytellerstudio.netlify.app")
|
||||
})
|
||||
@@ -13,7 +17,7 @@ pub fn add_storyteller_studio(cors: Cors, _is_production: bool) -> Cors {
|
||||
netlify_branch_domain_matches(origin, "pipeline-gottagofast.netlify.app")
|
||||
})
|
||||
.allowed_origin("http://localhost:5173")
|
||||
.allowed_origin("https://animate.storyteller.ai") // NB: Gen2 Studio
|
||||
.allowed_origin(STORYTELLER_ANIMATE.https_link_without_path()) // NB: Gen2 Studio
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "domains"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "domains"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Adds `Domain::https_url()`, returning a cached `&'static url::Url`.
|
||||
url = ["dep:url"]
|
||||
|
||||
# No dependencies by default: the domain constants and their string forms are
|
||||
# built entirely at compile time.
|
||||
[dependencies]
|
||||
url = { workspace = true, optional = true }
|
||||
@@ -0,0 +1,69 @@
|
||||
//! ArtCraft domains.
|
||||
|
||||
use crate::{Domain, domain};
|
||||
|
||||
/// Public marketing website (Next.js).
|
||||
pub const ARTCRAFT_WEBSITE: Domain = domain!("getartcraft.com");
|
||||
pub const ARTCRAFT_WEBSITE_WWW: Domain = domain!("www.getartcraft.com");
|
||||
|
||||
/// Browser web application.
|
||||
pub const ARTCRAFT_WEBAPP: Domain = domain!("app.getartcraft.com");
|
||||
|
||||
/// Desktop app landing page.
|
||||
pub const ARTCRAFT_DESKTOP: Domain = domain!("desktop.getartcraft.com");
|
||||
|
||||
/// HTTP API.
|
||||
pub const ARTCRAFT_API: Domain = domain!("api.getartcraft.com");
|
||||
|
||||
pub const ARTCRAFT_2D: Domain = domain!("2d.getartcraft.com");
|
||||
pub const ARTCRAFT_3D: Domain = domain!("3d.getartcraft.com");
|
||||
|
||||
/// Alternate marketing domain.
|
||||
pub const ARTCRAFT_AI: Domain = domain!("artcraft.ai");
|
||||
pub const ARTCRAFT_AI_WWW: Domain = domain!("www.artcraft.ai");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn website() {
|
||||
assert_eq!(ARTCRAFT_WEBSITE.https_link_without_path(), "https://getartcraft.com");
|
||||
assert_eq!(ARTCRAFT_WEBSITE_WWW.https_link_without_path(), "https://www.getartcraft.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webapp() {
|
||||
assert_eq!(ARTCRAFT_WEBAPP.https_link_without_path(), "https://app.getartcraft.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop() {
|
||||
assert_eq!(ARTCRAFT_DESKTOP.bare_domain(), "desktop.getartcraft.com");
|
||||
assert_eq!(ARTCRAFT_DESKTOP.https_link_without_path(), "https://desktop.getartcraft.com");
|
||||
assert_eq!(ARTCRAFT_DESKTOP.https_link_with_root_path(), "https://desktop.getartcraft.com/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api() {
|
||||
assert_eq!(ARTCRAFT_API.https_link_without_path(), "https://api.getartcraft.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_d_and_three_d() {
|
||||
assert_eq!(ARTCRAFT_2D.https_link_without_path(), "https://2d.getartcraft.com");
|
||||
assert_eq!(ARTCRAFT_3D.https_link_without_path(), "https://3d.getartcraft.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artcraft_ai() {
|
||||
assert_eq!(ARTCRAFT_AI.https_link_without_path(), "https://artcraft.ai");
|
||||
assert_eq!(ARTCRAFT_AI_WWW.https_link_without_path(), "https://www.artcraft.ai");
|
||||
}
|
||||
|
||||
#[cfg(feature = "url")]
|
||||
#[test]
|
||||
fn desktop_url() {
|
||||
assert_eq!(ARTCRAFT_DESKTOP.https_url().as_str(), "https://desktop.getartcraft.com/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/// A validated domain name, with its `https://` link forms precomputed at
|
||||
/// compile time. Build one with the [`domain!`](crate::domain) macro:
|
||||
///
|
||||
/// ```
|
||||
/// use domains::{domain, Domain};
|
||||
///
|
||||
/// const EXAMPLE: Domain = domain!("desktop.example.com");
|
||||
///
|
||||
/// assert_eq!(EXAMPLE.bare_domain(), "desktop.example.com");
|
||||
/// assert_eq!(EXAMPLE.https_link_without_path(), "https://desktop.example.com");
|
||||
/// assert_eq!(EXAMPLE.https_link_with_root_path(), "https://desktop.example.com/");
|
||||
/// ```
|
||||
///
|
||||
/// An invalid literal fails to compile:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// const BAD: domains::Domain = domains::domain!("https://example.com");
|
||||
/// ```
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Domain {
|
||||
bare: &'static str,
|
||||
https_without_path: &'static str,
|
||||
https_with_root_path: &'static str,
|
||||
}
|
||||
|
||||
/// Declares a [`Domain`] from a string literal. Validation runs at compile
|
||||
/// time: an invalid domain is a build error, never a runtime panic.
|
||||
#[macro_export]
|
||||
macro_rules! domain {
|
||||
($domain:literal) => {
|
||||
$crate::Domain::__from_macro(
|
||||
$domain,
|
||||
::core::concat!("https://", $domain),
|
||||
::core::concat!("https://", $domain, "/"),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
impl Domain {
|
||||
/// Only for the `domain!` macro, which guarantees the three strings agree.
|
||||
/// Panics (a compile error in const context) on an invalid domain.
|
||||
#[doc(hidden)]
|
||||
pub const fn __from_macro(
|
||||
bare: &'static str,
|
||||
https_without_path: &'static str,
|
||||
https_with_root_path: &'static str,
|
||||
) -> Self {
|
||||
crate::validate::assert_valid_domain(bare);
|
||||
Self { bare, https_without_path, https_with_root_path }
|
||||
}
|
||||
|
||||
/// eg. `desktop.getartcraft.com`
|
||||
pub const fn bare_domain(&self) -> &'static str {
|
||||
self.bare
|
||||
}
|
||||
|
||||
/// eg. `https://desktop.getartcraft.com`. This is also the browser `Origin`
|
||||
/// header value, so use it for CORS and origin allowlists.
|
||||
pub const fn https_link_without_path(&self) -> &'static str {
|
||||
self.https_without_path
|
||||
}
|
||||
|
||||
/// eg. `https://desktop.getartcraft.com/`
|
||||
pub const fn https_link_with_root_path(&self) -> &'static str {
|
||||
self.https_with_root_path
|
||||
}
|
||||
|
||||
/// eg. `https://desktop.getartcraft.com/` as a `Url`.
|
||||
///
|
||||
/// Parsed on first use per domain, then served from a process-wide cache,
|
||||
/// so every call returns the same `&'static Url`.
|
||||
#[cfg(feature = "url")]
|
||||
pub fn https_url(&self) -> &'static url::Url {
|
||||
crate::url_cache::https_url(self.bare, self.https_with_root_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Domain {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Domain({})", self.bare)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Domain {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.bare)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Domain;
|
||||
|
||||
const TEST_DOMAIN: Domain = domain!("desktop.example.com");
|
||||
|
||||
#[test]
|
||||
fn bare_domain() {
|
||||
assert_eq!(TEST_DOMAIN.bare_domain(), "desktop.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_links() {
|
||||
assert_eq!(TEST_DOMAIN.https_link_without_path(), "https://desktop.example.com");
|
||||
assert_eq!(TEST_DOMAIN.https_link_with_root_path(), "https://desktop.example.com/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usable_in_const_context() {
|
||||
const ORIGINS: &[&str] = &[TEST_DOMAIN.https_link_without_path()];
|
||||
assert_eq!(ORIGINS, &["https://desktop.example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_and_debug() {
|
||||
assert_eq!(TEST_DOMAIN.to_string(), "desktop.example.com");
|
||||
assert_eq!(format!("{TEST_DOMAIN:?}"), "Domain(desktop.example.com)");
|
||||
}
|
||||
|
||||
#[cfg(feature = "url")]
|
||||
mod url_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn https_url_has_root_path() {
|
||||
let url = TEST_DOMAIN.https_url();
|
||||
assert_eq!(url.as_str(), "https://desktop.example.com/");
|
||||
assert_eq!(url.scheme(), "https");
|
||||
assert_eq!(url.host_str(), Some("desktop.example.com"));
|
||||
assert_eq!(url.path(), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_url_is_parsed_once_and_shared() {
|
||||
let first = TEST_DOMAIN.https_url();
|
||||
let second = TEST_DOMAIN.https_url();
|
||||
assert!(std::ptr::eq(first, second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_url_is_shared_across_threads() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Domain>();
|
||||
assert_send_sync::<&'static url::Url>();
|
||||
|
||||
// Borrowed into 'static threads with no Arc or cloning; every thread
|
||||
// sees the same parsed instance.
|
||||
let main_thread_url = TEST_DOMAIN.https_url();
|
||||
let handles: Vec<_> = (0..8)
|
||||
.map(|_| std::thread::spawn(|| TEST_DOMAIN.https_url()))
|
||||
.collect();
|
||||
for handle in handles {
|
||||
assert!(std::ptr::eq(handle.join().unwrap(), main_thread_url));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_url_matches_string_forms() {
|
||||
let url = TEST_DOMAIN.https_url();
|
||||
assert_eq!(url.as_str(), TEST_DOMAIN.https_link_with_root_path());
|
||||
assert_eq!(url.origin().ascii_serialization(), TEST_DOMAIN.https_link_without_path());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//! FakeYou domains.
|
||||
|
||||
use crate::{Domain, domain};
|
||||
|
||||
/// Main website.
|
||||
pub const FAKEYOU: Domain = domain!("fakeyou.com");
|
||||
|
||||
/// HTTP API.
|
||||
pub const FAKEYOU_API: Domain = domain!("api.fakeyou.com");
|
||||
|
||||
/// Engine frontend (also used against production for integration testing).
|
||||
pub const FAKEYOU_ENGINE: Domain = domain!("engine.fakeyou.com");
|
||||
|
||||
pub const FAKEYOU_STAGING: Domain = domain!("staging.fakeyou.com");
|
||||
|
||||
/// Local development hosts (resolve to a developer machine).
|
||||
pub const FAKEYOU_DEV: Domain = domain!("dev.fakeyou.com");
|
||||
pub const FAKEYOU_DEV_PROXY: Domain = domain!("devproxy.fakeyou.com");
|
||||
|
||||
/// Production media CDN. Serves both FakeYou and Storyteller media.
|
||||
pub const FAKEYOU_CDN: Domain = domain!("cdn-2.fakeyou.com");
|
||||
|
||||
/// Development media CDN (a Cloudflare R2 public bucket). Serves both FakeYou
|
||||
/// and Storyteller development media, like `FAKEYOU_CDN` does in production.
|
||||
pub const FAKEYOU_DEV_CDN: Domain = domain!("pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn website() {
|
||||
assert_eq!(FAKEYOU.bare_domain(), "fakeyou.com");
|
||||
assert_eq!(FAKEYOU.https_link_without_path(), "https://fakeyou.com");
|
||||
assert_eq!(FAKEYOU.https_link_with_root_path(), "https://fakeyou.com/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api() {
|
||||
assert_eq!(FAKEYOU_API.https_link_without_path(), "https://api.fakeyou.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_and_staging() {
|
||||
assert_eq!(FAKEYOU_ENGINE.https_link_without_path(), "https://engine.fakeyou.com");
|
||||
assert_eq!(FAKEYOU_STAGING.https_link_without_path(), "https://staging.fakeyou.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn development() {
|
||||
assert_eq!(FAKEYOU_DEV.bare_domain(), "dev.fakeyou.com");
|
||||
assert_eq!(FAKEYOU_DEV_PROXY.bare_domain(), "devproxy.fakeyou.com");
|
||||
}
|
||||
|
||||
mod cdn {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn production() {
|
||||
assert_eq!(FAKEYOU_CDN.bare_domain(), "cdn-2.fakeyou.com");
|
||||
assert_eq!(FAKEYOU_CDN.https_link_without_path(), "https://cdn-2.fakeyou.com");
|
||||
assert_eq!(FAKEYOU_CDN.https_link_with_root_path(), "https://cdn-2.fakeyou.com/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn development() {
|
||||
assert_eq!(FAKEYOU_DEV_CDN.https_link_without_path(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev");
|
||||
}
|
||||
|
||||
#[cfg(feature = "url")]
|
||||
#[test]
|
||||
fn urls() {
|
||||
assert_eq!(FAKEYOU_CDN.https_url().as_str(), "https://cdn-2.fakeyou.com/");
|
||||
assert_eq!(FAKEYOU_DEV_CDN.https_url().as_str(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! First-party domain names, validated at compile time.
|
||||
//!
|
||||
//! ```
|
||||
//! use domains::artcraft::ARTCRAFT_DESKTOP;
|
||||
//!
|
||||
//! assert_eq!(ARTCRAFT_DESKTOP.https_link_without_path(), "https://desktop.getartcraft.com");
|
||||
//! ```
|
||||
//!
|
||||
//! Enable the `url` feature for `Domain::https_url()`, a cached `&'static url::Url`.
|
||||
|
||||
pub mod artcraft;
|
||||
pub mod fakeyou;
|
||||
pub mod storyteller;
|
||||
|
||||
mod domain;
|
||||
mod validate;
|
||||
|
||||
#[cfg(feature = "url")]
|
||||
mod url_cache;
|
||||
|
||||
pub use domain::Domain;
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Storyteller domains. Storyteller media is served from `fakeyou::FAKEYOU_CDN`.
|
||||
|
||||
use crate::{Domain, domain};
|
||||
|
||||
/// Main website.
|
||||
pub const STORYTELLER: Domain = domain!("storyteller.ai");
|
||||
|
||||
/// HTTP API.
|
||||
pub const STORYTELLER_API: Domain = domain!("api.storyteller.ai");
|
||||
|
||||
pub const STORYTELLER_ENGINE: Domain = domain!("engine.storyteller.ai");
|
||||
pub const STORYTELLER_STAGING: Domain = domain!("staging.storyteller.ai");
|
||||
|
||||
/// Storyteller Studio and its deploy environments.
|
||||
pub const STORYTELLER_STUDIO: Domain = domain!("studio.storyteller.ai");
|
||||
pub const STORYTELLER_STUDIO_STAGING: Domain = domain!("studio-staging.studio.storyteller.ai");
|
||||
pub const STORYTELLER_STUDIO_TESTING: Domain = domain!("studio-testing.studio.storyteller.ai");
|
||||
|
||||
/// Gen2 Studio.
|
||||
pub const STORYTELLER_ANIMATE: Domain = domain!("animate.storyteller.ai");
|
||||
|
||||
pub const STORYTELLER_BOARD: Domain = domain!("board.storyteller.ai");
|
||||
pub const STORYTELLER_RENDER: Domain = domain!("render.storyteller.ai");
|
||||
|
||||
/// Legacy hosts for the ArtCraft 2D and 3D apps.
|
||||
pub const STORYTELLER_2D: Domain = domain!("2d.storyteller.ai");
|
||||
pub const STORYTELLER_3D: Domain = domain!("3d.storyteller.ai");
|
||||
|
||||
/// Local development hosts (resolve to a developer machine).
|
||||
pub const STORYTELLER_DEV: Domain = domain!("dev.storyteller.ai");
|
||||
pub const STORYTELLER_DEV_PROXY: Domain = domain!("devproxy.storyteller.ai");
|
||||
|
||||
/// Storyteller Studio static assets.
|
||||
pub const STORYTELLER_CDN: Domain = domain!("cdn.storyteller.ai");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn website() {
|
||||
assert_eq!(STORYTELLER.bare_domain(), "storyteller.ai");
|
||||
assert_eq!(STORYTELLER.https_link_without_path(), "https://storyteller.ai");
|
||||
assert_eq!(STORYTELLER.https_link_with_root_path(), "https://storyteller.ai/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api() {
|
||||
assert_eq!(STORYTELLER_API.https_link_without_path(), "https://api.storyteller.ai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn studio() {
|
||||
assert_eq!(STORYTELLER_STUDIO.https_link_without_path(), "https://studio.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_STUDIO_STAGING.https_link_without_path(), "https://studio-staging.studio.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_STUDIO_TESTING.https_link_without_path(), "https://studio-testing.studio.storyteller.ai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apps() {
|
||||
assert_eq!(STORYTELLER_BOARD.https_link_without_path(), "https://board.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_RENDER.https_link_without_path(), "https://render.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_2D.https_link_without_path(), "https://2d.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_3D.https_link_without_path(), "https://3d.storyteller.ai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn development() {
|
||||
assert_eq!(STORYTELLER_DEV.bare_domain(), "dev.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_DEV_PROXY.bare_domain(), "devproxy.storyteller.ai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cdn() {
|
||||
assert_eq!(STORYTELLER_CDN.https_link_without_path(), "https://cdn.storyteller.ai");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//! Process-wide cache backing `Domain::https_url()`.
|
||||
//!
|
||||
//! `Url::parse` can't run at compile time, and `Domain` stays free of
|
||||
//! interior mutability so its constants remain usable in `const` contexts.
|
||||
//! So each domain's `Url` is parsed on first use and leaked into this map.
|
||||
//! Domains only come from `domain!` literals, so the set (and the leak) is
|
||||
//! bounded by the constants in the source code.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use url::Url;
|
||||
|
||||
static URLS: Mutex<BTreeMap<&'static str, &'static Url>> = Mutex::new(BTreeMap::new());
|
||||
|
||||
pub(crate) fn https_url(bare: &'static str, https_with_root_path: &'static str) -> &'static Url {
|
||||
// The map is only ever inserted into, so a poisoned lock still holds
|
||||
// valid entries.
|
||||
let mut urls = URLS.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
urls.entry(bare).or_insert_with(|| {
|
||||
// The domain was validated at compile time, so this parse can't fail.
|
||||
let url = Url::parse(https_with_root_path).expect("compile-time validated domain should parse");
|
||||
Box::leak(Box::new(url))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//! Compile-time domain name validation.
|
||||
//!
|
||||
//! `assert_valid_domain` is a `const fn` that panics on an invalid name. The
|
||||
//! `domain!` macro calls it while evaluating a `const`/`static`, where a panic
|
||||
//! is a compile error, so a bad domain literal never builds.
|
||||
|
||||
const MAX_DOMAIN_LEN: usize = 253;
|
||||
const MAX_LABEL_LEN: usize = 63;
|
||||
|
||||
/// Panics (a compile error in const context) unless `domain` is a canonical,
|
||||
/// fully-qualified DNS name: lowercase `a-z`, `0-9` and `-` labels separated
|
||||
/// by single dots, at least two labels, no scheme, port, path or trailing dot,
|
||||
/// and a non-numeric top-level label (so IP addresses are rejected).
|
||||
pub const fn assert_valid_domain(domain: &str) {
|
||||
match check_domain(domain) {
|
||||
Ok(()) => {}
|
||||
Err(DomainError::Empty) => panic!("invalid domain: empty"),
|
||||
Err(DomainError::TooLong) => panic!("invalid domain: longer than 253 bytes"),
|
||||
Err(DomainError::EmptyLabel) => panic!("invalid domain: empty label (leading, trailing or doubled dot)"),
|
||||
Err(DomainError::LabelTooLong) => panic!("invalid domain: label longer than 63 bytes"),
|
||||
Err(DomainError::InvalidCharacter) => panic!("invalid domain: only lowercase a-z, 0-9, '-' and '.' are allowed"),
|
||||
Err(DomainError::HyphenAtLabelEdge) => panic!("invalid domain: label starts or ends with '-'"),
|
||||
Err(DomainError::SingleLabel) => panic!("invalid domain: needs at least two labels (eg. example.com)"),
|
||||
Err(DomainError::NumericTopLevelLabel) => panic!("invalid domain: top-level label is numeric (IP addresses are not domains)"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum DomainError {
|
||||
Empty,
|
||||
TooLong,
|
||||
EmptyLabel,
|
||||
LabelTooLong,
|
||||
InvalidCharacter,
|
||||
HyphenAtLabelEdge,
|
||||
SingleLabel,
|
||||
NumericTopLevelLabel,
|
||||
}
|
||||
|
||||
pub(crate) const fn check_domain(domain: &str) -> Result<(), DomainError> {
|
||||
let bytes = domain.as_bytes();
|
||||
if bytes.is_empty() {
|
||||
return Err(DomainError::Empty);
|
||||
}
|
||||
if bytes.len() > MAX_DOMAIN_LEN {
|
||||
return Err(DomainError::TooLong);
|
||||
}
|
||||
|
||||
let mut labels = 0;
|
||||
let mut label_start = 0;
|
||||
let mut last_label_start = 0;
|
||||
let mut i = 0;
|
||||
// Walk one past the end so the final label is checked like the others.
|
||||
while i <= bytes.len() {
|
||||
if i == bytes.len() || bytes[i] == b'.' {
|
||||
match check_label(bytes, label_start, i) {
|
||||
Ok(()) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
labels += 1;
|
||||
last_label_start = label_start;
|
||||
label_start = i + 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if labels < 2 {
|
||||
return Err(DomainError::SingleLabel);
|
||||
}
|
||||
if is_all_digits(bytes, last_label_start, bytes.len()) {
|
||||
return Err(DomainError::NumericTopLevelLabel);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks `bytes[start..end]` as a single label.
|
||||
const fn check_label(bytes: &[u8], start: usize, end: usize) -> Result<(), DomainError> {
|
||||
let len = end - start;
|
||||
if len == 0 {
|
||||
return Err(DomainError::EmptyLabel);
|
||||
}
|
||||
if len > MAX_LABEL_LEN {
|
||||
return Err(DomainError::LabelTooLong);
|
||||
}
|
||||
let mut i = start;
|
||||
while i < end {
|
||||
match bytes[i] {
|
||||
b'a'..=b'z' | b'0'..=b'9' | b'-' => {}
|
||||
_ => return Err(DomainError::InvalidCharacter),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if bytes[start] == b'-' || bytes[end - 1] == b'-' {
|
||||
return Err(DomainError::HyphenAtLabelEdge);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn is_all_digits(bytes: &[u8], start: usize, end: usize) -> bool {
|
||||
let mut i = start;
|
||||
while i < end {
|
||||
if !bytes[i].is_ascii_digit() {
|
||||
return false;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DomainError::*;
|
||||
use super::check_domain;
|
||||
|
||||
mod valid {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apex_domain() {
|
||||
assert_eq!(check_domain("getartcraft.com"), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subdomains() {
|
||||
assert_eq!(check_domain("desktop.getartcraft.com"), Ok(()));
|
||||
assert_eq!(check_domain("studio-staging.studio.storyteller.ai"), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn digits_and_hyphens_inside_labels() {
|
||||
assert_eq!(check_domain("cdn-2.fakeyou.com"), Ok(()));
|
||||
assert_eq!(check_domain("3d.getartcraft.com"), Ok(()));
|
||||
assert_eq!(check_domain("pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev"), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_lengths() {
|
||||
let label_63 = "a".repeat(63);
|
||||
assert_eq!(check_domain(&format!("{label_63}.com")), Ok(()));
|
||||
|
||||
// 63 + 1 + 63 + 1 + 63 + 1 + 61 = 253
|
||||
let domain_253 = format!("{label_63}.{label_63}.{label_63}.{}", "a".repeat(61));
|
||||
assert_eq!(domain_253.len(), 253);
|
||||
assert_eq!(check_domain(&domain_253), Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
mod invalid {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(check_domain(""), Err(Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_long() {
|
||||
let label_63 = "a".repeat(63);
|
||||
let domain_254 = format!("{label_63}.{label_63}.{label_63}.{}", "a".repeat(62));
|
||||
assert_eq!(check_domain(&domain_254), Err(TooLong));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_labels() {
|
||||
assert_eq!(check_domain(".getartcraft.com"), Err(EmptyLabel));
|
||||
assert_eq!(check_domain("getartcraft.com."), Err(EmptyLabel));
|
||||
assert_eq!(check_domain("desktop..getartcraft.com"), Err(EmptyLabel));
|
||||
assert_eq!(check_domain("."), Err(EmptyLabel));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn label_too_long() {
|
||||
assert_eq!(check_domain(&format!("{}.com", "a".repeat(64))), Err(LabelTooLong));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uppercase() {
|
||||
assert_eq!(check_domain("GetArtCraft.com"), Err(InvalidCharacter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheme_port_path_and_whitespace() {
|
||||
assert_eq!(check_domain("https://getartcraft.com"), Err(InvalidCharacter));
|
||||
assert_eq!(check_domain("getartcraft.com:443"), Err(InvalidCharacter));
|
||||
assert_eq!(check_domain("getartcraft.com/"), Err(InvalidCharacter));
|
||||
assert_eq!(check_domain(" getartcraft.com"), Err(InvalidCharacter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ascii() {
|
||||
assert_eq!(check_domain("artcräft.com"), Err(InvalidCharacter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn underscore() {
|
||||
assert_eq!(check_domain("dev_proxy.fakeyou.com"), Err(InvalidCharacter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphen_at_label_edge() {
|
||||
assert_eq!(check_domain("-desktop.getartcraft.com"), Err(HyphenAtLabelEdge));
|
||||
assert_eq!(check_domain("desktop-.getartcraft.com"), Err(HyphenAtLabelEdge));
|
||||
assert_eq!(check_domain("getartcraft.com-"), Err(HyphenAtLabelEdge));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_label() {
|
||||
assert_eq!(check_domain("localhost"), Err(SingleLabel));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addresses() {
|
||||
assert_eq!(check_domain("127.0.0.1"), Err(NumericTopLevelLabel));
|
||||
assert_eq!(check_domain("example.123"), Err(NumericTopLevelLabel));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ workspace-build-acceleration.workspace = true
|
||||
# Internal
|
||||
actix_artcraft.workspace = true
|
||||
actix_cors_configs = { path = "../../../lib/actix_cors_configs" }
|
||||
domains = { workspace = true, features = ["url"] }
|
||||
actix_helpers.workspace = true
|
||||
artcraft_api_defs.workspace = true
|
||||
artcraft_api_keys.workspace = true
|
||||
|
||||
+42
-29
@@ -1,43 +1,56 @@
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use once_cell::sync::Lazy;
|
||||
use domains::Domain;
|
||||
use domains::fakeyou::{FAKEYOU_CDN, FAKEYOU_DEV_CDN};
|
||||
use server_environment::ServerEnvironment;
|
||||
use url::Url;
|
||||
|
||||
const FAKEYOU_CDN_STR: &str = "https://cdn-2.fakeyou.com";
|
||||
const STORYTELLER_CDN_STR: &str = "https://cdn-2.fakeyou.com";
|
||||
|
||||
const FAKEYOU_DEVELOPMENT_CDN_STR: &str = "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev";
|
||||
const STORYTELLER_DEVELOPMENT_CDN_STR: &str = "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev";
|
||||
|
||||
|
||||
const FAKEYOU_CDN: Lazy<Url> = Lazy::new(|| Url::parse(FAKEYOU_CDN_STR)
|
||||
.expect("should never fail"));
|
||||
|
||||
const STORYTELLER_CDN: Lazy<Url> = Lazy::new(|| Url::parse(STORYTELLER_CDN_STR)
|
||||
.expect("should never fail"));
|
||||
|
||||
const FAKEYOU_DEVELOPMENT_CDN: Lazy<Url> = Lazy::new(|| Url::parse(FAKEYOU_DEVELOPMENT_CDN_STR)
|
||||
.expect("should never fail"));
|
||||
|
||||
const STORYTELLER_DEVELOPMENT_CDN: Lazy<Url> = Lazy::new(|| Url::parse(STORYTELLER_DEVELOPMENT_CDN_STR)
|
||||
.expect("should never fail"));
|
||||
// NB: Storyteller media is served from the FakeYou CDN too.
|
||||
const FAKEYOU_PRODUCTION: Domain = FAKEYOU_CDN;
|
||||
const STORYTELLER_PRODUCTION: Domain = FAKEYOU_CDN;
|
||||
|
||||
const FAKEYOU_DEVELOPMENT: Domain = FAKEYOU_DEV_CDN;
|
||||
const STORYTELLER_DEVELOPMENT: Domain = FAKEYOU_DEV_CDN;
|
||||
|
||||
// TODO(bt,2025-01-31): Perhaps this should be config driven and configurable at runtime instead of hardcoded.
|
||||
pub fn get_cdn_host(media_domain: MediaDomain, server_environment: ServerEnvironment) -> &'static str {
|
||||
match (media_domain, server_environment) {
|
||||
(MediaDomain::FakeYou, ServerEnvironment::Development) => FAKEYOU_DEVELOPMENT_CDN_STR,
|
||||
(MediaDomain::FakeYou, ServerEnvironment::Production) => FAKEYOU_CDN_STR,
|
||||
(MediaDomain::Storyteller, ServerEnvironment::Development) => STORYTELLER_DEVELOPMENT_CDN_STR,
|
||||
(MediaDomain::Storyteller, ServerEnvironment::Production) => STORYTELLER_CDN_STR,
|
||||
}
|
||||
cdn_domain(media_domain, server_environment).https_link_without_path()
|
||||
}
|
||||
|
||||
pub fn new_cdn_url(media_domain: MediaDomain, server_environment: ServerEnvironment) -> Url {
|
||||
cdn_domain(media_domain, server_environment).https_url().clone()
|
||||
}
|
||||
|
||||
fn cdn_domain(media_domain: MediaDomain, server_environment: ServerEnvironment) -> Domain {
|
||||
match (media_domain, server_environment) {
|
||||
(MediaDomain::FakeYou, ServerEnvironment::Development) => FAKEYOU_DEVELOPMENT_CDN.clone(),
|
||||
(MediaDomain::FakeYou, ServerEnvironment::Production) => FAKEYOU_CDN.clone(),
|
||||
(MediaDomain::Storyteller, ServerEnvironment::Development) => STORYTELLER_DEVELOPMENT_CDN.clone(),
|
||||
(MediaDomain::Storyteller, ServerEnvironment::Production) => STORYTELLER_CDN.clone(),
|
||||
(MediaDomain::FakeYou, ServerEnvironment::Development) => FAKEYOU_DEVELOPMENT,
|
||||
(MediaDomain::FakeYou, ServerEnvironment::Production) => FAKEYOU_PRODUCTION,
|
||||
(MediaDomain::Storyteller, ServerEnvironment::Development) => STORYTELLER_DEVELOPMENT,
|
||||
(MediaDomain::Storyteller, ServerEnvironment::Production) => STORYTELLER_PRODUCTION,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::http_server::common_responses::media::cdn_link::{get_cdn_host, new_cdn_url};
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use server_environment::ServerEnvironment;
|
||||
|
||||
const PROD_CDN: &str = "https://cdn-2.fakeyou.com";
|
||||
const DEV_CDN: &str = "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev";
|
||||
|
||||
#[test]
|
||||
fn cdn_hosts() {
|
||||
assert_eq!(get_cdn_host(MediaDomain::FakeYou, ServerEnvironment::Production), PROD_CDN);
|
||||
assert_eq!(get_cdn_host(MediaDomain::Storyteller, ServerEnvironment::Production), PROD_CDN);
|
||||
assert_eq!(get_cdn_host(MediaDomain::FakeYou, ServerEnvironment::Development), DEV_CDN);
|
||||
assert_eq!(get_cdn_host(MediaDomain::Storyteller, ServerEnvironment::Development), DEV_CDN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cdn_urls() {
|
||||
assert_eq!(new_cdn_url(MediaDomain::FakeYou, ServerEnvironment::Production).as_str(), "https://cdn-2.fakeyou.com/");
|
||||
assert_eq!(new_cdn_url(MediaDomain::Storyteller, ServerEnvironment::Production).as_str(), "https://cdn-2.fakeyou.com/");
|
||||
assert_eq!(new_cdn_url(MediaDomain::FakeYou, ServerEnvironment::Development).as_str(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/");
|
||||
assert_eq!(new_cdn_url(MediaDomain::Storyteller, ServerEnvironment::Development).as_str(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/");
|
||||
}
|
||||
}
|
||||
+4
-16
@@ -1,17 +1,6 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use domains::fakeyou::FAKEYOU_CDN;
|
||||
use url::Url;
|
||||
|
||||
//const FAKEYOU_CDN_STR: &str = "https://storage.googleapis.com/dev-vocodes-public";
|
||||
const FAKEYOU_CDN_STR: &str = "https://cdn-2.fakeyou.com";
|
||||
|
||||
const STORYTELLER_CDN_STR: &str = "https://cdn-2.fakeyou.com";
|
||||
|
||||
const FAKEYOU_CDN: Lazy<Url> = Lazy::new(|| Url::parse(FAKEYOU_CDN_STR)
|
||||
.expect("should never fail"));
|
||||
|
||||
const STORYTELLER_CDN: Lazy<Url> = Lazy::new(|| Url::parse(STORYTELLER_CDN_STR)
|
||||
.expect("should never fail"));
|
||||
|
||||
/// Which domain to generate CDN, etc. links for.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum MediaDomain {
|
||||
@@ -22,14 +11,13 @@ pub enum MediaDomain {
|
||||
impl MediaDomain {
|
||||
pub fn new_cdn_url(&self) -> Url {
|
||||
match self {
|
||||
MediaDomain::FakeYou => FAKEYOU_CDN.clone(),
|
||||
MediaDomain::Storyteller => STORYTELLER_CDN.clone(),
|
||||
// NB: Storyteller media is served from the FakeYou CDN too.
|
||||
MediaDomain::FakeYou | MediaDomain::Storyteller => FAKEYOU_CDN.https_url().clone(),
|
||||
}
|
||||
}
|
||||
pub fn cdn_url_str(&self) -> &'static str {
|
||||
match self {
|
||||
MediaDomain::FakeYou => FAKEYOU_CDN_STR,
|
||||
MediaDomain::Storyteller => STORYTELLER_CDN_STR,
|
||||
MediaDomain::FakeYou | MediaDomain::Storyteller => FAKEYOU_CDN.https_link_without_path(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -6,6 +6,7 @@ use std::time::{Duration, Instant};
|
||||
use actix_web::HttpRequest;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use base64::Engine;
|
||||
use domains::artcraft::{ARTCRAFT_WEBAPP, ARTCRAFT_WEBSITE, ARTCRAFT_WEBSITE_WWW};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use rand::{
|
||||
rngs::{OsRng, StdRng},
|
||||
@@ -16,7 +17,11 @@ use sha2::{Digest, Sha256};
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
|
||||
const CODE_ALPHABET: &[u8] = b"BCDFGHJKLMNPQRSTVWXZ";
|
||||
const APPROVAL_ORIGINS: &[&str] = &["https://app.getartcraft.com", "https://getartcraft.com", "https://www.getartcraft.com"];
|
||||
const APPROVAL_ORIGINS: &[&str] = &[
|
||||
ARTCRAFT_WEBAPP.https_link_without_path(),
|
||||
ARTCRAFT_WEBSITE.https_link_without_path(),
|
||||
ARTCRAFT_WEBSITE_WWW.https_link_without_path(),
|
||||
];
|
||||
const WINDOW: Duration = Duration::from_secs(60);
|
||||
const MAX_BUCKETS: usize = 10_000;
|
||||
|
||||
|
||||
Reference in new issue
Block a user