Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: provide options to disable or customize http corss-origin settings #5450

Merged
merged 9 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
| `http.addr` | String | `127.0.0.1:4000` | The address to bind the HTTP server. |
| `http.timeout` | String | `30s` | HTTP request timeout. Set to 0 to disable timeout. |
| `http.body_limit` | String | `64MB` | HTTP request body limit.<br/>The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.<br/>Set to 0 to disable limit. |
| `http.enable_cors` | Bool | `true` | HTTP CORS support, it's turned on by default<br/>This allows browser to access http APIs without CORS restrictions |
| `http.cors_allowed_origins` | Array | Unset | Customize allowed origins for HTTP CORS. |
| `grpc` | -- | -- | The gRPC server options. |
| `grpc.addr` | String | `127.0.0.1:4001` | The address to bind the gRPC server. |
| `grpc.runtime_size` | Integer | `8` | The number of server worker threads. |
Expand Down Expand Up @@ -216,6 +218,8 @@
| `http.addr` | String | `127.0.0.1:4000` | The address to bind the HTTP server. |
| `http.timeout` | String | `30s` | HTTP request timeout. Set to 0 to disable timeout. |
| `http.body_limit` | String | `64MB` | HTTP request body limit.<br/>The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.<br/>Set to 0 to disable limit. |
| `http.enable_cors` | Bool | `true` | HTTP CORS support, it's turned on by default<br/>This allows browser to access http APIs without CORS restrictions |
| `http.cors_allowed_origins` | Array | Unset | Customize allowed origins for HTTP CORS. |
| `grpc` | -- | -- | The gRPC server options. |
| `grpc.addr` | String | `127.0.0.1:4001` | The address to bind the gRPC server. |
| `grpc.hostname` | String | `127.0.0.1:4001` | The hostname advertised to the metasrv,<br/>and used for connections from outside the host |
Expand Down
6 changes: 6 additions & 0 deletions config/frontend.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ timeout = "30s"
## The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.
## Set to 0 to disable limit.
body_limit = "64MB"
## HTTP CORS support, it's turned on by default
## This allows browser to access http APIs without CORS restrictions
enable_cors = true
## Customize allowed origins for HTTP CORS.
## @toml2docs:none-default
cors_allowed_origins = ["https://example.com"]

## The gRPC server options.
[grpc]
Expand Down
6 changes: 6 additions & 0 deletions config/standalone.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ timeout = "30s"
## The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.
## Set to 0 to disable limit.
body_limit = "64MB"
## HTTP CORS support, it's turned on by default
## This allows browser to access http APIs without CORS restrictions
enable_cors = true
## Customize allowed origins for HTTP CORS.
## @toml2docs:none-default
cors_allowed_origins = ["https://example.com"]

## The gRPC server options.
[grpc]
Expand Down
9 changes: 9 additions & 0 deletions src/cmd/tests/load_config_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use metric_engine::config::EngineConfig as MetricEngineConfig;
use mito2::config::MitoConfig;
use servers::export_metrics::ExportMetricsOption;
use servers::grpc::GrpcOptions;
use servers::http::HttpOptions;

#[allow(deprecated)]
#[test]
Expand Down Expand Up @@ -144,6 +145,10 @@ fn test_load_frontend_example_config() {
..Default::default()
},
grpc: GrpcOptions::default().with_hostname("127.0.0.1:4001"),
http: HttpOptions {
cors_allowed_origins: vec!["https://example.com".to_string()],
..Default::default()
},
..Default::default()
},
..Default::default()
Expand Down Expand Up @@ -234,6 +239,10 @@ fn test_load_standalone_example_config() {
remote_write: Some(Default::default()),
..Default::default()
},
http: HttpOptions {
cors_allowed_origins: vec!["https://example.com".to_string()],
..Default::default()
},
..Default::default()
},
..Default::default()
Expand Down
11 changes: 10 additions & 1 deletion src/servers/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use common_macro::stack_trace_debug;
use common_telemetry::{error, warn};
use datatypes::prelude::ConcreteDataType;
use headers::ContentType;
use http::header::InvalidHeaderValue;
use query::parser::PromQuery;
use serde_json::json;
use snafu::{Location, Snafu};
Expand Down Expand Up @@ -345,6 +346,14 @@ pub enum Error {
location: Location,
},

#[snafu(display("Invalid http header value"))]
InvalidHeaderValue {
#[snafu(source)]
error: InvalidHeaderValue,
#[snafu(implicit)]
location: Location,
},

