[tool] clean up https cert configuration handling (#178139)

It's only valid if both the cert and the cert key are set.
Makes the code a lot cleaner, too.
This commit is contained in:
Kevin Moore 2025-11-12 20:20:27 -08:00 committed by GitHub
parent d48aa42950
commit 017e4f00fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 103 additions and 24 deletions

View File

@ -1374,13 +1374,7 @@ class DebuggingOptions {
webDevServerConfig: WebDevServerConfig(
port: json['port'] is int ? json['port']! as int : 8080,
host: json['hostname'] is String ? json['hostname']! as String : 'localhost',
https: (json['tlsCertPath'] != null || json['tlsCertKeyPath'] != null)
? HttpsConfig(
certPath: json['tlsCertPath'] as String?,
certKeyPath: json['tlsCertKeyPath'] as String?,
)
: null,
https: HttpsConfig.parse(json['tlsCertPath'], json['tlsCertKeyPath']),
headers: (json['webHeaders']! as Map<dynamic, dynamic>).cast<String, String>(),
),
);

View File

@ -197,8 +197,7 @@ class WebAssetServer implements AssetReader {
}) async {
final String hostname = webDevServerConfig.host;
final int port = webDevServerConfig.port;
final String? tlsCertPath = webDevServerConfig.https?.certPath;
final String? tlsCertKeyPath = webDevServerConfig.https?.certKeyPath;
final HttpsConfig? httpsConfig = webDevServerConfig.https;
final Map<String, String> extraHeaders = webDevServerConfig.headers;
final List<ProxyRule> proxy = webDevServerConfig.proxy;
@ -217,10 +216,10 @@ class WebAssetServer implements AssetReader {
const kMaxRetries = 4;
for (var i = 0; i <= kMaxRetries; i++) {
try {
if (tlsCertPath != null && tlsCertKeyPath != null) {
if (httpsConfig != null) {
final serverContext = SecurityContext()
..useCertificateChain(tlsCertPath)
..usePrivateKey(tlsCertKeyPath);
..useCertificateChain(httpsConfig.certPath)
..usePrivateKey(httpsConfig.certKeyPath);
httpServer = await HttpServer.bindSecure(address, port, serverContext);
} else {
httpServer = await HttpServer.bind(address, port);
@ -260,7 +259,7 @@ class WebAssetServer implements AssetReader {
final int selectedPort = server.selectedPort;
final cleanHost = hostname == webDevAnyHostDefault ? 'localhost' : hostname;
final scheme = tlsCertPath != null && tlsCertKeyPath != null ? 'https' : 'http';
final scheme = httpsConfig != null ? 'https' : 'http';
server._baseUri = Uri(
scheme: scheme,
host: cleanHost,

View File

@ -189,28 +189,51 @@ WebDevServerConfig:
/// Represents the [HttpsConfig] for the web dev server
@immutable
class HttpsConfig {
const HttpsConfig({this.certPath, this.certKeyPath});
const HttpsConfig({required this.certPath, required this.certKeyPath});
factory HttpsConfig.fromYaml(YamlMap yaml) {
final String? certPath = _validateType<String>(value: yaml[_kCertPath], fieldName: _kCertPath);
if (certPath == null) {
throw ArgumentError.value(yaml, 'yaml', '"$_kCertPath" must be defined');
}
final String? certKeyPath = _validateType<String>(
value: yaml[_kCertKeyPath],
fieldName: _kCertKeyPath,
);
if (certKeyPath == null) {
throw ArgumentError.value(yaml, 'yaml', '"$_kCertKeyPath" must be defined');
}
return HttpsConfig(certPath: certPath, certKeyPath: certKeyPath);
}
/// Creates a copy of this [HttpsConfig] with optional overrides.
HttpsConfig copyWith({String? certPath, String? certKeyPath}) {
return HttpsConfig(
certPath: certPath ?? this.certPath,
certKeyPath: certKeyPath ?? this.certKeyPath,
);
}
/// If [tlsCertPath] and [tlsCertKeyPath] are both [String] return an instance.
///
/// If they are both `null`, return `null`.
///
/// Otherwise, throw an [Exception].
static HttpsConfig? parse(Object? tlsCertPath, Object? tlsCertKeyPath) =>
switch ((tlsCertPath, tlsCertKeyPath)) {
(final String certPath, final String certKeyPath) => HttpsConfig(
certPath: certPath,
certKeyPath: certKeyPath,
),
(null, null) => null,
(final Object? certPath, final Object? certKeyPath) => throw ArgumentError(
'When providing TLS certificates, both `tlsCertPath` and '
'`tlsCertKeyPath` must be provided as strings. '
'Found: tlsCertPath: ${certPath ?? 'null'}, tlsCertKeyPath: ${certKeyPath ?? 'null'}',
),
};
final String? certPath;
final String? certKeyPath;
/// Creates a copy of this [HttpsConfig] with optional overrides.
HttpsConfig copyWith({String? certPath, String? certKeyPath}) => HttpsConfig(
certPath: certPath ?? this.certPath,
certKeyPath: certKeyPath ?? this.certKeyPath,
);
final String certPath;
final String certKeyPath;
@override
String toString() {

View File

@ -0,0 +1,63 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/web/devfs_config.dart';
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
void main() {
group('parse', () {
test('returns HttpsConfig when both paths are provided', () {
final HttpsConfig result = HttpsConfig.parse('/path/to/cert', '/path/to/key')!;
expect(result.certPath, '/path/to/cert');
expect(result.certKeyPath, '/path/to/key');
});
test('returns null when both paths are null', () {
final HttpsConfig? result = HttpsConfig.parse(null, null);
expect(result, isNull);
});
test('throws ArgumentError when only one field is provided', () {
expect(() => HttpsConfig.parse('/path/to/cert', null), throwsArgumentError);
expect(() => HttpsConfig.parse(null, '/path/to/key'), throwsArgumentError);
});
test('throws ArgumentError when the field is the wrong type', () {
expect(() => HttpsConfig.parse(1, '/path/to/key'), throwsArgumentError);
expect(() => HttpsConfig.parse('/path/to/cert', 1), throwsArgumentError);
});
});
group('fromYaml', () {
test('fromYaml throws an ArgumentError if cert-path is not defined', () {
expect(
() => HttpsConfig.fromYaml(loadYaml('cert-key-path: /path/to/key') as YamlMap),
throwsArgumentError,
);
});
test('fromYaml throws an ArgumentError if cert-key-path is not defined', () {
expect(
() => HttpsConfig.fromYaml(loadYaml('cert-path: /path/to/cert') as YamlMap),
throwsArgumentError,
);
});
test(
'fromYaml creates an HttpsConfig object when both certificate and key paths are provided',
() {
final https = HttpsConfig.fromYaml(
loadYaml('''
cert-path: /path/to/cert
cert-key-path: /path/to/key''')
as YamlMap,
);
expect(https.certPath, '/path/to/cert');
expect(https.certKeyPath, '/path/to/key');
},
);
});
}