mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 10:18:16 +00:00
fix: loose and test origin checking of the preview server (#1353)
* fix: loose and test origin checking of the preview server * feat: check scheme * feat: check expected origin without hacking
This commit is contained in:
parent
22ce78dd1e
commit
4ce4e79d4c
1 changed files with 195 additions and 8 deletions
|
@ -4,6 +4,7 @@
|
|||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::LazyLock;
|
||||
use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
|
||||
|
||||
use futures::{SinkExt, StreamExt, TryStreamExt};
|
||||
|
@ -611,18 +612,54 @@ pub async fn make_http_server(
|
|||
}
|
||||
|
||||
fn is_valid_origin(h: &HeaderValue, expected_origin: &str, expected_port: u16) -> bool {
|
||||
*h == expected_origin || h.as_bytes().starts_with(b"vscode-webview://") || {
|
||||
struct SystemValidOriginContext;
|
||||
impl ValidOriginContext for SystemValidOriginContext {
|
||||
fn gitpod_suffix(&self) -> Option<&(String, String)> {
|
||||
// A bit of hack, this is the copy code from the `editors/vscode/src/gitpod.ts`
|
||||
// to ensure no regression is made. We should receive more reports from
|
||||
// users to check if we can have a way to check valid origins sensibly.
|
||||
static GITPOD_URL_SUFFIX: LazyLock<Option<(String, String)>> = LazyLock::new(|| {
|
||||
let workspace_id = std::env::var("GITPOD_WORKSPACE_ID").ok();
|
||||
let cluster_host = std::env::var("GITPOD_WORKSPACE_CLUSTER_HOST").ok();
|
||||
workspace_id.zip(cluster_host)
|
||||
});
|
||||
|
||||
GITPOD_URL_SUFFIX.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
is_valid_origin_impl(h, expected_origin, expected_port, &SystemValidOriginContext)
|
||||
}
|
||||
|
||||
trait ValidOriginContext {
|
||||
fn gitpod_suffix(&self) -> Option<&(String, String)>;
|
||||
}
|
||||
|
||||
fn is_valid_origin_impl(
|
||||
h: &HeaderValue,
|
||||
expected_origin: &str,
|
||||
expected_port: u16,
|
||||
context: &impl ValidOriginContext,
|
||||
) -> bool {
|
||||
h.as_bytes().starts_with(b"vscode-webview://") || {
|
||||
// Matches the origin of the local listening port.
|
||||
let matched = std::str::from_utf8(h.as_bytes()).ok().and_then(|h| {
|
||||
let url = Url::parse(h).ok()?;
|
||||
(url.port() == Some(expected_port)
|
||||
&& matches!(url.scheme(), "http" | "https" | "ws" | "wss")
|
||||
&& matches!(
|
||||
let valid = (matches!(url.scheme(), "http" | "https")
|
||||
&& url.host_str().is_some_and(|host| {
|
||||
// todo: allocate memory here.
|
||||
url.host().as_ref().map(ToString::to_string).as_deref(),
|
||||
Some("localhost" | "127.0.0.1")
|
||||
))
|
||||
.then_some(())
|
||||
(matches!(host, "localhost" | "127.0.0.1")
|
||||
|| Url::parse(&format!("http://{expected_origin}"))
|
||||
.ok()
|
||||
.is_some_and(|expected| Some(host) == expected.host_str())
|
||||
|| context
|
||||
.gitpod_suffix()
|
||||
.is_some_and(|(workspace_id, cluster_host)| {
|
||||
*host == format!("{expected_port}-{workspace_id}.{cluster_host}")
|
||||
}))
|
||||
}));
|
||||
|
||||
valid.then_some(())
|
||||
});
|
||||
matched.is_some()
|
||||
}
|
||||
|
@ -917,3 +954,153 @@ fn bind_streams(previewer: &mut Previewer, websocket_rx: mpsc::UnboundedReceiver
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn check_origin(origin: &'static str, expected: &str, port: u16) -> bool {
|
||||
is_valid_origin(&HeaderValue::from_static(origin), expected, port)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_origin_localhost() {
|
||||
assert!(check_origin("http://127.0.0.1:42", "127.0.0.1:42", 42));
|
||||
assert!(check_origin("http://127.0.0.1:42", "127.0.0.1:42", 42));
|
||||
assert!(check_origin("http://127.0.0.1:42", "127.0.0.1:0", 42));
|
||||
assert!(check_origin("http://localhost:42", "127.0.0.1:42", 42));
|
||||
assert!(check_origin("http://localhost:42", "127.0.0.1:0", 42));
|
||||
|
||||
assert!(check_origin("http://127.0.0.1:42", "localhost:42", 42));
|
||||
assert!(check_origin("http://127.0.0.1:42", "localhost:42", 42));
|
||||
assert!(check_origin("http://127.0.0.1:42", "localhost:0", 42));
|
||||
assert!(check_origin("http://localhost:42", "localhost:42", 42));
|
||||
assert!(check_origin("http://localhost:42", "localhost:0", 42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_origin_localhost() {
|
||||
assert!(!check_origin("https://huh.io:8080", "127.0.0.1:42", 42));
|
||||
assert!(!check_origin("http://huh.io:8080", "127.0.0.1:42", 42));
|
||||
assert!(!check_origin("https://huh.io:443", "127.0.0.1:42", 42));
|
||||
assert!(!check_origin("http://huh.io:42", "127.0.0.1:0", 42));
|
||||
assert!(!check_origin("http://huh.io", "127.0.0.1:42", 42));
|
||||
assert!(!check_origin("https://huh.io", "127.0.0.1:42", 42));
|
||||
|
||||
assert!(!check_origin("https://huh.io:8080", "localhost:42", 42));
|
||||
assert!(!check_origin("http://huh.io:8080", "localhost:42", 42));
|
||||
assert!(!check_origin("https://huh.io:443", "localhost:42", 42));
|
||||
assert!(!check_origin("http://huh.io:42", "localhost:0", 42));
|
||||
assert!(!check_origin("http://huh.io", "localhost:42", 42));
|
||||
assert!(!check_origin("https://huh.io", "localhost:42", 42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_origin_scheme() {
|
||||
assert!(!check_origin("ftp://127.0.0.1:42", "127.0.0.1:42", 42));
|
||||
assert!(!check_origin("ftp://localhost:42", "127.0.0.1:42", 42));
|
||||
assert!(!check_origin("ftp://127.0.0.1:42", "127.0.0.1:0", 42));
|
||||
assert!(!check_origin("ftp://localhost:42", "127.0.0.1:0", 42));
|
||||
|
||||
// The scheme must be specified.
|
||||
assert!(!check_origin("127.0.0.1:42", "127.0.0.1:0", 42));
|
||||
assert!(!check_origin("localhost:42", "127.0.0.1:0", 42));
|
||||
assert!(!check_origin("localhost:42", "127.0.0.1:42", 42));
|
||||
assert!(!check_origin("127.0.0.1:42", "127.0.0.1:42", 42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_origin_vscode() {
|
||||
assert!(check_origin("vscode-webview://it", "127.0.0.1:42", 42));
|
||||
assert!(check_origin("vscode-webview://it", "127.0.0.1:0", 42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_origin_manually_binding() {
|
||||
assert!(check_origin("https://huh.io:8080", "huh.io:42", 42));
|
||||
assert!(check_origin("http://huh.io:8080", "huh.io:42", 42));
|
||||
assert!(check_origin("https://huh.io:443", "huh.io:42", 42));
|
||||
assert!(check_origin("http://huh.io:42", "huh.io:0", 42));
|
||||
assert!(check_origin("http://huh.io", "huh.io:42", 42));
|
||||
assert!(check_origin("https://huh.io", "huh.io:42", 42));
|
||||
|
||||
assert!(check_origin("http://127.0.0.1:42", "huh.io:42", 42));
|
||||
assert!(check_origin("http://127.0.0.1:42", "huh.io:42", 42));
|
||||
assert!(check_origin("http://127.0.0.1:42", "huh.io:0", 42));
|
||||
assert!(check_origin("http://localhost:42", "huh.io:42", 42));
|
||||
assert!(check_origin("http://localhost:42", "huh.io:0", 42));
|
||||
|
||||
assert!(!check_origin("https://huh2.io:8080", "huh.io:42", 42));
|
||||
assert!(!check_origin("http://huh2.io:8080", "huh.io:42", 42));
|
||||
assert!(!check_origin("https://huh2.io:443", "huh.io:42", 42));
|
||||
assert!(!check_origin("http://huh2.io:42", "huh.io:0", 42));
|
||||
assert!(!check_origin("http://huh2.io", "huh.io:42", 42));
|
||||
assert!(!check_origin("https://huh2.io", "huh.io:42", 42));
|
||||
}
|
||||
|
||||
// https://github.com/Myriad-Dreamin/tinymist/issues/1350
|
||||
// the origin of code-server's proxy
|
||||
#[test]
|
||||
fn test_valid_origin_code_server_proxy() {
|
||||
assert!(check_origin(
|
||||
"http://localhost:8080/proxy/45411",
|
||||
"127.0.0.1:42",
|
||||
42
|
||||
));
|
||||
assert!(check_origin(
|
||||
"http://localhost/proxy/42",
|
||||
"127.0.0.1:42",
|
||||
42
|
||||
));
|
||||
}
|
||||
|
||||
// the origin of gitpod
|
||||
#[test]
|
||||
fn test_valid_origin_gitpod_proxy() {
|
||||
fn check_gitpod_origin(
|
||||
origin: &'static str,
|
||||
expected: &str,
|
||||
port: u16,
|
||||
workspace: &str,
|
||||
cluster_host: &str,
|
||||
) -> bool {
|
||||
struct MockValidOriginContext((String, String));
|
||||
impl ValidOriginContext for MockValidOriginContext {
|
||||
fn gitpod_suffix(&self) -> Option<&(String, String)> {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
is_valid_origin_impl(
|
||||
&HeaderValue::from_static(origin),
|
||||
expected,
|
||||
port,
|
||||
&MockValidOriginContext((workspace.to_owned(), cluster_host.to_owned())),
|
||||
)
|
||||
}
|
||||
|
||||
let check_gitpod_origin1 = |origin: &'static str| {
|
||||
let explicit =
|
||||
check_gitpod_origin(origin, "127.0.0.1:42", 42, "workspace_id", "gitpod.typ");
|
||||
let implicit =
|
||||
check_gitpod_origin(origin, "127.0.0.1:0", 42, "workspace_id", "gitpod.typ");
|
||||
|
||||
assert_eq!(explicit, implicit, "failed port binding");
|
||||
explicit
|
||||
};
|
||||
|
||||
assert!(check_gitpod_origin1("http://127.0.0.1:42"));
|
||||
assert!(check_gitpod_origin1("http://127.0.0.1:42"));
|
||||
assert!(check_gitpod_origin1("http://42-workspace_id.gitpod.typ"));
|
||||
assert!(check_gitpod_origin1(
|
||||
"http://42-workspace_id.gitpod.typ/path"
|
||||
));
|
||||
assert!(check_gitpod_origin1("http://42-workspace_id.gitpod.typ:42"));
|
||||
|
||||
assert!(!check_gitpod_origin1(
|
||||
"http://42-workspace_id2.gitpod.typ:42"
|
||||
));
|
||||
assert!(!check_gitpod_origin1("http://huh.io"));
|
||||
assert!(!check_gitpod_origin1("https://huh.io"));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue