mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 21:36:46 -06:00
M8: cap the people a client keeps track of and the deleted ids a session remembers (#1622)
* M8: cap the people a client keeps track of and the deleted ids a session remembers (#1121) A hostile host or server could grow a client's presence, seen-peers and who-is-here lists without bound, and a session's deleted-id set only grew. Clients keep at most MAX_PEERS (4,096) others, leaving newcomers out with a warning; max_people is at most that; a session remembers at most 262,144 deleted ids, the lowest forgotten first. * M8: a max_people above what clients track is lowered with a warning, not refused (#1121) Refusing it would stop a server whose existing config says more from starting after an upgrade. Config::people() lowers max_people to MAX_PEERS for the core's limits, and the startup warnings name the configured and the used value. * M8: the live welcome's handling in its own function (Sonar S3776)
This commit is contained in:
+2
-1
File diff suppressed because one or more lines are too long
@@ -147,6 +147,14 @@ impl Config {
|
||||
client_max - 1
|
||||
));
|
||||
}
|
||||
if self.people() < self.max_people {
|
||||
out.push(format!(
|
||||
"max_people is {}: the app keeps track of at most {} people in a file, so {} are let in",
|
||||
self.max_people,
|
||||
self.people(),
|
||||
self.people()
|
||||
));
|
||||
}
|
||||
if !self.open_publish {
|
||||
return out;
|
||||
}
|
||||
@@ -171,6 +179,12 @@ impl Config {
|
||||
out
|
||||
}
|
||||
|
||||
/// Most people in one file at once: `max_people`, lowered to what the app keeps track of
|
||||
/// ([`figcraft_collab::message::MAX_PEERS`]).
|
||||
pub fn people(&self) -> usize {
|
||||
self.max_people.min(figcraft_collab::message::MAX_PEERS)
|
||||
}
|
||||
|
||||
/// The trusted proxy's address, when set.
|
||||
pub fn proxy(&self) -> Result<Option<IpAddr>, String> {
|
||||
let Some(p) = self.trusted_proxy.as_deref().map(str::trim).filter(|p| !p.is_empty()) else { return Ok(None) };
|
||||
@@ -284,8 +298,7 @@ impl Config {
|
||||
/// most, and those batches also at most a quarter of a file's bytes (256 KiB at least).
|
||||
fn core(&self) -> figcraft_collab::Limits {
|
||||
let bytes = |mb: u64| usize::try_from(mb.saturating_mul(1 << 20)).unwrap_or(usize::MAX);
|
||||
let base =
|
||||
figcraft_collab::Limits { max_peers: self.max_people, max_log_bytes: bytes(self.max_log_mb), ..figcraft_collab::Limits::default() };
|
||||
let base = figcraft_collab::Limits { max_peers: self.people(), max_log_bytes: bytes(self.max_log_mb), ..figcraft_collab::Limits::default() };
|
||||
if !self.open_publish {
|
||||
return figcraft_collab::Limits { max_message_bytes: base.max_message_bytes.max(self.open_message()), ..base };
|
||||
}
|
||||
|
||||
@@ -481,6 +481,13 @@ fn open_mode_has_safe_defaults_and_warns_the_operator() {
|
||||
assert_eq!(set.file_mb(), 8);
|
||||
assert!(Config { trusted_proxy: Some("localhost".into()), ..set.clone() }.check().is_err());
|
||||
assert!(Config { terms_url: "javascript:alert(1)".into(), ..set.clone() }.check().is_err());
|
||||
// Clients keep track of at most `MAX_PEERS` others (#1121).
|
||||
let people = figcraft_collab::message::MAX_PEERS;
|
||||
let many = Config { max_people: people + 1, ..set.clone() };
|
||||
assert!(many.check().is_ok(), "a server configured for more still starts");
|
||||
assert_eq!(many.people(), people, "with that many");
|
||||
assert!(many.warnings().iter().any(|w| w.contains("max_people is 4097") && w.contains("4096 are let in")), "{:?}", many.warnings());
|
||||
assert_eq!(Config { max_people: people, ..set.clone() }.people(), people);
|
||||
assert!(Config { idle_days: f64::NAN, ..set }.check().is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//! A hostile server can't grow a guest's memory without bound (#1121): the presence waiting for
|
||||
//! the UI and the people a connection has seen hold at most [`MAX_PEERS`] others; past it,
|
||||
//! someone new is left out, and someone leaving (or the UI taking the presence) makes room.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use figcraft_collab::message::{Peer, Presence, ServerMsg};
|
||||
use figcraft_engine::live::{Event, MAX_PEERS};
|
||||
|
||||
use crate::guest::GuestConfig;
|
||||
use crate::inbox::Inbox;
|
||||
use crate::invite::Invite;
|
||||
use crate::join::{Seen, take};
|
||||
|
||||
fn inbox() -> Inbox {
|
||||
Inbox::new(Arc::new(|| {}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presence_from_more_people_than_the_cap_is_left_out() {
|
||||
let inbox = inbox();
|
||||
let over = MAX_PEERS as u32 + 500;
|
||||
for id in 0..over {
|
||||
inbox.presence(id, Presence::default());
|
||||
}
|
||||
// Someone already waiting is still updated.
|
||||
let moved = Presence { selection: Some(Vec::new()), ..Presence::default() };
|
||||
inbox.presence(3, moved.clone());
|
||||
let out = inbox.drain();
|
||||
assert_eq!(out.len(), MAX_PEERS);
|
||||
assert!(out.iter().all(|e| matches!(e, Event::Presence(id, _) if (*id as usize) < MAX_PEERS)));
|
||||
assert!(out.iter().any(|e| matches!(e, Event::Presence(3, p) if *p == moved)));
|
||||
// Taken by the UI: room again.
|
||||
inbox.presence(over, Presence::default());
|
||||
assert!(matches!(inbox.drain().as_slice(), [Event::Presence(id, _)] if *id == over));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joins_past_the_cap_are_left_out_until_someone_leaves() {
|
||||
let inbox = inbox();
|
||||
let cfg = GuestConfig::new(Invite::new("127.0.0.1:47900", "0123456789abcdef", "AbCdEfGhIjKlMnOpQrStUv"), "Ada".into());
|
||||
let resume = Mutex::new(None);
|
||||
let mut seen = Seen::default();
|
||||
let mut send = |m: ServerMsg| {
|
||||
take(m, &cfg, &inbox, &mut seen, &resume, &|| {});
|
||||
};
|
||||
let joined = |id: u32| ServerMsg::PeerJoined(Peer { client_id: id, name: format!("P{id}"), color: String::new() });
|
||||
let over = MAX_PEERS as u32 + 500;
|
||||
for id in 0..over {
|
||||
send(joined(id));
|
||||
}
|
||||
let count = |events: &[Event]| events.iter().filter(|e| matches!(e, Event::PeerJoined(_))).count();
|
||||
assert_eq!(count(&inbox.drain()), MAX_PEERS, "joins past the cap reach the UI");
|
||||
// Someone already here may be renamed; someone left out stays out until someone leaves.
|
||||
send(joined(5));
|
||||
send(joined(over));
|
||||
assert!(matches!(inbox.drain().as_slice(), [Event::PeerJoined(p)] if p.client_id == 5));
|
||||
send(ServerMsg::PeerLeft { client_id: 7 });
|
||||
send(joined(over));
|
||||
assert!(matches!(inbox.drain().as_slice(), [Event::PeerLeft(7), Event::PeerJoined(p)] if p.client_id == over));
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
|
||||
use figcraft_engine::live::{Event, Presence};
|
||||
use figcraft_engine::live::{Event, MAX_PEERS, Presence};
|
||||
|
||||
/// Wakes the UI (the desktop app passes egui's `request_repaint`).
|
||||
pub type Waker = Arc<dyn Fn() + Send + Sync>;
|
||||
@@ -23,7 +23,10 @@ struct State {
|
||||
syncs: VecDeque<(u64, Event)>,
|
||||
/// Every other event, capped at [`MAX_EVENTS`].
|
||||
others: VecDeque<(u64, Event)>,
|
||||
/// The latest presence of each peer, for at most [`MAX_PEERS`] peers.
|
||||
presence: BTreeMap<u32, Presence>,
|
||||
/// A presence was left out since the last drain (warned once).
|
||||
presence_full: bool,
|
||||
}
|
||||
|
||||
/// One session's mailbox (a new session gets a new one, so a stopped session's threads can't
|
||||
@@ -64,9 +67,17 @@ impl Inbox {
|
||||
});
|
||||
}
|
||||
|
||||
/// Peer `id`'s latest presence (replaces one not taken yet).
|
||||
/// Peer `id`'s latest presence (replaces one not taken yet). Past [`MAX_PEERS`] peers waiting,
|
||||
/// another peer's is left out.
|
||||
pub fn presence(&self, id: u32, p: Presence) {
|
||||
self.with(|s| {
|
||||
if s.presence.len() >= MAX_PEERS && !s.presence.contains_key(&id) {
|
||||
if !s.presence_full {
|
||||
log::warn!("live session: presence of more than {MAX_PEERS} people; leaving the rest out");
|
||||
s.presence_full = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
s.presence.insert(id, p);
|
||||
});
|
||||
}
|
||||
@@ -75,6 +86,7 @@ impl Inbox {
|
||||
/// peer.
|
||||
pub fn drain(&self) -> Vec<figcraft_engine::live::Event> {
|
||||
let mut s = self.state.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
s.presence_full = false;
|
||||
let mut syncs = std::mem::take(&mut s.syncs).into_iter().peekable();
|
||||
let mut others = std::mem::take(&mut s.others).into_iter().peekable();
|
||||
let mut out = Vec::with_capacity(syncs.len() + others.len() + s.presence.len());
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::time::Duration;
|
||||
|
||||
use figcraft_collab::Client;
|
||||
use figcraft_collab::message::{ClientMsg, Peer, Resume, ServerMsg};
|
||||
use figcraft_engine::live::{Event, Role, SessionInfo, Status, TOO_LARGE_TO_JOIN};
|
||||
use figcraft_engine::live::{Event, MAX_PEERS, Role, SessionInfo, Status, TOO_LARGE_TO_JOIN};
|
||||
|
||||
use crate::inbox::Inbox;
|
||||
use crate::invite::Invite;
|
||||
@@ -194,7 +194,7 @@ pub(crate) fn take(m: ServerMsg, cfg: &GuestConfig, inbox: &Inbox, seen: &mut Se
|
||||
*r = Some(Resume { session, client_id, seq, secret });
|
||||
}
|
||||
// The sync client opens the document (and fills in its name and pages).
|
||||
let peers = peers.clone();
|
||||
let peers: Vec<Peer> = peers.iter().take(MAX_PEERS).cloned().collect();
|
||||
let inv = &cfg.invite;
|
||||
let info = SessionInfo {
|
||||
role: Role::Guest,
|
||||
@@ -224,7 +224,16 @@ pub(crate) fn take(m: ServerMsg, cfg: &GuestConfig, inbox: &Inbox, seen: &mut Se
|
||||
inbox.push(Event::Welcome { info: info.clone(), peers: seen.peers.clone() });
|
||||
}
|
||||
_ => {
|
||||
seen.peers.retain(|q| q.client_id != p.client_id);
|
||||
match seen.peers.iter().position(|q| q.client_id == p.client_id) {
|
||||
Some(i) => {
|
||||
seen.peers.remove(i);
|
||||
}
|
||||
None if seen.peers.len() >= MAX_PEERS => {
|
||||
log::warn!("live session: {MAX_PEERS} people already here; leaving out another");
|
||||
return Taken::Continue;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
seen.peers.push(p.clone());
|
||||
inbox.push(Event::PeerJoined(p));
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ pub use link::{DEFAULT_PORT, NetLink, lan_ip};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use web::NetLink;
|
||||
|
||||
#[cfg(test)]
|
||||
mod client_cap_tests;
|
||||
#[cfg(test)]
|
||||
mod code_tests;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
//! A session remembers at most [`MAX_DELETED`] deleted node ids for undo (#1121): a long session
|
||||
//! deleting layer after layer, or a saved state listing more, keeps the highest ids and forgets the
|
||||
//! lowest.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use figcraft_doc::{Document, Node, NodeId, NodeKind};
|
||||
|
||||
use crate::message::ClientMsg;
|
||||
use crate::server::MAX_DELETED;
|
||||
use crate::{Client, Limits, Outgoing, Server};
|
||||
|
||||
fn filled() -> BTreeSet<NodeId> {
|
||||
(1..=MAX_DELETED as u64 + 10).map(NodeId).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_restored_state_keeps_the_highest_deleted_ids() {
|
||||
let mut d = Document::new("Shared");
|
||||
d.repair();
|
||||
let mut state = Server::new(d, "tok", Limits::default()).state().unwrap();
|
||||
state.deleted = filled();
|
||||
let kept = Server::restore(state, "tok", Limits::default()).unwrap().state().unwrap().deleted;
|
||||
assert_eq!(kept.len(), MAX_DELETED);
|
||||
assert_eq!(kept.first(), Some(&NodeId(11)), "the lowest went first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_past_the_cap_forgets_the_lowest() {
|
||||
let mut d = Document::new("Shared");
|
||||
let page = d.first_page().unwrap();
|
||||
// From an earlier session's id space, above every id in `filled`.
|
||||
let frame = NodeId((7 << 32) | 1);
|
||||
d.insert(Node::new(frame, NodeKind::Frame, "Frame", 100.0, 100.0), page, None).unwrap();
|
||||
let mut state = Server::new(d, "tok", Limits::default()).state().unwrap();
|
||||
state.deleted = filled();
|
||||
let mut s = Server::restore(state, "tok", Limits::default()).unwrap();
|
||||
let ClientMsg::Hello(hello) = Client::hello("f", "tok", "Ada", "#FF8800") else { panic!("not a hello") };
|
||||
let (id, out) = s.join(hello).unwrap();
|
||||
let Some(Outgoing::To(_, welcome)) = out.into_iter().next() else { panic!("no welcome") };
|
||||
let mut c = Client::join(welcome).unwrap();
|
||||
let mut edited = c.document().clone();
|
||||
edited.remove(frame).unwrap();
|
||||
let msg = c.edit(&edited).unwrap().expect("a change");
|
||||
s.receive(id, msg);
|
||||
let deleted = s.state().unwrap().deleted;
|
||||
assert_eq!(deleted.len(), MAX_DELETED);
|
||||
assert!(deleted.contains(&frame), "the newly deleted frame is remembered");
|
||||
assert!(!deleted.contains(&NodeId(11)));
|
||||
}
|
||||
@@ -133,6 +133,8 @@ mod codec_tests;
|
||||
#[cfg(test)]
|
||||
mod comment_access_tests;
|
||||
#[cfg(test)]
|
||||
mod deleted_cap_tests;
|
||||
#[cfg(test)]
|
||||
mod diff_tests;
|
||||
#[cfg(test)]
|
||||
mod entry_ids_tests;
|
||||
|
||||
@@ -375,6 +375,11 @@ pub enum Admin {
|
||||
/// The most files one [`Admin::Owned`] asks about.
|
||||
pub const MAX_OWNED: usize = 200;
|
||||
|
||||
/// Most other people a client keeps track of in one session (their names, presence and chat).
|
||||
/// A server holds far fewer (256 by default, a larger `max_people` lowered to this): past it, someone new is
|
||||
/// left out, so a hostile server can't grow a client's memory without bound.
|
||||
pub const MAX_PEERS: usize = 4_096;
|
||||
|
||||
/// A file its owner asked about ([`Admin::Owned`]).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OwnedFile {
|
||||
|
||||
@@ -64,7 +64,7 @@ pub struct Server {
|
||||
presence: BTreeMap<u32, Presence>,
|
||||
acked: BTreeMap<u32, u64>,
|
||||
/// Deleted node ids: only these may be created again (by undo); new ids must come from the
|
||||
/// creating client's id space.
|
||||
/// creating client's id space. At most [`MAX_DELETED`].
|
||||
deleted: BTreeSet<NodeId>,
|
||||
/// Removed document-level entries (styles, variables, collections, comments): these may be
|
||||
/// put back by anyone (undo); a new one must not have an id from another client's space.
|
||||
@@ -98,6 +98,11 @@ pub struct Server {
|
||||
/// first; a forgotten client joins as new).
|
||||
const MAX_REMEMBERED: usize = 65_536;
|
||||
|
||||
/// The most deleted node ids a session remembers for undo. Past it the lowest ids go first (the
|
||||
/// earliest clients' first layers): undoing the delete of one of those is then refused, unless the
|
||||
/// layer is in the undoing client's own id space, which it may always create.
|
||||
pub(crate) const MAX_DELETED: usize = 1 << 18;
|
||||
|
||||
/// What a session server needs to carry on after a restart: the document, the sequence number,
|
||||
/// the client-id counter (so ids are never handed out twice), the clients that can reconnect,
|
||||
/// the deleted ids and removed entries that may be created again and the log of recent batches.
|
||||
@@ -178,6 +183,7 @@ impl Server {
|
||||
server.next_client = server.next_client.max(saved);
|
||||
server.issued = state.issued.into_iter().filter(|(id, _)| *id < server.next_client).collect();
|
||||
server.deleted = state.deleted;
|
||||
server.forget_deleted();
|
||||
server.removed = state.removed;
|
||||
let contiguous = state.log.windows(2).all(|w| matches!(w, [a, b] if a.seq + 1 == b.seq));
|
||||
// History saved before the image checks may hold image entries they refuse. The snapshot
|
||||
@@ -251,6 +257,13 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// Beyond [`MAX_DELETED`] deleted ids, forget the lowest.
|
||||
fn forget_deleted(&mut self) {
|
||||
while self.deleted.len() > MAX_DELETED {
|
||||
self.deleted.pop_first();
|
||||
}
|
||||
}
|
||||
|
||||
/// Beyond [`MAX_REMEMBERED`] client ids, forget the oldest one that isn't connected.
|
||||
fn forget_oldest(&mut self) {
|
||||
if self.issued.len() > MAX_REMEMBERED
|
||||
@@ -522,6 +535,7 @@ impl Server {
|
||||
}
|
||||
}
|
||||
self.deleted.extend(applied.deleted);
|
||||
self.forget_deleted();
|
||||
for (entry, now) in entries {
|
||||
if now {
|
||||
self.removed.remove(&entry);
|
||||
|
||||
@@ -373,6 +373,8 @@ mod live_access_tests;
|
||||
#[cfg(test)]
|
||||
mod live_docs_tests;
|
||||
#[cfg(test)]
|
||||
mod live_peer_cap_tests;
|
||||
#[cfg(test)]
|
||||
mod live_share_tests;
|
||||
#[cfg(test)]
|
||||
mod live_signal_tests;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//! A hostile server can't grow a client's list of people without bound (#1121): past
|
||||
//! [`MAX_PEERS`], someone new is left out (their presence and chat with them), in a welcome or one
|
||||
//! join at a time; someone leaving makes room again.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::live::{Event, MAX_PEERS, Peer, Presence, Role};
|
||||
use crate::live_tests::{info, peer, push, session};
|
||||
|
||||
fn many(from: u32, n: usize) -> Vec<Peer> {
|
||||
(from..).take(n).map(|id| peer(id, "Someone")).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_flood_of_joins_keeps_at_most_the_cap() {
|
||||
let (mut s, script) = session();
|
||||
s.execute("collab.join", &json!({"link": "figcraft://join?host=a:1&file=f&token=t"})).unwrap();
|
||||
push(&script, vec![Event::Welcome { info: info(Role::Guest, 1), peers: many(2, MAX_PEERS + 100) }]);
|
||||
s.live_poll();
|
||||
assert_eq!(s.live().peers.len(), MAX_PEERS, "a welcome listing too many");
|
||||
let first_left_out = 2 + MAX_PEERS as u32;
|
||||
let joins = many(first_left_out, 1_000).into_iter().map(Event::PeerJoined).collect();
|
||||
push(&script, joins);
|
||||
s.live_poll();
|
||||
assert_eq!(s.live().peers.len(), MAX_PEERS, "joins past the cap");
|
||||
// Presence of someone left out isn't kept either.
|
||||
push(&script, vec![Event::Presence(first_left_out, Presence::default())]);
|
||||
s.live_poll();
|
||||
assert!(!s.live().presence.contains_key(&first_left_out));
|
||||
// Someone already here may still be renamed; someone leaving makes room.
|
||||
push(&script, vec![Event::PeerJoined(peer(2, "Renamed")), Event::PeerLeft(3), Event::PeerJoined(peer(first_left_out, "Late"))]);
|
||||
s.live_poll();
|
||||
assert_eq!(s.live().peers.len(), MAX_PEERS);
|
||||
assert_eq!(s.live().peers[&2].name, "Renamed");
|
||||
assert!(s.live().peers.contains_key(&first_left_out) && !s.live().peers.contains_key(&3));
|
||||
}
|
||||
@@ -71,9 +71,9 @@ pub(crate) use host::{clean_shared, share_answer};
|
||||
pub(crate) use join::start_join;
|
||||
pub use join::{can_remove_people, specs};
|
||||
pub use types::{
|
||||
Bind, DEFAULT_SERVER, Event, HostOptions, LiveLink, MAX_NAME, MAX_SELECTION, NO_HOSTING, NO_SESSION, NetReply, NetRequest, ONLINE_OFF, PEOPLE,
|
||||
PageInfo, PublishOptions, Published, Role, SessionId, SessionInfo, Share, Shared, SharedLink, Status, TOO_LARGE_TO_JOIN, Via, join_target,
|
||||
parse_color,
|
||||
Bind, DEFAULT_SERVER, Event, HostOptions, LiveLink, MAX_NAME, MAX_PEERS, MAX_SELECTION, NO_HOSTING, NO_SESSION, NetReply, NetRequest, ONLINE_OFF,
|
||||
PEOPLE, PageInfo, PublishOptions, Published, Role, SessionId, SessionInfo, Share, Shared, SharedLink, Status, TOO_LARGE_TO_JOIN, Via,
|
||||
join_target, parse_color,
|
||||
};
|
||||
|
||||
/// The view a follow last applied: any other view means the user moved.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::host::{clean_hosting, clean_shared};
|
||||
use super::types::{Event, PageInfo, Role, SessionId, Status};
|
||||
use super::types::{Event, MAX_PEERS, PageInfo, Role, SessionId, SessionInfo, Status};
|
||||
use super::{
|
||||
Access, Arc, Client, ClientMsg, DocState, Document, EngineError, Live, NodeId, PUBLISHING, Peer, Presence, Result, ServerMsg, Session, Signal,
|
||||
ToolId, display_name, editing_hint, editing_of, limited_by, sanitize, tool_left,
|
||||
@@ -27,6 +27,24 @@ fn reset_for_reconnect(live: &mut Live) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Joined, or joined again: who you are and who else is here (at most [`MAX_PEERS`] of them).
|
||||
fn welcome_in(live: &mut Live, mut info: SessionInfo, peers: Vec<Peer>) {
|
||||
// The shared file's name and pages come with the document (the sync welcome).
|
||||
if let Some(old) = live.info.as_ref().filter(|_| info.file_name.is_empty()) {
|
||||
info.file_name = old.file_name.clone();
|
||||
info.pages = old.pages.clone();
|
||||
}
|
||||
live.status = Status::Live;
|
||||
live.info = Some(info);
|
||||
live.peers = peers.into_iter().take(MAX_PEERS).map(|p| (p.client_id, p)).collect();
|
||||
live.presence.retain(|id, _| live.peers.contains_key(id));
|
||||
if live.follow.is_some_and(|f| !live.peers.contains_key(&f)) {
|
||||
live.unfollow();
|
||||
}
|
||||
// Others need your presence again.
|
||||
live.sent = None;
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// The active document (`None`: no document is open).
|
||||
pub(super) fn tab(&self) -> Option<u64> {
|
||||
@@ -160,25 +178,14 @@ impl Session {
|
||||
}
|
||||
live.status = st;
|
||||
}
|
||||
Event::Welcome { mut info, peers } => {
|
||||
// The shared file's name and pages come with the document (the sync welcome).
|
||||
if let Some(old) = live.info.as_ref().filter(|_| info.file_name.is_empty()) {
|
||||
info.file_name = old.file_name.clone();
|
||||
info.pages = old.pages.clone();
|
||||
}
|
||||
live.status = Status::Live;
|
||||
live.info = Some(info);
|
||||
live.peers = peers.into_iter().map(|p| (p.client_id, p)).collect();
|
||||
live.presence.retain(|id, _| live.peers.contains_key(id));
|
||||
if live.follow.is_some_and(|f| !live.peers.contains_key(&f)) {
|
||||
live.unfollow();
|
||||
}
|
||||
// Others need your presence again.
|
||||
live.sent = None;
|
||||
}
|
||||
Event::Welcome { info, peers } => welcome_in(live, info, peers),
|
||||
Event::PeerJoined(p) => {
|
||||
// Someone already here was renamed: no newcomer to announce.
|
||||
if !live.peers.contains_key(&p.client_id) {
|
||||
if live.peers.len() >= MAX_PEERS {
|
||||
log::warn!("live session: {MAX_PEERS} people already here; leaving out another");
|
||||
return;
|
||||
}
|
||||
self.message = Some(format!("{} joined{behind}", p.name));
|
||||
}
|
||||
live.peers.insert(p.client_id, p);
|
||||
|
||||
@@ -17,6 +17,8 @@ pub const DEFAULT_SERVER: &str = match option_env!("FIGCRAFT_DEFAULT_SERVER") {
|
||||
/// Most selected ids one presence update carries (the rest aren't shown to others).
|
||||
pub const MAX_SELECTION: usize = 10_000;
|
||||
|
||||
pub use figcraft_collab::message::MAX_PEERS;
|
||||
|
||||
/// Longest name shown to others (characters).
|
||||
pub const MAX_NAME: usize = Signal::MAX_NAME;
|
||||
|
||||
|
||||
@@ -955,6 +955,12 @@ chooses to share on the network; the default is the invite link with a random to
|
||||
characters; presence numbers must be finite and selections are cut to 10,000 ids; signals are
|
||||
at most 1 KiB, chat 100 characters without control characters, 20 a second (bursts of 40,
|
||||
the rest dropped), and only known kinds are passed on.
|
||||
- **A hostile host or server.** A client keeps track of at most 4,096 other people (their presence
|
||||
waiting for the UI, the people a connection has seen, the session's list of who is here); past
|
||||
that, someone new is left out with a logged warning, and someone leaving makes room. Events
|
||||
waiting for the UI are capped at 10,000 (document batches excepted: dropping one would end the
|
||||
session). The session core remembers at most 262,144 deleted layer ids for undo, forgetting the
|
||||
lowest first; undoing the delete of a forgotten layer outside your own id space is refused.
|
||||
- **Malformed input.** Anything that isn't a binary message of the current wire version that
|
||||
decodes in full is answered with `Refused` and a close; the first message must be `Hello` (for
|
||||
this file) or, on a server, `Manage`. The invite parser and the host's reader are fuzzed
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -291,7 +291,7 @@ are errors):
|
||||
| `max_file_mb` | 64 (16 when open) | Largest file (its snapshot), in MiB; a welcome carries the whole file, so messages may be as large as a file and 1 MiB (at least 64 MiB on a private server). The app opens files of up to 1023 MiB; above 64 with `open_publish` the server warns |
|
||||
| `max_links` | 100 | Most share links per file |
|
||||
| `max_connections` | 512 | Most open connections |
|
||||
| `max_people` | 256 | Most people in one file at once |
|
||||
| `max_people` | 256 | Most people in one file at once (a value above 4096 is lowered to it, with a warning at startup: clients keep track of no more) |
|
||||
| `compact_after` | 1000 | Compact a file's log into a new snapshot after this many entries… |
|
||||
| `compact_mb` | 16 | …or once its log is this large (MiB) |
|
||||
| `max_log_mb` | 64 | Most MiB of recent batches a file in use keeps in memory (and in its snapshot) for apps that reconnect; an app further behind catches up with the whole file. Open to the public, at most a quarter of `max_file_mb` too |
|
||||
|
||||
Reference in New Issue
Block a user