mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-24 14:44:07 +00:00
Merge branch 'master' into ak/fix-serialize-invalid-id
This commit is contained in:
commit
c742011961
28 changed files with 1144 additions and 32 deletions
|
|
@ -126,6 +126,10 @@ impl TestServeSession {
|
|||
&self.project_path
|
||||
}
|
||||
|
||||
pub fn port(&self) -> usize {
|
||||
self.port
|
||||
}
|
||||
|
||||
/// Waits for the `rojo serve` server to come online with expontential
|
||||
/// backoff.
|
||||
pub fn wait_to_come_online(&mut self) -> ServerInfoResponse {
|
||||
|
|
@ -239,6 +243,39 @@ impl TestServeSession {
|
|||
|
||||
client.post(url).body(body).send()
|
||||
}
|
||||
|
||||
/// Sends a GET to `/api/rojo` with the given extra request headers and
|
||||
/// returns the full response. Used to exercise the Host/Origin allowlist that
|
||||
/// guards against DNS rebinding, including asserting that a rejection reveals
|
||||
/// nothing about the server.
|
||||
pub fn api_rojo_response_with_headers(
|
||||
&self,
|
||||
headers: &[(&str, &str)],
|
||||
) -> reqwest::blocking::Response {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let url = format!("http://localhost:{}/api/rojo", self.port);
|
||||
|
||||
let mut request = client.get(url);
|
||||
for (name, value) in headers {
|
||||
request = request.header(*name, *value);
|
||||
}
|
||||
|
||||
request.send().expect("Failed to send request")
|
||||
}
|
||||
|
||||
/// Sends a POST to `/api/open/<id>` and returns the response status code.
|
||||
/// Used to verify that the local-only gate on `/api/open` admits loopback
|
||||
/// peers (the test harness always connects over loopback).
|
||||
pub fn api_open_status(&self, id: &str) -> reqwest::StatusCode {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let url = format!("http://localhost:{}/api/open/{}", self.port, id);
|
||||
|
||||
client
|
||||
.post(url)
|
||||
.send()
|
||||
.expect("Failed to send request")
|
||||
.status()
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_msgpack<T: Serialize>(value: T) -> Result<Vec<u8>, rmp_serde::encode::Error> {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ gen_build_tests! {
|
|||
no_name_default_project,
|
||||
no_name_project,
|
||||
no_name_top_level_project,
|
||||
plugin_init,
|
||||
}
|
||||
|
||||
fn run_build_test(test_name: &str) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,74 @@ use crate::rojo_test::{
|
|||
|
||||
use librojo::web_api::{SerializeResponse, SocketPacketType};
|
||||
|
||||
#[test]
|
||||
fn rejects_dns_rebinding_requests() {
|
||||
run_serve_test("empty", |session, _redactions| {
|
||||
let port = session.port();
|
||||
let local_host = format!("localhost:{port}");
|
||||
|
||||
// A request carrying a local Host header is served normally.
|
||||
assert_eq!(
|
||||
session
|
||||
.api_rojo_response_with_headers(&[("host", &local_host)])
|
||||
.status(),
|
||||
reqwest::StatusCode::OK,
|
||||
);
|
||||
|
||||
// A request whose Host is a foreign hostname, as a DNS-rebound page
|
||||
// would send, is rejected with a generic 404 that reveals nothing about
|
||||
// the server.
|
||||
assert_rejected(session.api_rojo_response_with_headers(&[("host", "evil.com")]));
|
||||
|
||||
// Even with a local Host, a present-but-foreign Origin is rejected.
|
||||
let foreign_origin = format!("http://evil.com:{port}");
|
||||
assert_rejected(
|
||||
session.api_rojo_response_with_headers(&[
|
||||
("host", &local_host),
|
||||
("origin", &foreign_origin),
|
||||
]),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Asserts that a Host/Origin rejection is a generic 404 whose body and
|
||||
/// content-type do not identify the server as Rojo.
|
||||
fn assert_rejected(response: reqwest::blocking::Response) {
|
||||
assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND);
|
||||
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
assert!(
|
||||
!content_type.contains("msgpack"),
|
||||
"rejection should not use the msgpack API content-type, got {content_type:?}",
|
||||
);
|
||||
|
||||
let body = response.text().expect("Failed to read response body");
|
||||
let body_lower = body.to_lowercase();
|
||||
assert!(
|
||||
!body_lower.contains("rojo") && !body_lower.contains("rebinding"),
|
||||
"rejection body should not identify the server, got {body:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_api_open_from_loopback_peer() {
|
||||
run_serve_test("empty", |session, _redactions| {
|
||||
// The harness always connects over loopback, so the local-only gate on
|
||||
// /api/open must let the request through. A bogus instance id then fails
|
||||
// id parsing with 400, which confirms we got past the gate rather than
|
||||
// being rejected with 403.
|
||||
assert_eq!(
|
||||
session.api_open_status("not-a-real-ref"),
|
||||
reqwest::StatusCode::BAD_REQUEST,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
run_serve_test("empty", |session, mut redactions| {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue