mz_environmentd/http/
console.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apach
9
10//! Console Impersonation HTTP endpoint.
11
12use std::sync::Arc;
13
14use axum::Extension;
15use axum::body::Body;
16use axum::http::{Request, StatusCode};
17use axum::response::{IntoResponse, Response};
18use http::HeaderValue;
19use http::header::HOST;
20use hyper::Uri;
21use hyper_tls::HttpsConnector;
22use hyper_util::client::legacy::Client;
23use hyper_util::client::legacy::connect::HttpConnector;
24use hyper_util::rt::TokioExecutor;
25
26pub(crate) struct ConsoleProxyConfig {
27    /// Hyper http client, supports https.
28    client: Client<HttpsConnector<HttpConnector>, Body>,
29
30    /// URL of upstream console to proxy to (e.g. <https://console.materialize.com>).
31    url: String,
32
33    /// Route this is being served from (e.g. /internal-console).
34    route_prefix: String,
35}
36
37impl ConsoleProxyConfig {
38    pub(crate) fn new(proxy_url: Option<String>, route_prefix: String) -> Self {
39        let mut url = proxy_url.unwrap_or_else(|| "https://console.materialize.com".to_string());
40        if let Some(new) = url.strip_suffix('/') {
41            url = new.to_string();
42        }
43        Self {
44            client: Client::builder(TokioExecutor::new()).build(HttpsConnector::new()),
45            url,
46            route_prefix,
47        }
48    }
49}
50
51/// The User Impersonation feature uses a Teleport proxy in front of the
52/// Internal HTTP Server, however Teleport has issues with CORS that prevent
53/// making requests to that Teleport-proxied app from our production console URLs.
54/// To avoid CORS and serve the Console from the same host as the Teleport app,
55/// this route proxies the upstream Console to handle requests for
56/// HTML, JS, and CSS static files.
57pub(crate) async fn handle_internal_console(
58    console_config: Extension<Arc<ConsoleProxyConfig>>,
59    mut req: Request<Body>,
60) -> Result<Response, StatusCode> {
61    let path = req.uri().path();
62    let mut path_query = req
63        .uri()
64        .path_and_query()
65        .map(|v| v.as_str())
66        .unwrap_or(path);
67    if let Some(stripped_path_query) = path_query.strip_prefix(&console_config.route_prefix) {
68        path_query = stripped_path_query;
69    }
70
71    let uri = Uri::try_from(format!("{}{}", &console_config.url, path_query)).unwrap();
72    let host = uri.host().unwrap().to_string();
73    // Preserve the request, but update the URI to point upstream.
74    *req.uri_mut() = uri;
75
76    // If vercel sees the request being served from a different host it tries to redirect to it's own.
77    req.headers_mut()
78        .insert(HOST, HeaderValue::from_str(&host).unwrap());
79
80    // Call this request against the upstream, return response directly.
81    Ok(console_config
82        .client
83        .request(req)
84        .await
85        .map_err(|err| {
86            tracing::warn!("Error retrieving console url: {}", err);
87            StatusCode::BAD_REQUEST
88        })?
89        .into_response())
90}