Give reports a switch, and say in discovery which way it is set
FN_CONTENT_REPORTS, on unless explicitly set to false/0/no/off. On by default because without it blocking is the only thing somebody being harassed can do, which is the state this feature exists to leave. Off is a real answer, not a broken relay: a self-hoster with nobody to read reports should be able to say so, rather than collect a queue nobody opens. Two things make that honest rather than silent: - `/.well-known/flamenet/relay` now carries `reports`, so a client can leave the Report option out of the interface instead of offering a button that fails. Additive and safe for the build in the field — v8 parses the descriptor with JSONObject.optString and ignores keys it does not know; checked rather than assumed. An older relay omits the key, and reading absent as yes is correct: every relay predating the switch takes reports. - The route refuses with `reports_not_accepted` (501), never a 404, so a client can tell "does not take reports" from "too old to know what a report is". One is a setting; the other is a reason to stop asking. Tests: on with nothing configured; off refuses with the code and writes nothing; discovery advertises both states; and only an explicit no disables it, so a typo cannot quietly stop a relay taking reports. 482 pass.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED by Scripts/build-doc-pages.sh from docs/CONFIGURATION.md.
|
||||
Edits made HERE are deleted the next time that script runs.
|
||||
Change the markdown source and re-run it.
|
||||
source-sha256: 41e0e20cc4e1f529 -->
|
||||
source-sha256: e592ce3fe1f1b4fe -->
|
||||
<article class="band doc doc-generated">
|
||||
<div class="wrap wrap-narrow">
|
||||
<h1 id="configuring-a-relay">Configuring a relay</h1>
|
||||
@@ -375,6 +375,41 @@ withholding one stops a wipe rather than causing one. When off, both
|
||||
routes refuse with <code>wipe_not_enabled</code> rather than a 404, so a
|
||||
client can tell a server that has this switched off from one too old to
|
||||
know what it is.</p>
|
||||
<h2 id="reports-about-content">Reports about content</h2>
|
||||
<p>On. A person can report something to whoever runs the server, so
|
||||
blocking is not the only thing they can do.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Default</th>
|
||||
<th>What it does</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>FN_CONTENT_REPORTS</code></td>
|
||||
<td>on</td>
|
||||
<td>Set to <code>false</code>, <code>0</code>, <code>no</code> or
|
||||
<code>off</code> to stop taking reports. Anything else, including a
|
||||
typo, leaves them on.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Off is a legitimate answer, not a broken relay. If nobody is going to
|
||||
read them, collecting reports is a promise the server cannot keep, and
|
||||
saying so plainly is better than a queue nobody opens.</p>
|
||||
<p>When off, two things happen so that nothing pretends otherwise:
|
||||
<code>/.well-known/flamenet/relay</code> reports
|
||||
<code>"reports": false</code>, which lets a client leave the Report
|
||||
option out of the interface entirely; and the route itself refuses with
|
||||
<code>reports_not_accepted</code> rather than a 404, so a client can
|
||||
tell "this server does not take reports" apart from "this server is too
|
||||
old to know what a report is".</p>
|
||||
<p>There is deliberately no route that <strong>reads</strong> reports.
|
||||
They are read out of band by whoever operates the server — an endpoint
|
||||
returning them would be a way to ask the relay what somebody reported,
|
||||
which is a question nobody but the operator gets to ask.</p>
|
||||
<h2 id="retention">Retention</h2>
|
||||
<p>The relay purges on its own timer, in-process. Nothing external is
|
||||
needed.</p>
|
||||
|
||||
@@ -147,6 +147,17 @@ struct Config: Sendable {
|
||||
/// carrying it is small. The reason to keep it off is honesty about what
|
||||
/// works, not fear of the route.
|
||||
let deviceWipeEnabled: Bool
|
||||
/// Whether this relay accepts reports about content (§6.25).
|
||||
///
|
||||
/// **On by default.** Without it, blocking is the only thing a person being
|
||||
/// harassed can do, and that is the state this feature exists to leave.
|
||||
///
|
||||
/// ⚠️ Off is a real answer, not a broken one. A self-hoster with nobody to
|
||||
/// read reports should be able to say so — collecting reports that will
|
||||
/// never be looked at is a promise the relay cannot keep. When off, the
|
||||
/// discovery document says so, so clients can leave the affordance out
|
||||
/// rather than offer a button that fails.
|
||||
let contentReportsEnabled: Bool
|
||||
let attachmentBytesPerHour: Int
|
||||
/// The rolling 30-day per-account upload budget (C3). The hourly limits
|
||||
/// above stop bursts; this stops a steady drip from quietly owning the
|
||||
@@ -284,6 +295,11 @@ struct Config: Sendable {
|
||||
let raw = (env["FN_DEVICE_WIPE"] ?? "").lowercased()
|
||||
return raw == "true" || raw == "1" || raw == "yes"
|
||||
}(),
|
||||
contentReportsEnabled: {
|
||||
// On unless explicitly switched off.
|
||||
let raw = (env["FN_CONTENT_REPORTS"] ?? "").lowercased()
|
||||
return !(raw == "false" || raw == "0" || raw == "no" || raw == "off")
|
||||
}(),
|
||||
attachmentBytesPerHour: int("FN_ATTACHMENT_BYTES_RATE", 200 * 1024 * 1024),
|
||||
attachmentBytesPerMonth: int("FN_ATTACHMENT_BYTES_PER_MONTH", 500 * 1024 * 1024),
|
||||
attachmentRetentionDays: int("FN_ATTACHMENT_RETENTION_DAYS", 30),
|
||||
|
||||
@@ -70,6 +70,13 @@ extension Routes {
|
||||
let accounts: String
|
||||
/// Absolute URL of that account service, when there is one.
|
||||
let accountsURL: String?
|
||||
/// Whether this relay takes reports about content (§6.25).
|
||||
///
|
||||
/// Advertised so a client can leave the Report affordance out entirely
|
||||
/// rather than offer a button that fails. An older relay omits the key,
|
||||
/// and a client reading it as absent-means-yes is right: every relay
|
||||
/// that predates the switch does accept reports.
|
||||
let reports: Bool
|
||||
}
|
||||
|
||||
/// `GET /.well-known/flamenet/relay`
|
||||
@@ -91,7 +98,8 @@ extension Routes {
|
||||
: (try? Curve25519.Signing.PrivateKey(rawRepresentation: config.federationKeyRaw ?? Data()))
|
||||
.map { $0.publicKey.rawRepresentation.base64EncodedString() },
|
||||
accounts: shared ? "shared" : "local",
|
||||
accountsURL: shared ? Self.accountsURL : nil
|
||||
accountsURL: shared ? Self.accountsURL : nil,
|
||||
reports: config.contentReportsEnabled
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,16 @@ extension Routes {
|
||||
/// that told its subject who filed it would be most dangerous to exactly
|
||||
/// the people most likely to need it.
|
||||
func submitReport(_ request: Request, now: Int) async throws -> Response {
|
||||
// ⚠️ Refused with a code, not a 404. A relay that has switched reports
|
||||
// off is a working relay that does not take them, and a client needs to
|
||||
// tell that apart from an old relay that has never heard of the route —
|
||||
// one is a setting, the other is a reason to stop asking. The
|
||||
// discovery document carries the same fact, so a client that reads it
|
||||
// never has to find out this way.
|
||||
guard config.contentReportsEnabled else {
|
||||
throw WireError("reports_not_accepted",
|
||||
"Whoever runs this server does not take reports.", 501)
|
||||
}
|
||||
let body = request.body.data.map { Data(buffer: $0) } ?? Data()
|
||||
let caller = try await authenticate(
|
||||
request, body: body, path: "/e2e/moderation/report", now: now)
|
||||
|
||||
@@ -153,4 +153,74 @@ struct ContentReportTests {
|
||||
#expect(readRoutes.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The switch
|
||||
|
||||
/// On by default: an operator who has configured nothing still takes
|
||||
/// reports, because the alternative leaves blocking as the only recourse.
|
||||
@Test("reports are accepted with nothing configured")
|
||||
func onByDefault() async throws {
|
||||
try await withRelay { h in
|
||||
let d = h.device(user: 1, id: "aaaa-0001")
|
||||
_ = try await h.register(d, on: h.db)
|
||||
#expect(try await file(h, as: d, body(about: 2)).status == .ok)
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔴 Off is refused with a code, not a 404.
|
||||
///
|
||||
/// A relay that has switched reports off is a working relay that does not
|
||||
/// take them. A client has to tell that apart from an old relay that never
|
||||
/// had the route: one is a setting, the other is a reason to stop asking.
|
||||
@Test("switched off, a report is refused in a way a client can read")
|
||||
func offRefusesWithACode() async throws {
|
||||
try await withRelay(env: ["FN_CONTENT_REPORTS": "false"]) { h in
|
||||
let d = h.device(user: 1, id: "aaaa-0001")
|
||||
_ = try await h.register(d, on: h.db)
|
||||
do {
|
||||
_ = try await self.file(h, as: d, self.body(about: 2))
|
||||
Issue.record("a report was accepted on a relay that does not take them")
|
||||
} catch let error as WireError {
|
||||
#expect(error.code == "reports_not_accepted")
|
||||
#expect(error.data.status == 501)
|
||||
}
|
||||
// And nothing was written.
|
||||
#expect(try await ContentReport.query(on: h.db).count() == 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The discovery document carries the same fact, so a client can leave the
|
||||
/// affordance out instead of finding out by failing.
|
||||
@Test("the relay says in discovery whether it takes reports")
|
||||
func discoveryAdvertisesIt() async throws {
|
||||
try await withRelay { h in
|
||||
let on = try await h.routes.relayDescriptor(h.anonymousRequest(.GET, "/.well-known/flamenet/relay"))
|
||||
#expect(String(buffer: on.body.buffer!).contains("\"reports\":true"))
|
||||
}
|
||||
try await withRelay(env: ["FN_CONTENT_REPORTS": "false"]) { h in
|
||||
let off = try await h.routes.relayDescriptor(h.anonymousRequest(.GET, "/.well-known/flamenet/relay"))
|
||||
#expect(String(buffer: off.body.buffer!).contains("\"reports\":false"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Only an explicit no turns it off — a typo must not silently stop the
|
||||
/// relay taking reports.
|
||||
@Test("only an explicit no switches reports off")
|
||||
func onlyExplicitNoDisables() throws {
|
||||
func flag(_ v: String?) throws -> Bool {
|
||||
var env = [
|
||||
"FN_ISSUER_PUBLIC_KEY": Curve25519.Signing.PrivateKey()
|
||||
.publicKey.rawRepresentation.base64EncodedString(),
|
||||
"FN_RELAY_AUDIENCE": "test.relay",
|
||||
]
|
||||
if let v { env["FN_CONTENT_REPORTS"] = v }
|
||||
return try Config.fromEnvironment(env).contentReportsEnabled
|
||||
}
|
||||
for on in [nil, "", "true", "yes", "1", "flase", "disabled"] {
|
||||
#expect(try flag(on) == true, "\(on ?? "unset") should leave reports on")
|
||||
}
|
||||
for off in ["false", "FALSE", "0", "no", "off"] {
|
||||
#expect(try flag(off) == false, "\(off) should switch reports off")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,19 @@ Off. The relay does not carry instructions for a device to erase itself.
|
||||
Off because the feature is not finished end to end, not because the route is dangerous. No client in the field can honour a wipe — the Android build released on 2026-08-31 (v8) never asks for one — and the issuing side still needs the passphrase re-entered before an order can be signed. A half-built remote erase that is on by default looks like a guarantee and is not one yet.
|
||||
|
||||
The relay is only ever a courier. The instruction is signed by the account key, which the relay does not hold, so it cannot forge one; and withholding one stops a wipe rather than causing one. When off, both routes refuse with `wipe_not_enabled` rather than a 404, so a client can tell a server that has this switched off from one too old to know what it is.
|
||||
## Reports about content
|
||||
|
||||
On. A person can report something to whoever runs the server, so blocking is not the only thing they can do.
|
||||
|
||||
| Variable | Default | What it does |
|
||||
|---|---|---|
|
||||
| `FN_CONTENT_REPORTS` | on | Set to `false`, `0`, `no` or `off` to stop taking reports. Anything else, including a typo, leaves them on. |
|
||||
|
||||
Off is a legitimate answer, not a broken relay. If nobody is going to read them, collecting reports is a promise the server cannot keep, and saying so plainly is better than a queue nobody opens.
|
||||
|
||||
When off, two things happen so that nothing pretends otherwise: `/.well-known/flamenet/relay` reports `"reports": false`, which lets a client leave the Report option out of the interface entirely; and the route itself refuses with `reports_not_accepted` rather than a 404, so a client can tell "this server does not take reports" apart from "this server is too old to know what a report is".
|
||||
|
||||
There is deliberately no route that **reads** reports. They are read out of band by whoever operates the server — an endpoint returning them would be a way to ask the relay what somebody reported, which is a question nobody but the operator gets to ask.
|
||||
|
||||
## Retention
|
||||
|
||||
|
||||
Reference in New Issue
Block a user