|  | // Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file | 
|  | // for details. 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:convert'; | 
|  | import 'dart:io'; | 
|  | import 'dart:typed_data'; | 
|  |  | 
|  | import 'package:crypto/crypto.dart'; | 
|  | import 'package:path/path.dart' as p; | 
|  | import 'package:pub/src/crc32c.dart'; | 
|  | import 'package:pub/src/source/hosted.dart'; | 
|  | import 'package:pub/src/utils.dart' show hexEncode; | 
|  | import 'package:pub_semver/pub_semver.dart'; | 
|  | import 'package:shelf/shelf.dart' as shelf; | 
|  | import 'package:shelf/shelf_io.dart' as shelf_io; | 
|  | import 'package:test/test.dart'; | 
|  | import 'package:test/test.dart' as test show expect; | 
|  |  | 
|  | import 'descriptor.dart' as d; | 
|  | import 'test_pub.dart'; | 
|  |  | 
|  | class PackageServer { | 
|  | /// The inner [shelf_io.IOServer] that this uses to serve its descriptors. | 
|  | final shelf_io.IOServer _inner; | 
|  |  | 
|  | /// Handlers of requests. Last matching handler will be used. | 
|  | final List<_PatternAndHandler> _handlers = []; | 
|  |  | 
|  | // A list of all the requests received up till now. | 
|  | final List<String> requestedPaths = <String>[]; | 
|  |  | 
|  | // Setting this to false will disable automatic calculation of content-hashes. | 
|  | bool serveContentHashes = true; | 
|  |  | 
|  | /// Whether the [shelf_io.IOServer] should compress the content, if possible. | 
|  | /// The default value is `false` (compression disabled). | 
|  | /// See [HttpServer.autoCompress] for details. | 
|  | bool get autoCompress => _inner.server.autoCompress; | 
|  | set autoCompress(bool shouldAutoCompress) => | 
|  | _inner.server.autoCompress = shouldAutoCompress; | 
|  |  | 
|  | // Setting this to false will disable automatic calculation of checksums. | 
|  | bool serveChecksums = true; | 
|  |  | 
|  | PackageServer._(this._inner) { | 
|  | final outerZone = Zone.current; | 
|  | _inner.mount((request) { | 
|  | try { | 
|  | final path = request.url.path; | 
|  | requestedPaths.add(path); | 
|  | final pathWithInitialSlash = '/$path'; | 
|  | for (final entry in _handlers.reversed) { | 
|  | final match = entry.pattern.matchAsPrefix(pathWithInitialSlash); | 
|  | if (match != null && match.end == pathWithInitialSlash.length) { | 
|  | final a = entry.handler(request); | 
|  | return a; | 
|  | } | 
|  | } | 
|  | return shelf.Response.notFound('Could not find ${request.url}'); | 
|  | } catch (e, st) { | 
|  | // Because shelf swallows all errors we catch here and redirect to the | 
|  | // zone error handler. | 
|  | outerZone.handleUncaughtError(e, st); | 
|  | _inner.close(); | 
|  | rethrow; | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | static final _versionInfoPattern = RegExp(r'/api/packages/([a-zA-Z_0-9]*)'); | 
|  | static final _advisoriesPattern = | 
|  | RegExp(r'/api/packages/([a-zA-Z_0-9]*)/advisories'); | 
|  |  | 
|  | static final _downloadPattern = | 
|  | RegExp(r'/packages/([^/]*)/versions/([^/]*).tar.gz'); | 
|  |  | 
|  | static Future<PackageServer> start() async { | 
|  | final server = PackageServer._( | 
|  | await shelf_io.IOServer.bind(InternetAddress.loopbackIPv4, 0), | 
|  | ); | 
|  | server.handle( | 
|  | _versionInfoPattern, | 
|  | (shelf.Request request) async { | 
|  | final parts = request.url.pathSegments; | 
|  | assert(parts[0] == 'api'); | 
|  | assert(parts[1] == 'packages'); | 
|  | final name = parts[2]; | 
|  |  | 
|  | final package = server._packages[name]; | 
|  | if (package == null) { | 
|  | return shelf.Response.notFound('No package named $name'); | 
|  | } | 
|  |  | 
|  | return shelf.Response.ok( | 
|  | jsonEncode({ | 
|  | 'name': name, | 
|  | 'uploaders': ['nweiz@google.com'], | 
|  | 'versions': [ | 
|  | for (final version in package.versions.values) | 
|  | { | 
|  | 'pubspec': version.pubspec, | 
|  | 'version': version.version.toString(), | 
|  | 'archive_url': | 
|  | '${server.url}/packages/$name/versions/${version.version}.tar.gz', | 
|  | if (version.isRetracted) 'retracted': true, | 
|  | if (version.sha256 != null || server.serveContentHashes) | 
|  | 'archive_sha256': version.sha256 ?? | 
|  | hexEncode( | 
|  | (await sha256.bind(version.contents()).first).bytes, | 
|  | ), | 
|  | }, | 
|  | ], | 
|  | if (package.isDiscontinued) 'isDiscontinued': true, | 
|  | if (package.advisoriesUpdated != null) | 
|  | 'advisoriesUpdated': package.advisoriesUpdated!.toIso8601String(), | 
|  | if (package.discontinuedReplacementText != null) | 
|  | 'replacedBy': package.discontinuedReplacementText, | 
|  | }), | 
|  | headers: { | 
|  | HttpHeaders.contentTypeHeader: 'application/vnd.pub.v2+json', | 
|  | }, | 
|  | ); | 
|  | }, | 
|  | ); | 
|  |  | 
|  | server.handle( | 
|  | _advisoriesPattern, | 
|  | (shelf.Request request) async { | 
|  | final parts = request.url.pathSegments; | 
|  | assert(parts[0] == 'api'); | 
|  | assert(parts[1] == 'packages'); | 
|  | final name = parts[2]; | 
|  | assert(parts[3] == 'advisories'); | 
|  |  | 
|  | final package = server._packages[name]; | 
|  | if (package == null) { | 
|  | return shelf.Response.notFound('No package named $name'); | 
|  | } | 
|  |  | 
|  | return shelf.Response.ok( | 
|  | jsonEncode({ | 
|  | 'advisoriesUpdated': defaultAdvisoriesUpdated.toIso8601String(), | 
|  | 'advisories': [ | 
|  | for (final advisory in package.advisories.values) | 
|  | { | 
|  | 'id': advisory.id, | 
|  | 'summary': 'Example', | 
|  | 'aliases': [...advisory.aliases], | 
|  | 'details': 'This is a dummy example.', | 
|  | 'modified': defaultAdvisoriesUpdated.toIso8601String(), | 
|  | 'published': defaultAdvisoriesUpdated.toIso8601String(), | 
|  | 'affected': [ | 
|  | for (final package in advisory.affectedPackages) | 
|  | { | 
|  | 'package': { | 
|  | 'name': package.name, | 
|  | 'ecosystem': package.ecosystem, | 
|  | }, | 
|  | 'versions': [...package.versions], | 
|  | }, | 
|  | ], | 
|  | if (advisory.displayUrl != null) | 
|  | 'database_specific': { | 
|  | 'pub_display_url': advisory.displayUrl, | 
|  | }, | 
|  | }, | 
|  | ], | 
|  | }), | 
|  | headers: { | 
|  | HttpHeaders.contentTypeHeader: 'application/vnd.pub.v2+json', | 
|  | }, | 
|  | ); | 
|  | }, | 
|  | ); | 
|  |  | 
|  | server.handle( | 
|  | _downloadPattern, | 
|  | (shelf.Request request) async { | 
|  | final parts = request.url.pathSegments; | 
|  | assert(parts[0] == 'packages'); | 
|  | final name = parts[1]; | 
|  | assert(parts[2] == 'versions'); | 
|  | final package = server._packages[name]; | 
|  | if (package == null) { | 
|  | return shelf.Response.notFound('No package $name'); | 
|  | } | 
|  |  | 
|  | final version = Version.parse( | 
|  | parts[3].substring(0, parts[3].length - '.tar.gz'.length), | 
|  | ); | 
|  | assert(parts[3].endsWith('.tar.gz')); | 
|  |  | 
|  | for (final packageVersion in package.versions.values) { | 
|  | if (packageVersion.version == version) { | 
|  | final headers = packageVersion.headers ?? {}; | 
|  | headers[HttpHeaders.contentTypeHeader] ??= [ | 
|  | 'application/octet-stream', | 
|  | ]; | 
|  |  | 
|  | // This gate enables tests to validate the CRC32C parser by | 
|  | // passing in arbitrary values for the checksum header. | 
|  | if (server.serveChecksums && | 
|  | !headers.containsKey(checksumHeaderName)) { | 
|  | headers[checksumHeaderName] = composeChecksumHeader( | 
|  | crc32c: await packageVersion.computeArchiveCrc32c(), | 
|  | ); | 
|  | } | 
|  |  | 
|  | return shelf.Response.ok( | 
|  | packageVersion.contents(), | 
|  | headers: headers, | 
|  | ); | 
|  | } | 
|  | } | 
|  | return shelf.Response.notFound('No version $version of $name'); | 
|  | }, | 
|  | ); | 
|  | return server; | 
|  | } | 
|  |  | 
|  | Future<void> close() async { | 
|  | await _inner.close(); | 
|  | } | 
|  |  | 
|  | /// The port used for the server. | 
|  | int get port => _inner.url.port; | 
|  |  | 
|  | /// The URL for the server. | 
|  | String get url => _inner.url.toString(); | 
|  |  | 
|  | /// From now on report errors on any request. | 
|  | void serveErrors() => _handlers | 
|  | ..clear() | 
|  | ..add( | 
|  | _PatternAndHandler( | 
|  | RegExp('.*'), | 
|  | (request) { | 
|  | fail('The HTTP server received an unexpected request:\n' | 
|  | '${request.method} ${request.requestedUri}'); | 
|  | }, | 
|  | ), | 
|  | ); | 
|  |  | 
|  | void handle(Pattern pattern, shelf.Handler handler) { | 
|  | _handlers.add( | 
|  | _PatternAndHandler( | 
|  | pattern, | 
|  | handler, | 
|  | ), | 
|  | ); | 
|  | } | 
|  |  | 
|  | // Installs a handler at [pattern] that expects to be called exactly once with | 
|  | // the given [method]. | 
|  | // | 
|  | // The handler is installed as the start to give it priority over more general | 
|  | // handlers. | 
|  | void expect(String method, Pattern pattern, shelf.Handler handler) { | 
|  | handle( | 
|  | pattern, | 
|  | expectAsync1( | 
|  | (request) { | 
|  | test.expect(request.method, method); | 
|  | return handler(request); | 
|  | }, | 
|  | ), | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Returns the path of [package] at [version], installed from this server, in | 
|  | /// the pub cache. | 
|  | String pathInCache(String package, String version) => | 
|  | p.join(cachingPath, '$package-$version'); | 
|  |  | 
|  | /// The location where pub will store the cache for this server. | 
|  | String get cachingPath => | 
|  | p.join(d.sandbox, cachePath, 'hosted', 'localhost%58$port'); | 
|  |  | 
|  | String get hashesCachingPath => | 
|  | p.join(d.sandbox, cachePath, 'hosted-hashes', 'localhost%58$port'); | 
|  |  | 
|  | /// A map from package names to the concrete packages to serve. | 
|  | final _packages = <String, _ServedPackage>{}; | 
|  |  | 
|  | /// Specifies that a package named [name] with [version] should be served. | 
|  | /// | 
|  | /// If [deps] is passed, it's used as the "dependencies" field of the pubspec. | 
|  | /// If [pubspec] is passed, it's used as the rest of the pubspec. | 
|  | /// | 
|  | /// If [contents] is passed, it's used as the contents of the package. By | 
|  | /// default, a package just contains a dummy lib directory. | 
|  | void serve( | 
|  | String name, | 
|  | String version, { | 
|  | Map<String, dynamic>? deps, | 
|  | Map<String, dynamic>? pubspec, | 
|  | List<d.Descriptor>? contents, | 
|  | String? sdk, | 
|  | Map<String, List<String>>? headers, | 
|  | }) { | 
|  | final pubspecFields = <String, dynamic>{ | 
|  | 'name': name, | 
|  | 'version': version, | 
|  | 'environment': {'sdk': sdk ?? '^3.0.0'}, | 
|  | }; | 
|  | if (pubspec != null) pubspecFields.addAll(pubspec); | 
|  | if (deps != null) pubspecFields['dependencies'] = deps; | 
|  |  | 
|  | contents ??= [d.libDir(name, '$name $version')]; | 
|  | contents = [d.file('pubspec.yaml', yaml(pubspecFields)), ...contents]; | 
|  |  | 
|  | final package = _packages.putIfAbsent(name, _ServedPackage.new); | 
|  | package.versions[version] = _ServedPackageVersion( | 
|  | pubspecFields, | 
|  | headers: headers, | 
|  | contents: () => tarFromDescriptors(contents ?? []), | 
|  | ); | 
|  | } | 
|  |  | 
|  | // Mark a package discontinued. | 
|  | void discontinue( | 
|  | String name, { | 
|  | bool isDiscontinued = true, | 
|  | String? replacementText, | 
|  | }) { | 
|  | _packages[name]! | 
|  | ..isDiscontinued = isDiscontinued | 
|  | ..discontinuedReplacementText = replacementText; | 
|  | } | 
|  |  | 
|  | static final defaultAdvisoriesUpdated = | 
|  | DateTime.fromMicrosecondsSinceEpoch(0); | 
|  |  | 
|  | /// Add a security advisory which affects versions in [affectedPackages]. | 
|  | void addAdvisory({ | 
|  | required String advisoryId, | 
|  | String? displayUrl, | 
|  | DateTime? advisoriesUpdated, | 
|  | List<String> aliases = const <String>[], | 
|  | required List<AffectedPackage> affectedPackages, | 
|  | }) { | 
|  | for (final package in affectedPackages) { | 
|  | _packages[package.name]!.advisoriesUpdated = | 
|  | advisoriesUpdated ?? defaultAdvisoriesUpdated; | 
|  | _packages[package.name]!.advisories.putIfAbsent( | 
|  | advisoryId, | 
|  | () => _ServedAdvisory( | 
|  | advisoryId, | 
|  | affectedPackages, | 
|  | aliases, | 
|  | displayUrl, | 
|  | ), | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Clears all existing packages from this builder. | 
|  | void clearPackages() { | 
|  | _packages.clear(); | 
|  | } | 
|  |  | 
|  | void retractPackageVersion(String name, String version) { | 
|  | _packages[name]!.versions[version]!.isRetracted = true; | 
|  | } | 
|  |  | 
|  | /// Useful for testing handling of a wrong hash. | 
|  | void overrideArchiveSha256(String name, String version, String sha256) { | 
|  | _packages[name]!.versions[version]!.sha256 = sha256; | 
|  | } | 
|  |  | 
|  | Future<String> peekArchiveSha256(String name, String version) async { | 
|  | final v = _packages[name]!.versions[version]!; | 
|  | return v.sha256 ?? hexEncode((await sha256.bind(v.contents()).first).bytes); | 
|  | } | 
|  |  | 
|  | Future<String?> peekArchiveChecksumHeader(String name, String version) async { | 
|  | final v = _packages[name]!.versions[version]!; | 
|  |  | 
|  | // If the test configured an overriding header value. | 
|  | var checksumHeader = v.headers?[checksumHeaderName]; | 
|  |  | 
|  | // Otherwise, compute from package contents. | 
|  | if (serveChecksums) { | 
|  | checksumHeader ??= | 
|  | composeChecksumHeader(crc32c: await v.computeArchiveCrc32c()); | 
|  | } | 
|  |  | 
|  | return checksumHeader?.join(','); | 
|  | } | 
|  |  | 
|  | static List<String> composeChecksumHeader({ | 
|  | int? crc32c, | 
|  | String? md5 = '5f4dcc3b5aa765d61d8327deb882cf99', | 
|  | }) { | 
|  | final header = <String>[]; | 
|  |  | 
|  | if (crc32c != null) { | 
|  | final bytes = Uint8List(4)..buffer.asByteData().setUint32(0, crc32c); | 
|  | header.add('crc32c=${base64.encode(bytes)}'); | 
|  | } | 
|  |  | 
|  | if (md5 != null) { | 
|  | header.add('md5=${base64.encode(utf8.encode(md5))}'); | 
|  | } | 
|  |  | 
|  | return header; | 
|  | } | 
|  | } | 
|  |  | 
|  | class _ServedPackage { | 
|  | final versions = <String, _ServedPackageVersion>{}; | 
|  | bool isDiscontinued = false; | 
|  | String? discontinuedReplacementText; | 
|  | DateTime? advisoriesUpdated; | 
|  | final advisories = <String, _ServedAdvisory>{}; | 
|  | } | 
|  |  | 
|  | /// A package that's intended to be served. | 
|  | class _ServedPackageVersion { | 
|  | final Map pubspec; | 
|  | final Stream<List<int>> Function() contents; | 
|  | final Map<String, List<String>>? headers; | 
|  | bool isRetracted = false; | 
|  | // Overrides the calculated sha256. | 
|  | String? sha256; | 
|  |  | 
|  | Version get version => Version.parse(pubspec['version'] as String); | 
|  |  | 
|  | _ServedPackageVersion(this.pubspec, {required this.contents, this.headers}); | 
|  |  | 
|  | Future<int> computeArchiveCrc32c() async { | 
|  | return await Crc32c.computeByConsumingStream(contents()); | 
|  | } | 
|  | } | 
|  |  | 
|  | class _ServedAdvisory { | 
|  | String id; | 
|  | List<String> aliases; | 
|  | String? displayUrl; | 
|  | List<AffectedPackage> affectedPackages; | 
|  |  | 
|  | _ServedAdvisory( | 
|  | this.id, | 
|  | this.affectedPackages, | 
|  | this.aliases, | 
|  | this.displayUrl, | 
|  | ); | 
|  | } | 
|  |  | 
|  | class AffectedPackage { | 
|  | String name; | 
|  | String ecosystem; | 
|  | List<String> versions; | 
|  |  | 
|  | AffectedPackage({ | 
|  | required this.name, | 
|  | this.ecosystem = 'Pub', | 
|  | required this.versions, | 
|  | }); | 
|  | } | 
|  |  | 
|  | class _PatternAndHandler { | 
|  | Pattern pattern; | 
|  | shelf.Handler handler; | 
|  |  | 
|  | _PatternAndHandler(this.pattern, this.handler); | 
|  | } |