Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ Joins or refreshes a peer.
}
```

`id` is at most 128 characters without whitespace or control characters, and
`address` at most 2048 characters; both are sent to other peers in every peer
list.

Returns `201` for a new peer or `200` for an authenticated refresh:

```json
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ All notable changes to OpenStreamGrid are documented in this file.

### Changed

- Peer joins reject IDs over 128 characters or containing whitespace or control
characters, and addresses over 2048 characters. Both were bounded only by
the 1 MB body limit and are repeated in every peer list and inventory update.
- Each peer may report at most 16 K characters of segment IDs per second
(bursts of 256 K), across WebSocket and REST; beyond that its WebSocket is
closed with `1008` and REST reports get `429`. REST segment reports also get
Expand Down
27 changes: 25 additions & 2 deletions tracker/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,29 @@ const readJson = async (request: IncomingMessage): Promise<JsonObject> => {
}
};

/**
* Peer IDs and addresses appear in every peer list and in each inventory
* update about the peer, so they are kept to identifier and URL sizes.
*/
const MAX_PEER_ID_LENGTH = 128;
const MAX_PEER_ADDRESS_LENGTH = 2_048;
const PEER_ID_PATTERN = /^[^\s\p{Cc}]+$/u;

const boundedString = (value: string, key: string, maxLength: number): string => {
if (value.length > maxLength) {
throw new RequestError(`'${key}' must be at most ${maxLength} characters`);
}
return value;
};

const peerIdentifier = (value: string): string => {
boundedString(value, "id", MAX_PEER_ID_LENGTH);
if (!PEER_ID_PATTERN.test(value)) {
throw new RequestError("'id' must not contain whitespace or control characters");
}
return value;
};

const requiredString = (body: JsonObject, key: string): string => {
const value = body[key];
if (typeof value !== "string" || value.trim() === "") {
Expand Down Expand Up @@ -1261,8 +1284,8 @@ export const createTrackerHandler = (
);
const metadata = optionalMetadata(body);
const join: PeerJoinRequest = {
id: requiredString(body, "id"),
address: requiredString(body, "address"),
id: peerIdentifier(requiredString(body, "id")),
address: boundedString(requiredString(body, "address"), "address", MAX_PEER_ADDRESS_LENGTH),
...(uploadBandwidthBps !== undefined ? { uploadBandwidthBps } : {}),
...(metadata ? { metadata } : {}),
};
Expand Down
24 changes: 24 additions & 0 deletions tracker/test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2180,3 +2180,27 @@ test("validates and rate-limits REST segment reports with the WebSocket budget",
now = 60_000;
assert.equal((await report(["v".repeat(500)])).status, 200);
});

test("bounds peer IDs and addresses on join", async () => {
const handler = createTrackerHandler(new TrackerStore());
await invoke(handler, "POST", "/api/v1/broadcasts", {
id: "live",
playlistUrl: "http://origin/live.m3u8",
});
const join = (id: string, address = `http://peer:9090`) =>
invoke(handler, "POST", "/api/v1/broadcasts/live/peers", { id, address });

assert.equal((await join("peer-a.example_01:x")).status, 201);
assert.equal((await join("p".repeat(128), "http://peer-b:9090")).status, 201);
const cases: Array<[string, string, string]> = [
["p".repeat(129), "http://peer-c:9090", "'id' must be at most 128 characters"],
["peer c", "http://peer-c:9090", "'id' must not contain whitespace or control characters"],
["peer\u0000c", "http://peer-c:9090", "'id' must not contain whitespace or control characters"],
["peer-c", `http://peer-c:9090/${"a".repeat(2_048)}`, "'address' must be at most 2048 characters"],
];
for (const [id, address, error] of cases) {
const response = await join(id, address);
assert.equal(response.status, 400, error);
assert.deepEqual(response.json, { error });
}
});
Loading