Skip to content

Commit

Permalink
Merge pull request #36 from dreautall/selfsignedcert
Browse files Browse the repository at this point in the history
Enable input of own SSL certificates to allow non-valid certificates
  • Loading branch information
dreautall authored May 12, 2023
2 parents fb1523e + 90ff292 commit 419d521
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 9 deletions.
29 changes: 26 additions & 3 deletions lib/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ import 'package:waterflyiii/generated/swagger_fireflyiii_api/firefly_iii.swagger

final Logger log = Logger("Auth");

class SSLHttpOverride extends HttpOverrides {
SSLHttpOverride(this.validCert);
final String validCert;

@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback = (X509Certificate cert, _, __) {
log.fine("Using SSLHttpOverride");
return cert.pem.replaceAll("\r", "").trim() ==
validCert.replaceAll("\r", "").trim();
};
}
}

class AuthError implements Exception {
const AuthError(this.cause);

Expand Down Expand Up @@ -170,16 +185,17 @@ class FireflyService with ChangeNotifier {
_storageSignInException = null;
String? apiHost = await storage.read(key: 'api_host');
String? apiKey = await storage.read(key: 'api_key');
String? cert = await storage.read(key: 'api_cert');

log.config(
"storage: $apiHost, apiKey ${apiKey?.isEmpty ?? true ? "unset" : "set"}");
"storage: $apiHost, apiKey ${apiKey?.isEmpty ?? true ? "unset" : "set"}, cert ${cert?.isEmpty ?? true ? "unset" : "set"}");

if (apiHost == null || apiKey == null) {
return false;
}

try {
final bool success = await signIn(apiHost, apiKey);
final bool success = await signIn(apiHost, apiKey, cert);
return success;
} catch (e) {
_storageSignInException = e;
Expand All @@ -202,11 +218,17 @@ class FireflyService with ChangeNotifier {
notifyListeners();
}

Future<bool> signIn(String host, String apiKey) async {
Future<bool> signIn(String host, String apiKey, [String? cert]) async {
log.config("FireflyService->signIn($host)");
host = host.strip().rightStrip('/');
apiKey = apiKey.strip();

if (cert != null && cert.isNotEmpty) {
HttpOverrides.global = SSLHttpOverride(cert);
} else {
HttpOverrides.global = null;
}

_lastTriedHost = host;
_currentUser = await AuthUser.create(host, apiKey);
if (_currentUser == null || !hasApi) return false;
Expand All @@ -220,6 +242,7 @@ class FireflyService with ChangeNotifier {

storage.write(key: 'api_host', value: host);
storage.write(key: 'api_key', value: apiKey);
storage.write(key: 'api_cert', value: cert);

return true;
}
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@
"settingsThemeValue": "{theme, select, dark{Dunkel} light{Hell} other{Systemeinstellung}}",
"settingsVersion": "App Version",
"settingsVersionChecking": "Überprüfe...",
"splashCustomSSLCert": "Eigenes SSL Zertifikat",
"splashFormLabelCustomSSLCertPEM": "Zertifikats-Datei (PEM)",
"transactionAttachments": "Anhänge",
"transactionDeleteConfirm": "Soll diese Transaktion wirklich gelöscht werden?",
"transactionDialogAttachmentsDelete": "Anhang löschen",
Expand Down
8 changes: 8 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,14 @@
"@settingsVersionChecking": {
"description": "Shown while checking for app version"
},
"splashCustomSSLCert": "Custom SSL certificate",
"@splashCustomSSLCert": {
"description": "Button text & Dialog title for using a custom SSL certificate"
},
"splashFormLabelCustomSSLCertPEM": "Certificate File (PEM)",
"@splashFormLabelCustomSSLCertPEM": {
"description": "Label for certificate file text input in PEM format"
},
"transactionAttachments": "Attachments",
"@transactionAttachments": {
"description": "Button Label: Attachments"
Expand Down
83 changes: 77 additions & 6 deletions lib/pages/splash.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class _SplashPageState extends State<SplashPage> {

Object? _loginError;

void _login(String? host, String? apiKey) async {
void _login(String? host, String? apiKey, [String? cert]) async {
log.fine(() => "SplashPage->_login()");

bool success = false;
Expand All @@ -38,11 +38,12 @@ class _SplashPageState extends State<SplashPage> {
} else {
log.finer(() =>
"SplashPage->_login() with credentials: $host, apiKey apiKey ${apiKey.isEmpty ? "unset" : "set"}");
success = await context.read<FireflyService>().signIn(host, apiKey);
success =
await context.read<FireflyService>().signIn(host, apiKey, cert);
}
} catch (e, stackTrace) {
log.warning(
"_login got exceptionassigning to _loginError", e, stackTrace);
"_login got exception, assigning to _loginError", e, stackTrace);
setState(() {
_loginError = e;
});
Expand Down Expand Up @@ -89,6 +90,7 @@ class _SplashPageState extends State<SplashPage> {
);
} else {
log.finer(() => "_loginError available --> show error");
bool showCertButton = false;
String errorDetails =
"Host: ${context.read<FireflyService>().lastTriedHost}";
final String errorDescription = () {
Expand All @@ -104,6 +106,7 @@ class _SplashPageState extends State<SplashPage> {
errorDetails += S.of(context).errorStatusCode(errorType.code);
return errorType.cause;
case HandshakeException:
showCertButton = true;
return S.of(context).errorInvalidSSLCert;
default:
errorDetails += "\n$_loginError";
Expand All @@ -123,7 +126,9 @@ class _SplashPageState extends State<SplashPage> {
child: Text(
errorDescription,
style: TextStyle(
height: 2, color: Theme.of(context).colorScheme.error),
height: 2,
color: Theme.of(context).colorScheme.error,
),
),
),
),
Expand All @@ -137,14 +142,39 @@ class _SplashPageState extends State<SplashPage> {
child: Text(
errorDetails,
style: TextStyle(
height: 2, color: Theme.of(context).colorScheme.error),
height: 2,
color: Theme.of(context).colorScheme.error,
),
),
),
),
),
const SizedBox(height: 12),
showCertButton
? FilledButton(
onPressed: () async {
String? cert = await showDialog<String>(
context: context,
builder: (BuildContext context) =>
const SSLCertDialog(),
);
if (cert == null || cert.isEmpty) {
return;
}
setState(() {
_loginError = null;
});
_login(widget.host, widget.apiKey, cert);
},
child: Text(S.of(context).splashCustomSSLCert),
)
: const SizedBox.shrink(),
showCertButton
? const SizedBox(height: 12)
: const SizedBox.shrink(),
OverflowBar(
alignment: MainAxisAlignment.center,
spacing: 12,
children: <Widget>[
OutlinedButton(
onPressed: () {
Expand All @@ -159,7 +189,6 @@ class _SplashPageState extends State<SplashPage> {
MaterialLocalizations.of(context).backButtonTooltip)
: Text(S.of(context).formButtonResetLogin),
),
const SizedBox(width: 12),
FilledButton(
onPressed: () {
setState(() {
Expand Down Expand Up @@ -200,3 +229,45 @@ class _SplashPageState extends State<SplashPage> {
);
}
}

class SSLCertDialog extends StatelessWidget {
const SSLCertDialog({
super.key,
});

@override
Widget build(BuildContext context) {
final TextEditingController textController = TextEditingController();

return AlertDialog(
icon: const Icon(Icons.policy),
title: Text(S.of(context).splashCustomSSLCert),
clipBehavior: Clip.hardEdge,
actions: <Widget>[
TextButton(
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text(MaterialLocalizations.of(context).saveButtonLabel),
onPressed: () {
Navigator.of(context).pop(textController.text);
},
),
],
content: TextField(
controller: textController,
decoration: InputDecoration(
filled: true,
labelText: S.of(context).splashFormLabelCustomSSLCertPEM,
),
autocorrect: false,
autofocus: true,
expands: true,
maxLines: null,
),
);
}
}

0 comments on commit 419d521

Please sign in to comment.