🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2022-09-17 13:21:48.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-09-17 13:21:48.0 +01:00:00
commit
034015d3cf93969dad02e4a4c231645867c09042 [patch]
tree
adf001c3fff5c64d73ec1583c3b88404f2abf26c
parent
aae650877f16b744e509057eb75d2ec3324f2e94
download
034015d3cf93969dad02e4a4c231645867c09042.tar.gz

Add shared rate limiting to all endpoints



Diff

 Cargo.lock                                               |  94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-web/Cargo.toml                                 |   2 ++
 chartered-web/src/main.rs                                |  11 ++++++++---
 chartered-web/src/middleware/mod.rs                      |   1 +
 chartered-web/src/middleware/rate_limit.rs               | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-web/src/endpoints/cargo_api/mod.rs             |  29 ++++++++++++++++++++++++-----
 chartered-web/src/endpoints/web_api/mod.rs               |  26 ++++++++++++++++----------
 chartered-web/src/endpoints/web_api/auth/mod.rs          |  43 ++++++++++++++++++++++++++++++++++++-------
 chartered-web/src/endpoints/web_api/crates/mod.rs        |  39 ++++++++++++++++++++++++++++++---------
 chartered-web/src/endpoints/web_api/organisations/mod.rs |  18 ++++++++++++++----
 chartered-web/src/endpoints/web_api/sessions/mod.rs      |  11 ++++++++---
 chartered-web/src/endpoints/web_api/users/mod.rs         |  20 +++++++++++++++-----
 12 files changed, 367 insertions(+), 52 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index c444881..b9a9124 100644
--- a/Cargo.lock
+++ a/Cargo.lock
@@ -738,10 +738,12 @@
 "chrono",
 "clap",
 "futures",
 "governor",
 "headers",
 "hex",
 "nom",
 "nom-bytes",
 "nonzero_ext",
 "oauth2",
 "once_cell",
 "openid",
@@ -901,6 +903,16 @@
]

[[package]]
name = "crossbeam-utils"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
dependencies = [
 "cfg-if",
 "once_cell",
]

[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -947,6 +959,19 @@
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
dependencies = [
 "cipher 0.3.0",
]

[[package]]
name = "dashmap"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
dependencies = [
 "cfg-if",
 "hashbrown",
 "lock_api",
 "once_cell",
 "parking_lot_core",
]

[[package]]
@@ -1215,6 +1240,12 @@
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"

[[package]]
name = "futures-timer"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"

[[package]]
name = "futures-util"
@@ -1255,6 +1286,23 @@
 "libc",
 "wasi 0.11.0+wasi-snapshot-preview1",
 "wasm-bindgen",
]

[[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]]
@@ -1611,6 +1659,15 @@
]

[[package]]
name = "mach"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
dependencies = [
 "libc",
]

[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1716,6 +1773,12 @@
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1733,6 +1796,12 @@
 "bytes",
 "nom",
]

[[package]]
name = "nonzero_ext"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"

[[package]]
name = "num"
@@ -2102,6 +2171,22 @@
]

[[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.0+wasi-snapshot-preview1",
 "web-sys",
 "winapi",
]

[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2149,6 +2234,15 @@
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
 "getrandom",
]

[[package]]
name = "raw-cpuid"
version = "10.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb"
dependencies = [
 "bitflags",
]

[[package]]
diff --git a/chartered-web/Cargo.toml b/chartered-web/Cargo.toml
index d8e5973..c8e23d1 100644
--- a/chartered-web/Cargo.toml
+++ a/chartered-web/Cargo.toml
@@ -19,8 +19,10 @@
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "3", features = ["cargo", "derive", "std", "suggestions", "color"] }
futures = "0.3"
governor = "0.4"
headers = "0.3"
hex = "0.4"
nonzero_ext = "0.3.0"
nom = "7"
nom-bytes = { git = "https://github.com/w4/nom-bytes" }
oauth2 = "4.2"
diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs
index 42dae1b..1083c5b 100644
--- a/chartered-web/src/main.rs
+++ a/chartered-web/src/main.rs
@@ -6,12 +6,15 @@
mod endpoints;
mod middleware;

