SaFi Bank Space : HTTP/3 Flutter

HTTP/3 is the third major version of the Hypertext Transfer Protocol used to exchange information on the World Wide Web, complementing the widely-deployed HTTP/1.1 and HTTP/2. Unlike previous versions which relied on the well-established TCP (published in 1974),[1] HTTP/3 uses QUIC, a multiplexed transport protocol built on UDP.[2] On 6 June 2022, IETF published HTTP/3 as a Proposed Standard in RFC 9114.[3]
- https://en.wikipedia.org/wiki/HTTP/3

Benefits

QUIC will help fix some of HTTP/2's biggest shortcomings:

  • Developing a workaround for the sluggish performance when a smartphone switches from WiFi to cellular data (such as when leaving the house or office)

  • Decreasing the effects of packet loss — when one packet of information does not make it to its destination, it will no longer block all streams of information (a problem known as “head-of-line blocking”)

Other benefits include:

  • Faster connection establishment: QUIC allows TLS version negotiation to happen at the same time as the cryptographic and transport handshakes

  • Zero round-trip time (0-RTT): For servers they have already connected to, clients can skip the handshake requirement (the process of acknowledging and verifying each other to determine how they will communicate)

  • More comprehensive encryption: QUIC’s new approach to handshakes will provide encryption by default — a huge upgrade from HTTP/2 — and will help mitigate the risk of attacks

Flutter Package

