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
43 changes: 31 additions & 12 deletions mobile/lib/core/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@ class Env {
static String host = defaultHost;
static String port = defaultPort;

static bool get _isSecure =>
port == "443" || host.contains("commandlinecoding.in");
/// Whether to use HTTPS/WSS. Chosen explicitly in Server settings so a
/// self-hosted TLS server on a non-443 port isn't contacted in plain text.
static bool useTls = true;

static String get httpBaseUrl {
final protocol = _isSecure ? "https" : "http";
final portSuffix = (port == "80" || port == "443") ? "" : ":$port";
return "$protocol://$host$portSuffix/api";
}
static String get httpBaseUrl =>
buildUrl(host: host, port: port, tls: useTls, path: '/api');

static String get wsBaseUrl =>
buildUrl(host: host, port: port, tls: useTls, path: '/api/ws', ws: true);

static String get wsBaseUrl {
final protocol = _isSecure ? "wss" : "ws";
final portSuffix = (port == "80" || port == "443") ? "" : ":$port";
return "$protocol://$host$portSuffix/api/ws";
/// Builds a server URL, omitting the port when it's the scheme's default.
static String buildUrl({
required String host,
required String port,
required bool tls,
required String path,
bool ws = false,
}) {
final scheme = ws ? (tls ? 'wss' : 'ws') : (tls ? 'https' : 'http');
final isDefaultPort = port == (tls ? '443' : '80');
return '$scheme://$host${isDefaultPort ? '' : ':$port'}$path';
}

static bool get isDefaultServer => host == defaultHost && port == defaultPort;
Expand All @@ -29,15 +37,26 @@ class Env {
const storage = FlutterSecureStorage();
host = await storage.read(key: "custom_host") ?? defaultHost;
port = await storage.read(key: "custom_port") ?? defaultPort;
final storedTls = await storage.read(key: "custom_use_tls");
// Servers saved before the setting existed keep their old behaviour.
useTls = storedTls != null
? storedTls == "true"
: port == "443" || host.contains("commandlinecoding.in");
}

static Future<void> updateConfig(String newHost, String newPort) async {
static Future<void> updateConfig(
String newHost,
String newPort, {
bool tls = true,
}) async {
host = newHost.trim().isEmpty ? defaultHost : newHost.trim();
port = newPort.trim().isEmpty ? defaultPort : newPort.trim();
useTls = tls;

const storage = FlutterSecureStorage();
await storage.write(key: "custom_host", value: host);
await storage.write(key: "custom_port", value: port);
await storage.write(key: "custom_use_tls", value: useTls.toString());
}
}

Expand Down
51 changes: 42 additions & 9 deletions mobile/lib/pages/settings/server_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class _ServerSettingsFormState extends State<_ServerSettingsForm> {
late final TextEditingController _port = TextEditingController(
text: Env.port,
);
late bool _tls = Env.useTls;
_Health _health = _Health.unknown;

@override
Expand All @@ -42,10 +43,14 @@ class _ServerSettingsFormState extends State<_ServerSettingsForm> {
super.dispose();
}

String _baseUrl(String host, String port) {
final secure = port == '443' || host.contains('commandlinecoding.in');
final suffix = (port == '80' || port == '443') ? '' : ':$port';
return '${secure ? 'https' : 'http'}://$host$suffix/api';
/// Port 80 almost always means no TLS and 443 always means TLS, so
/// follow the port when it's one of those; otherwise keep the choice.
void _onPortChanged(String port) {
setState(() {
if (port == '80') _tls = false;
if (port == '443') _tls = true;
_health = _Health.unknown;
});
}

Future<void> _test() async {
Expand All @@ -57,12 +62,18 @@ class _ServerSettingsFormState extends State<_ServerSettingsForm> {
: _port.text.trim();
setState(() => _health = _Health.checking);
try {
final url = Env.buildUrl(
host: host,
port: port,
tls: _tls,
path: '/api/health',
);
final res = await Dio(
BaseOptions(
connectTimeout: const Duration(seconds: 6),
receiveTimeout: const Duration(seconds: 6),
),
).get('${_baseUrl(host, port)}/health');
).get(url);
final up = res.data is Map && res.data['data']?['postgres'] == 'up';
setState(() => _health = up ? _Health.up : _Health.dbDown);
} on DioException catch (e) {
Expand All @@ -78,7 +89,11 @@ class _ServerSettingsFormState extends State<_ServerSettingsForm> {

Future<void> _save({bool reset = false}) async {
final navigator = Navigator.of(context);
await Env.updateConfig(reset ? '' : _host.text, reset ? '' : _port.text);
await Env.updateConfig(
reset ? '' : _host.text,
reset ? '' : _port.text,
tls: reset || _tls,
);
navigator.pop();
if (mounted) {
showSnack(
Expand Down Expand Up @@ -120,13 +135,31 @@ class _ServerSettingsFormState extends State<_ServerSettingsForm> {
controller: _port,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (_) => setState(() => _health = _Health.unknown),
onChanged: _onPortChanged,
decoration: const InputDecoration(
labelText: 'Port',
hintText: Env.defaultPort,
prefixIcon: Icon(Icons.numbers_rounded),
helperText:
'Port 443 uses HTTPS/WSS; other ports use plain HTTP/WS.',
),
),
const SizedBox(height: Insets.md),
GlassSurface(
borderRadius: BorderRadius.circular(Radii.md),
child: Material(
type: MaterialType.transparency,
child: SwitchListTile(
value: _tls,
onChanged: (value) => setState(() {
_tls = value;
_health = _Health.unknown;
}),
title: const Text('Use HTTPS/WSS'),
subtitle: Text(
_tls
? 'Traffic to the server is encrypted with TLS.'
: 'Your password and session travel unencrypted. Only use this on a network you trust.',
),
),
),
),
const SizedBox(height: Insets.lg),
Expand Down
51 changes: 51 additions & 0 deletions mobile/test/env_url_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/core/constants.dart';

void main() {
group('Env.buildUrl', () {
test('omits the default TLS port', () {
expect(
Env.buildUrl(
host: 'chat.example',
port: '443',
tls: true,
path: '/api',
),
'https://chat.example/api',
);
});

test('keeps TLS on a non-standard port', () {
expect(
Env.buildUrl(
host: 'chat.example',
port: '8443',
tls: true,
path: '/api',
),
'https://chat.example:8443/api',
);
expect(
Env.buildUrl(
host: 'chat.example',
port: '8443',
tls: true,
path: '/api/ws',
ws: true,
),
'wss://chat.example:8443/api/ws',
);
});

test('plain HTTP omits port 80 only', () {
expect(
Env.buildUrl(host: '10.0.0.2', port: '80', tls: false, path: '/api'),
'http://10.0.0.2/api',
);
expect(
Env.buildUrl(host: '10.0.0.2', port: '8080', tls: false, path: '/api'),
'http://10.0.0.2:8080/api',
);
});
});
}
Loading