1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// Copyright Materialize, Inc. and contributors. All rights reserved.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apach

//! Console Impersonation HTTP endpoint.

use std::sync::Arc;

use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Extension;
use http::header::HOST;
use http::HeaderValue;
use hyper::Uri;
use hyper_tls::HttpsConnector;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;

pub(crate) struct ConsoleProxyConfig {
    /// Hyper http client, supports https.
    client: Client<HttpsConnector<HttpConnector>, Body>,

    /// URL of upstream console to proxy to (e.g. <https://console.materialize.com>).
    url: String,

    /// Route this is being served from (e.g. /internal-console).
    route_prefix: String,
}

impl ConsoleProxyConfig {
    pub(crate) fn new(proxy_url: Option<String>, route_prefix: String) -> Self {
        let mut url = proxy_url.unwrap_or("https://console.materialize.com".to_string());
        if let Some(new) = url.strip_suffix('/') {
            url = new.to_string();
        }
        Self {
            client: Client::builder(TokioExecutor::new()).build(HttpsConnector::new()),
            url,
            route_prefix,
        }
    }
}

/// The User Impersonation feature uses a Teleport proxy in front of the
/// Internal HTTP Server, however Teleport has issues with CORS that prevent
/// making requests to that Teleport-proxied app from our production console URLs.
/// To avoid CORS and serve the Console from the same host as the Teleport app,
/// this route proxies the upstream Console to handle requests for
/// HTML, JS, and CSS static files.
pub(crate) async fn handle_internal_console(
    console_config: Extension<Arc<ConsoleProxyConfig>>,
    mut req: Request<Body>,
) -> Result<Response, StatusCode> {
    let path = req.uri().path();
    let mut path_query = req
        .uri()
        .path_and_query()
        .map(|v| v.as_str())
        .unwrap_or(path);
    if let Some(stripped_path_query) = path_query.strip_prefix(&console_config.route_prefix) {
        path_query = stripped_path_query;
    }

    let uri = Uri::try_from(format!("{}{}", &console_config.url, path_query)).unwrap();
    let host = uri.host().unwrap().to_string();
    // Preserve the request, but update the URI to point upstream.
    *req.uri_mut() = uri;

    // If vercel sees the request being served from a different host it tries to redirect to it's own.
    req.headers_mut()
        .insert(HOST, HeaderValue::from_str(&host).unwrap());

    // Call this request against the upstream, return response directly.
    Ok(console_config
        .client
        .request(req)
        .await
        .map_err(|err| {
            tracing::warn!("Error retrieving console url: {}", err);
            StatusCode::BAD_REQUEST
        })?
        .into_response())
}