mirror of
https://github.com/block/buzz.git
synced 2026-11-12 04:12:21 +00:00
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>
87 lines
2.8 KiB
Dart
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);
|
|
}
|