// 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({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 {}, this.host = webDevAnyHostDefault, this.port = 0, this.https, this.proxy = const [], }); factory WebDevServerConfig.fromYaml(YamlMap yaml, Logger logger) { final String? host = _validateType(value: yaml[_kHost], fieldName: _kHost); final int? port = _validateType(value: yaml[_kPort], fieldName: _kPort); final YamlMap? https = _validateType(value: yaml[_kHttps], fieldName: _kHttps); final YamlList? headersList = _validateType( value: yaml[_kHeaders], fieldName: _kHeaders, ); final headers = {}; 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(value: yaml[_kProxy], fieldName: _kProxy); final proxyRules = [ ...?proxyList?.whereType().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 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. /// /// The override parameters (`host`, `port`, `https`, `headers`, `proxy`) /// take precedence over this config's values when provided (non-null). /// This implements the CLI > config file precedence. WebDevServerConfig copyWith({ String? host, int? port, HttpsConfig? https, Map? headers, List? proxy, }) { return WebDevServerConfig( host: host ?? this.host, port: port ?? this.port, https: https ?? this.https, headers: {...this.headers, ...?headers}, proxy: proxy ?? this.proxy, ); } final Map headers; final String host; final int port; final HttpsConfig? https; final List 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(value: yaml[_kCertPath], fieldName: _kCertPath); if (certPath == null) { throw ArgumentError.value(yaml, 'yaml', '"$_kCertPath" must be defined'); } final String? certKeyPath = _validateType( 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 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; }