mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-07-18 12:20:48 +00:00
Compare commits
3 Commits
update-seg
...
with_rate_
Author | SHA1 | Date | |
---|---|---|---|
6678491212 | |||
a82f8aacde | |||
5cf71c6014 |
105
Cargo.lock
generated
105
Cargo.lock
generated
@ -34,6 +34,18 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-governor"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fbf4afa1e2f7c28040febe2a7199ad0a5fed564dd645da06ab12642c7d22483"
|
||||
dependencies = [
|
||||
"actix-http",
|
||||
"actix-web",
|
||||
"futures",
|
||||
"governor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.2.2"
|
||||
@ -1017,6 +1029,19 @@ dependencies = [
|
||||
"syn 1.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.12.3",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.11.2"
|
||||
@ -1449,6 +1474,12 @@ version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.25"
|
||||
@ -1500,7 +1531,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1540,6 +1571,23 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19775995ee20209163239355bc3ad2f33f83da35d9ef72dea26e5af753552c87"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"futures",
|
||||
"futures-timer",
|
||||
"no-std-compat",
|
||||
"nonzero_ext",
|
||||
"parking_lot",
|
||||
"quanta",
|
||||
"rand",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grenad"
|
||||
version = "0.4.4"
|
||||
@ -2232,6 +2280,15 @@ dependencies = [
|
||||
"crc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "manifest-dir-macros"
|
||||
version = "0.1.16"
|
||||
@ -2270,6 +2327,7 @@ name = "meilisearch"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"actix-cors",
|
||||
"actix-governor",
|
||||
"actix-http",
|
||||
"actix-rt",
|
||||
"actix-web",
|
||||
@ -2512,7 +2570,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
@ -2536,6 +2594,12 @@ name = "nelson"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/meilisearch/nelson.git?rev=675f13885548fb415ead8fbb447e9e6d9314000a#675f13885548fb415ead8fbb447e9e6d9314000a"
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.1"
|
||||
@ -2557,6 +2621,12 @@ dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonzero_ext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.0"
|
||||
@ -2992,6 +3062,22 @@ version = "2.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"libc",
|
||||
"mach",
|
||||
"once_cell",
|
||||
"raw-cpuid",
|
||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
@ -3061,6 +3147,15 @@ dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "10.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.5.3"
|
||||
@ -4083,6 +4178,12 @@ dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
63
config.toml
63
config.toml
@ -73,6 +73,69 @@ ignore_dump_if_db_exists = false
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#ignore-dump-if-db-exists
|
||||
|
||||
|
||||
#####################
|
||||
### RATE LIMITING ###
|
||||
#####################
|
||||
|
||||
rate_limiting_disable_all = false
|
||||
# Prevents a Meilisearch instance from performing any rate limiting.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-all
|
||||
|
||||
rate_limiting_disable_global = false
|
||||
# Prevents a Meilisearch instance from performing rate limiting global to all queries.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-global
|
||||
|
||||
rate_limiting_global_pool = 100000
|
||||
# The maximum pool of search requests that can be performed before they are rejected.
|
||||
#
|
||||
# The pool starts full at the provided value, then each search request diminishes the pool by 1.
|
||||
# When the pool is empty the search request is rejected.
|
||||
# The pool is replenished by 1 depending on the cooldown period.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-global-pool
|
||||
|
||||
rate_limiting_global_cooldown_ns = 50000
|
||||
# The amount of time, in nanoseconds, before the pool of available search requests is replenished by 1 again.
|
||||
#
|
||||
# The maximum number of available search requests is given by `rate_limiting_global_pool`.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-global-cooldown-ns
|
||||
|
||||
rate_limiting_disable_ip = false
|
||||
# Prevents a Meilisearch instance from performing rate limiting per IP address.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-ip
|
||||
|
||||
rate_limiting_ip_pool = 200
|
||||
# The maximum pool of search requests that can be performed from a specific IP before they are rejected.
|
||||
#
|
||||
# The pool starts full at the provided value, then each search request from the same IP address diminishes the pool by 1.
|
||||
# When the pool is empty the search request is rejected.
|
||||
# The pool is replenished by 1 depending on the cooldown period.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-ip-pool
|
||||
|
||||
rate_limiting_ip_cooldown_ns = 50000000
|
||||
# The amount of time, in nanoseconds, before the pool of available search requests for a specific IP address is replenished by 1 again.
|
||||
#
|
||||
# The maximum number of available search requests for a specific IP address is given by `rate_limiting_ip_pool`.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-ip-cooldown-ns
|
||||
|
||||
rate_limiting_disable_api_key = false
|
||||
# Prevents a Meilisearch instance from performing rate limiting per API key.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-api-key
|
||||
|
||||
rate_limiting_api_key_pool = 10000
|
||||
# The maximum pool of search requests that can be performed using a specific API key before they are rejected.
|
||||
#
|
||||
# The pool starts full at the provided value, then each search request using the same API key diminishes the pool by 1.
|
||||
# When the pool is empty the search request is rejected.
|
||||
# The pool is replenished by 1 depending on the cooldown period.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-api-key-pool
|
||||
|
||||
rate_limiting_api_key_cooldown_ns = 500000
|
||||
# The amount of time, in nanoseconds, before the pool of available search requests using a specific API key is replenished by 1 again.
|
||||
#
|
||||
# The maximum number of available search requests using a specific API key is given by `rate_limiting_api_key_pool`.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-api-key-cooldown-ns
|
||||
|
||||
|
||||
#################
|
||||
### SNAPSHOTS ###
|
||||
#################
|
||||
|
@ -8,6 +8,7 @@ version = "0.30.1"
|
||||
|
||||
[dependencies]
|
||||
actix-cors = "0.6.3"
|
||||
actix-governor = "0.3.2"
|
||||
actix-http = { version = "3.2.2", default-features = false, features = ["compress-brotli", "compress-gzip", "rustls"] }
|
||||
actix-web = { version = "4.2.1", default-features = false, features = ["macros", "compress-brotli", "compress-gzip", "cookies", "rustls"] }
|
||||
actix-web-static-files = { git = "https://github.com/kilork/actix-web-static-files.git", rev = "2d3b6160", optional = true }
|
||||
@ -98,17 +99,7 @@ zip = { version = "0.6.2", optional = true }
|
||||
default = ["analytics", "meilisearch-types/default", "mini-dashboard"]
|
||||
metrics = ["prometheus"]
|
||||
analytics = ["segment"]
|
||||
mini-dashboard = [
|
||||
"actix-web-static-files",
|
||||
"static-files",
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"hex",
|
||||
"reqwest",
|
||||
"sha-1",
|
||||
"tempfile",
|
||||
"zip",
|
||||
]
|
||||
mini-dashboard = ["actix-web-static-files", "static-files", "anyhow", "cargo_toml", "hex", "reqwest", "sha-1", "tempfile", "zip"]
|
||||
chinese = ["meilisearch-types/chinese"]
|
||||
hebrew = ["meilisearch-types/hebrew"]
|
||||
japanese = ["meilisearch-types/japanese"]
|
||||
|
@ -25,7 +25,9 @@ use uuid::Uuid;
|
||||
|
||||
use super::{config_user_id_path, DocumentDeletionKind, MEILISEARCH_CONFIG_PATH};
|
||||
use crate::analytics::Analytics;
|
||||
use crate::option::{default_http_addr, IndexerOpts, MaxMemory, MaxThreads, SchedulerConfig};
|
||||
use crate::option::{
|
||||
default_http_addr, IndexerOpts, MaxMemory, MaxThreads, RateLimiterConfig, SchedulerConfig,
|
||||
};
|
||||
use crate::routes::indexes::documents::UpdateDocumentsQuery;
|
||||
use crate::routes::tasks::TasksFilterQueryRaw;
|
||||
use crate::routes::{create_all_stats, Stats};
|
||||
@ -241,6 +243,16 @@ struct Infos {
|
||||
ssl_require_auth: bool,
|
||||
ssl_resumption: bool,
|
||||
ssl_tickets: bool,
|
||||
rate_limiting_disable_all: bool,
|
||||
rate_limiting_disable_global: bool,
|
||||
rate_limiting_global_pool: u32,
|
||||
rate_limiting_global_cooldown_ns: u64,
|
||||
rate_limiting_disable_ip: bool,
|
||||
rate_limiting_ip_pool: u32,
|
||||
rate_limiting_ip_cooldown_ns: u64,
|
||||
rate_limiting_disable_api_key: bool,
|
||||
rate_limiting_api_key_pool: u32,
|
||||
rate_limiting_api_key_cooldown_ns: u64,
|
||||
}
|
||||
|
||||
impl From<Opt> for Infos {
|
||||
@ -278,6 +290,7 @@ impl From<Opt> for Infos {
|
||||
scheduler_options,
|
||||
config_file_path,
|
||||
generate_master_key: _,
|
||||
rate_limiter_options,
|
||||
#[cfg(all(not(debug_assertions), feature = "analytics"))]
|
||||
no_analytics: _,
|
||||
} = options;
|
||||
@ -289,6 +302,18 @@ impl From<Opt> for Infos {
|
||||
max_indexing_memory,
|
||||
max_indexing_threads,
|
||||
} = indexer_options;
|
||||
let RateLimiterConfig {
|
||||
rate_limiting_disable_all,
|
||||
rate_limiting_disable_global,
|
||||
rate_limiting_global_pool,
|
||||
rate_limiting_global_cooldown_ns,
|
||||
rate_limiting_disable_ip,
|
||||
rate_limiting_ip_pool,
|
||||
rate_limiting_ip_cooldown_ns,
|
||||
rate_limiting_disable_api_key,
|
||||
rate_limiting_api_key_pool,
|
||||
rate_limiting_api_key_cooldown_ns,
|
||||
} = rate_limiter_options;
|
||||
|
||||
// We're going to override every sensible information.
|
||||
// We consider information sensible if it contains a path, an address, or a key.
|
||||
@ -321,6 +346,16 @@ impl From<Opt> for Infos {
|
||||
ssl_require_auth,
|
||||
ssl_resumption,
|
||||
ssl_tickets,
|
||||
rate_limiting_disable_all,
|
||||
rate_limiting_disable_global,
|
||||
rate_limiting_global_pool,
|
||||
rate_limiting_global_cooldown_ns,
|
||||
rate_limiting_disable_ip,
|
||||
rate_limiting_ip_pool,
|
||||
rate_limiting_ip_cooldown_ns,
|
||||
rate_limiting_disable_api_key,
|
||||
rate_limiting_api_key_pool,
|
||||
rate_limiting_api_key_cooldown_ns,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,13 @@ use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_governor::{
|
||||
GlobalKeyExtractor, Governor, GovernorConfigBuilder, KeyExtractor, PeerIpKeyExtractor,
|
||||
};
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_web::dev::{ServiceFactory, ServiceResponse};
|
||||
use actix_web::error::JsonPayloadError;
|
||||
use actix_web::middleware::Condition;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{middleware, web, HttpRequest};
|
||||
use analytics::Analytics;
|
||||
@ -42,6 +46,7 @@ use meilisearch_types::tasks::KindWithContent;
|
||||
use meilisearch_types::versioning::{check_version_file, create_version_file};
|
||||
use meilisearch_types::{compression, milli, VERSION_FILE_NAME};
|
||||
pub use option::Opt;
|
||||
use option::RateLimiterConfig;
|
||||
|
||||
use crate::error::MeilisearchHttpError;
|
||||
|
||||
@ -78,6 +83,7 @@ pub fn create_app(
|
||||
InitError = (),
|
||||
>,
|
||||
> {
|
||||
let rate_limiters = configure_rate_limiters(&opt.rate_limiter_options);
|
||||
let app = actix_web::App::new()
|
||||
.configure(|s| {
|
||||
configure_data(
|
||||
@ -88,7 +94,7 @@ pub fn create_app(
|
||||
analytics.clone(),
|
||||
)
|
||||
})
|
||||
.configure(routes::configure)
|
||||
.configure(|cfg| routes::configure(cfg, rate_limiters))
|
||||
.configure(|s| dashboard(s, enable_dashboard));
|
||||
#[cfg(feature = "metrics")]
|
||||
let app = app.configure(|s| configure_metrics_route(s, opt.enable_metrics_route));
|
||||
@ -386,6 +392,123 @@ pub fn configure_data(
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper struct to implement rate-limiting depending on the API key.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ApiKeyExtractor;
|
||||
|
||||
impl KeyExtractor for ApiKeyExtractor {
|
||||
/// `Some(api_key)` for requests containing an API key, `None` otherwise
|
||||
type Key = Option<String>;
|
||||
|
||||
/// Error indicating that the request header could not be converted to a `String` representation.
|
||||
type KeyExtractionError = actix_http::header::ToStrError;
|
||||
|
||||
/// Extracts an API key from a request header, if one is present.
|
||||
///
|
||||
/// Returns Ok(None) if there is no authorization header.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - `Self::KeyExtractionError`: if an authorization header is present, but not representable as a `String` (e.g. non-UTF8)
|
||||
fn extract(
|
||||
&self,
|
||||
req: &actix_web::dev::ServiceRequest,
|
||||
) -> Result<Self::Key, Self::KeyExtractionError> {
|
||||
let key = req.headers().get("Authorization").map(|token| token.to_str()).transpose()?;
|
||||
Ok(key.and_then(|token| token.strip_prefix("Bearer ")).map(|key| key.trim().to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates a conditionally enabled rate-limiter.
|
||||
///
|
||||
/// This struct can be turned into an Actix middleware using [`Self::into_middleware`],
|
||||
/// allowing to add it to some routes.
|
||||
pub struct RateLimiter<K: KeyExtractor> {
|
||||
enabled: bool,
|
||||
governor: Governor<K>,
|
||||
}
|
||||
|
||||
/// The available rate limiters.
|
||||
pub struct RateLimiters {
|
||||
/// Limits globally regardless of the origin of the query.
|
||||
pub global: RateLimiter<GlobalKeyExtractor>,
|
||||
/// Limits depending on the IP address of origin.
|
||||
pub ip: RateLimiter<PeerIpKeyExtractor>,
|
||||
/// Limits depending on the API Key in the Authorization header.
|
||||
pub api_key: RateLimiter<ApiKeyExtractor>,
|
||||
}
|
||||
|
||||
impl<K: KeyExtractor> RateLimiter<K> {
|
||||
fn disabled(key_extractor: K) -> Self {
|
||||
let governor = Governor::new(
|
||||
&GovernorConfigBuilder::default()
|
||||
.methods(vec![])
|
||||
.key_extractor(key_extractor)
|
||||
.finish()
|
||||
.unwrap(),
|
||||
);
|
||||
Self { enabled: false, governor }
|
||||
}
|
||||
|
||||
fn enabled(key_extractor: K, pool_size: u32, cooldown_ns: u64) -> Self {
|
||||
let governor = Governor::new(
|
||||
&GovernorConfigBuilder::default()
|
||||
.key_extractor(key_extractor)
|
||||
.burst_size(pool_size)
|
||||
.per_nanosecond(cooldown_ns)
|
||||
.use_headers()
|
||||
.finish()
|
||||
.unwrap(),
|
||||
);
|
||||
Self { enabled: true, governor }
|
||||
}
|
||||
|
||||
/// Turns this into a middleware that is enabled only if the rate limiter was enabled.
|
||||
pub fn into_middleware(self) -> Condition<Governor<K>> {
|
||||
Condition::new(self.enabled, self.governor)
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_rate_limiters(rate_limiter_options: &RateLimiterConfig) -> RateLimiters {
|
||||
if rate_limiter_options.rate_limiting_disable_all {
|
||||
return RateLimiters {
|
||||
global: RateLimiter::disabled(GlobalKeyExtractor),
|
||||
ip: RateLimiter::disabled(PeerIpKeyExtractor),
|
||||
api_key: RateLimiter::disabled(ApiKeyExtractor),
|
||||
};
|
||||
}
|
||||
let global = if rate_limiter_options.rate_limiting_disable_global {
|
||||
RateLimiter::disabled(GlobalKeyExtractor)
|
||||
} else {
|
||||
RateLimiter::enabled(
|
||||
GlobalKeyExtractor,
|
||||
rate_limiter_options.rate_limiting_global_pool,
|
||||
rate_limiter_options.rate_limiting_global_cooldown_ns,
|
||||
)
|
||||
};
|
||||
|
||||
let ip = if rate_limiter_options.rate_limiting_disable_ip {
|
||||
RateLimiter::disabled(PeerIpKeyExtractor)
|
||||
} else {
|
||||
RateLimiter::enabled(
|
||||
PeerIpKeyExtractor,
|
||||
rate_limiter_options.rate_limiting_ip_pool,
|
||||
rate_limiter_options.rate_limiting_ip_cooldown_ns,
|
||||
)
|
||||
};
|
||||
|
||||
let api_key = if rate_limiter_options.rate_limiting_disable_api_key {
|
||||
RateLimiter::disabled(ApiKeyExtractor)
|
||||
} else {
|
||||
RateLimiter::enabled(
|
||||
ApiKeyExtractor,
|
||||
rate_limiter_options.rate_limiting_api_key_pool,
|
||||
rate_limiter_options.rate_limiting_api_key_cooldown_ns,
|
||||
)
|
||||
};
|
||||
RateLimiters { global, ip, api_key }
|
||||
}
|
||||
|
||||
#[cfg(feature = "mini-dashboard")]
|
||||
pub fn dashboard(config: &mut web::ServiceConfig, enable_frontend: bool) {
|
||||
use actix_web::HttpResponse;
|
||||
|
@ -50,6 +50,22 @@ const MEILI_IGNORE_DUMP_IF_DB_EXISTS: &str = "MEILI_IGNORE_DUMP_IF_DB_EXISTS";
|
||||
const MEILI_DUMP_DIR: &str = "MEILI_DUMP_DIR";
|
||||
const MEILI_LOG_LEVEL: &str = "MEILI_LOG_LEVEL";
|
||||
const MEILI_GENERATE_MASTER_KEY: &str = "MEILI_GENERATE_MASTER_KEY";
|
||||
|
||||
// rate limiting
|
||||
|
||||
const MEILI_RATE_LIMITING_DISABLE_ALL: &str = "MEILI_RATE_LIMITING_DISABLE_ALL";
|
||||
const MEILI_RATE_LIMITING_DISABLE_GLOBAL: &str = "MEILI_RATE_LIMITING_DISABLE_GLOBAL";
|
||||
const MEILI_RATE_LIMITING_DISABLE_IP: &str = "MEILI_RATE_LIMITING_DISABLE_IP";
|
||||
const MEILI_RATE_LIMITING_DISABLE_API_KEY: &str = "MEILI_RATE_LIMITING_DISABLE_API_KEY";
|
||||
|
||||
const MEILI_RATE_LIMITING_GLOBAL_POOL: &str = "MEILI_RATE_LIMITING_GLOBAL_POOL";
|
||||
const MEILI_RATE_LIMITING_IP_POOL: &str = "MEILI_RATE_LIMITING_IP_POOL";
|
||||
const MEILI_RATE_LIMITING_API_KEY_POOL: &str = "MEILI_RATE_LIMITING_API_KEY_POOL";
|
||||
|
||||
const MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS: &str = "MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS";
|
||||
const MEILI_RATE_LIMITING_IP_COOLDOWN_NS: &str = "MEILI_RATE_LIMITING_IP_COOLDOWN_NS";
|
||||
const MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS: &str = "MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS";
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
const MEILI_ENABLE_METRICS_ROUTE: &str = "MEILI_ENABLE_METRICS_ROUTE";
|
||||
|
||||
@ -70,6 +86,15 @@ const MEILI_MAX_INDEXING_THREADS: &str = "MEILI_MAX_INDEXING_THREADS";
|
||||
const DISABLE_AUTO_BATCHING: &str = "DISABLE_AUTO_BATCHING";
|
||||
const DEFAULT_LOG_EVERY_N: usize = 100000;
|
||||
|
||||
const DEFAULT_GLOBAL_RATE_LIMITING_POOL: u32 = 100_000;
|
||||
const DEFAULT_GLOBAL_RATE_LIMITING_COOLDOWN_NS: u64 = 50_000; // pool replenishes in 5s
|
||||
|
||||
const DEFAULT_IP_RATE_LIMITING_POOL: u32 = 200;
|
||||
const DEFAULT_IP_RATE_LIMITING_COOLDOWN_NS: u64 = 50_000_000; // pool replenishes in 10s
|
||||
|
||||
const DEFAULT_API_KEY_RATE_LIMITING_POOL: u32 = 10_000;
|
||||
const DEFAULT_API_KEY_RATE_LIMITING_COOLDOWN_NS: u64 = 500_000; // pool replenishes in 10s
|
||||
|
||||
#[derive(Debug, Clone, Parser, Deserialize)]
|
||||
#[clap(version, next_display_order = None)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
@ -252,6 +277,10 @@ pub struct Opt {
|
||||
#[clap(flatten)]
|
||||
pub scheduler_options: SchedulerConfig,
|
||||
|
||||
#[serde(flatten)]
|
||||
#[clap(flatten)]
|
||||
pub rate_limiter_options: RateLimiterConfig,
|
||||
|
||||
/// Set the path to a configuration file that should be used to setup the engine.
|
||||
/// Format must be TOML.
|
||||
#[clap(long)]
|
||||
@ -340,6 +369,7 @@ impl Opt {
|
||||
ignore_missing_dump: _,
|
||||
ignore_dump_if_db_exists: _,
|
||||
config_file_path: _,
|
||||
rate_limiter_options,
|
||||
#[cfg(all(not(debug_assertions), feature = "analytics"))]
|
||||
no_analytics,
|
||||
#[cfg(feature = "metrics")]
|
||||
@ -393,6 +423,7 @@ impl Opt {
|
||||
}
|
||||
indexer_options.export_to_env();
|
||||
scheduler_options.export_to_env();
|
||||
rate_limiter_options.export_to_env();
|
||||
}
|
||||
|
||||
pub fn get_ssl_config(&self) -> anyhow::Result<Option<rustls::ServerConfig>> {
|
||||
@ -537,6 +568,142 @@ impl Default for IndexerOpts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Options related to the configuration of the rate limiters.
|
||||
#[derive(Debug, Clone, Parser, Default, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub struct RateLimiterConfig {
|
||||
/// When provided, completely disables all rate limiting.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_DISABLE_ALL)]
|
||||
#[serde(default)]
|
||||
pub rate_limiting_disable_all: bool,
|
||||
|
||||
/// When provided, disables the global rate limiting that applies to all search requests.
|
||||
///
|
||||
/// Disabling the global rate limiting does not disable IP-based and API-key-based rate limitings.
|
||||
/// To disable all rate limiting regardless of the origin use `--rate-limiting-disable-all`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_DISABLE_GLOBAL)]
|
||||
#[serde(default)]
|
||||
pub rate_limiting_disable_global: bool,
|
||||
/// The maximum pool of search requests that can be performed before they are rejected.
|
||||
///
|
||||
/// The pool starts full at the provided value, then each search request diminishes the pool by 1.
|
||||
/// When the pool is empty the search request is rejected.
|
||||
/// The pool is replenished by 1 depending on the cooldown period.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_GLOBAL_POOL, default_value_t = default_rate_limiting_global_pool())]
|
||||
#[serde(default = "default_rate_limiting_global_pool")]
|
||||
pub rate_limiting_global_pool: u32,
|
||||
/// The amount of time, in nanoseconds, before the pool of available search requests is replenished by 1 again.
|
||||
///
|
||||
/// The maximum number of available search requests is given by `--rate-limiting-global-pool`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS, default_value_t = default_rate_limiting_global_cooldown_ns())]
|
||||
#[serde(default = "default_rate_limiting_global_cooldown_ns")]
|
||||
pub rate_limiting_global_cooldown_ns: u64,
|
||||
|
||||
/// When provided, disables the rate limiting that applies to all search requests originating with a specific IP address.
|
||||
///
|
||||
/// Disabling the IP rate limiting does not disable the rate limiting that applies to all requests ("global") nor the API-key-based rate limiting.
|
||||
/// To disable all rate limiting regardless of the origin use `--rate-limiting-disable-all`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_DISABLE_IP)]
|
||||
#[serde(default)]
|
||||
pub rate_limiting_disable_ip: bool,
|
||||
/// The maximum pool of search requests that can be performed from a specific IP before they are rejected.
|
||||
///
|
||||
/// The pool starts full at the provided value, then each search request from the same IP address diminishes the pool by 1.
|
||||
/// When the pool is empty the search request is rejected.
|
||||
/// The pool is replenished by 1 depending on the cooldown period.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_IP_POOL, default_value_t = default_rate_limiting_ip_pool())]
|
||||
#[serde(default = "default_rate_limiting_ip_pool")]
|
||||
pub rate_limiting_ip_pool: u32,
|
||||
/// The amount of time, in nanoseconds, before the pool of available search requests for a specific IP address is replenished by 1 again.
|
||||
///
|
||||
/// The maximum number of available search requests for a specific IP address is given by `--rate-limiting-ip-pool`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_IP_COOLDOWN_NS, default_value_t = default_rate_limiting_ip_cooldown_ns())]
|
||||
#[serde(default = "default_rate_limiting_ip_cooldown_ns")]
|
||||
pub rate_limiting_ip_cooldown_ns: u64,
|
||||
|
||||
/// When provided, disables the rate limiting that applies to all search requests originating with a specific API key.
|
||||
///
|
||||
/// Disabling the API key limiting does not disable the rate limiting that applies to all requests ("global") nor the IP-based rate limiting.
|
||||
/// To disable all rate limiting regardless of the origin use `--rate-limiting-disable-all`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_DISABLE_API_KEY)]
|
||||
#[serde(default)]
|
||||
pub rate_limiting_disable_api_key: bool,
|
||||
/// The maximum pool of search requests that can be performed using a specific API key before they are rejected.
|
||||
///
|
||||
/// The pool starts full at the provided value, then each search request using the same API key diminishes the pool by 1.
|
||||
/// When the pool is empty the search request is rejected.
|
||||
/// The pool is replenished by 1 depending on the cooldown period.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_API_KEY_POOL, default_value_t = default_rate_limiting_api_key_pool())]
|
||||
#[serde(default = "default_rate_limiting_api_key_pool")]
|
||||
pub rate_limiting_api_key_pool: u32,
|
||||
/// The amount of time, in nanoseconds, before the pool of available search requests using a specific API key is replenished by 1 again.
|
||||
///
|
||||
/// The maximum number of available search requests using a specific API key is given by `--rate-limiting-api-key-pool`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS, default_value_t = default_rate_limiting_api_key_cooldown_ns())]
|
||||
#[serde(default = "default_rate_limiting_api_key_cooldown_ns")]
|
||||
pub rate_limiting_api_key_cooldown_ns: u64,
|
||||
}
|
||||
|
||||
impl RateLimiterConfig {
|
||||
/// Exports the values to their corresponding env vars if they are not set.
|
||||
pub fn export_to_env(self) {
|
||||
let RateLimiterConfig {
|
||||
rate_limiting_disable_all: disable_rate_limiting,
|
||||
rate_limiting_disable_global: disable_global_rate_limiting,
|
||||
rate_limiting_global_pool: global_rate_limiting_pool,
|
||||
rate_limiting_global_cooldown_ns: global_rate_limiting_cooldown_ns,
|
||||
rate_limiting_disable_ip: disable_ip_rate_limiting,
|
||||
rate_limiting_ip_pool: ip_rate_limiting_pool,
|
||||
rate_limiting_ip_cooldown_ns: ip_rate_limiting_cooldown_ns,
|
||||
rate_limiting_disable_api_key: disable_api_key_rate_limiting,
|
||||
rate_limiting_api_key_pool: api_key_rate_limiting_pool,
|
||||
rate_limiting_api_key_cooldown_ns: api_key_rate_limiting_cooldown_ns,
|
||||
} = self;
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_DISABLE_ALL,
|
||||
disable_rate_limiting.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_DISABLE_GLOBAL,
|
||||
disable_global_rate_limiting.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_DISABLE_IP,
|
||||
disable_ip_rate_limiting.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_DISABLE_API_KEY,
|
||||
disable_api_key_rate_limiting.to_string(),
|
||||
);
|
||||
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_GLOBAL_POOL,
|
||||
global_rate_limiting_pool.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_IP_POOL,
|
||||
ip_rate_limiting_pool.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_API_KEY_POOL,
|
||||
api_key_rate_limiting_pool.to_string(),
|
||||
);
|
||||
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS,
|
||||
global_rate_limiting_cooldown_ns.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_IP_COOLDOWN_NS,
|
||||
ip_rate_limiting_cooldown_ns.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS,
|
||||
api_key_rate_limiting_cooldown_ns.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A type used to detect the max memory available and use 2/3 of it.
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
pub struct MaxMemory(Option<Byte>);
|
||||
@ -729,6 +896,30 @@ fn default_log_every_n() -> usize {
|
||||
DEFAULT_LOG_EVERY_N
|
||||
}
|
||||
|
||||
fn default_rate_limiting_global_pool() -> u32 {
|
||||
DEFAULT_GLOBAL_RATE_LIMITING_POOL
|
||||
}
|
||||
|
||||
fn default_rate_limiting_ip_pool() -> u32 {
|
||||
DEFAULT_IP_RATE_LIMITING_POOL
|
||||
}
|
||||
|
||||
fn default_rate_limiting_api_key_pool() -> u32 {
|
||||
DEFAULT_API_KEY_RATE_LIMITING_POOL
|
||||
}
|
||||
|
||||
fn default_rate_limiting_global_cooldown_ns() -> u64 {
|
||||
DEFAULT_GLOBAL_RATE_LIMITING_COOLDOWN_NS
|
||||
}
|
||||
|
||||
fn default_rate_limiting_ip_cooldown_ns() -> u64 {
|
||||
DEFAULT_IP_RATE_LIMITING_COOLDOWN_NS
|
||||
}
|
||||
|
||||
fn default_rate_limiting_api_key_cooldown_ns() -> u64 {
|
||||
DEFAULT_API_KEY_RATE_LIMITING_COOLDOWN_NS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
|
@ -15,12 +15,13 @@ use crate::analytics::Analytics;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::{AuthenticationError, GuardedData};
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::RateLimiters;
|
||||
|
||||
pub mod documents;
|
||||
pub mod search;
|
||||
pub mod settings;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
pub fn configure(cfg: &mut web::ServiceConfig, rate_limiters: RateLimiters) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
.route(web::get().to(list_indexes))
|
||||
@ -36,7 +37,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
)
|
||||
.service(web::resource("/stats").route(web::get().to(SeqHandler(get_index_stats))))
|
||||
.service(web::scope("/documents").configure(documents::configure))
|
||||
.service(web::scope("/search").configure(search::configure))
|
||||
.service(web::scope("/search").configure(|cfg| search::configure(cfg, rate_limiters)))
|
||||
.service(web::scope("/settings").configure(settings::configure)),
|
||||
);
|
||||
}
|
||||
|
@ -17,10 +17,14 @@ use crate::search::{
|
||||
DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT,
|
||||
DEFAULT_SEARCH_OFFSET,
|
||||
};
|
||||
use crate::RateLimiters;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
pub fn configure(cfg: &mut web::ServiceConfig, rate_limiters: RateLimiters) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
.wrap(rate_limiters.global.into_middleware())
|
||||
.wrap(rate_limiters.ip.into_middleware())
|
||||
.wrap(rate_limiters.api_key.into_middleware())
|
||||
.route(web::get().to(SeqHandler(search_with_url_query)))
|
||||
.route(web::post().to(SeqHandler(search_with_post))),
|
||||
);
|
||||
|
@ -16,6 +16,7 @@ use self::indexes::IndexStats;
|
||||
use crate::analytics::Analytics;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::RateLimiters;
|
||||
|
||||
mod api_key;
|
||||
mod dump;
|
||||
@ -23,14 +24,14 @@ pub mod indexes;
|
||||
mod swap_indexes;
|
||||
pub mod tasks;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
pub fn configure(cfg: &mut web::ServiceConfig, rate_limiters: RateLimiters) {
|
||||
cfg.service(web::scope("/tasks").configure(tasks::configure))
|
||||
.service(web::resource("/health").route(web::get().to(get_health)))
|
||||
.service(web::scope("/keys").configure(api_key::configure))
|
||||
.service(web::scope("/dumps").configure(dump::configure))
|
||||
.service(web::resource("/stats").route(web::get().to(get_stats)))
|
||||
.service(web::resource("/version").route(web::get().to(get_version)))
|
||||
.service(web::scope("/indexes").configure(indexes::configure))
|
||||
.service(web::scope("/indexes").configure(|cfg| indexes::configure(cfg, rate_limiters)))
|
||||
.service(web::scope("/swap-indexes").configure(swap_indexes::configure));
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ use actix_web::dev::ServiceResponse;
|
||||
use actix_web::http::StatusCode;
|
||||
use byte_unit::{Byte, ByteUnit};
|
||||
use clap::Parser;
|
||||
use meilisearch::option::{IndexerOpts, MaxMemory, Opt};
|
||||
use meilisearch::option::{IndexerOpts, MaxMemory, Opt, RateLimiterConfig};
|
||||
use meilisearch::{analytics, create_app, setup_meilisearch};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde_json::{json, Value};
|
||||
@ -192,6 +192,10 @@ pub fn default_settings(dir: impl AsRef<Path>) -> Opt {
|
||||
max_task_db_size: Byte::from_unit(1.0, ByteUnit::GiB).unwrap(),
|
||||
http_payload_size_limit: Byte::from_unit(10.0, ByteUnit::MiB).unwrap(),
|
||||
snapshot_dir: ".".into(),
|
||||
rate_limiter_options: RateLimiterConfig {
|
||||
rate_limiting_disable_all: true,
|
||||
..Parser::parse_from(None as Option<&str>)
|
||||
},
|
||||
indexer_options: IndexerOpts {
|
||||
// memory has to be unlimited because several meilisearch are running in test context.
|
||||
max_indexing_memory: MaxMemory::unlimited(),
|
||||
|
Reference in New Issue
Block a user