#[snafu(display("Error accessing catalog"))]
Catalog {
source: catalog::error::Error,
Expand Down Expand Up @@ -678,7 +687,7 @@ impl ErrorExt for Error {
#[cfg(feature = "mem-prof")]
DumpProfileData { source, .. } => source.status_code(),

InvalidUtf8Value { .. } => StatusCode::InvalidArguments,
InvalidUtf8Value { .. } | InvalidHeaderValue { .. } => StatusCode::InvalidArguments,

ParsePromQL { source, .. } => source.status_code(),
Other { source, .. } => source.status_code(),
Expand Down
174 changes: 153 additions & 21 deletions src/servers/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ use datatypes::schema::SchemaRef;
use datatypes::value::transform_value_ref_to_json_value;
use event::{LogState, LogValidatorRef};
use futures::FutureExt;
use http::Method;
use http::{HeaderValue, Method};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use snafu::{ensure, ResultExt};
use tokio::sync::oneshot::{self, Sender};
use tokio::sync::Mutex;
use tower::ServiceBuilder;
use tower_http::cors::{Any, CorsLayer};
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
use tower_http::decompression::RequestDecompressionLayer;
use tower_http::trace::TraceLayer;

Expand All @@ -52,7 +52,8 @@ use self::result::table_result::TableResponse;
use crate::configurator::ConfiguratorRef;
use crate::elasticsearch;
use crate::error::{
AddressBindSnafu, AlreadyStartedSnafu, Error, InternalIoSnafu, Result, ToJsonSnafu,
AddressBindSnafu, AlreadyStartedSnafu, Error, InternalIoSnafu, InvalidHeaderValueSnafu, Result,
ToJsonSnafu,
};
use crate::http::influxdb::{influxdb_health, influxdb_ping, influxdb_write_v1, influxdb_write_v2};
use crate::http::prometheus::{
Expand Down Expand Up @@ -140,6 +141,10 @@ pub struct HttpOptions {
pub body_limit: ReadableSize,

pub is_strict_mode: bool,

pub cors_allowed_origins: Vec<String>,

pub enable_cors: bool,
}

impl Default for HttpOptions {
Expand All @@ -150,6 +155,8 @@ impl Default for HttpOptions {
disable_dashboard: false,
body_limit: DEFAULT_BODY_LIMIT,
is_strict_mode: false,
cors_allowed_origins: Vec::new(),
enable_cors: true,
}
}
}
Expand Down Expand Up @@ -715,7 +722,7 @@ impl HttpServer {

/// Attaches middlewares and debug routes to the router.
/// Callers should call this method after [HttpServer::make_app()].
pub fn build(&self, router: Router) -> Router {
pub fn build(&self, router: Router) -> Result<Router> {
let timeout_layer = if self.options.timeout != Duration::default() {
Some(ServiceBuilder::new().layer(DynamicTimeoutLayer::new(self.options.timeout)))
} else {
Expand All @@ -731,26 +738,45 @@ impl HttpServer {
info!("HTTP server body limit is disabled");
None
};
let cors_layer = if self.options.enable_cors {
Some(
CorsLayer::new()
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::HEAD,
])
.allow_origin(if self.options.cors_allowed_origins.is_empty() {
AllowOrigin::from(Any)
} else {
AllowOrigin::from(
self.options
.cors_allowed_origins
.iter()
.map(|s| {
HeaderValue::from_str(s.as_str())
.context(InvalidHeaderValueSnafu)
})
.collect::<Result<Vec<HeaderValue>>>()?,
)
})
.allow_headers(Any),
)
} else {
info!("HTTP server cross-origin is disabled");
None
};

router
Ok(router
// middlewares
.layer(
ServiceBuilder::new()
// disable on failure tracing. because printing out isn't very helpful,
// and we have impl IntoResponse for Error. It will print out more detailed error messages
.layer(TraceLayer::new_for_http().on_failure(()))
.layer(
CorsLayer::new()
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::HEAD,
])
.allow_origin(Any)
.allow_headers(Any),
)
.option_layer(cors_layer)
.option_layer(timeout_layer)
.option_layer(body_limit_layer)
// auth layer
Expand All @@ -772,7 +798,7 @@ impl HttpServer {
.route("/cpu", routing::post(pprof::pprof_handler))
.route("/mem", routing::post(mem_prof::mem_prof_handler)),
),
)
))
}

