I'm trying add HTTPS enforcement to my Warp-based web app on GKE.
The GKE platform is mostly irrelevant; the cromulent detail is that the load balancer terminates SSL/TLS connections, so the “real” scheme is provided in the X-Forwarded-Proto
header. The literal scheme parsed by Warp will always be HTTP
.
The logic goes as follows:
- If the scheme is
HTTPS
, process requests normally. - If the scheme is
HTTP
, send a 301 redirect to the equivalentHTTPS
URL. - If the scheme is anything else, send a 421 (misdirected request) error.
- If the
X-Forwarded-Proto
header is missing (or any other realistically impossible scenario occurs), send a 400 (bad request) error.
The error responses have no body content in this example, and all HTTPS requests should respond with the text Hello, world!
.
The problem:
error[E0277]: the trait bound `std::result::Result<(), warp::reject::Rejection>: core::future::future::Future` is not satisfied
--> src/main.rs:23:10
|
23 | .and_then(|scheme_header: Option<String>, host: String, path: FullPath| {
| ^^^^^^^^ the trait `core::future::future::Future` is not implemented for `std::result::Result<(), warp::reject::Rejection>`
|
= note: required because of the requirements on the impl of `futures_core::future::TryFuture` for `std::result::Result<(), warp::reject::Rejection>`
error[E0599]: no method named `and` found for type `warp::filter::and_then::AndThen<warp::filter::and::And<warp::filter::and::And<impl warp::filter::Filter+std::marker::Copy, impl warp::filter::Filter+std::marker::Copy>, impl warp::filter::Filter+std::marker::Copy>, [closure@src/main.rs:23:19: 43:10]>` in the current scope
--> src/main.rs:44:10
|
44 | .and(filter)
| ^^^ method not found in `warp::filter::and_then::AndThen<warp::filter::and::And<warp::filter::and::And<impl warp::filter::Filter+std::marker::Copy, impl warp::filter::Filter+std::marker::Copy>, impl warp::filter::Filter+std::marker::Copy>, [closure@src/main.rs:23:19: 43:10]>`
|
= note: the method `and` exists but the following trait bounds were not satisfied:
`&mut warp::filter::and_then::AndThen<warp::filter::and::And<warp::filter::and::And<impl warp::filter::Filter+std::marker::Copy, impl warp::filter::Filter+std::marker::Copy>, impl warp::filter::Filter+std::marker::Copy>, [closure@src/main.rs:23:19: 43:10]> : warp::filter::Filter`
`&warp::filter::and_then::AndThen<warp::filter::and::And<warp::filter::and::And<impl warp::filter::Filter+std::marker::Copy, impl warp::filter::Filter+std::marker::Copy>, impl warp::filter::Filter+std::marker::Copy>, [closure@src/main.rs:23:19: 43:10]> : warp::filter::Filter`
`warp::filter::and_then::AndThen<warp::filter::and::And<warp::filter::and::And<impl warp::filter::Filter+std::marker::Copy, impl warp::filter::Filter+std::marker::Copy>, impl warp::filter::Filter+std::marker::Copy>, [closure@src/main.rs:23:19: 43:10]> : warp::filter::Filter`
Clearly I’m missing something obvious here, so I’m hoping someone can nudge me in the right direction!
use futures::{FutureExt, StreamExt};
use warp::{Filter, Rejection};
use warp::filters::path::{FullPath};
use warp::http::{StatusCode, Uri};
use warp::http::uri::{Parts, Scheme};
use warp::reply::Reply;
enum SchemeError {
InsecureScheme(Uri),
UnknownScheme,
MissingScheme,
}
impl warp::reject::Reject for SchemeError {}
async fn requires_https(filter: impl Filter<Extract = (Scheme,), Error = Rejection> + Copy) -> impl Filter<Extract = (), Error = Rejection> + Copy {
warp::header::optional("X-Forwarded-Proto")
.and(warp::header("Host"))
.and(warp::path::full())
.and_then(|scheme_header: Option<String>, host: String, path: FullPath| {
if let Some(scheme) = scheme_header {
match scheme.to_ascii_lowercase().as_str() {
"https" => Ok(()),
"http" => {
let mut uri_parts = Parts::default();
uri_parts.scheme = Some(Scheme::HTTPS);
uri_parts.authority = Some(host.parse().unwrap());
uri_parts.path_and_query = Some(path.as_str().parse().unwrap());
let uri_parts = uri_parts;
let new_uri = Uri::from_parts(uri_parts).unwrap();
println!("Redirecting to secure URL: {}", new_uri);
Err(warp::reject::custom(SchemeError::InsecureScheme(new_uri)))
},
_ => Err(warp::reject::custom(SchemeError::UnknownScheme)),
}
} else {
Err(warp::reject::custom(SchemeError::MissingScheme))
}
})
.and(filter)
.recover(|err: Rejection| {
if let Some(scheme_error) = err.find::<SchemeError>() {
match scheme_error {
SchemeError::InsecureScheme(new_uri) => Ok(warp::redirect(new_uri)),
SchemeError::UnknownScheme => Ok(StatusCode::MISDIRECTED_REQUEST),
SchemeError::MissingScheme => Ok(StatusCode::BAD_REQUEST),
}
} else {
Err(err)
}
})
}
#[tokio::main]
async fn main() {
let routes = requires_https(warp::any().map(|| "Hello, world!"));
warp::serve(routes)
.run(([0, 0, 0, 0], 8080))
.await;
}