Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support automatic HTTP -> HTTPS redirect #2424

Closed
hut8 opened this issue Dec 12, 2022 · 1 comment
Closed

Support automatic HTTP -> HTTPS redirect #2424

hut8 opened this issue Dec 12, 2022 · 1 comment
Labels
declined A declined request or suggestion request Request for new functionality

Comments

@hut8
Copy link

hut8 commented Dec 12, 2022

Is your feature request motivated by a concrete problem? Please describe.

Rocket supports TLS. It can serve TLS requests directly. Most sites that serve TLS (i.e., most sites generally 馃槃) will also listen on plaintext HTTP on port 80 and simply redirect to HTTPS.

Why this feature can't or shouldn't live outside of Rocket

HTTPS is already implemented inside of Rocket, and for good reason. One of Rust's strengths generally is simple deployment; I end up with a statically linked binary. In my case, I'm even trying to include static assets like icons and JS files inside at compile time (that feature doesn't have to live in Rocket, though!). The fact that HTTPS is a core rocket feature, but then I can't do HTTP -> HTTPS redirects easily, really limits the utility.

Ideal Solution

Ideally, Rocket could by default, when HTTPS is configured do what nginx does with these lines:

server {
	listen 80 default_server;
	listen [::]:80 default_server;
	server_name _;
	return 301 https://$host$request_uri;
}

and, in addition, this could be disabled. Or vice versa (it could be optionally enabled). I think that if we're listening on port 443 for HTTPS, then it's perfectly reasonable to also listen on 80 for the purpose of redirecting to 443.

Alternatives Considered

I could make a second rocket (or hyper?) instance that just does this one thing, or I could bring in nginx for this one purpose. There's no documentation on having multiple rocket apps in one binary, and it seems like overkill.

Additional Context

Caddy does what I describe, plus it even fetches the certs for you - https://caddyserver.com/docs/automatic-https

@hut8 hut8 added the request Request for new functionality label Dec 12, 2022
@SergioBenitez
Copy link
Member

I don't think Rocket should do this automatically. Rocket is a web server that listens on a single logical connection. This is different from what both caddy and nginx are: proxies that can also run any number of servers.

Instead, I would indeed advocate for launching a separate instance of Rocket that does the redirection. Here's a simple but fairly complete and robust implementation:

mod redirector {
    use rocket::http::Status;
    use rocket::{route, Error, Request, Data, Route, Orbit, Rocket};
    use rocket::fairing::{Fairing, Info, Kind};
    use rocket::response::Redirect;

    #[derive(Debug, Copy, Clone)]
    pub struct Redirector {
        pub port: u16
    }

    impl Redirector {
        fn redirect<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> {
            // FIXME: We should check the host against a whitelist!
            if let Some(host) = req.host() {
                let https_uri = format!("https://{}{}", host, req.uri());
                route::Outcome::from(req, Redirect::permanent(https_uri)).pin()
            } else {
                route::Outcome::from(req, Status::BadRequest).pin()
            }
        }

        pub async fn try_launch(&self) -> Result<(), Error> {
            use rocket::config::{Config, TlsConfig};
            use rocket::http::Method::*;

            let redirect_all = [Get, Put, Post, Delete, Options, Head, Trace, Connect, Patch]
                .into_iter()
                .map(|m| Route::new(m, "/<path..>", Self::redirect))
                .collect::<Vec<_>>();

            let config = Config::figment()
                .merge((Config::TLS, None::<Option<TlsConfig>>))
                .merge((Config::PORT, self.port))
                .merge((Config::LOG_LEVEL, "critical"));

            rocket::custom(config)
                .mount("/", redirect_all)
                .launch()
                .await?;

            Ok(())
        }
    }

    #[rocket::async_trait]
    impl Fairing for Redirector {
        fn info(&self) -> Info {
            Info { name: "HTTP -> HTTPS Redirector", kind: Kind::Liftoff }
        }

        async fn on_liftoff(&self, rkt: &Rocket<Orbit>) {
            let (this, shutdown) = (*self, rkt.shutdown());
            let _ = rocket::tokio::spawn(async move {
                if let Err(e) = this.try_launch().await {
                    error!("Failed to start HTTP -> HTTPS redirector.");
                    info_!("Error: {}", e);
                    error_!("Shutting down main instance.");
                    shutdown.notify();
                }
            });
        }
    }
}

#[launch]
async fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![..])
        .attach(redirector::Redirector { port: 3000 })
}

@SergioBenitez SergioBenitez added the declined A declined request or suggestion label Mar 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
declined A declined request or suggestion request Request for new functionality
Projects
None yet
Development

No branches or pull requests

2 participants