Files
buzz/mobile/lib/shared/relay/relay_websocket.dart
Will Pfleger 65e864301d feat: send a User-Agent on relay WebSocket upgrades (#9246)
Relay WebSocket clients in this repo now send a `User-Agent` of the form
`<name>/<version>` on the upgrade request. The version comes from the
build.

| Client | User-Agent |
|---|---|
| `buzz-acp` | `buzz-acp/<crate version>` |
| `buzz-cli` | `buzz-cli/<crate version>` |
| `buzz-ws-client` (default) | `buzz-ws-client/<crate version>` |
| `buzz-pairing-cli` | `buzz-pairing-cli/<crate version>` |
| `buzz-audio-client` (default) | `buzz-audio-client/<crate version>` |
| mobile (relay and huddle sockets) | `Buzz-Mobile/<app version>` |

- **`buzz-ws-client`**: every connect path sends the default. A
`User-Agent` in the caller's `connect_with_headers` headers replaces it,
so other consumers can identify themselves without any new API.
`buzz-cli` uses this to send its own name.
- **`buzz-audio-client`**: keeps a `User-Agent` already on the
host-supplied request, and adds its default otherwise.
- **Mobile**: `openRelayWebSocket` sets the HTTP client's `userAgent`,
which also covers the huddle transport. Startup waits up to 500 ms for
the app version from `package_info_plus` before the app runs, so the
first socket carries it; if the lookup fails or times out, the value is
`Buzz-Mobile`.

Each client has a test asserting the header on the upgrade request.

---------

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
2026-11-08 20:43:55 -05:00

87 lines
2.8 KiB
Dart

import 'dart:async';
import 'dart:io';
/// Bounds how long a relay WebSocket may take to connect and upgrade.
///
/// The OS gives up on a stalled TCP or TLS handshake only after minutes, and
/// reconnecting starts only once this attempt fails, so an unbounded attempt
/// leaves the app reconnecting for as long as the network stays stalled.
const relayWebSocketConnectTimeout = Duration(seconds: 10);
/// `User-Agent` sent on every relay WebSocket upgrade; [setRelayUserAgent]
/// adds the app version once it is known.
String relayUserAgent = 'Buzz-Mobile';
void setRelayUserAgent(String version) =>
relayUserAgent = 'Buzz-Mobile/$version';
/// Sets [relayUserAgent] from [lookupVersion] before any socket opens, giving
/// up after [timeout] so a hung or failed lookup cannot stall launch.
Future<void> loadRelayUserAgent(
Future<String> Function() lookupVersion, {
Duration timeout = const Duration(milliseconds: 500),
}) async {
try {
setRelayUserAgent(await lookupVersion().timeout(timeout));
} catch (_) {
// Keep the unversioned fallback.
}
}
/// WebSocket's default HTTP client follows redirects, including custom headers.
/// Keep key-bound assertions at their original origin and bound upgrade time.
///
/// Each attempt gets its own HTTP client so a timed-out handshake can be
/// force-closed instead of left running until the OS gives up on it.
Future<WebSocket> openRelayWebSocket(
Uri uri, {
required Map<String, String> headers,
required Duration pingInterval,
Duration connectTimeout = relayWebSocketConnectTimeout,
}) async {
final HttpClient client = headers.isEmpty ? HttpClient() : _UpgradeClient();
client.userAgent = relayUserAgent;
try {
return await WebSocket.connect(
uri.toString(),
headers: headers.isEmpty ? null : headers,
customClient: client,
)
.then((socket) => socket..pingInterval = pingInterval)
.timeout(
connectTimeout,
onTimeout: () {
client.close(force: true);
throw TimeoutException('Relay connection timed out');
},
);
} finally {
client.close();
}
}
/// WebSocket.connect uses only openUrl on its supplied HTTP client. Keep this
/// adapter limited to upgrades instead of exposing a second general transport.
class _UpgradeClient implements HttpClient {
final _inner = HttpClient();
@override
Future<HttpClientRequest> openUrl(String method, Uri url) async {
final request = await _inner.openUrl(method, url);
request.followRedirects = false;
return request;
}
@override
String? get userAgent => _inner.userAgent;
@override
set userAgent(String? value) => _inner.userAgent = value;
@override
void close({bool force = false}) => _inner.close(force: force);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}