Dart has not yet officially support HTTP/3 in their SDK, It is an open issue to support HTTP/3 (https://github.com/dart-lang/sdk/issues/38595). But they have an experimental packages for Flutter iOS and Android separately. It is:

Both packages can be used easily since they are using he high-level interface defined by package:http Client.

How to implement

Since we have a http client, so we only need to change the wrapper. Previously we are using BaseIOClient, now we only need to use the package Client factory.

HttpClient
  • Setup Client without SSL Pinning

void setUpClient({
  Client? mockClient,
}) {
  final http3Factory = BaseHttp3Client.clientFactory();
  client = mockClient ?? http3Factory();
  imageClient = mockClient ?? http3Factory();
}
  • Setup Client with SSL Pinning

void setUpCertPinning({
  required String certBase64,
  String? certAltBase64,
}) {
  void setUpCert(io.SecurityContext securityContext, String cert) {
    final certBytes = base64Decode(cert);
    try {
      securityContext.setTrustedCertificatesBytes(certBytes);
    } catch (e) {
      if (e is io.TlsException &&
          e.osError?.errorCode == _tlsCertAlreadyRegisteredErrorCode) {
        // Cert already registered, so ignore the error.
        return;
      }
      rethrow;
    }
  }

  final securityContext = io.SecurityContext();

  setUpCert(securityContext, certBase64);
  if (certAltBase64 != null) {
    setUpCert(securityContext, certAltBase64);
  }
  
  /// This client is no longer used,
  /// And http3 package is not supporting SSL Pinning yet
  final httpClient = io.HttpClient(context: securityContext)
    ..badCertificateCallback = (cert, host, port) {
      LOG.error('Alert bad certificate exceptions $host $port');
      return !Features.enableCertPinning.value;
    };

  final http3Factory = BaseHttp3Client.clientFactory();
  client = http3Factory();
  imageClient = http3Factory();
}

BaseHtttp3Client is our static class to give the respective client factory. If its iOS/macOS it would return CoupertinoClient, If its Android then CronetClient, otherwise it will return default Client;

BaseHtttp3Client
import 'dart:io';

import 'package:cronet_http/cronet_client.dart' as cronet;
import 'package:cupertino_http/cupertino_client.dart' as coupertino;
import 'package:http/http.dart' as http;

typedef _CreateClient = http.Client Function();

class BaseHttp3Client {
  static _CreateClient clientFactory() {
    _CreateClient clientFactory = http.Client.new; // The default Client.
    if (Platform.isIOS || Platform.isMacOS) {
      clientFactory =
          coupertino.CupertinoClient.defaultSessionConfiguration.call;
    } else if (Platform.isAndroid) {
      clientFactory = cronet.CronetClient.new;
    }
    return clientFactory;
  }
}

It is easy to implement and have a minimal change, but we are losing our ability to have SSL Pinning and Upload Progress Stream. We can’t have SSL Pinning because there is way to add SecurityContext to this client. And Upload Progress Stream is can’t be implemented in this client since they send the data on a big chunk and wait the process till completed.

BaseIOClient.send

CoupertinoClient.send

CronetClient.send

final stream = request.finalize();
final StreamController<double> sc = StreamController<double>();

final contentLength = request.contentLength ?? -1;
final ioRequest = (await _inner!.openUrl(request.method, request.url))
  ..followRedirects = request.followRedirects
  ..maxRedirects = request.maxRedirects
  ..contentLength = contentLength
  ..persistentConnection = request.persistentConnection;
request.headers.forEach((name, value) {
  ioRequest.headers.set(name, value);
});
int uploadedDataCount = 0;
uploadStreamCompleter?.complete(sc.stream);
final processCompleter = Completer();
final requestCompleter = Completer();
final sl = stream.listen(
  (value) {
    ioRequest.add(value);
    uploadedDataCount += value.length;
    final percentage = uploadedDataCount / contentLength;
    sc.add(percentage.isNaN
        ? GeneralConstant.ninetyNinePercent
        : percentage);
  },
  onError: (e, st) {
    ioRequest.addError(e, st);
    sc.addError(e, st);
  },
  onDone: () {
    ioRequest.close().then((value) {
      sc
        ..add(1)
        ..close();
      processCompleter.complete();
      requestCompleter.complete(value);
      return;
    }).catchError((e, st) {
      sc
        ..addError(e, st)
        ..close();
      return;
    });
  },
);
await processCompleter.future;
await sl.cancel();

final HttpClientResponse response = await requestCompleter.future;
final stream = request.finalize();

final bytes = await stream.toBytes();
final d = Data.fromUint8List(bytes);

final urlRequest = MutableURLRequest.fromUrl(request.url)
  ..httpMethod = request.method
  ..httpBody = d;

// This will preserve Apple default headers - is that what we want?
request.headers.forEach(urlRequest.setValueForHttpHeaderField);

final task = _urlSession.dataTaskWithRequest(urlRequest);
final taskTracker = _TaskTracker(request);
_tasks[task.taskIdentifier] = taskTracker;
task.resume();

final maxRedirects = request.followRedirects ? request.maxRedirects : 0;

final result = await taskTracker.responseCompleter.future;
final response = result as HTTPURLResponse;
final stream = request.finalize();

final body = await stream.toBytes();

var headers = request.headers;
if (body.isNotEmpty &&
    !headers.keys.any((h) => h.toLowerCase() == 'content-type')) {
  // Cronet requires that requests containing upload data set a
  // 'Content-Type' header.
  headers = {...headers, 'content-type': 'application/octet-stream'};
}

final response = await _api.start(messages.StartRequest(
  engineId: _engine!._engineId,
  url: request.url.toString(),
  method: request.method,
  headers: headers,
  body: body,
  followRedirects: request.followRedirects,
  maxRedirects: request.maxRedirects,
));

How to test

We need to enable HTTP/3 support in our server first. In this case, we are using CloudFlare and they have toggle for it. So we need to turn on HTTP/3 (with QUIC) and 0-RTT Connection Resumption.

Once those toggles enabled, We add a debug pointer on the http result then make a request from the app side. HTTP/3 response will return a response header called alt-svc and we must see h3=:443. That is indicating that the request is handled using HTTP/3.

Summary

Flutter is support HTTP/3 by using their experimental packages. But we are losing our ability to have SSL Pinning which is feature that recommended by our Penetration Tester. Other than that, we are can’t have an Upload Progress Stream, because the http/3 client doesn’t pipe the body stream they send it in a big chunk.

Sources

Attachments: