Add support for setting headers for all requests (#1060)

diff --git a/pkgs/cupertino_http/CHANGELOG.md b/pkgs/cupertino_http/CHANGELOG.md
index 48d990e..47e25cb 100644
--- a/pkgs/cupertino_http/CHANGELOG.md
+++ b/pkgs/cupertino_http/CHANGELOG.md
@@ -1,4 +1,7 @@
-## 1.1.1-wip
+## 1.2.0-wip
+
+* Add support for setting additional http headers in
+  `URLSessionConfiguration`.
 
 ## 1.1.0
 
diff --git a/pkgs/cupertino_http/example/integration_test/url_session_configuration_test.dart b/pkgs/cupertino_http/example/integration_test/url_session_configuration_test.dart
index 85386bc..ab117c3 100644
--- a/pkgs/cupertino_http/example/integration_test/url_session_configuration_test.dart
+++ b/pkgs/cupertino_http/example/integration_test/url_session_configuration_test.dart
@@ -2,10 +2,38 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:io';
+
 import 'package:cupertino_http/cupertino_http.dart';
 import 'package:integration_test/integration_test.dart';
 import 'package:test/test.dart';
 
+/// Make a HTTP request using the given configuration and return the headers
+/// received by the server.
+Future<Map<String, List<String>>> sentHeaders(
+    URLSessionConfiguration config) async {
+  final session = URLSession.sessionWithConfiguration(config);
+  final headers = <String, List<String>>{};
+  final server = (await HttpServer.bind('localhost', 0))
+    ..listen((request) async {
+      request.headers.forEach((k, v) => headers[k] = v);
+      await request.drain<void>();
+      request.response.headers.set('Content-Type', 'text/plain');
+      request.response.write('Hello World');
+      await request.response.close();
+    });
+
+  final task = session.dataTaskWithRequest(URLRequest.fromUrl(
+      Uri(scheme: 'http', host: 'localhost', port: server.port)))
+    ..resume();
+  while (task.state != URLSessionTaskState.urlSessionTaskStateCompleted) {
+    await pumpEventQueue();
+  }
+
+  await server.close();
+  return headers;
+}
+
 void testProperties(URLSessionConfiguration config) {
   group('properties', () {
     test('allowsCellularAccess', () {
@@ -32,6 +60,22 @@
       config.discretionary = false;
       expect(config.discretionary, false);
     });
+    test('httpAdditionalHeaders', () async {
+      expect(config.httpAdditionalHeaders, isNull);
+
+      config.httpAdditionalHeaders = {
+        'User-Agent': 'My Client',
+        'MyHeader': 'myvalue'
+      };
+      expect(config.httpAdditionalHeaders,
+          {'User-Agent': 'My Client', 'MyHeader': 'myvalue'});
+      final headers = await sentHeaders(config);
+      expect(headers, containsPair('user-agent', ['My Client']));
+      expect(headers, containsPair('myheader', ['myvalue']));
+
+      config.httpAdditionalHeaders = null;
+      expect(config.httpAdditionalHeaders, isNull);
+    });
     test('httpCookieAcceptPolicy', () {
       config.httpCookieAcceptPolicy =
           HTTPCookieAcceptPolicy.httpCookieAcceptPolicyAlways;
diff --git a/pkgs/cupertino_http/example/lib/main.dart b/pkgs/cupertino_http/example/lib/main.dart
index edfceac..32c2b08 100644
--- a/pkgs/cupertino_http/example/lib/main.dart
+++ b/pkgs/cupertino_http/example/lib/main.dart
@@ -15,7 +15,10 @@
 void main() {
   var clientFactory = Client.new; // The default Client.
   if (Platform.isIOS || Platform.isMacOS) {
-    clientFactory = CupertinoClient.defaultSessionConfiguration.call;
+    final config = URLSessionConfiguration.ephemeralSessionConfiguration()
+      ..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024)
+      ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'};
+    clientFactory = () => CupertinoClient.fromSessionConfiguration(config);
   }
   runWithClient(() => runApp(const BookSearchApp()), clientFactory);
 }
diff --git a/pkgs/cupertino_http/lib/src/cupertino_api.dart b/pkgs/cupertino_http/lib/src/cupertino_api.dart
index 01c4853..5cebd7e 100644
--- a/pkgs/cupertino_http/lib/src/cupertino_api.dart
+++ b/pkgs/cupertino_http/lib/src/cupertino_api.dart
@@ -344,6 +344,32 @@
   bool get discretionary => _nsObject.discretionary;
   set discretionary(bool value) => _nsObject.discretionary = value;
 
+  /// Additional headers to send with each request.
+  ///
+  /// Note that the getter for this field returns a **copy** of the headers.
+  ///
+  /// See [NSURLSessionConfiguration.HTTPAdditionalHeaders](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1411532-httpadditionalheaders)
+  Map<String, String>? get httpAdditionalHeaders {
+    if (_nsObject.HTTPAdditionalHeaders case var additionalHeaders?) {
+      final headers = ncb.NSDictionary.castFrom(additionalHeaders);
+      return stringDictToMap(headers);
+    }
+    return null;
+  }
+
+  set httpAdditionalHeaders(Map<String, String>? headers) {
+    if (headers == null) {
+      _nsObject.HTTPAdditionalHeaders = null;
+      return;
+    }
+    final d = ncb.NSMutableDictionary.alloc(linkedLibs).init();
+    headers.forEach((key, value) {
+      d.setObject_forKey_(
+          value.toNSString(linkedLibs), key.toNSString(linkedLibs));
+    });
+    _nsObject.HTTPAdditionalHeaders = d;
+  }
+
   /// What policy to use when deciding whether to accept cookies.
   ///
   /// See [NSURLSessionConfiguration.HTTPCookieAcceptPolicy](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1408933-httpcookieacceptpolicy).
@@ -441,6 +467,7 @@
       'allowsConstrainedNetworkAccess=$allowsConstrainedNetworkAccess '
       'allowsExpensiveNetworkAccess=$allowsExpensiveNetworkAccess '
       'discretionary=$discretionary '
+      'httpAdditionalHeaders=$httpAdditionalHeaders '
       'httpCookieAcceptPolicy=$httpCookieAcceptPolicy '
       'httpShouldSetCookies=$httpShouldSetCookies '
       'httpMaximumConnectionsPerHost=$httpMaximumConnectionsPerHost '
diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml
index 824a581..81905aa 100644
--- a/pkgs/cupertino_http/pubspec.yaml
+++ b/pkgs/cupertino_http/pubspec.yaml
@@ -1,5 +1,5 @@
 name: cupertino_http
-version: 1.1.1-wip
+version: 1.2.0-wip
 description: >-
   A macOS/iOS Flutter plugin that provides access to the Foundation URL
   Loading System.