Commit a2ca8d4f authored by Leon Tappe's avatar Leon Tappe 🔥
Browse files

add more error handling for certificate change

parent 055f9f10
......@@ -5,6 +5,20 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
class TicketResponse {
final String? ticket;
final VerificationErrorType? error;
TicketResponse(this.ticket, {this.error});
bool get hasError => error != null;
Map get toMap => {'ticket': ticket, 'error': error.toString()};
@override
String toString() => '[TicketResponse $toMap]';
}
class VerificationApi {
final Logger _log = Logger('VerificationApi');
......@@ -29,13 +43,22 @@ class VerificationApi {
_log.fine('no color available');
_log.finest(response.body);
return VerificationResponse.fromMapNew(json.decode(response.body));
} else {
} else if (response.statusCode >= 500) {
_log.severe(response.body);
return VerificationResponse.error(VerificationErrorType.server);
} else if (response.statusCode >= 400) {
_log.severe(response.body);
return VerificationResponse.empty();
if (response.body.contains('signing')) {
return VerificationResponse.error(VerificationErrorType.signing);
} else if (response.body.contains('encryption')) {
return VerificationResponse.error(VerificationErrorType.encryption);
}
}
_log.severe(response.body);
return VerificationResponse.error(VerificationErrorType.other);
}
Future<String?> generateTicket(String seed) async {
Future<TicketResponse> generateTicket(String seed) async {
final response = await _client.post(
Uri.parse('$baseUrl/genticket'),
body: json.encode(seed),
......@@ -47,13 +70,32 @@ class VerificationApi {
if (response.statusCode == 200) {
_log.finest(response.body);
return json.decode(response.body);
} else {
return TicketResponse(json.decode(response.body));
} else if (response.statusCode >= 500) {
_log.severe(response.body);
return TicketResponse(null, error: VerificationErrorType.server);
} else if (response.statusCode >= 400) {
_log.severe(response.body);
if (response.body.contains('oldseed')) {
return TicketResponse(null, error: VerificationErrorType.oldseed);
} else if (response.body.contains('signing')) {
return TicketResponse(null, error: VerificationErrorType.signing);
}
}
_log.severe(response.body);
return TicketResponse(null, error: VerificationErrorType.other);
}
}
enum VerificationErrorType {
signing,
encryption,
expired,
oldseed,
server,
other,
}
class VerificationResponse {
final bool verified;
final int? color;
......@@ -61,11 +103,20 @@ class VerificationResponse {
final DateTime timestamp;
VerificationResponse({required this.verified, this.color, this.inverse, required this.timestamp});
final VerificationErrorType? error;
VerificationResponse(
{required this.verified, this.color, this.inverse, required this.timestamp, this.error});
factory VerificationResponse.empty() =>
VerificationResponse(verified: false, timestamp: DateTime.now());
factory VerificationResponse.error(VerificationErrorType error) => VerificationResponse(
verified: false,
error: error,
timestamp: DateTime.now(),
);
factory VerificationResponse.fromMap(Map map) => VerificationResponse(
verified: map['verified'],
color: map['color'] != null ? int.tryParse(map['color'], radix: 16) : null,
......@@ -80,11 +131,14 @@ class VerificationResponse {
timestamp: DateTime.now(),
);
bool get hasError => error != null;
Map<String, dynamic> get toMap => {
'verified': verified,
'color': color?.toRadixString(16),
'inverse': inverse,
'timestamp': timestamp.toIso8601String(),
'error': error.toString(),
};
@override
......
......@@ -6,7 +6,7 @@ PODS:
- MTBBarcodeScanner (5.0.11)
- package_info (0.0.1):
- Flutter
- path_provider (0.0.1):
- path_provider_ios (0.0.1):
- Flutter
- qr_code_scanner (0.2.0):
- Flutter
......@@ -21,7 +21,7 @@ DEPENDENCIES:
- connectivity (from `.symlinks/plugins/connectivity/ios`)
- Flutter (from `Flutter`)
- package_info (from `.symlinks/plugins/package_info/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
......@@ -38,8 +38,8 @@ EXTERNAL SOURCES:
:path: Flutter
package_info:
:path: ".symlinks/plugins/package_info/ios"
path_provider:
:path: ".symlinks/plugins/path_provider/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
shared_preferences:
......@@ -52,7 +52,7 @@ SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
shared_preferences: 5033afbb22d372e15aff8ff766df9021b845f273
......
......@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:connectivity/connectivity.dart';
import 'package:digital_3g_common/backend_switcher.dart';
import 'package:digital_3g_common/ui.dart';
import 'package:digital_3g_common/verification_api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
......@@ -135,15 +136,109 @@ class _HomePageState extends State<HomePage> {
style: Theme.of(context).textTheme.headline4,
)
else if (state == VerificationState.unverified)
Text(
'Die Gültigkeit des 3G-Nachweises konnte nicht bestätigt werden',
style: Theme.of(context).textTheme.headline4,
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Dein Nachweis ist nicht mehr gültig',
style: Theme.of(context).textTheme.headline4,
),
const Divider(),
ElevatedButton(
onPressed: _onDeleteTicket,
child: const Text('Nachweis löschen'),
),
],
)
else
Text(
'Keine UUID vorhanden',
style: Theme.of(context).textTheme.headline4,
),
else if (state == VerificationState.error)
if (_bloc!.error == VerificationErrorType.encryption)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Der Schlüssel deines Nachweises ist ungültig',
style: Theme.of(context).textTheme.headline4,
),
const Divider(),
ElevatedButton(
onPressed: _onDeleteTicket,
child: const Text('Nachweis löschen'),
),
],
)
else if (_bloc!.error == VerificationErrorType.encryption)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Die Signatur deines Nachweises ist ungültig',
style: Theme.of(context).textTheme.headline4,
),
const Divider(),
ElevatedButton(
onPressed: _onDeleteTicket,
child: const Text('Nachweis löschen'),
),
],
)
else if (_bloc!.error == VerificationErrorType.oldseed)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Dieser QR-Code ist bereits abgelaufen',
style: Theme.of(context).textTheme.headline4,
),
const Divider(),
ElevatedButton(
onPressed: _bloc!.onRevoke,
child: const Text('Erneut probieren'),
),
],
)
else if (_bloc!.error == VerificationErrorType.signing)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Die Signatur deines Nachweises konnte nicht bestätigt werden. Hast du aus Versehen das Debug-Backend an?',
style: Theme.of(context).textTheme.headline4,
),
const Divider(),
if (BlocProvider.of<BackendSwitcher>(context).mode ==
BackendMode.development)
ElevatedButton(
onPressed: () =>
Navigator.of(context).pushNamed('/settings'),
child: const Text('Zu den Einstellungen'),
)
else
ElevatedButton(
onPressed: _onDeleteTicket,
child: const Text('Nachweis löschen'),
),
],
)
else if (_bloc!.error == VerificationErrorType.server)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Es gibt ein Problem bei der Serververbindung',
style: Theme.of(context).textTheme.headline4,
),
const Divider(),
ElevatedButton(
onPressed: _bloc!.onCheck,
child: const Text('Erneut probieren'),
),
],
)
else
Text(
'Ein unbekanntes Problem wurde festgestellt',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
......@@ -300,6 +395,27 @@ class _HomePageState extends State<HomePage> {
}
}
void _onDeleteTicket() async {
final result = await showDialog<bool>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Der Nachweis ist danach endgültig gelöscht. Bist du dir sicher?'),
actions: [
MaterialButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Ja'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Nein'),
),
],
),
);
if (result ?? false) _bloc!.onRevoke();
}
Future<void> _onDismissHelp() async {
setState(() {
_showHelp = false;
......
......@@ -24,12 +24,14 @@ class VerificationBloc extends Bloc<VerificationEvent, VerificationState> {
VerificationApi? _api;
VerificationResponse? _verification;
VerificationErrorType? _error;
VerificationBloc(String url) : super(VerificationState.busy) {
_api = VerificationApi(http.Client(), url);
_initVerificationStore().then((_) => _initTicketFile().then((value) => _loadTicket()));
}
VerificationErrorType? get error => _error;
String? get ticket => _ticket;
VerificationResponse? get verification => _verification;
......@@ -46,12 +48,14 @@ class VerificationBloc extends Bloc<VerificationEvent, VerificationState> {
} catch (e) {
_log.severe('failed to check ticket:\n$e');
}
if (response?.verified ?? false) {
_verification = response!;
if ((response?.verified ?? false) && !(response?.hasError ?? false)) {
_verification = response;
await _verificationStore!.writeAsString(json.encode(_verification!.toMap));
yield VerificationState.verified;
} else if (((await Connectivity().checkConnectivity()) == ConnectivityResult.none) &&
(_verificationStore?.existsSync() ?? false)) {
} else if ((((await Connectivity().checkConnectivity()) == ConnectivityResult.none) &&
(_verificationStore?.existsSync() ?? false)) ||
((response?.hasError ?? false) && response!.error == VerificationErrorType.server)) {
// get verification from local storage
final loaded =
VerificationResponse.fromMap(json.decode(await _verificationStore!.readAsString()));
if (loaded.timestamp.isAfter(DateTime.now().subtract(const Duration(minutes: 30)))) {
......@@ -60,37 +64,47 @@ class VerificationBloc extends Bloc<VerificationEvent, VerificationState> {
} else {
yield VerificationState.unverified;
}
} else {
} else if (!(response?.verified ?? false) && !(response?.hasError ?? false)) {
yield VerificationState.unverified;
} else {
_verification = response;
_error = _verification!.error;
yield VerificationState.error;
}
}
break;
case EventType.revoke:
_ticketStore!.writeAsString('', flush: true);
_ticket = null;
_error = null;
yield VerificationState.noTicket;
break;
case EventType.add:
// get ticket from backend if longer than 728 (backwards compatibility)
String? ticket;
TicketResponse? response;
if ((event.seed?.length ?? 0) > 768) {
try {
ticket = await _api!.generateTicket(event.seed!);
response = await _api!.generateTicket(event.seed!);
} catch (e) {
_log.severe('error while generating ticket');
_error = VerificationErrorType.other;
yield VerificationState.error;
}
} else {
ticket = event.seed;
response = TicketResponse(event.seed);
}
if (response!.hasError) {
_error = response.error;
yield VerificationState.error;
}
// save ticket
if (!_ticketStore!.existsSync()) {
await _ticketStore!.create(recursive: true);
}
if (ticket != null && ticket.isNotEmpty) {
_ticket = ticket;
if (response.ticket != null && response.ticket!.isNotEmpty) {
_ticket = response.ticket;
await _ticketStore!.writeAsString(_ticket!, flush: true);
yield VerificationState.unverified;
add(VerificationEvent.check());
......@@ -103,6 +117,12 @@ class VerificationBloc extends Bloc<VerificationEvent, VerificationState> {
void onAdd(String ticket) => add(VerificationEvent.add(ticket));
@override
void onChange(Change<VerificationState> change) {
_log.finer('new state: ${change.nextState}');
super.onChange(change);
}
void onCheck() => add(VerificationEvent.check());
void onRevoke() => add(VerificationEvent.revoke());
......
......@@ -206,21 +206,35 @@ packages:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
version: "2.0.7"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.2"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.0.3"
path_provider_platform_interface:
dependency: transitive
description:
......@@ -234,7 +248,7 @@ packages:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
version: "2.0.4"
platform:
dependency: transitive
description:
......@@ -283,7 +297,7 @@ packages:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.0.3"
shared_preferences_macos:
dependency: transitive
description:
......@@ -311,7 +325,7 @@ packages:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.0.3"
sky_engine:
dependency: transitive
description: flutter
......@@ -351,7 +365,7 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.12"
version: "6.0.13"
url_launcher_linux:
dependency: transitive
description:
......@@ -400,7 +414,7 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.10"
version: "2.3.0"
xdg_directories:
dependency: transitive
description:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment