Add --base-path CLI option to override the URL path in the tilejson (#1205)

Override URL path in the tilejson's `tiles` field when used behind a proxy that is not setting `X-Rewrite-URL` header

Fixes #1185
This commit is contained in:
Lucas 2024-02-28 00:10:29 +08:00 committed by GitHub
parent e0c960a1b6
commit 99cd99eb51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 75 additions and 14 deletions

View File

@ -21,6 +21,10 @@ keep_alive: 75
# The socket address to bind [default: 0.0.0.0:3000]
listen_addresses: '0.0.0.0:3000'
# Set TileJSON URL path prefix, ignoring X-Rewrite-URL header. Must begin with a `/`
base_path: /tiles
# Number of web server workers
worker_processes: 8

View File

@ -27,6 +27,8 @@ Options:
-l, --listen-addresses <LISTEN_ADDRESSES>
The socket address to bind. [DEFAULT: 0.0.0.0:3000]
--base-path <BASE_PATH>
Set TileJSON URL path prefix, ignoring X-Rewrite-URL header. Must begin with a `/`. Examples: `/`, `/tiles`
-W, --workers <WORKERS>
Number of web server workers

View File

@ -13,5 +13,4 @@ mod root;
pub use root::{Args, ExtraArgs, MetaArgs};
mod srv;
pub use srv::PreferredEncoding;
pub use srv::SrvArgs;
pub use srv::{PreferredEncoding, SrvArgs};

View File

@ -1,7 +1,8 @@
use crate::srv::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT};
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use crate::srv::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT};
#[derive(clap::Args, Debug, PartialEq, Default)]
#[command(about, version)]
pub struct SrvArgs {
@ -9,6 +10,9 @@ pub struct SrvArgs {
pub keep_alive: Option<u64>,
#[arg(help = format!("The socket address to bind. [DEFAULT: {}]", LISTEN_ADDRESSES_DEFAULT), short, long)]
pub listen_addresses: Option<String>,
/// Set TileJSON URL path prefix, ignoring X-Rewrite-URL header. Must begin with a `/`. Examples: `/`, `/tiles`
#[arg(long)]
pub base_path: Option<String>,
/// Number of web server workers
#[arg(short = 'W', long)]
pub workers: Option<usize>,
@ -42,5 +46,8 @@ impl SrvArgs {
if self.preferred_encoding.is_some() {
srv_config.preferred_encoding = self.preferred_encoding;
}
if self.base_path.is_some() {
srv_config.base_path = self.base_path;
}
}
}

View File

@ -19,7 +19,7 @@ use crate::source::{TileInfoSources, TileSources};
#[cfg(feature = "sprites")]
use crate::sprites::{SpriteConfig, SpriteSources};
use crate::srv::{SrvConfig, RESERVED_KEYWORDS};
use crate::utils::{CacheValue, MainCache, OptMainCache};
use crate::utils::{parse_base_path, CacheValue, MainCache, OptMainCache};
use crate::MartinError::{ConfigLoadError, ConfigParseError, ConfigWriteError, NoSources};
use crate::{IdResolver, MartinResult, OptOneMany};
@ -71,6 +71,10 @@ impl Config {
let mut res = UnrecognizedValues::new();
copy_unrecognized_config(&mut res, "", &self.unrecognized);
if let Some(path) = &self.srv.base_path {
self.srv.base_path = Some(parse_base_path(path)?);
}
#[cfg(feature = "postgres")]
for pg in self.postgres.iter_mut() {
res.extend(pg.finalize()?);

View File

@ -10,6 +10,7 @@ pub const LISTEN_ADDRESSES_DEFAULT: &str = "0.0.0.0:3000";
pub struct SrvConfig {
pub keep_alive: Option<u64>,
pub listen_addresses: Option<String>,
pub base_path: Option<String>,
pub worker_processes: Option<usize>,
pub preferred_encoding: Option<PreferredEncoding>,
}
@ -35,6 +36,7 @@ mod tests {
listen_addresses: some("0.0.0.0:3000"),
worker_processes: Some(8),
preferred_encoding: None,
base_path: None,
}
);
assert_eq!(
@ -50,6 +52,7 @@ mod tests {
listen_addresses: some("0.0.0.0:3000"),
worker_processes: Some(8),
preferred_encoding: Some(PreferredEncoding::Brotli),
base_path: None
}
);
assert_eq!(
@ -65,6 +68,7 @@ mod tests {
listen_addresses: some("0.0.0.0:3000"),
worker_processes: Some(8),
preferred_encoding: Some(PreferredEncoding::Brotli),
base_path: None,
}
);
}

View File

@ -9,6 +9,7 @@ use serde::Deserialize;
use tilejson::{tilejson, TileJSON};
use crate::source::{Source, TileSources};
use crate::srv::SrvConfig;
#[derive(Deserialize)]
pub struct SourceIDsRequest {
@ -26,17 +27,19 @@ async fn get_source_info(
req: HttpRequest,
path: Path<SourceIDsRequest>,
sources: Data<TileSources>,
srv_config: Data<SrvConfig>,
) -> ActixResult<HttpResponse> {
let sources = sources.get_sources(&path.source_ids, None)?.0;
// Get `X-REWRITE-URL` header value, and extract its `path` component.
// If the header is not present or cannot be parsed as a URL, return the request path.
let tiles_path = req
.headers()
let tiles_path = if let Some(base_path) = &srv_config.base_path {
format!("{base_path}/{}", path.source_ids)
} else {
req.headers()
.get("x-rewrite-url")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<Uri>().ok())
.map_or_else(|| req.path().to_owned(), |v| v.path().to_owned());
.map_or_else(|| req.path().to_owned(), |v| v.path().to_owned())
};
let query_string = req.query_string();
let path_and_query = if query_string.is_empty() {
@ -155,7 +158,7 @@ pub fn merge_tilejson(sources: &[&dyn Source], tiles_url: String) -> TileJSON {
pub mod tests {
use std::collections::BTreeMap;
use tilejson::{tilejson, Bounds, VectorLayer};
use tilejson::{Bounds, VectorLayer};
use super::*;
use crate::srv::server::tests::TestSource;

View File

@ -43,6 +43,9 @@ pub enum MartinError {
#[error("Unable to bind to {1}: {0}")]
BindingError(io::Error, String),
#[error("Base path must be a valid URL path, and must begin with a '/' symbol, but is '{0}'")]
BasePathError(String),
#[error("Unable to load config file {}: {0}", .1.display())]
ConfigLoadError(io::Error, PathBuf),

View File

@ -1,8 +1,12 @@
use std::io::{Read as _, Write as _};
use actix_web::http::Uri;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use crate::MartinError::BasePathError;
use crate::MartinResult;
pub fn decode_gzip(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new();
@ -28,3 +32,34 @@ pub fn encode_brotli(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
encoder.write_all(data)?;
Ok(encoder.into_inner())
}
pub fn parse_base_path(path: &str) -> MartinResult<String> {
if !path.starts_with('/') {
return Err(BasePathError(path.to_string()));
}
if let Ok(uri) = path.parse::<Uri>() {
return Ok(uri.path().trim_end_matches('/').to_string());
}
Err(BasePathError(path.to_string()))
}
#[cfg(test)]
pub mod tests {
use crate::utils::parse_base_path;
#[test]
fn test_parse_base_path() {
for (path, expected) in [
("/", Some("")),
("//", Some("")),
("/foo/bar", Some("/foo/bar")),
("/foo/bar/", Some("/foo/bar")),
("", None),
("foo/bar", None),
] {
match expected {
Some(v) => assert_eq!(v, parse_base_path(path).unwrap()),
None => assert!(parse_base_path(path).is_err()),
}
}
}
}