block / block/buzz

mobile: invite-link joins downgrade wss:// to plaintext ws:// (and break HTTP query/upload)

Open
#3,211 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

## Summary

On mobile, joining a community via an **invite deep link** stores a `wss://` base URL, but `RelayConfig.wsUrl` only maps `https → wss`. A `wss` scheme falls through to the `else` branch and is rewritten to **plaintext `ws://`**, which is then dialed. The same stored value also produces `wss://…` URLs that are handed to `package:http`, which cannot use that scheme.

The pairing path is unaffected (desktop sends `https://`), which is why this hasn't surfaced.

## The defect

```dart
// mobile/lib/shared/relay/relay_provider.dart:21-25
String get wsUrl {
final uri = Uri.parse(baseUrl);
final scheme = uri.scheme == 'https' ? 'wss' : 'ws'; // 'wss' → 'ws'
return uri.replace(scheme: scheme).toString();
}
```

## Reproduction

Running the method body verbatim under Dart 3.11.5:

```
https://r.example.com -> wss://r.example.com ✅
wss://r.example.com -> ws://r.example.com ❌ downgraded
http://localhost:3000 -> ws://localhost:3000 ✅
```

## Chain to a live connection

1. `deep_link.dart:174-178` — `parseInviteDeepLink` normalizes to `Uri(scheme: relayUri.scheme, host:, port:)`. `validateInviteRelayUri` requires `ws`/`wss` and rejects non-`wss` outside `kDebugMode` (`relay_validation.dart:28-30`), so in release builds this is always `wss://host`.
2. `invite_join_provider.dart:157-159` — `Community.create(relayUrl: invite.relayUrl)` stores it unchanged (`community.dart:23-37` does no normalization).
3. `relay_provider.dart:49` — `RelayConfig(baseUrl: active.relayUrl)`.
4. `relay_provider.dart:21-25` — `wsUrl` → **`ws://host`**.
5. `relay_session.dart:361-362` — `_socketFactory(wsUrl: config.wsUrl)` dials it.

## Second failure on the same value

`baseUrl` is also used directly for HTTP:

- `relay_session.dart:136` — `Uri.parse(config.baseUrl).resolve('/query')` → `wss://host/query` passed to `http.Client()`
- `media_upload.dart:364` — `Uri.parse(_baseUrl).resolve(path)` → `wss://…` for a `PUT`

Both are unsupported schemes for `package:http`.

## Impact

Every release-build invite-link join attempts a **plaintext WebSocket** to a TLS-only relay: NIP-42 AUTH (including the signed challenge) would go unencrypted, and in practice the connection just fails against a relay that only serves `wss`. Queries and media upload fail separately on the scheme error.

## Why the tests miss it

`invite_join_provider_test.dart:154-155` asserts the *stored* value is `'wss://relay.example.com'` but never drives it through `wsUrl`. Every `RelayConfig` in `relay_session_test.dart` (`:28, :94, :218, :234`) uses `https://`, so the `wss` input is never exercised.

## Suggested fix

Make `wsUrl` idempotent for already-WS schemes, and add a case to the test matrix:

```dart
String get wsUrl {
final uri = Uri.parse(baseUrl);
final scheme = switch (uri.scheme) {
'https' || 'wss' => 'wss',
_ => 'ws',
};
return uri.replace(scheme: scheme).toString();
}
```

The HTTP seam needs the mirror treatment — either normalize the stored `relayUrl` to `https://` at the invite boundary (matching what pairing stores), or give `RelayConfig` an explicit `httpUrl` getter. Normalizing at the boundary is probably cleaner, since it makes the pairing and invite paths store the same shape.

## Environment

`block/buzz` @ `75588eaff`. Found by code inspection while evaluating self-hosted-relay support on iOS; not observed on a device.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.