dartmole_plugin 0.1.0
dartmole_plugin: ^0.1.0 copied to clipboard
Routes a Flutter app's HTTP through Dartmole, so it can be inspected and mocked.
dartmole_plugin #
Lets Dartmole inspect and mock your Flutter app's HTTP traffic. Add it to the app under test, pair the device with Dartmole by scanning a QR code, and the app's Dart HTTP calls go through Dartmole's proxy.
- TLS stays on. The plugin adds Dartmole's certificate authority to the trusted ones. It never turns certificate checking off.
- Off by default. Until a device is paired and interception is switched
on,
createHttpClient()returns an ordinaryHttpClientthat behaves as if the plugin weren't there. - Nothing is compiled in. The certificate authority arrives at pairing time, after someone scans the QR code, and goes away on Forget.
The example is a small weather app with everything below wired up.
Install #
Install the Dartmole app on your Mac from dartmole.dev, then add the plugin to your app:
flutter pub add dartmole_plugin
iOS #
The QR scanner needs the camera. Without this key, iOS terminates the app the
moment the scanner opens. Add both keys to ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>Scans the QR code in Dartmole to pair with it.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Connects to Dartmole on this network to inspect the app's traffic.</string>
The second key is the text iOS shows when it asks for local network access. The app needs that access to reach Dartmole on your Mac.
Android #
Flutter's templates only grant INTERNET to debug and profile builds. If a
release-mode build (an internal build for testers, say) should pair, add it to
android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>
The camera permission comes with the scanner.
Route your HTTP through it #
Dartmole().createHttpClient() returns a dart:io HttpClient. Give it to
whatever does your networking.
There's one catch. HTTP libraries create their HttpClient once and keep it,
so a client made before pairing never goes through Dartmole. Listen to
Dartmole(), which is a ChangeNotifier, and swap the client when it changes.
Dio #
import 'package:dartmole_plugin/dartmole_plugin.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/widgets.dart';
final dio = Dio();
void main() {
WidgetsFlutterBinding.ensureInitialized();
_useDartmole();
Dartmole().addListener(_useDartmole);
runApp(const MyApp());
}
void _useDartmole() {
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: Dartmole().createHttpClient,
);
}
package:http #
import 'package:dartmole_plugin/dartmole_plugin.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
http.Client client = IOClient(Dartmole().createHttpClient());
void main() {
WidgetsFlutterBinding.ensureInitialized();
Dartmole().addListener(() {
client.close();
client = IOClient(Dartmole().createHttpClient());
});
runApp(const MyApp());
}
Read client at the moment of each call rather than storing it elsewhere, so
calls pick up the new one.
HttpClient #
Create one per batch of requests with Dartmole().createHttpClient() rather
than HttpClient().
Add the pairing panel #
DartmoleSettings pairs, shows which machine the device is paired with,
and switches interception on and off. Put it somewhere testers can reach and
store users can't, like a debug menu:
Scaffold(
appBar: AppBar(title: const Text('Dartmole')),
body: const SingleChildScrollView(
padding: EdgeInsets.all(24),
child: DartmoleSettings(),
),
);
To pair, open Dartmole on your Mac and go to Devices, then on the phone tap Scan QR code. Simulators and devices without a camera can type the host and port shown in Dartmole instead. The phone and the Mac must be on the same network.
Remember the pairing (optional) #
The plugin keeps nothing on disk. Without help, every launch starts unpaired.
To remember a pairing, store pairing.toJson() and pass it back to
reconnect at launch:
final remembered = Pairing.fromJson(storedJson);
try {
await Dartmole().reconnect(remembered);
} on PairingException catch (error) {
// Dartmole isn't running, or its certificate changed. Stay unpaired.
}
reconnect trusts nothing until that machine answers. It also refuses a
certificate authority that differs from the remembered one, so only a new scan
can accept a changed one. Store the JSON somewhere only the app can write,
because it contains the certificate authority the app will trust.
The example's lib/pairing_store.dart does this
with shared_preferences, and keeps the store in step with the panel.
Native traffic (optional) #
All of the above covers Dart's HttpClient. Code that uses the platform's own
networking doesn't go through it: URLSession, OkHttp, cupertino_http,
cronet_http, WebViews, and native SDKs. Dartmole can show that traffic too,
if the device sends it to the proxy and trusts Dartmole's certificate
authority.
Once a device is paired, the panel shows Also intercept native traffic. It opens a page served by Dartmole with step-by-step instructions written for testers. Your part is making sure the build can use the result:
-
iOS: nothing to do. A certificate profile the user installs and fully trusts applies to every app.
-
Android: since Android 7, an app ignores certificates the user installs unless its network security config opts in. Opt in for debug and profile builds only, so release builds keep trusting the system's authorities alone.
android/app/src/debug/res/xml/network_security_config.xml, and the same file insrc/profile:<?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config> <trust-anchors> <certificates src="system" /> <certificates src="user" /> </trust-anchors> </base-config> </network-security-config>In
android/app/src/debug/AndroidManifest.xml, and the same insrc/profile:<application android:networkSecurityConfig="@xml/network_security_config"/>Use
base-configrather thandebug-overrides: profile builds aren't debuggable, and Android ignoresdebug-overridesin them.
The plugin checks this itself. In a build that doesn't opt in, the panel
offers no button, and tells the tester the build can't show its native traffic.
The debug console also prints what to add.
UserCertificateTrust.check() gives you the same answer in code.
Is it working? #
The panel doesn't just show the switch. It sends a request down each path and says what happened: whether the app's Dart HTTP actually reaches Dartmole, and whether native traffic does. If not, it says what's missing: no proxy on the device, a proxy pointing somewhere else, or a certificate that isn't trusted yet. It checks again when the panel opens, and when the app comes back to the foreground, so a tester returning from the device's Settings sees the result straight away. For anything that changes while the app stays open, like Dartmole's proxy restarting or another Wi-Fi network, the refresh button next to the machine checks on demand, and the row says when it last checked. The debug console prints each result in full.
In code, Dartmole().checkDartTraffic() and checkNativeTraffic() return an
InterceptionCheck. Both return null when not paired, or when paired with a
Dartmole too old to answer the check.
Security #
- The device gets the certificate authority over plain HTTP at pairing time. Someone able to intercept your local network at that moment could substitute their own. Pair on a network you trust.
- An app that remembers a pairing keeps trusting that Mac whenever it's reachable, until someone taps Forget.
- Intercepting native traffic installs Dartmole's authority on the whole device, for every app, until it's removed. Dartmole's setup page tells testers how to remove it.
- Anyone with a build can pair it with their own Dartmole. If that matters, keep the panel out of builds that leave your team.