mirror of
https://github.com/immich-app/immich.git
synced 2025-01-04 02:46:47 +01:00
feat(mobile): allow self-signed certificate on the mobile app (#4051)
* WIP: self-signed certs accept * WIP: format * WIP: pushing up adding settings menu * Add serverEndpointURL check * Add translation update * Handle errors properly * format * typo * cleanup * styling and permission * remove deadcode * put pack condition * styling * remove hiding settings options * format + match drop shadow * match color * remove deadcode --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
a678590ccd
commit
fb20381f98
10 changed files with 157 additions and 42 deletions
|
@ -7,6 +7,8 @@
|
||||||
"advanced_settings_tile_title": "Advanced",
|
"advanced_settings_tile_title": "Advanced",
|
||||||
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||||
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
||||||
|
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||||
|
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||||
"album_info_card_backup_album_excluded": "EXCLUDED",
|
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||||
"album_info_card_backup_album_included": "INCLUDED",
|
"album_info_card_backup_album_included": "INCLUDED",
|
||||||
"album_thumbnail_card_item": "1 item",
|
"album_thumbnail_card_item": "1 item",
|
||||||
|
@ -174,6 +176,7 @@
|
||||||
"library_page_sort_title": "Album title",
|
"library_page_sort_title": "Album title",
|
||||||
"login_disabled": "Login has been disabled",
|
"login_disabled": "Login has been disabled",
|
||||||
"login_form_api_exception": "API exception. Please check the server URL and try again.",
|
"login_form_api_exception": "API exception. Please check the server URL and try again.",
|
||||||
|
"login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
|
||||||
"login_form_button_text": "Login",
|
"login_form_button_text": "Login",
|
||||||
"login_form_email_hint": "youremail@email.com",
|
"login_form_email_hint": "youremail@email.com",
|
||||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||||
|
|
|
@ -29,6 +29,7 @@ import 'package:immich_mobile/shared/services/immich_logger.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/local_notification.service.dart';
|
import 'package:immich_mobile/shared/services/local_notification.service.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
||||||
|
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||||
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
@ -41,6 +42,7 @@ void main() async {
|
||||||
final db = await loadDb();
|
final db = await loadDb();
|
||||||
await initApp();
|
await initApp();
|
||||||
await migrateDatabaseIfNeeded(db);
|
await migrateDatabaseIfNeeded(db);
|
||||||
|
HttpOverrides.global = HttpSSLCertOverride();
|
||||||
runApp(getMainWidget(db));
|
runApp(getMainWidget(db));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -88,6 +89,16 @@ class LoginForm extends HookConsumerWidget {
|
||||||
isPasswordLoginEnable.value = true;
|
isPasswordLoginEnable.value = true;
|
||||||
isLoadingServer.value = false;
|
isLoadingServer.value = false;
|
||||||
return false;
|
return false;
|
||||||
|
} on HandshakeException {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'login_form_handshake_exception'.tr(),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
isOauthEnable.value = false;
|
||||||
|
isPasswordLoginEnable.value = true;
|
||||||
|
isLoadingServer.value = false;
|
||||||
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -226,6 +237,7 @@ class LoginForm extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSelectServer() {
|
buildSelectServer() {
|
||||||
|
const buttonRadius = 25.0;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
@ -235,24 +247,51 @@ class LoginForm extends HookConsumerWidget {
|
||||||
onSubmit: getServerLoginCredential,
|
onSubmit: getServerLoginCredential,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
ElevatedButton.icon(
|
Row(
|
||||||
style: ElevatedButton.styleFrom(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
Expanded(
|
||||||
),
|
child: ElevatedButton.icon(
|
||||||
onPressed: isLoadingServer.value ? null : getServerLoginCredential,
|
style: ElevatedButton.styleFrom(
|
||||||
icon: const Icon(Icons.arrow_forward_rounded),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
label: const Text(
|
shape: const RoundedRectangleBorder(
|
||||||
'login_form_next_button',
|
borderRadius: BorderRadius.only(
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
topLeft: Radius.circular(buttonRadius),
|
||||||
).tr(),
|
bottomLeft: Radius.circular(buttonRadius),
|
||||||
),
|
),
|
||||||
if (isLoadingServer.value)
|
),
|
||||||
const Padding(
|
),
|
||||||
padding: EdgeInsets.only(top: 18.0),
|
onPressed: () =>
|
||||||
child: Center(
|
AutoRouter.of(context).push(const SettingsRoute()),
|
||||||
child: CircularProgressIndicator(),
|
icon: const Icon(Icons.settings_rounded),
|
||||||
|
label: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 1),
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topRight: Radius.circular(buttonRadius),
|
||||||
|
bottomRight: Radius.circular(buttonRadius),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
isLoadingServer.value ? null : getServerLoginCredential,
|
||||||
|
icon: const Icon(Icons.arrow_forward_rounded),
|
||||||
|
label: const Text(
|
||||||
|
'login_form_next_button',
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
if (isLoadingServer.value) const LoadingIcon(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -285,18 +324,7 @@ class LoginForm extends HookConsumerWidget {
|
||||||
// Note: This used to have an AnimatedSwitcher, but was removed
|
// Note: This used to have an AnimatedSwitcher, but was removed
|
||||||
// because of https://github.com/flutter/flutter/issues/120874
|
// because of https://github.com/flutter/flutter/issues/120874
|
||||||
isLoading.value
|
isLoading.value
|
||||||
? const Padding(
|
? const LoadingIcon()
|
||||||
padding: EdgeInsets.only(top: 18.0),
|
|
||||||
child: SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: FittedBox(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Column(
|
: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
@ -572,3 +600,23 @@ class OAuthLoginButton extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LoadingIcon extends StatelessWidget {
|
||||||
|
const LoadingIcon({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 18.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: FittedBox(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ enum AppSettingsEnum<T> {
|
||||||
mapThemeMode<bool>(StoreKey.mapThemeMode, null, false),
|
mapThemeMode<bool>(StoreKey.mapThemeMode, null, false),
|
||||||
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
|
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
|
||||||
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
|
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
|
||||||
|
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
|
||||||
;
|
;
|
||||||
|
|
||||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||||
|
|
|
@ -1,23 +1,29 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState;
|
||||||
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
|
||||||
import 'package:immich_mobile/shared/services/immich_logger.service.dart';
|
import 'package:immich_mobile/shared/services/immich_logger.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
class AdvancedSettings extends HookConsumerWidget {
|
class AdvancedSettings extends HookConsumerWidget {
|
||||||
const AdvancedSettings({super.key});
|
const AdvancedSettings({super.key});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
bool isLoggedIn = Store.tryGet(StoreKey.currentUser) != null;
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
final isEnabled =
|
final isEnabled =
|
||||||
useState(AppSettingsEnum.advancedTroubleshooting.defaultValue);
|
useState(AppSettingsEnum.advancedTroubleshooting.defaultValue);
|
||||||
final levelId = useState(AppSettingsEnum.logLevel.defaultValue);
|
final levelId = useState(AppSettingsEnum.logLevel.defaultValue);
|
||||||
final preferRemote =
|
final preferRemote =
|
||||||
useState(AppSettingsEnum.preferRemoteImage.defaultValue);
|
useState(AppSettingsEnum.preferRemoteImage.defaultValue);
|
||||||
|
final allowSelfSignedSSLCert =
|
||||||
|
useState(AppSettingsEnum.allowSelfSignedSSLCert.defaultValue);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
|
@ -27,6 +33,8 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||||
levelId.value = appSettingService.getSetting(AppSettingsEnum.logLevel);
|
levelId.value = appSettingService.getSetting(AppSettingsEnum.logLevel);
|
||||||
preferRemote.value =
|
preferRemote.value =
|
||||||
appSettingService.getSetting(AppSettingsEnum.preferRemoteImage);
|
appSettingService.getSetting(AppSettingsEnum.preferRemoteImage);
|
||||||
|
allowSelfSignedSSLCert.value = appSettingService
|
||||||
|
.getSetting(AppSettingsEnum.allowSelfSignedSSLCert);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
@ -88,6 +96,17 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||||
title: "advanced_settings_prefer_remote_title".tr(),
|
title: "advanced_settings_prefer_remote_title".tr(),
|
||||||
subtitle: "advanced_settings_prefer_remote_subtitle".tr(),
|
subtitle: "advanced_settings_prefer_remote_subtitle".tr(),
|
||||||
),
|
),
|
||||||
|
SettingsSwitchListTile(
|
||||||
|
enabled: !isLoggedIn,
|
||||||
|
appSettingService: appSettingService,
|
||||||
|
valueNotifier: allowSelfSignedSSLCert,
|
||||||
|
settingsEnum: AppSettingsEnum.allowSelfSignedSSLCert,
|
||||||
|
title: "advanced_settings_self_signed_ssl_title".tr(),
|
||||||
|
subtitle: "advanced_settings_self_signed_ssl_subtitle".tr(),
|
||||||
|
onChanged: (value) {
|
||||||
|
HttpOverrides.global = HttpSSLCertOverride();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ class SettingsSwitchListTile extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
final Function(bool)? onChanged;
|
||||||
|
|
||||||
SettingsSwitchListTile({
|
SettingsSwitchListTile({
|
||||||
required this.appSettingService,
|
required this.appSettingService,
|
||||||
|
@ -16,19 +17,26 @@ class SettingsSwitchListTile extends StatelessWidget {
|
||||||
required this.title,
|
required this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
|
this.onChanged,
|
||||||
}) : super(key: Key(settingsEnum.name));
|
}) : super(key: Key(settingsEnum.name));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
|
selectedTileColor: enabled ? null : Theme.of(context).disabledColor,
|
||||||
value: valueNotifier.value,
|
value: valueNotifier.value,
|
||||||
onChanged: !enabled
|
onChanged: (bool value) {
|
||||||
? null
|
if (enabled) {
|
||||||
: (value) {
|
valueNotifier.value = value;
|
||||||
valueNotifier.value = value;
|
appSettingService.setSetting(settingsEnum, value);
|
||||||
appSettingService.setSetting(settingsEnum, value);
|
}
|
||||||
},
|
if (onChanged != null) {
|
||||||
activeColor: Theme.of(context).primaryColor,
|
onChanged!(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activeColor: enabled
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: Theme.of(context).disabledColor,
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
title,
|
title,
|
||||||
|
|
|
@ -125,7 +125,6 @@ part 'router.gr.dart';
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: SettingsPage,
|
page: SettingsPage,
|
||||||
guards: [
|
guards: [
|
||||||
AuthGuard,
|
|
||||||
DuplicateGuard,
|
DuplicateGuard,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -550,10 +550,7 @@ class _$AppRouter extends RootStackRouter {
|
||||||
RouteConfig(
|
RouteConfig(
|
||||||
SettingsRoute.name,
|
SettingsRoute.name,
|
||||||
path: '/settings-page',
|
path: '/settings-page',
|
||||||
guards: [
|
guards: [duplicateGuard],
|
||||||
authGuard,
|
|
||||||
duplicateGuard,
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
RouteConfig(
|
RouteConfig(
|
||||||
AppLogRoute.name,
|
AppLogRoute.name,
|
||||||
|
|
|
@ -178,6 +178,7 @@ enum StoreKey<T> {
|
||||||
mapThemeMode<bool>(117, type: bool),
|
mapThemeMode<bool>(117, type: bool),
|
||||||
mapShowFavoriteOnly<bool>(118, type: bool),
|
mapShowFavoriteOnly<bool>(118, type: bool),
|
||||||
mapRelativeDate<int>(119, type: int),
|
mapRelativeDate<int>(119, type: int),
|
||||||
|
selfSignedCert<bool>(120, type: bool),
|
||||||
;
|
;
|
||||||
|
|
||||||
const StoreKey(
|
const StoreKey(
|
||||||
|
|
37
mobile/lib/utils/http_ssl_cert_override.dart
Normal file
37
mobile/lib/utils/http_ssl_cert_override.dart
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
class HttpSSLCertOverride extends HttpOverrides {
|
||||||
|
@override
|
||||||
|
HttpClient createHttpClient(SecurityContext? context) {
|
||||||
|
return super.createHttpClient(context)
|
||||||
|
..badCertificateCallback = (X509Certificate cert, String host, int port) {
|
||||||
|
var log = Logger("HttpSSLCertOverride");
|
||||||
|
|
||||||
|
AppSettingsEnum setting = AppSettingsEnum.allowSelfSignedSSLCert;
|
||||||
|
|
||||||
|
// Check if user has allowed self signed SSL certificates.
|
||||||
|
bool selfSignedCertsAllowed =
|
||||||
|
Store.get(setting.storeKey as StoreKey<bool>, setting.defaultValue);
|
||||||
|
|
||||||
|
bool isLoggedIn = Store.tryGet(StoreKey.currentUser) != null;
|
||||||
|
|
||||||
|
// Conduct server host checks if user is logged in to avoid making
|
||||||
|
// insecure SSL connections to services that are not the immich server.
|
||||||
|
if (isLoggedIn && selfSignedCertsAllowed) {
|
||||||
|
String serverHost =
|
||||||
|
Uri.parse(Store.tryGet(StoreKey.serverEndpoint) ?? "").host;
|
||||||
|
|
||||||
|
selfSignedCertsAllowed &= serverHost.contains(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selfSignedCertsAllowed) {
|
||||||
|
log.severe("Invalid SSL certificate for $host:$port");
|
||||||
|
}
|
||||||
|
|
||||||
|
return selfSignedCertsAllowed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue