Skip to content

Add BLOCKED_ROUTES environment variable #3892

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

Merged
merged 3 commits into from
Sep 26, 2021
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
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod database_pools;

pub use self::base::Base;
pub use self::database_pools::DatabasePools;
use std::collections::HashSet;

pub struct Server {
pub base: Base,
Expand All @@ -28,6 +29,7 @@ pub struct Server {
pub use_test_database_pool: bool,
pub instance_metrics_log_every_seconds: Option<u64>,
pub force_unconditional_redirects: bool,
pub blocked_routes: HashSet<String>,
}

impl Default for Server {
Expand Down Expand Up @@ -58,6 +60,8 @@ impl Default for Server {
/// If the environment variable is not present instance metrics are not logged.
/// - `FORCE_UNCONDITIONAL_REDIRECTS`: Whether to force unconditional redirects in the download
/// endpoint even with a healthy database pool.
/// - `BLOCKED_ROUTES`: A comma separated list of HTTP route patterns that are manually blocked
/// by an operator (e.g. `/crates/:crate_id/:version/download`).
///
/// # Panics
///
Expand Down Expand Up @@ -100,6 +104,9 @@ impl Default for Server {
use_test_database_pool: false,
instance_metrics_log_every_seconds: env_optional("INSTANCE_METRICS_LOG_EVERY_SECONDS"),
force_unconditional_redirects: dotenv::var("FORCE_UNCONDITIONAL_REDIRECTS").is_ok(),
blocked_routes: env_optional("BLOCKED_ROUTES")
.map(|routes: String| routes.split(',').map(|s| s.into()).collect())
.unwrap_or_else(HashSet::new),
}
}
}
Expand Down
14 changes: 12 additions & 2 deletions src/router.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::sync::Arc;

use conduit::{Handler, HandlerResult, RequestExt};
use conduit_router::{RequestParams, RouteBuilder};
use conduit_router::{RequestParams, RouteBuilder, RoutePattern};

use crate::controllers::*;
use crate::util::errors::{std_error, AppError};
use crate::middleware::app::RequestApp;
use crate::util::errors::{std_error, AppError, RouteBlocked};
use crate::util::EndpointResult;
use crate::{App, Env};

Expand Down Expand Up @@ -150,6 +151,15 @@ struct C(pub fn(&mut dyn RequestExt) -> EndpointResult);

impl Handler for C {
fn call(&self, req: &mut dyn RequestExt) -> HandlerResult {
// Allow blocking individual routes by their pattern through the `BLOCKED_ROUTES`
// environment variable. This is not in a middleware because we need access to
// `RoutePattern` before executing the response handler.
if let Some(pattern) = req.extensions().find::<RoutePattern>() {
if req.app().config.blocked_routes.contains(pattern.pattern()) {
return Ok(RouteBlocked.response().unwrap());
}
}

let C(f) = *self;
match f(req) {
Ok(resp) => Ok(resp),
Expand Down
1 change: 1 addition & 0 deletions src/tests/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use diesel::prelude::*;
mod account_lock;
mod authentication;
mod badge;
mod blocked_routes;
mod builders;
mod categories;
mod category;
Expand Down
42 changes: 42 additions & 0 deletions src/tests/blocked_routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::builders::{CrateBuilder, VersionBuilder};
use crate::util::{RequestHelper, TestApp};
use conduit::StatusCode;

#[test]
fn test_non_blocked_download_route() {
let (app, anon, user) = TestApp::init()
.with_config(|config| {
config.blocked_routes.clear();
})
.with_user();

app.db(|conn| {
CrateBuilder::new("foo", user.as_model().id)
.version(VersionBuilder::new("1.0.0"))
.expect_build(conn);
});

let status = anon.get::<()>("/api/v1/crates/foo/1.0.0/download").status();
assert_eq!(StatusCode::FOUND, status);
}

#[test]
fn test_blocked_download_route() {
let (app, anon, user) = TestApp::init()
.with_config(|config| {
config.blocked_routes.clear();
config
.blocked_routes
.insert("/crates/:crate_id/:version/download".into());
})
.with_user();

app.db(|conn| {
CrateBuilder::new("foo", user.as_model().id)
.version(VersionBuilder::new("1.0.0"))
.expect_build(conn);
});

let status = anon.get::<()>("/api/v1/crates/foo/1.0.0/download").status();
assert_eq!(StatusCode::SERVICE_UNAVAILABLE, status);
}
2 changes: 2 additions & 0 deletions src/tests/util/test_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use cargo_registry::git::Repository as WorkerRepository;
use diesel::PgConnection;
use git2::Repository as UpstreamRepository;
use reqwest::{blocking::Client, Proxy};
use std::collections::HashSet;
use swirl::Runner;
use url::Url;

Expand Down Expand Up @@ -335,6 +336,7 @@ fn simple_config() -> config::Server {
use_test_database_pool: true,
instance_metrics_log_every_seconds: None,
force_unconditional_redirects: false,
blocked_routes: HashSet::new(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/util/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ mod json;
pub use json::TOKEN_FORMAT_ERROR;
pub(crate) use json::{
InsecurelyGeneratedTokenRevoked, MetricsDisabled, NotFound, OwnershipInvitationExpired,
ReadOnlyMode, TooManyRequests,
ReadOnlyMode, RouteBlocked, TooManyRequests,
};

/// Returns an error with status 200 and the provided description as JSON
Expand Down
18 changes: 18 additions & 0 deletions src/util/errors/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,21 @@ impl fmt::Display for MetricsDisabled {
f.write_str("Metrics are disabled on this crates.io instance")
}
}

#[derive(Debug)]
pub(crate) struct RouteBlocked;

impl AppError for RouteBlocked {
fn response(&self) -> Option<AppResponse> {
Some(json_error(
&self.to_string(),
StatusCode::SERVICE_UNAVAILABLE,
))
}
}

impl fmt::Display for RouteBlocked {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("This route is temporarily blocked. See https://status.crates.io.")
}
}