mobile: invite-link joins downgrade wss:// to plaintext ws:// (and break HTTP query/upload)
- 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
Assessment
This issue has not been assessed yet.