flutter_cloudflare_dns 0.3.0
flutter_cloudflare_dns: ^0.3.0 copied to clipboard
Flutter toolkit for diagnosing and managing Cloudflare DNS: DoH health checks, safe API gateways, and Material 3 widgets.
import 'package:flutter/material.dart';
import 'package:flutter_cloudflare_dns/flutter_cloudflare_dns.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cloudflare DNS',
debugShowCheckedModeBanner: false,
theme: _buildTheme(Brightness.light),
darkTheme: _buildTheme(Brightness.dark),
themeMode: ThemeMode.system,
home: const ExampleDashboard(),
);
}
ThemeData _buildTheme(Brightness brightness) {
final colors = ColorScheme.fromSeed(
seedColor: const Color(0xFFF6821F),
brightness: brightness,
);
return ThemeData(
colorScheme: colors,
scaffoldBackgroundColor: colors.surface,
useMaterial3: true,
appBarTheme: AppBarTheme(
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
surfaceTintColor: colors.surfaceTint,
centerTitle: false,
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: colors.surfaceContainerLow,
modalBackgroundColor: colors.surfaceContainerLow,
showDragHandle: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colors.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: BorderSide(color: colors.primary, width: 2),
),
),
);
}
}
class ExampleDashboard extends StatefulWidget {
const ExampleDashboard({super.key});
@override
State<ExampleDashboard> createState() => _ExampleDashboardState();
}
class _ExampleDashboardState extends State<ExampleDashboard> {
final gateway = DemoDnsGateway();
@override
Widget build(BuildContext context) {
return Scaffold(
body: CloudflareDnsDashboard(
gateway: gateway,
diagnostics: DemoDiagnostics(gateway),
initialZoneName: 'seungpyo.online',
propagationCheckInterval: const Duration(seconds: 2),
),
);
}
}
/// Stable public-health result for a deterministic, offline-friendly demo.
class DemoDiagnostics extends DnsDiagnostics {
DemoDiagnostics(this.gateway);
/// Records still in the zone; a deleted record fades from the resolvers.
final DemoDnsGateway gateway;
final _polls = <String, int>{};
@override
Future<DnsHealthReport> check(
String domain, {
Iterable<DnsRecordType> types = const <DnsRecordType>[],
Iterable<DnsRecord> expectedRecords = const <DnsRecord>[],
}) async {
// Long enough for the example to demonstrate the synchronized refresh
// motion without making the dashboard feel artificially slow.
await Future<void>.delayed(const Duration(milliseconds: 900));
return DnsHealthReport(
domain: domain,
checkedAt: DateTime.now().toUtc(),
answers: const <String, List<DnsRecord>>{},
issues: const <DnsIssue>[],
dnssecValidated: true,
);
}
@override
Future<List<DnsResolverComparison>> compareResolvers(
Iterable<DnsRecord> records, {
Map<String, Uri>? resolvers,
}) async {
await Future<void>.delayed(const Duration(milliseconds: 600));
final list = records.toList(growable: false);
// A saved or deleted record is watched alone; Google sees the change
// from the third poll, Cloudflare at once.
if (list.length == 1) {
final record = list.single;
final deleted = !gateway.contains(record);
final key =
'${deleted ? 'deleted:' : ''}${record.name}|${record.type.wireName}';
final polls = _polls[key] = (_polls[key] ?? 0) + 1;
final googleCaughtUp = polls >= 3;
final googleHasRecord = deleted ? !googleCaughtUp : googleCaughtUp;
return <DnsResolverComparison>[
DnsResolverComparison(
name: record.name,
type: record.type,
answers: <String, List<DnsRecord>>{
'Cloudflare': deleted ? const <DnsRecord>[] : <DnsRecord>[record],
'Google': googleHasRecord
? <DnsRecord>[record]
: const <DnsRecord>[],
},
),
];
}
return <DnsResolverComparison>[
for (var i = 0; i < list.length; i++)
DnsResolverComparison(
name: list[i].name,
type: list[i].type,
answers: <String, List<DnsRecord>>{
'Cloudflare': <DnsRecord>[list[i]],
// The last record simulates a change Google has not seen yet.
'Google': i == list.length - 1
? const <DnsRecord>[]
: <DnsRecord>[list[i]],
},
),
];
}
}
/// In-memory gateway so the example is safe to run without a Cloudflare token.
///
/// It supports batches, so zone file imports are applied in one step.
class DemoDnsGateway implements BatchCapableGateway {
/// Whether a record with [record]'s name, type, and content is in the zone.
/// The dashboard watches records by their fully qualified name.
bool contains(DnsRecord record) => records.any(
(r) =>
r.type == record.type &&
r.canonicalContent == record.canonicalContent &&
(r.name == record.name || record.name.startsWith('${r.name}.')),
);
final records = <DnsRecord>[
const DnsRecord(
id: 'web',
type: DnsRecordType.a,
name: 'seungpyo.online',
content: '76.76.21.21',
proxied: false,
),
const DnsRecord(
id: 'verify',
type: DnsRecordType.txt,
name: 'seungpyo.online',
content: 'google-site-verification=example',
proxied: false,
),
DnsRecord.srv(
id: 'minecraft',
name: '_minecraft._tcp.seungpyo.online',
priority: 0,
weight: 0,
port: 25565,
target: 'mc.seungpyo.online',
),
DnsRecord.mx(
id: 'mail',
name: 'seungpyo.online',
priority: 10,
mailServer: 'mail.seungpyo.online',
),
DnsRecord.caa(
id: 'caa',
name: 'seungpyo.online',
tag: 'issue',
value: 'letsencrypt.org',
),
DnsRecord.https(
id: 'https',
name: 'seungpyo.online',
priority: 1,
params: 'alpn="h3,h2"',
),
];
@override
Future<List<DnsZone>> listZones() async => const <DnsZone>[
DnsZone(id: 'demo-zone', name: 'seungpyo.online', status: 'active'),
];
@override
Future<List<DnsRecord>> listRecords(String zoneId) async => List.of(records);
@override
Future<DnsRecord> createRecord(String zoneId, DnsRecord record) async {
final created = record.copyWith(id: 'demo-${records.length + 1}');
records.add(created);
return created;
}
@override
Future<DnsRecord> updateRecord(
String zoneId,
String recordId,
DnsRecord record,
) async {
final index = records.indexWhere((item) => item.id == recordId);
final updated = record.copyWith(id: recordId);
records[index] = updated;
return updated;
}
@override
Future<void> deleteRecord(String zoneId, String recordId) async {
records.removeWhere((record) => record.id == recordId);
}
@override
Future<DnsBatchResult> batch(
String zoneId, {
Iterable<DnsRecord> posts = const <DnsRecord>[],
Iterable<DnsRecord> patches = const <DnsRecord>[],
Iterable<String> deletes = const <String>[],
}) async {
final ids = deletes.toSet();
records.removeWhere((record) => ids.contains(record.id));
final updated = <DnsRecord>[
for (final patch in patches) await updateRecord(zoneId, patch.id!, patch),
];
final created = <DnsRecord>[
for (final post in posts) await createRecord(zoneId, post),
];
return DnsBatchResult(
posts: created,
patches: updated,
deletes: ids.toList(growable: false),
);
}
}