// 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 'dart:typed_data'; import 'package:dwds/data/build_result.dart'; import 'package:dwds/dwds.dart'; import 'package:logging/logging.dart' as logging; import 'package:meta/meta.dart'; import 'package:mime/mime.dart' as mime; import 'package:package_config/package_config.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf; import '../artifacts.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/net.dart'; import '../base/platform.dart'; import '../build_info.dart'; import '../cache.dart'; import '../convert.dart'; import '../globals.dart' as globals; import '../web/bootstrap.dart'; import '../web/chrome.dart'; import '../web/compile.dart'; import '../web/devfs_config.dart'; import '../web/devfs_proxy.dart'; import '../web/memory_fs.dart'; import '../web/module_metadata.dart'; import '../web/web_constants.dart'; import '../web_template.dart'; import 'proxy_middleware.dart'; import 'release_asset_server.dart'; import 'web_server_utilities.dart'; // A minimal index for projects that do not yet support web. A meta tag is used // to ensure loaded scripts are always parsed as UTF-8. const _kDefaultIndex = ''' '''; typedef DwdsLauncher = Future Function({ required AssetReader assetReader, required Stream buildResults, required ConnectionProvider chromeConnection, required ToolConfiguration toolConfiguration, bool useDwdsWebSocketConnection, }); const kLuciEnvName = 'LUCI_CONTEXT'; /// A web server which handles serving JavaScript and assets. /// /// This is only used in development mode. class WebAssetServer implements AssetReader { @visibleForTesting WebAssetServer( this._httpServer, this._packages, this.internetAddress, this._modules, this._digests, this._ddcModuleSystem, this._canaryFeatures, { required this.webRenderer, required this.useLocalCanvasKit, required this.fileSystem, required this.logger, Map webDefines = const {}, }) : basePath = WebTemplate.baseHref(htmlTemplate(fileSystem, 'index.html', _kDefaultIndex)), _webDefines = webDefines { // TODO(srujzs): Remove this assertion when the library bundle format is // supported without canary mode. if (_ddcModuleSystem) { assert(_canaryFeatures); } } // Fallback to "application/octet-stream" on null which // makes no claims as to the structure of the data. static const _kDefaultMimeType = 'application/octet-stream'; final Map _modules; final Map _digests; int get selectedPort => _httpServer.port; /// Given a list of [modules] that need to be loaded, compute module names and /// digests. void updateModulesAndDigests(List modules) { for (final module in modules) { // We skip computing the digest by using the hashCode of the underlying buffer. // Whenever a file is updated, the corresponding Uint8List.view it corresponds // to will change. final String moduleName = module.startsWith('/') ? module.substring(1) : module; final String name = moduleName.replaceAll('.lib.js', ''); final String path = moduleName.replaceAll('.js', ''); _modules[name] = path; _digests[name] = _webMemoryFS.files[moduleName].hashCode.toString(); } } static const _reloadedSourcesFileName = 'reloaded_sources.json'; /// Given a list of [modules] that need to be reloaded during a hot restart or /// hot reload, writes a file that contains a list of objects each with three /// fields: /// /// `src`: A string that corresponds to the file path containing a DDC library /// bundle. To support embedded libraries, the path should include the /// `baseUri` of the web server. /// `module`: The name of the library bundle in `src`. /// `libraries`: An array of strings containing the libraries that were /// compiled in `src`. /// /// For example: /// ```json /// [ /// { /// "src": "/", /// "module": "", /// "libraries": ["", ""], /// }, /// ] /// ``` /// /// The path of the output file should stay consistent across the lifetime of /// the app. void writeReloadedSources(List modules) { final moduleToLibrary = >[]; for (final module in modules) { final metadata = ModuleMetadata.fromJson( json.decode(utf8.decode(_webMemoryFS.metadataFiles['$module.metadata']!.toList())) as Map, ); final List libraries = metadata.libraries.keys.toList(); final moduleUri = '$baseUri/$module'; moduleToLibrary.add({ 'src': moduleUri, 'module': metadata.name, 'libraries': libraries, }); } writeFile(_reloadedSourcesFileName, json.encode(moduleToLibrary)); } @visibleForTesting List write(File codeFile, File manifestFile, File sourcemapFile, File metadataFile) { return _webMemoryFS.write(codeFile, manifestFile, sourcemapFile, metadataFile); } Uri get baseUri => _baseUri; late Uri _baseUri; /// Start the web asset server with configuration provided by [webDevServerConfig]. /// /// If [testMode] is true, do not actually initialize dwds or the shelf static /// server. /// /// Unhandled exceptions will throw a [ToolExit] with the error and stack /// trace. static Future start( ChromiumLauncher? chromiumLauncher, UrlTunneller? urlTunneller, bool useSseForDebugProxy, bool useSseForDebugBackend, bool useSseForInjectedClient, BuildInfo buildInfo, bool enableDwds, DartDevelopmentServiceConfiguration ddsConfig, Uri entrypoint, ExpressionCompiler? expressionCompiler, { required bool crossOriginIsolation, required WebDevServerConfig webDevServerConfig, required WebRendererMode webRenderer, required bool isWasm, required bool useLocalCanvasKit, bool testMode = false, DwdsLauncher dwdsLauncher = Dwds.start, // TODO(markzipan): Make sure this default value aligns with that in the debugger options. bool ddcModuleSystem = false, bool canaryFeatures = false, bool useDwdsWebSocketConnection = false, required FileSystem fileSystem, required Logger logger, required Platform platform, bool shouldEnableMiddleware = true, Map webDefines = const {}, }) async { final String hostname = webDevServerConfig.host; final int port = webDevServerConfig.port; final HttpsConfig? httpsConfig = webDevServerConfig.https; final Map extraHeaders = webDevServerConfig.headers; final List proxy = webDevServerConfig.proxy; // TODO(srujzs): Remove this assertion when the library bundle format is // supported without canary mode. if (ddcModuleSystem) { assert(canaryFeatures); } final InternetAddress address; if (hostname == webDevAnyHostDefault) { address = InternetAddress.anyIPv4; } else { address = (await InternetAddress.lookup(hostname)).first; } HttpServer? httpServer; const kMaxRetries = 4; for (var i = 0; i <= kMaxRetries; i++) { try { if (httpsConfig != null) { final serverContext = SecurityContext() ..useCertificateChain(httpsConfig.certPath) ..usePrivateKey(httpsConfig.certKeyPath); httpServer = await HttpServer.bindSecure(address, port, serverContext); } else { httpServer = await HttpServer.bind(address, port); } break; } on SocketException catch (e, s) { if (i >= kMaxRetries) { logger.printError('Failed to bind web development server:\n$e', stackTrace: s); throwToolExit('Failed to bind web development server:\n$e'); } await Future.delayed(const Duration(milliseconds: 100)); } } // Allow rendering in a iframe. httpServer!.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN'); if (crossOriginIsolation) { for (final MapEntry header in kCrossOriginIsolationHeaders.entries) { httpServer.defaultResponseHeaders.add(header.key, header.value); } } for (final MapEntry header in extraHeaders.entries) { httpServer.defaultResponseHeaders.add(header.key, header.value); } final PackageConfig packageConfig = buildInfo.packageConfig; final modules = {}; final digests = {}; final server = WebAssetServer( httpServer, packageConfig, address, modules, digests, ddcModuleSystem, canaryFeatures, webRenderer: webRenderer, useLocalCanvasKit: useLocalCanvasKit, fileSystem: fileSystem, logger: logger, webDefines: webDefines, ); final int selectedPort = server.selectedPort; final cleanHost = hostname == webDevAnyHostDefault ? 'localhost' : hostname; final scheme = httpsConfig != null ? 'https' : 'http'; server._baseUri = Uri( scheme: scheme, host: cleanHost, port: selectedPort, path: server.basePath, ); if (testMode) { return server; } // In release builds (or wasm builds) deploy a simpler proxy server. if (buildInfo.mode != BuildMode.debug || isWasm) { final releaseAssetServer = ReleaseAssetServer( entrypoint, fileSystem: fileSystem, platform: platform, flutterRoot: Cache.flutterRoot, webBuildDirectory: getWebBuildDirectory(), basePath: server.basePath, needsCoopCoep: crossOriginIsolation, ); runZonedGuarded( () { shelf.serveRequests(httpServer!, releaseAssetServer.handle); }, (Object e, StackTrace s) { logger.printTrace('Release asset server: error serving requests: $e:$s'); }, ); return server; } // Return a version string for all active modules. This is populated // along with the `moduleProvider` update logic. Future> digestProvider() async => digests; // Ensure dwds is present and provide middleware to avoid trying to // load the through the isolate APIs. final Directory directory = await loadDwdsDirectory(fileSystem, logger); shelf.Handler middleware(FutureOr Function(shelf.Request) innerHandler) { return (shelf.Request request) async { if (request.url.path.endsWith('dwds/src/injected/client.js')) { final Uri uri = directory.uri.resolve('src/injected/client.js'); final String result = await fileSystem.file(uri.toFilePath()).readAsString(); return shelf.Response.ok( result, headers: {HttpHeaders.contentTypeHeader: 'application/javascript'}, ); } return innerHandler(request); }; } logging.Logger.root.level = logging.Level.ALL; logging.Logger.root.onRecord.listen((logging.LogRecord event) => log(logger, event)); // In debug builds, spin up DWDS and the full asset server. final Dwds dwds = await dwdsLauncher( assetReader: server, buildResults: const Stream.empty(), chromeConnection: () async { final Chromium chromium = await chromiumLauncher!.connectedInstance; return chromium.chromeConnection; }, toolConfiguration: ToolConfiguration( loadStrategy: ddcModuleSystem ? FrontendServerDdcLibraryBundleStrategyProvider( ReloadConfiguration.none, server, PackageUriMapper(packageConfig), digestProvider, BuildSettings( appEntrypoint: packageConfig.toPackageUri( fileSystem.file(entrypoint).absolute.uri, ), canaryFeatures: canaryFeatures, ), packageConfigPath: buildInfo.packageConfigPath, reloadedSourcesUri: server._baseUri.replace( pathSegments: List.from(server._baseUri.pathSegments) ..add(_reloadedSourcesFileName), ), ).strategy : FrontendServerRequireStrategyProvider( ReloadConfiguration.none, server, PackageUriMapper(packageConfig), digestProvider, BuildSettings( appEntrypoint: packageConfig.toPackageUri( fileSystem.file(entrypoint).absolute.uri, ), canaryFeatures: canaryFeatures, ), packageConfigPath: buildInfo.packageConfigPath, ).strategy, debugSettings: DebugSettings( enableDebugExtension: true, urlEncoder: urlTunneller, useSseForDebugProxy: useSseForDebugProxy, useSseForDebugBackend: useSseForDebugBackend, useSseForInjectedClient: useSseForInjectedClient, expressionCompiler: expressionCompiler, ddsConfiguration: ddsConfig, ), appMetadata: AppMetadata(hostname: hostname), ), // Use DWDS WebSocket-based connection instead of Chrome-based connection for debugging useDwdsWebSocketConnection: useDwdsWebSocketConnection, ); var pipeline = const shelf.Pipeline(); if (shouldEnableMiddleware) { pipeline = pipeline.addMiddleware(middleware).addMiddleware(dwds.middleware); } pipeline = pipeline.addMiddleware(proxyMiddleware(proxy, globals.logger)); final shelf.Handler dwdsHandler = pipeline.addHandler(server.handleRequest); final shelf.Cascade cascade = shelf.Cascade().add(dwds.handler).add(dwdsHandler); runZonedGuarded( () { shelf.serveRequests(httpServer!, cascade.handler); }, (Object e, StackTrace s) { logger.printTrace('Dwds server: error serving requests: $e:$s'); }, ); server.dwds = dwds; server._dwdsInit = true; return server; } final bool _ddcModuleSystem; final bool _canaryFeatures; final Map _webDefines; final HttpServer _httpServer; final _webMemoryFS = WebMemoryFS(); final PackageConfig _packages; final InternetAddress internetAddress; late final Dwds dwds; late Directory entrypointCacheDirectory; var _dwdsInit = false; WebMemoryFS get webMemoryFS => _webMemoryFS; @visibleForTesting HttpHeaders get defaultResponseHeaders => _httpServer.defaultResponseHeaders; @visibleForTesting Uint8List? getFile(String path) => _webMemoryFS.files[path]; @visibleForTesting Uint8List? getSourceMap(String path) => _webMemoryFS.sourcemaps[path]; @visibleForTesting Uint8List? getMetadata(String path) => _webMemoryFS.metadataFiles[path]; /// The base path to serve from. /// /// It should have no leading or trailing slashes. @visibleForTesting @override String basePath; // handle requests for JavaScript source, dart sources maps, or asset files. @visibleForTesting Future handleRequest(shelf.Request request) async { if (request.method != 'GET') { // Assets are served via GET only. return shelf.Response.notFound(''); } final String? requestPath = stripBasePath(request.url.path, basePath); if (requestPath == null) { return shelf.Response.notFound(''); } // If the response is `/`, then we are requesting the index file. if (requestPath == '/' || requestPath.isEmpty) { return _serveIndexHtml(); } if (requestPath == 'flutter_bootstrap.js') { return _serveFlutterBootstrapJs(); } final headers = {}; // Track etag headers for better caching of resources. final String? ifNoneMatch = request.headers[HttpHeaders.ifNoneMatchHeader]; headers[HttpHeaders.cacheControlHeader] = 'max-age=0, must-revalidate'; // If this is a JavaScript file, it must be in the in-memory cache. // Attempt to look up the file by URI. final String webServerPath = requestPath.replaceFirst('.dart.js', '.dart.lib.js'); if (_webMemoryFS.files.containsKey(requestPath) || _webMemoryFS.files.containsKey(webServerPath)) { final List? bytes = getFile(requestPath) ?? getFile(webServerPath); // Use the underlying buffer hashCode as a revision string. This buffer is // replaced whenever the frontend_server produces new output files, which // will also change the hashCode. final etag = bytes.hashCode.toString(); if (ifNoneMatch == etag) { return shelf.Response.notModified(); } headers[HttpHeaders.contentTypeHeader] = 'application/javascript'; headers[HttpHeaders.etagHeader] = etag; return shelf.Response.ok(bytes, headers: headers); } // If this is a sourcemap file, then it might be in the in-memory cache. // Attempt to lookup the file by URI. if (_webMemoryFS.sourcemaps.containsKey(requestPath)) { final List? bytes = getSourceMap(requestPath); final etag = bytes.hashCode.toString(); if (ifNoneMatch == etag) { return shelf.Response.notModified(); } headers[HttpHeaders.contentTypeHeader] = 'application/json'; headers[HttpHeaders.etagHeader] = etag; return shelf.Response.ok(bytes, headers: headers); } // If this is a metadata file, then it might be in the in-memory cache. // Attempt to lookup the file by URI. if (_webMemoryFS.metadataFiles.containsKey(requestPath)) { final List? bytes = getMetadata(requestPath); final etag = bytes.hashCode.toString(); if (ifNoneMatch == etag) { return shelf.Response.notModified(); } headers[HttpHeaders.contentTypeHeader] = 'application/json'; headers[HttpHeaders.etagHeader] = etag; return shelf.Response.ok(bytes, headers: headers); } File file = _resolveDartFile(requestPath); if (!file.existsSync() && requestPath.startsWith('canvaskit/')) { final Directory canvasKitDirectory = fileSystem.directory( fileSystem.path.join( globals.artifacts!.getHostArtifact(HostArtifact.flutterWebSdk).path, 'canvaskit', ), ); final Uri potential = canvasKitDirectory.uri.resolve( requestPath.replaceFirst('canvaskit/', ''), ); file = fileSystem.file(potential); } // If all of the lookups above failed, the file might have been an asset. // Try and resolve the path relative to the built asset directory. if (!file.existsSync()) { final Uri potential = fileSystem .directory(getAssetBuildDirectory()) .uri .resolve(requestPath.replaceFirst('assets/', '')); file = fileSystem.file(potential); } if (!file.existsSync()) { final Uri webPath = fileSystem.currentDirectory .childDirectory('web') .uri .resolve(requestPath); file = fileSystem.file(webPath); } if (!file.existsSync()) { // Paths starting with these prefixes should've been resolved above. if (requestPath.startsWith('assets/') || requestPath.startsWith('packages/') || requestPath.startsWith('canvaskit/')) { return shelf.Response.notFound(''); } return _serveIndexHtml(); } // For real files, use a serialized file stat plus path as a revision. // This allows us to update between canvaskit and non-canvaskit SDKs. final String etag = file.lastModifiedSync().toIso8601String() + Uri.encodeComponent(file.path); if (ifNoneMatch == etag) { return shelf.Response.notModified(); } final int length = file.lengthSync(); // Attempt to determine the file's mime type. if this is not provided some // browsers will refuse to render images/show video etc. If the tool // cannot determine a mime type, fall back to application/octet-stream. final String mimeType = mime.lookupMimeType( file.path, headerBytes: await file.openRead(0, mime.defaultMagicNumbersMaxLength).first, ) ?? _kDefaultMimeType; headers[HttpHeaders.contentLengthHeader] = length.toString(); headers[HttpHeaders.contentTypeHeader] = mimeType; headers[HttpHeaders.etagHeader] = etag; return shelf.Response.ok(file.openRead(), headers: headers); } /// Tear down the http server running. Future dispose() async { if (_dwdsInit) { await dwds.stop(); } return _httpServer.close(); } /// Write a single file into the in-memory cache. void writeFile(String filePath, String contents) { writeBytes(filePath, const Utf8Encoder().convert(contents)); } void writeBytes(String filePath, Uint8List contents) { _webMemoryFS.files[filePath] = contents; } /// Determines what rendering backed to use. final WebRendererMode webRenderer; final bool useLocalCanvasKit; final FileSystem fileSystem; final Logger logger; String get _buildConfigString { final buildConfig = { 'engineRevision': globals.flutterVersion.engineRevision, 'builds': [ { 'compileTarget': 'dartdevc', 'renderer': webRenderer.name, 'mainJsPath': 'main.dart.js', }, ], if (useLocalCanvasKit) 'useLocalCanvasKit': true, }; return ''' if (!window._flutter) { window._flutter = {}; } _flutter.buildConfig = ${jsonEncode(buildConfig)}; '''; } File get _flutterJsFile => fileSystem.file( fileSystem.path.join( globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path, 'flutter.js', ), ); String get _flutterBootstrapJsContent { final WebTemplate bootstrapTemplate = getWebTemplate( fileSystem, 'flutter_bootstrap.js', generateDefaultFlutterBootstrapScript(includeServiceWorkerSettings: false), ); return bootstrapTemplate.withSubstitutions( baseHref: '/', serviceWorkerVersion: null, buildConfig: _buildConfigString, flutterJsFile: _flutterJsFile, logger: logger, webDefines: _webDefines, ); } shelf.Response _serveFlutterBootstrapJs() { return shelf.Response.ok( _flutterBootstrapJsContent, headers: {HttpHeaders.contentTypeHeader: 'text/javascript'}, ); } shelf.Response _serveIndexHtml() { final WebTemplate indexHtml = getWebTemplate(fileSystem, 'index.html', _kDefaultIndex); return shelf.Response.ok( indexHtml.withSubstitutions( // Currently, we don't support --base-href for the "run" command. baseHref: '/', // Currently, we don't support --static-assets-url for the "run" command. staticAssetsUrl: '/', serviceWorkerVersion: null, buildConfig: _buildConfigString, flutterJsFile: _flutterJsFile, flutterBootstrapJs: _flutterBootstrapJsContent, logger: logger, webDefines: _webDefines, ), encoding: utf8, headers: {HttpHeaders.contentTypeHeader: 'text/html'}, ); } // Attempt to resolve `path` to a dart file. File _resolveDartFile(String path) { // Return the actual file objects so that local engine changes are automatically picked up. switch (path) { case 'dart_sdk.js': return _resolveDartSdkJsFile; case 'dart_sdk.js.map': return _resolveDartSdkJsMapFile; } // This is the special generated entrypoint. if (path == 'web_entrypoint.dart') { return entrypointCacheDirectory.childFile('web_entrypoint.dart'); } // If this is a dart file, it must be on the local file system and is // likely coming from a source map request. The tool doesn't currently // consider the case of Dart files as assets. final File dartFile = fileSystem.file(fileSystem.currentDirectory.uri.resolve(path)); if (dartFile.existsSync()) { return dartFile; } final List segments = path.split('/'); if (segments.first.isEmpty) { segments.removeAt(0); } // The file might have been a package file which is signaled by a // `/packages//` request. if (segments.first == 'packages') { final Uri? filePath = _packages.resolve( Uri(scheme: 'package', pathSegments: segments.skip(1)), ); if (filePath != null) { final File packageFile = fileSystem.file(filePath); if (packageFile.existsSync()) { return packageFile; } } } // Otherwise it must be a Dart SDK source or a Flutter Web SDK source. final Directory dartSdkParent = fileSystem .directory( globals.artifacts!.getArtifactPath( Artifact.engineDartSdkPath, platform: TargetPlatform.web_javascript, ), ) .parent; final File dartSdkFile = fileSystem.file(dartSdkParent.uri.resolve(path)); if (dartSdkFile.existsSync()) { return dartSdkFile; } final Directory flutterWebSdk = fileSystem.directory( globals.artifacts!.getHostArtifact(HostArtifact.flutterWebSdk), ); final File webSdkFile = fileSystem.file(flutterWebSdk.uri.resolve(path)); return webSdkFile; } File get _resolveDartSdkJsFile { final Map dartSdkArtifactMap = _ddcModuleSystem ? kDdcLibraryBundleDartSdkJsArtifactMap : kAmdDartSdkJsArtifactMap; return fileSystem.file(globals.artifacts!.getHostArtifact(dartSdkArtifactMap[webRenderer]!)); } File get _resolveDartSdkJsMapFile { final Map dartSdkArtifactMap = _ddcModuleSystem ? kDdcLibraryBundleDartSdkJsMapArtifactMap : kAmdDartSdkJsMapArtifactMap; return fileSystem.file(globals.artifacts!.getHostArtifact(dartSdkArtifactMap[webRenderer]!)); } @override Future dartSourceContents(String serverPath) async { serverPath = stripBasePath(serverPath, basePath)!; final File result = _resolveDartFile(serverPath); if (result.existsSync()) { return result.readAsString(); } return null; } @override Future sourceMapContents(String serverPath) async { serverPath = stripBasePath(serverPath, basePath)!; return utf8.decode(_webMemoryFS.sourcemaps[serverPath]!); } @override Future metadataContents(String serverPath) async { final String? resultPath = stripBasePath(serverPath, basePath); if (resultPath == 'main_module.ddc_merged_metadata') { return _webMemoryFS.mergedMetadata; } if (_webMemoryFS.metadataFiles.containsKey(resultPath)) { return utf8.decode(_webMemoryFS.metadataFiles[resultPath]!); } throw Exception('Could not find metadata contents for $serverPath'); } @override Future close() async {} }