use crate::middleware::rate_limit::RateLimit;
use axum::{
    http::{header, Method},
    routing::get,
    Extension, Router,
};
use clap::{crate_name, crate_version, Parser};
use governor::Quota;
use nonzero_ext::nonzero;
use std::{fmt::Formatter, path::PathBuf, sync::Arc};
use thiserror::Error;
use tower::ServiceBuilder;
@@ -68,11 +71,13 @@
        .user_agent(format!("{}/{}", crate_name!(), crate_version!()))
        .build()?;

    let rate_limit = RateLimit::new(Quota::per_hour(nonzero!(5000_u32)));

    let app = Router::new()
        .route("/", get(hello_world))
        .nest(
            "/web/v1",
            endpoints::web_api::authenticated_routes().layer(
            endpoints::web_api::authenticated_routes(&rate_limit).layer(
                ServiceBuilder::new()
                    .layer_fn(crate::middleware::web_auth::WebAuthMiddleware)
                    .into_inner(),
@@ -80,11 +85,11 @@
        )
        .nest(
            "/web/v1/public",
            endpoints::web_api::unauthenticated_routes(),
            endpoints::web_api::unauthenticated_routes(&rate_limit),
        )
        .nest(
            "/a/:key/o/:organisation/api/v1",
            endpoints::cargo_api::routes().layer(
            endpoints::cargo_api::routes(&rate_limit).layer(
                ServiceBuilder::new()
                    .layer_fn(crate::middleware::cargo_auth::CargoAuthMiddleware)
                    .into_inner(),
diff --git a/chartered-web/src/middleware/mod.rs b/chartered-web/src/middleware/mod.rs
index d452bf7..0016701 100644
--- a/chartered-web/src/middleware/mod.rs
+++ a/chartered-web/src/middleware/mod.rs
@@ -1,3 +1,4 @@
pub mod cargo_auth;
pub mod logging;
pub mod rate_limit;
pub mod web_auth;
diff --git a/chartered-web/src/middleware/rate_limit.rs b/chartered-web/src/middleware/rate_limit.rs
new file mode 100644
index 0000000..8ec2d6f 100644
--- /dev/null
+++ a/chartered-web/src/middleware/rate_limit.rs
@@ -1,0 +1,125 @@
use crate::endpoints::ErrorResponse;

use axum::{
    body::{boxed, Body, BoxBody},
    extract::{self, FromRequest, RequestParts},
    http::{Request, StatusCode},
    response::Response,
};
use futures::future::BoxFuture;
use governor::{clock::DefaultClock, state::keyed::DefaultKeyedStateStore, Quota, RateLimiter};
use tower::{Layer, Service};

use std::{
    net::IpAddr,
    num::NonZeroU32,
    sync::{
        atomic::{AtomicUsize, Ordering},
        Arc,
    },
    task::{Context, Poll},
};

pub struct RateLimit {
    governor: Arc<RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>>,
    counter: Arc<AtomicUsize>,
}

impl RateLimit {
    pub fn new(quota: Quota) -> Self {
        Self {
            governor: Arc::new(RateLimiter::keyed(quota)),
            counter: Arc::new(AtomicUsize::new(0)),
        }
    }

    pub fn with_cost(&self, cost: u32) -> RateLimitLayer {
        RateLimitLayer {
            governor: self.governor.clone(),
            counter: self.counter.clone(),
            cost: NonZeroU32::new(cost).unwrap(),
        }
    }
}

pub struct RateLimitLayer {
    governor: Arc<RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>>,
    counter: Arc<AtomicUsize>,
    cost: NonZeroU32,
}

impl<S> Layer<S> for RateLimitLayer {
    type Service = RateLimitMiddleware<S>;

    fn layer(&self, inner: S) -> Self::Service {
        RateLimitMiddleware {
            inner,
            governor: self.governor.clone(),
            counter: self.counter.clone(),
            cost: self.cost,
        }
    }
}

#[derive(Clone)]
pub struct RateLimitMiddleware<S> {
    inner: S,
    governor: Arc<RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>>,
    counter: Arc<AtomicUsize>,
    cost: NonZeroU32,
}

impl<S, ReqBody> Service<Request<ReqBody>> for RateLimitMiddleware<S>
where
    S: Service<Request<ReqBody>, Response = Response<BoxBody>> + Clone + Send + 'static,
    S::Future: Send + 'static,
    ReqBody: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
        // ensure we take the instance that has already been poll_ready'd
        let clone = self.clone();
        let mut this = std::mem::replace(self, clone);

        Box::pin(async move {
            let mut req = RequestParts::new(req);
            let socket_addr = extract::ConnectInfo::<std::net::SocketAddr>::from_request(&mut req)
                .await
                .map(|v| v.0);

            if let Ok(socket_addr) = socket_addr {
                // TODO: cloudflare?
                let addr = socket_addr.ip();

                if let Err(_e) = this.governor.check_key_n(&addr, this.cost) {
                    return Ok(Response::builder()
                        .status(StatusCode::TOO_MANY_REQUESTS)
                        .body(boxed(Body::from(
                            serde_json::to_vec(&ErrorResponse {
                                error: Some(
                                    "You are being rate limited. Please wait a bit and try again."
                                        .into(),
                                ),
                            })
                            .unwrap(),
                        )))
                        .unwrap());
                }

                // every 500 requests, clear out keys that haven't been used in a while
                if this.counter.fetch_add(1, Ordering::AcqRel) % 500 == 0 {
                    this.governor.retain_recent();
                }
            }

            this.inner.call(req.try_into_request().unwrap()).await
        })
    }
}
diff --git a/chartered-web/src/endpoints/cargo_api/mod.rs b/chartered-web/src/endpoints/cargo_api/mod.rs
index 5800477..258e3e8 100644
--- a/chartered-web/src/endpoints/cargo_api/mod.rs
+++ a/chartered-web/src/endpoints/cargo_api/mod.rs
@@ -11,20 +11,37 @@
mod publish;
mod yank;

use crate::RateLimit;
use axum::{
    handler::Handler,
    routing::{delete, get, put},
    Router,
};

// requests are already authenticated before this router
pub fn routes() -> Router {
pub fn routes(rate_limit: &RateLimit) -> Router {
    Router::new()
        .route("/crates/new", put(publish::handle))
        .route(
            "/crates/new",
            put(publish::handle.layer(rate_limit.with_cost(200))),
        )
        // .route("/crates/search", get(hello_world))
        .route("/crates/:crate/owners", get(owners::handle_get))
        .route(
            "/crates/:crate/owners",
            get(owners::handle_get.layer(rate_limit.with_cost(1))),
        )
        // .route("/crates/:crate/owners", put(hello_world))
        // .route("/crates/:crate/owners", delete(hello_world))
        .route("/crates/:crate/:version/yank", delete(yank::handle_yank))
        .route("/crates/:crate/:version/unyank", put(yank::handle_unyank))
        .route("/crates/:crate/:version/download", get(download::handle))
        .route(
            "/crates/:crate/:version/yank",
            delete(yank::handle_yank.layer(rate_limit.with_cost(50))),
        )
        .route(
            "/crates/:crate/:version/unyank",
            put(yank::handle_unyank.layer(rate_limit.with_cost(50))),
        )
        .route(
            "/crates/:crate/:version/download",
            get(download::handle.layer(rate_limit.with_cost(1))),
        )
}
diff --git a/chartered-web/src/endpoints/web_api/mod.rs b/chartered-web/src/endpoints/web_api/mod.rs
index 08450e8..292612d 100644
--- a/chartered-web/src/endpoints/web_api/mod.rs
+++ a/chartered-web/src/endpoints/web_api/mod.rs
@@ -5,25 +5,31 @@
mod ssh_key;
mod users;

use crate::RateLimit;
use axum::{
    handler::Handler,
    routing::{delete, get},
    Router,
};

pub fn authenticated_routes() -> Router {
pub fn authenticated_routes(rate_limit: &RateLimit) -> Router {
    Router::new()
        .nest("/organisations", organisations::routes())
        .nest("/crates", crates::routes())
        .nest("/users", users::routes())
        .nest("/auth", auth::authenticated_routes())
        .nest("/sessions", sessions::routes())
        .nest("/organisations", organisations::routes(rate_limit))
        .nest("/crates", crates::routes(rate_limit))
        .nest("/users", users::routes(rate_limit))
        .nest("/auth", auth::authenticated_routes(rate_limit))
        .nest("/sessions", sessions::routes(rate_limit))
        .route(
            "/ssh-key",
            get(ssh_key::handle_get).put(ssh_key::handle_put),
            get(ssh_key::handle_get.layer(rate_limit.with_cost(1)))
                .put(ssh_key::handle_put.layer(rate_limit.with_cost(24))),
        )
        .route("/ssh-key/:id", delete(ssh_key::handle_delete))
        .route(
            "/ssh-key/:id",
            delete(ssh_key::handle_delete.layer(rate_limit.with_cost(24))),
        )
}

pub fn unauthenticated_routes() -> Router {
    Router::new().nest("/auth", auth::unauthenticated_routes())
pub fn unauthenticated_routes(rate_limit: &RateLimit) -> Router {
    Router::new().nest("/auth", auth::unauthenticated_routes(rate_limit))
}
diff --git a/chartered-web/src/endpoints/web_api/auth/mod.rs b/chartered-web/src/endpoints/web_api/auth/mod.rs
index 4f0f6cc..de7ff37 100644
--- a/chartered-web/src/endpoints/web_api/auth/mod.rs
+++ a/chartered-web/src/endpoints/web_api/auth/mod.rs
@@ -1,3 +1,4 @@
use axum::handler::Handler;
use axum::{
    extract,
    routing::{get, post},
@@ -8,27 +9,49 @@
    uuid::Uuid,
    ConnectionPool,
};

use serde::Serialize;

use crate::middleware::rate_limit::RateLimit;

pub mod extend;
pub mod logout;
pub mod openid;
pub mod password;

pub fn authenticated_routes() -> Router {
pub fn authenticated_routes(rate_limit: &RateLimit) -> Router {
    Router::new()
        .route("/logout", get(logout::handle))
        .route("/extend", get(extend::handle))
        .route(
            "/logout",
            get(logout::handle.layer(rate_limit.with_cost(5))),
        )
        .route(
            "/extend",
            get(extend::handle.layer(rate_limit.with_cost(1))),
        )
}

pub fn unauthenticated_routes() -> Router {
pub fn unauthenticated_routes(rate_limit: &RateLimit) -> Router {
    Router::new()
        .route("/register/password", post(password::handle_register))
        .route("/login/password", post(password::handle_login))
        .route("/login/oauth/:provider/begin", get(openid::begin_oidc))
        .route("/login/oauth/complete", get(openid::complete_oidc))
        .route("/login/oauth/providers", get(openid::list_providers))
        .route(
            "/register/password",
            post(password::handle_register.layer(rate_limit.with_cost(200))),
        )
        .route(
            "/login/password",
            post(password::handle_login.layer(rate_limit.with_cost(100))),
        )
        .route(
            "/login/oauth/:provider/begin",
            get(openid::begin_oidc.layer(rate_limit.with_cost(1))),
        )
        .route(
            "/login/oauth/complete",
            get(openid::complete_oidc.layer(rate_limit.with_cost(75))),
        )
        .route(
            "/login/oauth/providers",
            get(openid::list_providers.layer(rate_limit.with_cost(1))),
        )
}

#[derive(Serialize)]
diff --git a/chartered-web/src/endpoints/web_api/crates/mod.rs b/chartered-web/src/endpoints/web_api/crates/mod.rs
index 91163cc..dcac16b 100644
--- a/chartered-web/src/endpoints/web_api/crates/mod.rs
+++ a/chartered-web/src/endpoints/web_api/crates/mod.rs
@@ -5,20 +5,37 @@
mod recently_updated;
mod search;

use crate::middleware::rate_limit::RateLimit;
use axum::handler::Handler;
use axum::{routing::get, Router};

pub fn routes() -> Router {
pub fn routes(rate_limit: &RateLimit) -> Router {
    Router::new()
        .route("/:org/:crate", get(info::handle))
        .route(
            "/:org/:crate",
            get(info::handle.layer(rate_limit.with_cost(1))),
        )
        .route(
            "/:org/:crate/members",
            get(members::handle_get)
                .patch(members::handle_patch)
                .put(members::handle_put)
                .delete(members::handle_delete),
        )
        .route("/recently-updated", get(recently_updated::handle))
        .route("/recently-created", get(recently_created::handle))
        .route("/most-downloaded", get(most_downloaded::handle))
        .route("/search", get(search::handle))
            get(members::handle_get.layer(rate_limit.with_cost(1)))
                .patch(members::handle_patch.layer(rate_limit.with_cost(10)))
                .put(members::handle_put.layer(rate_limit.with_cost(10)))
                .delete(members::handle_delete.layer(rate_limit.with_cost(10))),
        )
        .route(
            "/recently-updated",
            get(recently_updated::handle.layer(rate_limit.with_cost(1))),
        )
        .route(
            "/recently-created",
            get(recently_created::handle.layer(rate_limit.with_cost(1))),
        )
        .route(
            "/most-downloaded",
            get(most_downloaded::handle.layer(rate_limit.with_cost(1))),
        )
        .route(
            "/search",
            get(search::handle.layer(rate_limit.with_cost(5))),
        )
}
diff --git a/chartered-web/src/endpoints/web_api/organisations/mod.rs b/chartered-web/src/endpoints/web_api/organisations/mod.rs
index a79f5de..f723916 100644
--- a/chartered-web/src/endpoints/web_api/organisations/mod.rs
+++ a/chartered-web/src/endpoints/web_api/organisations/mod.rs
@@ -1,21 +1,31 @@
mod crud;
mod info;
mod list;
mod members;

use crate::middleware::rate_limit::RateLimit;
use axum::{
    handler::Handler,
    routing::{get, patch},
    Router,
};

pub fn routes() -> Router {
pub fn routes(rate_limit: &RateLimit) -> Router {
    Router::new()
        .route("/", get(list::handle_get).put(crud::handle_put))
        .route("/:org", get(info::handle_get))
        .route(
            "/",
            get(list::handle_get.layer(rate_limit.with_cost(1)))
                .put(crud::handle_put.layer(rate_limit.with_cost(100))),
        )
        .route(
            "/:org",
            get(info::handle_get.layer(rate_limit.with_cost(1))),
        )
        .route(
            "/:org/members",
            patch(members::handle_patch)
                .put(members::handle_put)
                .delete(members::handle_delete),
                .delete(members::handle_delete)
                .layer(rate_limit.with_cost(10)),
        )
}
diff --git a/chartered-web/src/endpoints/web_api/sessions/mod.rs b/chartered-web/src/endpoints/web_api/sessions/mod.rs
index b234f9c..32e7b65 100644
--- a/chartered-web/src/endpoints/web_api/sessions/mod.rs
+++ a/chartered-web/src/endpoints/web_api/sessions/mod.rs
@@ -1,8 +1,13 @@
mod delete;
mod list;

use axum::{routing::get, Router};
use crate::RateLimit;
use axum::{handler::Handler, routing::get, Router};

pub fn routes() -> Router {
    Router::new().route("/", get(list::handle_get).delete(delete::handle_delete))
pub fn routes(rate_limit: &RateLimit) -> Router {
    Router::new().route(
        "/",
        get(list::handle_get.layer(rate_limit.with_cost(1)))
            .delete(delete::handle_delete.layer(rate_limit.with_cost(5))),
    )
}
diff --git a/chartered-web/src/endpoints/web_api/users/mod.rs b/chartered-web/src/endpoints/web_api/users/mod.rs
index eca9e4b..8afaa9b 100644
--- a/chartered-web/src/endpoints/web_api/users/mod.rs
+++ a/chartered-web/src/endpoints/web_api/users/mod.rs
@@ -1,12 +1,22 @@
mod heatmap;
mod info;
mod search;

use axum::{routing::get, Router};
use crate::RateLimit;
use axum::{handler::Handler, routing::get, Router};

pub fn routes() -> Router {
pub fn routes(rate_limit: &RateLimit) -> Router {
    Router::new()
        .route("/search", get(search::handle))
        .route("/info/:uuid", get(info::handle))
        .route("/info/:uuid/heatmap", get(heatmap::handle))
        .route(
            "/search",
            get(search::handle.layer(rate_limit.with_cost(5))),
        )
        .route(
            "/info/:uuid",
            get(info::handle.layer(rate_limit.with_cost(1))),
        )
        .route(
            "/info/:uuid/heatmap",
            get(heatmap::handle.layer(rate_limit.with_cost(1))),
        )
}