dartmole_plugin 0.1.0 copy "dartmole_plugin: ^0.1.0" to clipboard
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 ordinary HttpClient that 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 in src/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 in src/profile:

    <application android:networkSecurityConfig="@xml/network_security_config"/>
    

    Use base-config rather than debug-overrides: profile builds aren't debuggable, and Android ignores debug-overrides in 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.
0
likes
150
points
--
downloads

Documentation

API reference

Publisher

verified publisherskystoneapps.com

Routes a Flutter app's HTTP through Dartmole, so it can be inspected and mocked.

Homepage

Topics

#http #network #debugging #proxy #mock

License

MIT (license)

Dependencies

flutter, mobile_scanner, url_launcher

More

Packages that depend on dartmole_plugin

Packages that implement dartmole_plugin