mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 02:16:56 -06:00
Merge fix/705
This commit is contained in:
@@ -135,9 +135,23 @@ impl Config {
|
||||
/// abuse contact, or one whose clients would all count as its proxy).
|
||||
pub fn warnings(&self) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let client_max = NetLimits::default().max_server_message >> 20;
|
||||
if usize::try_from(self.file_mb()).is_ok_and(|mb| mb >= client_max) {
|
||||
out.push(format!(
|
||||
"max_file_mb is {}: the app opens files of up to {} MiB, so a file that large can't be opened",
|
||||
self.file_mb(),
|
||||
client_max - 1
|
||||
));
|
||||
}
|
||||
if !self.open_publish {
|
||||
return out;
|
||||
}
|
||||
if self.file_mb() > 64 {
|
||||
out.push(format!(
|
||||
"open_publish is on with max_file_mb = {}: anyone may send messages that large, and the memory set aside for reading and queueing them grows to eight times it; keep it at 64 or less on a public server",
|
||||
self.file_mb()
|
||||
));
|
||||
}
|
||||
if self.operator_contact.trim().is_empty() {
|
||||
out.push("open_publish is on but operator_contact is empty: people can't reach you about abuse; set it (and terms_url)".into());
|
||||
}
|
||||
@@ -253,8 +267,9 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Largest message a client may send when open to the public: a whole file (or plugin bundle)
|
||||
/// and a margin, not the core's 64 MiB.
|
||||
/// Largest message a client may send: a whole file (or plugin bundle) and a margin. Open to the
|
||||
/// public this is also the cap (strangers' files stay small); a private server never goes below
|
||||
/// the core's 64 MiB, and a `max_file_mb` above it raises the cap with it.
|
||||
fn open_message(&self) -> usize {
|
||||
usize::try_from(self.file_mb().max(self.max_plugin_mb).saturating_add(1).saturating_mul(1 << 20)).unwrap_or(usize::MAX)
|
||||
}
|
||||
@@ -265,9 +280,9 @@ impl Config {
|
||||
fn core(&self) -> figcraft_collab::Limits {
|
||||
let base = figcraft_collab::Limits { max_peers: self.max_people, ..figcraft_collab::Limits::default() };
|
||||
if !self.open_publish {
|
||||
return base;
|
||||
return figcraft_collab::Limits { max_message_bytes: base.max_message_bytes.max(self.open_message()), ..base };
|
||||
}
|
||||
let max = self.open_message().min(base.max_message_bytes);
|
||||
let max = self.open_message();
|
||||
let file = usize::try_from(self.file_mb().saturating_mul(1 << 20)).unwrap_or(usize::MAX);
|
||||
figcraft_collab::Limits { max_message_bytes: max, max_log_bytes: (file / 4).max(256 << 10), ..base }
|
||||
}
|
||||
@@ -279,21 +294,24 @@ impl Config {
|
||||
pub fn net(&self) -> NetLimits {
|
||||
let base = NetLimits { max_connections: self.max_connections, trusted_proxy: self.proxy().ok().flatten(), ..NetLimits::default() };
|
||||
if !self.open_publish {
|
||||
return base;
|
||||
let max = base.max_client_message.max(self.open_message());
|
||||
return NetLimits { max_client_message: max, max_queued_bytes: base.max_queued_bytes.max(max.saturating_add(1 << 20)), ..base };
|
||||
}
|
||||
let max = self.open_message().min(base.max_client_message);
|
||||
let max = self.open_message();
|
||||
// Shared budgets: one block of addresses (an IPv4 address, an IPv6 /48) gets at most an
|
||||
// eighth of each, and its biggest consumers are dropped first.
|
||||
let (queued, reading) = (256usize << 20, 192usize << 20);
|
||||
// eighth of each, and its biggest consumers are dropped first. Each still fits one whole
|
||||
// message, or a file as large as `max_file_mb` could never arrive.
|
||||
let one = max.saturating_add(1 << 20);
|
||||
let (queued, reading) = ((256usize << 20).max(one.saturating_mul(8)), (192usize << 20).max(one.saturating_mul(8)));
|
||||
NetLimits {
|
||||
max_per_address: self.max_connections_per_ip,
|
||||
max_client_message: max,
|
||||
max_queued_bytes: max.saturating_add(1 << 20),
|
||||
max_queued_bytes: one,
|
||||
max_queued_block: queued / 8,
|
||||
max_queued_total: queued,
|
||||
max_reading_block: max.saturating_add(1 << 20).min(reading / 8),
|
||||
max_reading_block: one.min(reading / 8),
|
||||
max_reading_total: reading,
|
||||
max_in_flight: 128 << 20,
|
||||
max_in_flight: (128usize << 20).max(one),
|
||||
reserve_for_sessions: self.max_connections / 8,
|
||||
request_idle: std::time::Duration::from_secs(20),
|
||||
..base
|
||||
|
||||
@@ -413,7 +413,7 @@ fn open_mode_has_safe_defaults_and_warns_the_operator() {
|
||||
assert_eq!(open.net().request_idle, Duration::from_secs(20), "idle one-shot requests go quickly");
|
||||
assert_eq!(open.net().reserve_for_sessions, 512 / 8);
|
||||
assert_eq!(open.net().max_client_message, 17 << 20);
|
||||
assert_eq!(private.net().max_client_message, NetLimits::default().max_client_message, "unchanged when private");
|
||||
assert_eq!(private.net().max_client_message, 65 << 20, "private: a 64 MiB file and a margin");
|
||||
let w = open.warnings().join("\n");
|
||||
assert!(w.contains("operator_contact is empty") && w.contains("trusted_proxy"), "{w}");
|
||||
let set = Config { operator_contact: "abuse@example.com".into(), trusted_proxy: Some("127.0.0.1".into()), max_file_mb: Some(8), ..open };
|
||||
@@ -493,3 +493,34 @@ fn open_connections_never_reach_what_needs_the_key() {
|
||||
assert!(!format!("{info:?}").contains(&k));
|
||||
server.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_above_64_mib_fit_every_limit() {
|
||||
// `max_file_mb` may go up to 4096: every cap a whole file's message meets on its way in must
|
||||
// follow it, or the upload is dropped by the transport before the server can say why.
|
||||
for open_publish in [false, true] {
|
||||
let cfg = Config { open_publish, max_file_mb: Some(256), public_url: Some("wss://figcraft.example.com/".into()), ..Config::default() };
|
||||
let (net, core, file) = (cfg.net(), cfg.store().core, 256usize << 20);
|
||||
assert!(net.max_client_message > file, "open={open_publish}: {}", net.max_client_message);
|
||||
assert!(net.max_queued_bytes > file && net.max_reading_block > file && net.max_in_flight > file, "open={open_publish}: {net:?}");
|
||||
assert!(net.max_reading_total >= net.max_reading_block && net.max_queued_total >= net.max_queued_block);
|
||||
assert!(core.max_message_bytes > file, "open={open_publish}: {}", core.max_message_bytes);
|
||||
}
|
||||
// A client takes such a file too, but caps what any server may make it buffer at 1 GiB.
|
||||
assert_eq!(NetLimits::default().max_server_message, 1 << 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_files_warn_the_operator() {
|
||||
let public_url = Some("wss://figcraft.example.com/".into());
|
||||
let warns = |open_publish, mb| {
|
||||
Config { open_publish, max_file_mb: Some(mb), public_url: public_url.clone(), ..Config::default() }.warnings().join(
|
||||
"
|
||||
",
|
||||
)
|
||||
};
|
||||
assert!(!warns(true, 64).contains("max_file_mb"), "{}", warns(true, 64));
|
||||
assert!(warns(true, 65).contains("open_publish is on with max_file_mb = 65"), "{}", warns(true, 65));
|
||||
assert!(!warns(false, 1023).contains("max_file_mb"), "{}", warns(false, 1023));
|
||||
assert!(warns(false, 1024).contains("can't be opened"), "{}", warns(false, 1024));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ pub struct NetLimits {
|
||||
/// before: its hello.
|
||||
pub max_client_message: usize,
|
||||
pub max_hello: usize,
|
||||
/// Largest message a client accepts from the host (a welcome carries the whole document).
|
||||
/// Largest message a client accepts from the host (a welcome carries the whole document): 1 GiB,
|
||||
/// room for any file up to 1023 MiB and well past the servers' defaults, while a hostile server
|
||||
/// (any link can name one) can't make the app or a browser tab buffer more than that.
|
||||
pub max_server_message: usize,
|
||||
/// Messages a client may send per second on average, and in a burst; beyond that it is
|
||||
/// disconnected.
|
||||
@@ -61,7 +63,7 @@ impl Default for NetLimits {
|
||||
max_connections: 64,
|
||||
max_client_message: figcraft_collab::Limits::default().max_message_bytes,
|
||||
max_hello: 64 << 10,
|
||||
max_server_message: figcraft_collab::Limits::default().max_message_bytes,
|
||||
max_server_message: 1 << 30,
|
||||
rate: 100.0,
|
||||
burst: 400.0,
|
||||
hello_timeout: Duration::from_secs(10),
|
||||
|
||||
+2
-2
@@ -282,7 +282,7 @@ are errors):
|
||||
| `data` | `figcraft-data` | The data directory |
|
||||
| `public_url` | (the listen address) | How links name the server, when people reach it another way (a reverse proxy: `wss://figcraft.example.com/`) |
|
||||
| `max_files` | 1000 | Most files kept |
|
||||
| `max_file_mb` | 64 (16 when open) | Largest file (its snapshot), in MiB; a welcome carries the whole file, and messages are at most 64 MiB |
|
||||
| `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 |
|
||||
@@ -490,7 +490,7 @@ are larger.
|
||||
|
||||
| Resource | Limit |
|
||||
|---|---|
|
||||
| Message size | A file's size and 1 MiB (`max_file_mb`, 16 by default when open, or `max_plugin_mb` if larger): checked as the message is read, before anything is decoded; 64 KiB on a connection that only browses plugins (a client sending more is disconnected with the limit as the reason; the app keeps its own copy). The core's 64 MiB otherwise |
|
||||
| Message size | A file's size and 1 MiB (`max_file_mb`, 16 by default when open, or `max_plugin_mb` if larger): checked as the message is read, before anything is decoded; 64 KiB on a connection that only browses plugins (a client sending more is disconnected with the limit as the reason; the app keeps its own copy). On a private server, the larger of that and the core's 64 MiB |
|
||||
| A file as it grows | Every batch is checked first (*always*): past `max_file_mb`, past its publisher's `max_mb_per_ip` or past `max_total_mb`, it is refused with the reason and the app rolls it back. A long log is compacted to make room at most once a minute per file (a compaction rewrites the whole file). A file's snapshot also holds the recent batches kept for reconnecting clients, at most a quarter of `max_file_mb` |
|
||||
| Recent batches in memory | A quarter of `max_file_mb` (256 KiB at least) per open file; older ones are dropped (a client that missed them catches up with the whole file) |
|
||||
| Bytes queued for slow clients | The largest message and 1 MiB per connection, 32 MiB per block of addresses, 256 MiB for everyone. A message sent to many people counts once for everyone and once per block (twenty colleagues behind one office address cost their block one copy of a broadcast), and welcomes (bounded by the file's size) don't count against blocks, so a whole office can join a file at once. A client that doesn't read its answers is dropped; when everyone's budget runs out, the connection holding the most is dropped |
|
||||
|
||||
Reference in New Issue
Block a user