fn route_metrics<S>(metrics_handler: MetricsHandler) -> Router<S> {
Expand Down Expand Up @@ -1032,7 +1058,7 @@ impl Server for HttpServer {
if let Some(configurator) = self.plugins.get::<ConfiguratorRef>() {
app = configurator.config_http(app);
}
let app = self.build(app);
let app = self.build(app)?;
let listener = tokio::net::TcpListener::bind(listening)
.await
.context(AddressBindSnafu { addr: listening })?
Expand Down Expand Up @@ -1177,17 +1203,123 @@ mod test {
}

fn make_test_app(tx: mpsc::Sender<(String, Vec<u8>)>) -> Router {
make_test_app_custom(tx, HttpOptions::default())
}

fn make_test_app_custom(tx: mpsc::Sender<(String, Vec<u8>)>, options: HttpOptions) -> Router {
let instance = Arc::new(DummyInstance { _tx: tx });
let sql_instance = ServerSqlQueryHandlerAdapter::arc(instance.clone());
let server = HttpServerBuilder::new(HttpOptions::default())
let server = HttpServerBuilder::new(options)
.with_sql_handler(sql_instance)
.build();
server.build(server.make_app()).route(
server.build(server.make_app()).unwrap().route(
"/test/timeout",
get(forever.layer(ServiceBuilder::new().layer(timeout()))),
)
}

#[tokio::test]
pub async fn test_cors() {
// cors is on by default
let (tx, _rx) = mpsc::channel(100);
let app = make_test_app(tx);
let client = TestClient::new(app).await;

let res = client.get("/health").send().await;

assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers()
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
.expect("expect cors header origin"),
"*"
);

let res = client
.options("/health")
.header("Access-Control-Request-Headers", "x-greptime-auth")
.header("Access-Control-Request-Method", "DELETE")
.header("Origin", "https://example.com")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers()
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
.expect("expect cors header origin"),
"*"
);
assert_eq!(
res.headers()
.get(http::header::ACCESS_CONTROL_ALLOW_HEADERS)
.expect("expect cors header headers"),
"*"
);
assert_eq!(
res.headers()
.get(http::header::ACCESS_CONTROL_ALLOW_METHODS)
.expect("expect cors header methods"),
"GET,POST,PUT,DELETE,HEAD"
);
}

#[tokio::test]
pub async fn test_cors_custom_origins() {
// cors is on by default
let (tx, _rx) = mpsc::channel(100);
let origin = "https://example.com";

let options = HttpOptions {
cors_allowed_origins: vec![origin.to_string()],
..Default::default()
};

let app = make_test_app_custom(tx, options);
let client = TestClient::new(app).await;

let res = client.get("/health").header("Origin", origin).send().await;

assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers()
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
.expect("expect cors header origin"),
origin
);

let res = client
.get("/health")
.header("Origin", "https://notallowed.com")
.send()
.await;

assert_eq!(res.status(), StatusCode::OK);
sunng87 marked this conversation as resolved.
Show resolved Hide resolved
assert!(!res
.headers()
.contains_key(http::header::ACCESS_CONTROL_ALLOW_ORIGIN));
}

#[tokio::test]
pub async fn test_cors_disabled() {
// cors is on by default
let (tx, _rx) = mpsc::channel(100);

let options = HttpOptions {
enable_cors: false,
..Default::default()
};

let app = make_test_app_custom(tx, options);
let client = TestClient::new(app).await;

let res = client.get("/health").send().await;

assert_eq!(res.status(), StatusCode::OK);
assert!(!res
.headers()
.contains_key(http::header::ACCESS_CONTROL_ALLOW_ORIGIN));
}

#[test]
fn test_http_options_default() {
let default = HttpOptions::default();
Expand Down
2 changes: 1 addition & 1 deletion src/servers/tests/http/influxdb_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ fn make_test_app(tx: Arc<mpsc::Sender<(String, String)>>, db_name: Option<&str>)
.with_user_provider(Arc::new(user_provider))
.with_influxdb_handler(instance)
.build();
server.build(server.make_app())
server.build(server.make_app()).unwrap()
}

#[tokio::test]
Expand Down
2 changes: 1 addition & 1 deletion src/servers/tests/http/opentsdb_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ fn make_test_app(tx: mpsc::Sender<String>) -> Router {
.with_sql_handler(instance.clone())
.with_opentsdb_handler(instance)
.build();
server.build(server.make_app())
server.build(server.make_app()).unwrap()
}

#[tokio::test]
Expand Down
2 changes: 1 addition & 1 deletion src/servers/tests/http/prom_store_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ fn make_test_app(tx: mpsc::Sender<(String, Vec<u8>)>) -> Router {
.with_sql_handler(instance.clone())
.with_prom_handler(instance, true, is_strict_mode)
.build();
server.build(server.make_app())
server.build(server.make_app()).unwrap()
}

#[tokio::test]
Expand Down
Loading
Loading