| // 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'; | |
| const _kBaseHref = 'base-href'; | |
| /// 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>[], | |
| this.baseHref, | |
| }); | |
| 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, | |
| ]; | |
| final String? baseHref = _validateType<String>(value: yaml[_kBaseHref], fieldName: _kBaseHref); | |
| return WebDevServerConfig( | |
| headers: headers, | |
| host: host ?? webDevAnyHostDefault, | |
| port: port ?? 0, | |
| https: https == null ? null : HttpsConfig.fromYaml(https), | |
| proxy: proxyRules, | |
| baseHref: baseHref, | |
| ); | |
| } | |
| 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. | |
| /// | |
| /// 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<String, String>? headers, | |
| List<ProxyRule>? proxy, | |
| String? baseHref, | |
| }) { | |
| return WebDevServerConfig( | |
| host: host ?? this.host, | |
| port: port ?? this.port, | |
| https: https ?? this.https, | |
| headers: {...this.headers, ...?headers}, | |
| proxy: proxy ?? this.proxy, | |
| baseHref: baseHref ?? this.baseHref, | |
| ); | |
| } | |
| final Map<String, String> headers; | |
| final String host; | |
| final int port; | |
| final HttpsConfig? https; | |
| final List<ProxyRule> proxy; | |
| final String? baseHref; | |
| @override | |
| String toString() { | |
| return ''' | |
| WebDevServerConfig: | |
| $_kHeaders: $headers | |
| $_kHost: $host | |
| $_kPort: $port | |
| $_kHttps: $https | |
| $_kProxy: $proxy | |
| $_kBaseHref: $baseHref'''; | |
| } | |
| } | |
| /// 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; | |
| } |