From f2642cc6ce99e4328310462fc295d1ecc2ae13df Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 19 Jan 2026 17:48:04 +0000 Subject: [PATCH 1/4] Ensure public address always present --- src/config.rs | 41 +++++++++++++++++++++++++++++++++++++---- src/handlers.rs | 8 ++++---- src/main.rs | 44 +++++++++++++++++++++----------------------- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/src/config.rs b/src/config.rs index 8080cb1..9e5b905 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,17 +2,23 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::Path; use config::{Config, ConfigError, File}; -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; use url::Url; #[derive(Deserialize, Debug, Clone)] pub struct GlazedConfig { + #[serde(default = "default_bind_address")] pub bind_address: SocketAddr, - pub public_address: Option, + #[serde( + default = "default_public_address", + deserialize_with = "valid_public_address" + )] + pub public_address: Url, pub tiled_client: TiledClientConfig, #[serde(default)] pub log_level: LogLevel, } + impl GlazedConfig { pub fn from_file(path: &Path) -> Result { let config = Config::builder().add_source(File::from(path)).build()?; @@ -21,14 +27,22 @@ impl GlazedConfig { pub fn default() -> Self { GlazedConfig { - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 3000), - public_address: None, + bind_address: default_bind_address(), + public_address: default_public_address(), tiled_client: TiledClientConfig { address: Url::parse("http://localhost:8000").expect("Static URL is valid"), }, log_level: LogLevel::Info, } } + pub fn endpoint(&self, endpoint: &str) -> String { + let mut addr = self.public_address.clone(); + addr.path_segments_mut() + .expect("base address can be a base") + .pop_if_empty() + .push(endpoint); + addr.to_string() + } } #[derive(Deserialize, Debug, Clone)] @@ -57,6 +71,25 @@ impl From for tracing::level_filters::LevelFilter { } } +fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 3000) +} + +fn default_public_address() -> Url { + "http://localhost:3000" + .parse() + .expect("Static str is valid URL") +} + +fn valid_public_address<'de, D: Deserializer<'de>>(des: D) -> Result { + let url = Url::deserialize(des)?; + if url.cannot_be_a_base() { + Err(serde::de::Error::custom("URL cannot be a base")) + } else { + Ok(url) + } +} + #[cfg(test)] mod tests { use tracing::level_filters::LevelFilter; diff --git a/src/handlers.rs b/src/handlers.rs index d4347db..bb0ddc7 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -52,13 +52,13 @@ pub async fn graphql_ws_handler( } pub async fn graphiql_handler( - graphql_endpoint: Option, - subscription_endpoint: Option, + graphql_endpoint: String, + subscription_endpoint: String, ) -> impl IntoResponse { Html( GraphiQLSource::build() - .endpoint(graphql_endpoint.as_deref().unwrap_or("/graphql")) - .subscription_endpoint(subscription_endpoint.as_deref().unwrap_or("/ws")) + .endpoint(&graphql_endpoint) + .subscription_endpoint(&subscription_endpoint) .finish(), ) } diff --git a/src/main.rs b/src/main.rs index a36a377..cb3c38a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,25 +57,26 @@ async fn main() -> Result<(), Box> { pub struct RootAddress(Url); async fn serve(config: GlazedConfig) -> Result<(), Box> { - let client = TiledClient::new(config.tiled_client.address); - let public_address = config - .public_address - .clone() - .unwrap_or_else(|| Url::parse(&format!("http://{}", config.bind_address)).unwrap()); + let client = TiledClient::new(config.tiled_client.address.clone()); let schema = Schema::build(TiledQuery, EmptyMutation, TiledSubscription) - .data(RootAddress(public_address.clone())) + .data(RootAddress(config.public_address.clone())) .data(client.clone()) .finish(); - let graphql_endpoint = config - .public_address - .as_ref() - .map(|u| u.join("graphql").unwrap().to_string()); + let graphql_endpoint = config.endpoint("graphql"); + let graphiql_endpoint = config.endpoint("graphiql"); + let subscription_endpoint = config.endpoint("subscribe"); + info!( + "Redirecting traffic to public address: {}", + config.public_address + ); + info!("Public graphql endpoint available at {}", graphql_endpoint); + + let page_not_found = ( + StatusCode::NOT_FOUND, + not_found_page(&graphql_endpoint, &graphiql_endpoint), + ); - let subscription_endpoint = config - .public_address - .as_ref() - .map(|u| u.join("subscribe").unwrap().to_string()); let app = Router::new() .route("/graphql", post(graphql_handler).get(graphql_get_warning)) .route("/subscribe", get(graphql_ws_handler)) @@ -89,7 +90,7 @@ async fn serve(config: GlazedConfig) -> Result<(), Box> { ) .route("/asset/{run}/{stream}/{det}/{id}", get(download_handler)) .with_state(client) - .fallback((StatusCode::NOT_FOUND, not_found_page(&public_address))) + .fallback(page_not_found) .layer(Extension(schema)); let listener = tokio::net::TcpListener::bind(config.bind_address).await?; @@ -100,9 +101,7 @@ async fn serve(config: GlazedConfig) -> Result<(), Box> { .await?) } -fn not_found_page(public_address: &Url) -> Html { - let graphql = public_address.join("graphql").unwrap(); - let graphiql = public_address.join("graphiql").unwrap(); +fn not_found_page(graphql: &str, graphiql: &str) -> Html { Html(format!( include_str!("../templates/404.html"), graphql_address = graphql, @@ -132,15 +131,14 @@ async fn signal_handler() { #[cfg(test)] mod tests { - use url::Url; - use super::not_found_page; #[test] fn test_404() { - let public_address = Url::parse("http://example.com/glazed/").unwrap(); - - let response = not_found_page(&public_address); + let response = not_found_page( + "http://example.com/glazed/graphql", + "http://example.com/glazed/graphiql", + ); assert_eq!( response.0, From 4c184bbe991a4da184e5d2e5c938402ba1ce056f Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Tue, 4 Aug 2026 16:13:12 +0100 Subject: [PATCH 2/4] Use constant for default port --- src/config.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index 9e5b905..76a1f4c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,8 @@ use config::{Config, ConfigError, File}; use serde::{Deserialize, Deserializer}; use url::Url; +const DEFAULT_PORT: u16 = 3000; + #[derive(Deserialize, Debug, Clone)] pub struct GlazedConfig { #[serde(default = "default_bind_address")] @@ -72,13 +74,13 @@ impl From for tracing::level_filters::LevelFilter { } fn default_bind_address() -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 3000) + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), DEFAULT_PORT) } fn default_public_address() -> Url { - "http://localhost:3000" - .parse() - .expect("Static str is valid URL") + let mut addr = Url::parse("http://localhost").expect("Static URL is valid"); + addr.set_port(Some(DEFAULT_PORT)).unwrap(); + addr } fn valid_public_address<'de, D: Deserializer<'de>>(des: D) -> Result { From 9a127a02b8abb35b54b7ccdb434c8cc26101eae2 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Tue, 4 Aug 2026 16:34:35 +0100 Subject: [PATCH 3/4] Extract graphql building to test trailing slash handling --- src/config.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 76a1f4c..3128a3f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -95,8 +95,9 @@ fn valid_public_address<'de, D: Deserializer<'de>>(des: D) -> Result Date: Thu, 6 Aug 2026 15:18:00 +0100 Subject: [PATCH 4/4] Up the coverage --- src/config.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 3128a3f..f8c3cef 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,7 +13,7 @@ pub struct GlazedConfig { pub bind_address: SocketAddr, #[serde( default = "default_public_address", - deserialize_with = "valid_public_address" + deserialize_with = "validate_public_address" )] pub public_address: Url, pub tiled_client: TiledClientConfig, @@ -83,7 +83,7 @@ fn default_public_address() -> Url { addr } -fn valid_public_address<'de, D: Deserializer<'de>>(des: D) -> Result { +fn validate_public_address<'de, D: Deserializer<'de>>(des: D) -> Result { let url = Url::deserialize(des)?; if url.cannot_be_a_base() { Err(serde::de::Error::custom("URL cannot be a base")) @@ -97,7 +97,7 @@ mod tests { use tracing::level_filters::LevelFilter; use url::Url; - use crate::config::{GlazedConfig, LogLevel}; + use crate::config::{GlazedConfig, LogLevel, default_bind_address, default_public_address}; #[test] fn level_conversion() { @@ -116,4 +116,14 @@ mod tests { config.public_address = Url::parse(base).unwrap(); assert_eq!(config.endpoint("graphql"), complete) } + + #[test] + fn default_addresses() { + assert_eq!(default_bind_address().to_string(), "127.0.0.1:3000"); + assert!(!default_public_address().cannot_be_a_base()); + assert_eq!( + default_public_address().to_string(), + "http://localhost:3000/" + ); + } }