Compare commits

...

3 Commits

Author SHA1 Message Date
6678491212 Use actix-governor to perform rate-limiting 2023-01-03 16:25:30 +01:00
a82f8aacde Add actix-governor 2023-01-03 16:25:30 +01:00
5cf71c6014 Add rate-limiting options 2023-01-03 16:25:30 +01:00
10 changed files with 535 additions and 21 deletions

105
Cargo.lock generated
View File

@ -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"

View File

@ -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 ###
#################

View File

@ -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"]

View File

@ -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,
}
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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)),
);
}

View File

@ -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))),
);

View File

@ -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));
}

View File

@ -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(),