diff --git a/src/config.rs b/src/config.rs index 8080cb1..f8c3cef 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,17 +2,25 @@ 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; +const DEFAULT_PORT: u16 = 3000; + #[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 = "validate_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 +29,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,11 +73,31 @@ impl From for tracing::level_filters::LevelFilter { } } +fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), DEFAULT_PORT) +} + +fn default_public_address() -> Url { + let mut addr = Url::parse("http://localhost").expect("Static URL is valid"); + addr.set_port(Some(DEFAULT_PORT)).unwrap(); + addr +} + +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")) + } else { + Ok(url) + } +} + #[cfg(test)] mod tests { use tracing::level_filters::LevelFilter; + use url::Url; - use crate::config::LogLevel; + use crate::config::{GlazedConfig, LogLevel, default_bind_address, default_public_address}; #[test] fn level_conversion() { @@ -69,4 +105,25 @@ mod tests { assert_eq!(LevelFilter::from(LogLevel::Debug), LevelFilter::DEBUG); assert_eq!(LevelFilter::from(LogLevel::Trace), LevelFilter::TRACE); } + + #[rstest::rstest] + #[case::no_trailing_slash("http://example.com", "http://example.com/graphql")] + #[case::trailing_slash("http://example.com/", "http://example.com/graphql")] + #[case::non_empty_path("http://example.com/extra", "http://example.com/extra/graphql")] + #[case::path_and_trailing("http://example.com/extra/", "http://example.com/extra/graphql")] + fn graphql_endpoint(#[case] base: &str, #[case] complete: &str) { + let mut config = GlazedConfig::default(); + 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/" + ); + } } 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,