blob: 7b4de5b2af4524191bb2b9e2d1eadae7b6e17701 [file] [log] [blame]
// 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 'dart:async';
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';
import '../base/common.dart';
import '../base/file_system.dart' as fs show File, FileSystem;
import '../base/logger.dart' show Logger;
import '../base/os.dart';
import 'devfs_proxy.dart';
const webDevServerConfigFilePath = 'web_dev_config.yaml';
/// Represents the default value for the web dev server.
///
/// Maps to `localhost` and/or `127.0.0.1`.
const webDevAnyHostDefault = 'any';
const _kLogEntryPrefix = '[WebDevServer]';
const _kServer = 'server';
const _kName = 'name';
const _kValue = 'value';
const _kHost = 'host';
const _kPort = 'port';
const _kHttps = 'https';
const _kProxy = 'proxy';
const _kHeaders = 'headers';
const _kCertKeyPath = 'cert-key-path';
const _kCertPath = 'cert-path';
/// Checks if a given [value] has the expected type [T].
///
/// Throws a [ToolExit] if the [value] is not null and has the wrong type.
T? _validateType<T>({required Object? value, required String fieldName}) {
if (value != null && value is! T) {
throwToolExit('$_kLogEntryPrefix $fieldName must be a $T. Found ${value.runtimeType}');
}
return value as T?;
}
/// Represents the configuration for the web development server as a [WebDevServerConfig].
@immutable
class WebDevServerConfig {
const WebDevServerConfig({
this.headers = const <String, String>{},
this.host = webDevAnyHostDefault,
this.port = 0,
this.https,
this.proxy = const <ProxyRule>[],
});
factory WebDevServerConfig.fromYaml(YamlMap yaml, Logger logger) {
final String? host = _validateType<String>(value: yaml[_kHost], fieldName: _kHost);
final int? port = _validateType<int>(value: yaml[_kPort], fieldName: _kPort);
final YamlMap? https = _validateType<YamlMap>(value: yaml[_kHttps], fieldName: _kHttps);
final YamlList? headersList = _validateType<YamlList>(
value: yaml[_kHeaders],
fieldName: _kHeaders,
);
final headers = <String, String>{};
if (headersList != null) {
for (final Object? item in headersList) {
if (item is YamlMap) {
final YamlMap headerMap = item;
if (!headerMap.containsKey(_kName) || !headerMap.containsKey(_kValue)) {
throwToolExit(
'$_kLogEntryPrefix Each header entry must contain "$_kName" and "$_kValue" keys.',
);
}
final Object? name = headerMap[_kName];
if (name is! String) {
throwToolExit(
'$_kLogEntryPrefix Header "$_kName" must be a non-null String. Found ${name.runtimeType}',
);
}
final Object? value = headerMap[_kValue];
if (value is! String) {
throwToolExit(
'$_kLogEntryPrefix Header "$_kValue" must be a non-null String. Found ${value.runtimeType}',
);
}
headers[name] = value;
} else {
throwToolExit(
'$_kLogEntryPrefix Each header entry must be a map. Found ${item.runtimeType}',
);
}
}
}
final YamlList? proxyList = _validateType<YamlList>(value: yaml[_kProxy], fieldName: _kProxy);
final proxyRules = <ProxyRule>[
...?proxyList?.whereType<YamlMap>().map((e) => ProxyRule.fromYaml(e, logger)).nonNulls,
];
return WebDevServerConfig(
headers: headers,
host: host ?? webDevAnyHostDefault,
port: port ?? 0,
https: https == null ? null : HttpsConfig.fromYaml(https),
proxy: proxyRules,
);
}
static var _loadFromFileAlreadyLogged = false;
/// Creates a [WebDevServerConfig] from the `web_dev_config.yaml` file.
///
/// This method is responsible for loading and parsing the configuration
static Future<WebDevServerConfig> loadFromFile({
required fs.FileSystem fileSystem,
required Logger logger,
}) async {
final fs.File webDevServerConfigFile = fileSystem.file(webDevServerConfigFilePath);
if (!webDevServerConfigFile.existsSync()) {
return const WebDevServerConfig();
}
try {
final String fileContent = await webDevServerConfigFile.readAsString();
final YamlDocument yamlDoc = loadYamlDocument(fileContent);
final YamlNode contents = yamlDoc.contents;
if (contents is! YamlMap ||
!contents.containsKey(_kServer) ||
contents[_kServer] is! YamlMap) {
throwToolExit(
'$_kLogEntryPrefix Found $webDevServerConfigFilePath configuration file but it was malformed.',
);
}
final serverYaml = contents[_kServer] as YamlMap;
final fileConfig = WebDevServerConfig.fromYaml(serverYaml, logger);
if (!_loadFromFileAlreadyLogged) {
logger.printStatus(
'$_kLogEntryPrefix Loaded configuration from $webDevServerConfigFilePath',
);
logger.printTrace(fileConfig.toString());
_loadFromFileAlreadyLogged = true;
}
return fileConfig;
} on Exception catch (e) {
throwToolExit('$_kLogEntryPrefix Error: Failed to parse $webDevServerConfigFilePath: $e');
}
}
/// Creates a copy of a [WebDevServerConfig] with optional overrides.
WebDevServerConfig copyWith({
String? host,
int? port,
HttpsConfig? https,
Map<String, String>? headers,
List<ProxyRule>? proxy,
}) {
return WebDevServerConfig(
host: host ?? this.host,
port: port ?? this.port,
https: https ?? this.https,
headers: {...?headers, ...this.headers},
proxy: proxy ?? this.proxy,
);
}
final Map<String, String> headers;
final String host;
final int port;
final HttpsConfig? https;
final List<ProxyRule> proxy;
@override
String toString() {
return '''
WebDevServerConfig:
$_kHeaders: $headers
$_kHost: $host
$_kPort: $port
$_kHttps: $https
$_kProxy: $proxy''';
}
}
/// Represents the [HttpsConfig] for the web dev server
@immutable
class HttpsConfig {
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);
}
/// 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'}',
),
};
/// 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() {
return '''
HttpsConfig:
$_kCertPath: $certPath
$_kCertKeyPath: $certKeyPath''';
}
}
/// Finds a free port or validates the provided port is within the valid range
Future<int> resolvePort(int? port, OperatingSystemUtils os) async {
if (port == null) {
return os.findFreePort();
}
if (port < 0 || port > 65535) {
throwToolExit('''
Invalid port: $port
Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
''');
}
return port;
}