Implement replay-safe browser synchronization
This commit is contained in:
parent
06a7ac56b7
commit
459ffa04c6
1 changed files with 2734 additions and 25 deletions
|
|
@ -29,9 +29,12 @@ use time::OffsetDateTime;
|
|||
use tokio_tungstenite::{
|
||||
MaybeTlsStream, WebSocketStream, connect_async,
|
||||
tungstenite::{
|
||||
Message,
|
||||
Error as WebSocketError, Message,
|
||||
client::IntoClientRequest,
|
||||
http::{HeaderValue, header::AUTHORIZATION},
|
||||
http::{
|
||||
HeaderValue,
|
||||
header::{AUTHORIZATION, ORIGIN},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -40,6 +43,7 @@ mod support;
|
|||
use support::{mint_provider_token, oidc_provider, required_env, revoke_remora_grants};
|
||||
|
||||
type Socket = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
|
||||
const BROWSER_ORIGIN: &str = "https://remora.test";
|
||||
|
||||
struct Harness {
|
||||
pool: PgPool,
|
||||
|
|
@ -158,6 +162,8 @@ impl Harness {
|
|||
event_retention: Duration::from_millis(500),
|
||||
event_poll_interval: Duration::from_millis(250),
|
||||
event_heartbeat_interval: Duration::from_millis(100),
|
||||
event_ticket_ttl: Duration::from_secs(30),
|
||||
event_allowed_origins: vec!["https://remora.test".into()],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -227,7 +233,50 @@ impl Harness {
|
|||
response.json().await.unwrap()
|
||||
}
|
||||
|
||||
async fn issue_ticket(&self, token: &str) -> String {
|
||||
let response = self
|
||||
.client
|
||||
.post(format!("{}/event-tickets", self.http_base))
|
||||
.bearer_auth(token)
|
||||
.header("Origin", BROWSER_ORIGIN)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::CREATED);
|
||||
assert_eq!(response.headers().get("Cache-Control").unwrap(), "no-store");
|
||||
response.json::<Value>().await.unwrap()["ticket"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
async fn connect(&self, token: &str, cursor: Option<&str>) -> Socket {
|
||||
let ticket = self.issue_ticket(token).await;
|
||||
self.connect_ticket(&ticket, cursor, BROWSER_ORIGIN)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn connect_ticket(
|
||||
&self,
|
||||
ticket: &str,
|
||||
cursor: Option<&str>,
|
||||
origin: &str,
|
||||
) -> Result<Socket, WebSocketError> {
|
||||
let url = cursor.map_or_else(
|
||||
|| format!("{}?ticket={ticket}", self.ws_base),
|
||||
|cursor| format!("{}?ticket={ticket}&cursor={cursor}", self.ws_base),
|
||||
);
|
||||
let mut request = url.into_client_request().unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.insert(ORIGIN, HeaderValue::from_str(origin).unwrap());
|
||||
let (socket, response) = connect_async(request).await?;
|
||||
assert_eq!(response.status(), 101);
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
async fn connect_bearer(&self, token: &str, cursor: Option<&str>) -> Socket {
|
||||
let url = cursor.map_or_else(
|
||||
|| self.ws_base.clone(),
|
||||
|cursor| format!("{}?cursor={cursor}", self.ws_base),
|
||||
|
|
@ -255,6 +304,60 @@ async fn live_event_replay_authorization_progress_and_resync_converge() {
|
|||
return;
|
||||
};
|
||||
|
||||
let ticket = harness.issue_ticket(&harness.token).await;
|
||||
let forbidden_ticket = harness
|
||||
.client
|
||||
.post(format!("{}/event-tickets", harness.http_base))
|
||||
.bearer_auth(&harness.token)
|
||||
.header("Origin", "https://attacker.invalid")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(forbidden_ticket.status(), StatusCode::FORBIDDEN);
|
||||
assert!(
|
||||
harness
|
||||
.connect_ticket(&ticket, None, "https://attacker.invalid")
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
let valid = harness
|
||||
.connect_ticket(&ticket, None, BROWSER_ORIGIN)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(valid);
|
||||
assert!(
|
||||
harness
|
||||
.connect_ticket(&ticket, None, BROWSER_ORIGIN)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let concurrent_ticket = harness.issue_ticket(&harness.token).await;
|
||||
let (left, right) = tokio::join!(
|
||||
harness.connect_ticket(&concurrent_ticket, None, BROWSER_ORIGIN),
|
||||
harness.connect_ticket(&concurrent_ticket, None, BROWSER_ORIGIN)
|
||||
);
|
||||
assert_eq!(usize::from(left.is_ok()) + usize::from(right.is_ok()), 1);
|
||||
|
||||
let expired_ticket = harness.issue_ticket(&harness.token).await;
|
||||
sqlx::query(
|
||||
"UPDATE live_event_tickets
|
||||
SET expires_at = created_at + interval '1 microsecond'
|
||||
WHERE expires_at > now()",
|
||||
)
|
||||
.execute(&harness.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
harness
|
||||
.connect_ticket(&expired_ticket, None, BROWSER_ORIGIN)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let mut bearer = harness.connect_bearer(&harness.token, None).await;
|
||||
assert_resync(next_frame(&mut bearer).await, ResyncReasonDto::CursorAbsent);
|
||||
|
||||
let mut absent = harness.connect(&harness.token, None).await;
|
||||
assert_resync(next_frame(&mut absent).await, ResyncReasonDto::CursorAbsent);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue