From b9c47a1bde2d1865b9814dbece64f01b0029a05e Mon Sep 17 00:00:00 2001 From: vin-fandemand Date: Mon, 27 Jul 2020 16:07:17 -0400 Subject: [PATCH 1/8] file transfer not allowed with API gateway. --- lib/src/command/lish.dart | 40 ++++++++++++++++--- lib/src/source/hosted.dart | 28 ++++++++----- lib/src/validator.dart | 12 ++++-- .../validator/relative_version_numbering.dart | 10 ++++- 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index d2a710bd7..e951df055 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart @@ -51,6 +51,9 @@ class LishCommand extends PubCommand { /// Whether the publish requires confirmation. bool get force => argResults['force']; + /// Optional API key for package server to which to upload this package. + String get apiKey => argResults['apiKey']; + LishCommand() { argParser.addFlag('dry-run', abbr: 'n', @@ -62,6 +65,9 @@ class LishCommand extends PubCommand { help: 'Publish without confirmation if there are no errors.'); argParser.addOption('server', help: 'The package server to which to upload this package.'); + argParser.addOption('apiKey', + help: + 'Optional API key for package server to which to upload this package.'); } Future _publish(List packageBytes) async { @@ -72,14 +78,20 @@ class LishCommand extends PubCommand { // TODO(nweiz): Cloud Storage can provide an XML-formatted error. We // should report that error and exit. var newUri = server.resolve('/api/packages/versions/new'); - var response = await client.get(newUri, headers: pubApiHeaders); - var parameters = parseJsonResponse(response); + var _pubApiHeaders = {}; + _pubApiHeaders.addEntries(pubApiHeaders.entries); + if (apiKey != null) { + _pubApiHeaders['X-API-Key'] = apiKey; + } + var response = await client.get(newUri, headers: _pubApiHeaders); + var parameters = parseJsonResponse(response); + print('1'); var url = _expectField(parameters, 'url', response); if (url is! String) invalidServerResponse(response); cloudStorageUrl = Uri.parse(url); var request = http.MultipartRequest('POST', cloudStorageUrl); - + print('2'); var fields = _expectField(parameters, 'fields', response); if (fields is! Map) invalidServerResponse(response); fields.forEach((key, value) { @@ -87,12 +99,21 @@ class LishCommand extends PubCommand { request.fields[key] = value; }); + print('3'); request.followRedirects = false; request.files.add(http.MultipartFile.fromBytes('file', packageBytes, filename: 'package.tar.gz')); + if (apiKey != null) { + request.headers.addAll(_pubApiHeaders); + } + print('3.75 ${request.url}'); + request.files.forEach((element) { + print( + '${element.contentType.toString()} ${element.filename} ${element.field} ${element.length}'); + }); var postResponse = await http.Response.fromStream(await client.send(request)); - + print('4'); var location = postResponse.headers['location']; if (location == null) throw PubHttpException(postResponse); handleJsonSuccess(await client.get(location, headers: pubApiHeaders)); @@ -162,8 +183,15 @@ class LishCommand extends PubCommand { final warnings = []; final errors = []; - await Validator.runAll(entrypoint, packageSize, server.toString(), - hints: hints, warnings: warnings, errors: errors); + await Validator.runAll( + entrypoint, + packageSize, + server.toString(), + hints: hints, + warnings: warnings, + errors: errors, + apiKey: apiKey, + ); if (errors.isNotEmpty) { log.error('Sorry, your package is missing ' diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index d8c3ef7e0..19d34d5da 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart @@ -74,26 +74,29 @@ class HostedSource extends Source { /// /// If [url] is passed, it's the URL of the pub server from which the package /// should be downloaded. It can be a [Uri] or a [String]. - PackageRef refFor(String name, {url}) => - PackageRef(name, this, _descriptionFor(name, url)); + PackageRef refFor(String name, {url, String apiKey}) { + return PackageRef(name, this, _descriptionFor(name, url, apiKey)); + } /// Returns an ID for a hosted package named [name] at [version]. /// /// If [url] is passed, it's the URL of the pub server from which the package /// should be downloaded. It can be a [Uri] or a [String]. - PackageId idFor(String name, Version version, {url}) => - PackageId(name, this, version, _descriptionFor(name, url)); + PackageId idFor(String name, Version version, {url, String apiKey}) { + return PackageId(name, this, version, _descriptionFor(name, url, apiKey)); + } /// Returns the description for a hosted package named [name] with the /// given package server [url]. - dynamic _descriptionFor(String name, [url]) { + dynamic _descriptionFor(String name, [url, String apiKey]) { if (url == null) return name; if (url is! String && url is! Uri) { throw ArgumentError.value(url, 'url', 'must be a Uri or a String.'); } - - return {'name': name, 'url': url.toString()}; + return apiKey != null + ? {'name': name, 'url': url.toString(), 'apiKey': apiKey} + : {'name': name, 'url': url.toString()}; } @override @@ -179,12 +182,16 @@ class BoundHostedSource extends CachedSource { var url = _makeUrl( ref.description, (server, package) => '$server/api/packages/$package'); log.io('Get versions from $url.'); - + var _pubApiHeaders = {}; + _pubApiHeaders.addEntries(pubApiHeaders.entries); + if (ref.description['apiKey'] != null) { + _pubApiHeaders['X-API-Key'] = ref.description['apiKey']; + } String body; try { // TODO(sigurdm): Implement cancellation of requests. This probably // requires resolution of: https://github.com/dart-lang/sdk/issues/22265. - body = await httpClient.read(url, headers: pubApiHeaders); + body = await httpClient.read(url, headers: _pubApiHeaders); } catch (error, stackTrace) { var parsed = source._parseDescription(ref.description); _throwFriendlyError(error, stackTrace, parsed.first, parsed.last); @@ -195,7 +202,8 @@ class BoundHostedSource extends CachedSource { var pubspec = Pubspec.fromMap(map['pubspec'], systemCache.sources, expectedName: ref.name, location: url); var id = source.idFor(ref.name, pubspec.version, - url: _serverFor(ref.description)); + url: _serverFor(ref.description), + apiKey: _pubApiHeaders['X-API-Key']); final archiveUrlValue = map['archive_url']; final archiveUrl = archiveUrlValue is String ? Uri.tryParse(archiveUrlValue) : null; diff --git a/lib/src/validator.dart b/lib/src/validator.dart index 2a3ef89e9..c14449ab0 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart @@ -116,8 +116,14 @@ abstract class Validator { /// package, in bytes. This is used to validate that it's not too big to /// upload to the server. static Future runAll( - Entrypoint entrypoint, Future packageSize, String serverUrl, - {List hints, List warnings, List errors}) { + Entrypoint entrypoint, + Future packageSize, + String serverUrl, { + List hints, + List warnings, + List errors, + String apiKey, + }) { var validators = [ PubspecValidator(entrypoint), LicenseValidator(entrypoint), @@ -135,7 +141,7 @@ abstract class Validator { StrictDependenciesValidator(entrypoint), FlutterPluginFormatValidator(entrypoint), LanguageVersionValidator(entrypoint), - RelativeVersionNumberingValidator(entrypoint, serverUrl), + RelativeVersionNumberingValidator(entrypoint, serverUrl, apiKey), ]; if (packageSize != null) { validators.add(SizeValidator(entrypoint, packageSize)); diff --git a/lib/src/validator/relative_version_numbering.dart b/lib/src/validator/relative_version_numbering.dart index 5e227fc13..10305dc36 100644 --- a/lib/src/validator/relative_version_numbering.dart +++ b/lib/src/validator/relative_version_numbering.dart @@ -20,8 +20,10 @@ class RelativeVersionNumberingValidator extends Validator { 'https://dart.dev/tools/pub/versioning#semantic-versions'; final String _server; + final String _apiKey; - RelativeVersionNumberingValidator(Entrypoint entrypoint, this._server) + RelativeVersionNumberingValidator( + Entrypoint entrypoint, this._server, this._apiKey) : super(entrypoint); @override @@ -31,7 +33,11 @@ class RelativeVersionNumberingValidator extends Validator { try { existingVersions = await hostedSource .bind(entrypoint.cache) - .getVersions(hostedSource.refFor(entrypoint.root.name, url: _server)); + .getVersions(hostedSource.refFor( + entrypoint.root.name, + url: _server, + apiKey: _apiKey, + )); } on PackageNotFoundException { existingVersions = []; } From e34e8397ee6749a956b011e0ee36c925c11aaea6 Mon Sep 17 00:00:00 2001 From: vin-fandemand Date: Tue, 28 Jul 2020 17:01:17 -0400 Subject: [PATCH 2/8] Oauth2 working with hosted url --- lib/src/command/lish.dart | 42 ++--- lib/src/command/logout.dart | 2 +- lib/src/null_safety_analysis.dart | 1 + lib/src/oauth2.dart | 162 +++++++++++++++--- lib/src/source/hosted.dart | 34 ++-- lib/src/validator.dart | 6 +- .../validator/relative_version_numbering.dart | 7 +- .../relative_version_numbering_test.dart | 4 +- 8 files changed, 191 insertions(+), 67 deletions(-) diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index e951df055..2f66e4124 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart @@ -52,7 +52,10 @@ class LishCommand extends PubCommand { bool get force => argResults['force']; /// Optional API key for package server to which to upload this package. - String get apiKey => argResults['apiKey']; + bool get isHosted => argResults['server'] != null; + + /// Optional override to use Identity Token in Authorization Header + bool get useIdToken => argResults['useIdToken'] != null; LishCommand() { argParser.addFlag('dry-run', @@ -64,10 +67,11 @@ class LishCommand extends PubCommand { negatable: false, help: 'Publish without confirmation if there are no errors.'); argParser.addOption('server', - help: 'The package server to which to upload this package.'); - argParser.addOption('apiKey', - help: - 'Optional API key for package server to which to upload this package.'); + abbr: 's', help: 'The package server to which to upload this package.'); + argParser.addFlag('useIdToken', + negatable: false, + abbr: 'u', + help: 'Use Identity Token in Authorization Header'); } Future _publish(List packageBytes) async { @@ -78,20 +82,15 @@ class LishCommand extends PubCommand { // TODO(nweiz): Cloud Storage can provide an XML-formatted error. We // should report that error and exit. var newUri = server.resolve('/api/packages/versions/new'); - var _pubApiHeaders = {}; - _pubApiHeaders.addEntries(pubApiHeaders.entries); - if (apiKey != null) { - _pubApiHeaders['X-API-Key'] = apiKey; - } - var response = await client.get(newUri, headers: _pubApiHeaders); + var response = await client.get(newUri, headers: pubApiHeaders); var parameters = parseJsonResponse(response); - print('1'); + var url = _expectField(parameters, 'url', response); if (url is! String) invalidServerResponse(response); cloudStorageUrl = Uri.parse(url); var request = http.MultipartRequest('POST', cloudStorageUrl); - print('2'); + var fields = _expectField(parameters, 'fields', response); if (fields is! Map) invalidServerResponse(response); fields.forEach((key, value) { @@ -99,26 +98,18 @@ class LishCommand extends PubCommand { request.fields[key] = value; }); - print('3'); request.followRedirects = false; request.files.add(http.MultipartFile.fromBytes('file', packageBytes, filename: 'package.tar.gz')); - if (apiKey != null) { - request.headers.addAll(_pubApiHeaders); - } - print('3.75 ${request.url}'); - request.files.forEach((element) { - print( - '${element.contentType.toString()} ${element.filename} ${element.field} ${element.length}'); - }); + var postResponse = await http.Response.fromStream(await client.send(request)); - print('4'); + var location = postResponse.headers['location']; if (location == null) throw PubHttpException(postResponse); handleJsonSuccess(await client.get(location, headers: pubApiHeaders)); }); - }); + }, hostedURLName: isHosted ? server.host : null, useIdToken: useIdToken); } on PubHttpException catch (error) { var url = error.response.request.url; if (url == cloudStorageUrl) { @@ -190,7 +181,8 @@ class LishCommand extends PubCommand { hints: hints, warnings: warnings, errors: errors, - apiKey: apiKey, + isHosted: isHosted, + serverHost: isHosted ? server.host : null, ); if (errors.isNotEmpty) { diff --git a/lib/src/command/logout.dart b/lib/src/command/logout.dart index e22f0ca3b..a09f9db31 100644 --- a/lib/src/command/logout.dart +++ b/lib/src/command/logout.dart @@ -20,6 +20,6 @@ class LogoutCommand extends PubCommand { @override Future run() async { - oauth2.logout(cache); + oauth2.logout(cache, null); } } diff --git a/lib/src/null_safety_analysis.dart b/lib/src/null_safety_analysis.dart index 976ab18c3..d87e6979d 100644 --- a/lib/src/null_safety_analysis.dart +++ b/lib/src/null_safety_analysis.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:analyzer/dart/analysis/context_builder.dart'; import 'package:analyzer/dart/analysis/context_locator.dart'; +// ignore: unused_import import 'package:analyzer/dart/ast/token.dart'; import 'package:cli_util/cli_util.dart'; import 'package:source_span/source_span.dart'; diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index 6095640fe..baca80c73 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -5,11 +5,13 @@ import 'dart:async'; import 'dart:io'; -import 'package:oauth2/oauth2.dart'; +//import 'package:oauth2/oauth2.dart'; import 'package:path/path.dart' as path; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; +// ignore: avoid_relative_lib_imports +import '../oauth2/lib/oauth2.dart'; import 'http.dart'; import 'io.dart'; import 'log.dart' as log; @@ -62,22 +64,30 @@ final _scopes = ['openid', 'https://www.googleapis.com/auth/userinfo.email']; /// /// This should always be the same as the credentials file stored in the system /// cache. -Credentials _credentials; +final Map globalCredentials = {}; /// Delete the cached credentials, if they exist. -void _clearCredentials(SystemCache cache) { - _credentials = null; - var credentialsFile = _credentialsFile(cache); +void _clearCredentials(SystemCache cache, String hostedURLName) { + if (hostedURLName != null) { + globalCredentials.remove(hostedURLName); + } else { + globalCredentials.remove('default'); + } + var credentialsFile = hostedURLName != null + ? _hostedURLNameCredentialsFile(cache, hostedURLName) + : _credentialsFile(cache); if (entryExists(credentialsFile)) deleteEntry(credentialsFile); } /// Try to delete the cached credentials. -void logout(SystemCache cache) { - var credentialsFile = _credentialsFile(cache); - if (entryExists(_credentialsFile(cache))) { +void logout(SystemCache cache, String hostedURLName) { + var credentialsFile = hostedURLName != null + ? _hostedURLNameCredentialsFile(cache, hostedURLName) + : _credentialsFile(cache); + if (entryExists(credentialsFile)) { log.message('Logging out of pub.dartlang.org.'); log.message('Deleting $credentialsFile'); - _clearCredentials(cache); + _clearCredentials(cache, hostedURLName); } else { log.message( 'No existing credentials file $credentialsFile. Cannot log out.'); @@ -90,26 +100,27 @@ void logout(SystemCache cache) { /// This takes care of loading and saving the client's credentials, as well as /// prompting the user for their authorization. It will also re-authorize and /// re-run [fn] if a recoverable authorization error is detected. -Future withClient(SystemCache cache, Future Function(Client) fn) { - return _getClient(cache).then((client) { +Future withClient(SystemCache cache, Future Function(Client) fn, + {String hostedURLName, bool useIdToken = false}) { + return _getClient(cache, hostedURLName, useIdToken).then((client) { return fn(client).whenComplete(() { client.close(); // Be sure to save the credentials even when an error happens. - _saveCredentials(cache, client.credentials); + _saveCredentials(cache, client.credentials, hostedURLName); }); }).catchError((error) { if (error is ExpirationException) { log.error("Pub's authorization to upload packages has expired and " "can't be automatically refreshed."); - return withClient(cache, fn); + return withClient(cache, fn, hostedURLName: hostedURLName); } else if (error is AuthorizationException) { var message = 'OAuth2 authorization failed'; if (error.description != null) { message = '$message (${error.description})'; } log.error('$message.'); - _clearCredentials(cache); - return withClient(cache, fn); + _clearCredentials(cache, hostedURLName); + return withClient(cache, fn, hostedURLName: hostedURLName); } else { throw error; } @@ -120,17 +131,31 @@ Future withClient(SystemCache cache, Future Function(Client) fn) { /// /// If saved credentials are available, those are used; otherwise, the user is /// prompted to authorize the pub client. -Future _getClient(SystemCache cache) async { - var credentials = _loadCredentials(cache); - if (credentials == null) return await _authorize(); +Future getClient( + {SystemCache cache, String hostedURLName, bool useIdToken}) => + _getClient(cache, hostedURLName, useIdToken); + +/// Gets a new OAuth2 client. +/// +/// If saved credentials are available, those are used; otherwise, the user is +/// prompted to authorize the pub client. +Future _getClient( + SystemCache cache, String hostedURLName, bool useIdToken) async { + var credentials = _loadCredentials(cache, hostedURLName); + if (credentials == null) { + return hostedURLName == null + ? await _authorize() + : await _authorizeHostedUrl(hostedURLName, credentials, useIdToken); + } var client = Client(credentials, identifier: _identifier, secret: _secret, // Google's OAuth2 API doesn't support basic auth. basicAuth: false, - httpClient: httpClient); - _saveCredentials(cache, client.credentials); + httpClient: httpClient, + useIdToken: useIdToken); + _saveCredentials(cache, client.credentials, hostedURLName); return client; } @@ -139,13 +164,24 @@ Future _getClient(SystemCache cache) async { /// /// If the credentials can't be loaded for any reason, the returned [Future] /// completes to `null`. -Credentials _loadCredentials(SystemCache cache) { +Credentials _loadCredentials(SystemCache cache, String hostedURLName) { log.fine('Loading OAuth2 credentials.'); try { - if (_credentials != null) return _credentials; + if (hostedURLName != null) { + if (globalCredentials.containsKey(hostedURLName)) { + return globalCredentials[hostedURLName]; + } + } else { + if (globalCredentials.containsKey('default')) { + return globalCredentials['default']; + } + } + + var path = hostedURLName != null + ? _hostedURLNameCredentialsFile(cache, hostedURLName) + : _credentialsFile(cache); - var path = _credentialsFile(cache); if (!fileExists(path)) return null; var credentials = Credentials.fromJson(readTextFile(path)); @@ -165,10 +201,17 @@ Credentials _loadCredentials(SystemCache cache) { /// Save the user's OAuth2 credentials to the in-memory cache and the /// filesystem. -void _saveCredentials(SystemCache cache, Credentials credentials) { +void _saveCredentials( + SystemCache cache, Credentials credentials, String hostedURLName) { log.fine('Saving OAuth2 credentials.'); - _credentials = credentials; - var credentialsPath = _credentialsFile(cache); + if (hostedURLName != null) { + globalCredentials[hostedURLName] = credentials; + } else { + globalCredentials['default'] = credentials; + } + var credentialsPath = hostedURLName != null + ? _hostedURLNameCredentialsFile(cache, hostedURLName) + : _credentialsFile(cache); ensureDir(path.dirname(credentialsPath)); writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); } @@ -177,6 +220,10 @@ void _saveCredentials(SystemCache cache, Credentials credentials) { String _credentialsFile(SystemCache cache) => path.join(cache.rootDir, 'credentials.json'); +/// The path to the file in which the user's OAuth2 credentials are stored. +String _hostedURLNameCredentialsFile(SystemCache cache, String hostedURLName) => + path.join(cache.rootDir, '$hostedURLName.json'); + /// Gets the user to authorize pub as a client of pub.dartlang.org via oauth2. /// /// Returns a Future that completes to a fully-authorized [Client]. @@ -221,6 +268,69 @@ Future _authorize() async { 'Waiting for your authorization...'); var client = await completer.future; + globalCredentials['default'] = client.credentials; + log.message('Successfully authorized.\n'); + return client; +} + +/// Gets the user to authorize pub as a client of hostedUrl via oauth2. +/// +/// Returns a Future that completes to a fully-authorized [Client]. +Future _authorizeHostedUrl( + String hostedURLName, Credentials credentials, bool useIdToken) async { + const identifier = '32555940559.apps.googleusercontent.com'; + final authorizationEndpoint = _authorizationEndpoint; + const secret = 'ZmssLNjJy2998hD4CTg2ejr2'; + const scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/appengine.admin", + "openid", + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/accounts.reauth" + ]; + var grant = + AuthorizationCodeGrant(identifier, authorizationEndpoint, tokenEndpoint, + secret: secret, + // Google's OAuth2 API doesn't support basic auth. + basicAuth: false, + httpClient: httpClient); + + // Spin up a one-shot HTTP server to receive the authorization code from the + // Google OAuth2 server via redirect. This server will close itself as soon as + // the code is received. + var completer = Completer(); + var server = await bindServer('localhost', 0); + shelf_io.serveRequests(server, (request) { + if (request.url.path.isNotEmpty) { + return shelf.Response.notFound('Invalid URI.'); + } + + log.message('Authorization received, processing...'); + var queryString = request.url.query ?? ''; + + // Closing the server here is safe, since it will wait until the response + // is sent to actually shut down. + server.close(); + completer.complete(grant.handleAuthorizationResponse( + queryToMap(queryString), + useIdToken: useIdToken)); + + return shelf.Response.found('https://pub.dartlang.org/authorized'); + }); + + var authUrl = grant.getAuthorizationUrl( + Uri.parse('http://localhost:${server.port}'), + scopes: scopes); + + log.message( + 'Pub needs your authorization to upload packages on your behalf.\n' + 'In a web browser, go to $authUrl\n' + 'Then click "Allow access".\n\n' + 'Waiting for your authorization...'); + + var client = await completer.future; + globalCredentials[hostedURLName] = client.credentials; log.message('Successfully authorized.\n'); return client; } diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 19d34d5da..a765f6345 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart @@ -17,6 +17,7 @@ import '../exceptions.dart'; import '../http.dart'; import '../io.dart'; import '../log.dart' as log; +import '../oauth2.dart' as oauth2 show getClient; import '../package.dart'; import '../package_name.dart'; import '../pubspec.dart'; @@ -74,28 +75,37 @@ class HostedSource extends Source { /// /// If [url] is passed, it's the URL of the pub server from which the package /// should be downloaded. It can be a [Uri] or a [String]. - PackageRef refFor(String name, {url, String apiKey}) { - return PackageRef(name, this, _descriptionFor(name, url, apiKey)); + PackageRef refFor(String name, {url, bool isHosted, String serverHost}) { + return PackageRef( + name, this, _descriptionFor(name, url, isHosted, serverHost)); } /// Returns an ID for a hosted package named [name] at [version]. /// /// If [url] is passed, it's the URL of the pub server from which the package /// should be downloaded. It can be a [Uri] or a [String]. - PackageId idFor(String name, Version version, {url, String apiKey}) { - return PackageId(name, this, version, _descriptionFor(name, url, apiKey)); + PackageId idFor(String name, Version version, + {url, bool isHosted, String serverHost}) { + return PackageId( + name, this, version, _descriptionFor(name, url, isHosted, serverHost)); } /// Returns the description for a hosted package named [name] with the /// given package server [url]. - dynamic _descriptionFor(String name, [url, String apiKey]) { + dynamic _descriptionFor(String name, + [url, bool isHosted, String serverHost]) { if (url == null) return name; if (url is! String && url is! Uri) { throw ArgumentError.value(url, 'url', 'must be a Uri or a String.'); } - return apiKey != null - ? {'name': name, 'url': url.toString(), 'apiKey': apiKey} + return isHosted != null + ? { + 'name': name, + 'url': url.toString(), + 'isHosted': isHosted, + 'serverHost': serverHost + } : {'name': name, 'url': url.toString()}; } @@ -184,9 +194,12 @@ class BoundHostedSource extends CachedSource { log.io('Get versions from $url.'); var _pubApiHeaders = {}; _pubApiHeaders.addEntries(pubApiHeaders.entries); - if (ref.description['apiKey'] != null) { - _pubApiHeaders['X-API-Key'] = ref.description['apiKey']; + if (ref.description['serverHost'] != null) { + final client = await oauth2.getClient( + cache: systemCache, hostedURLName: ref.description['serverHost']); + _pubApiHeaders['Authorization'] = 'Bearer ${client.credentials.idToken}'; } + String body; try { // TODO(sigurdm): Implement cancellation of requests. This probably @@ -203,7 +216,8 @@ class BoundHostedSource extends CachedSource { expectedName: ref.name, location: url); var id = source.idFor(ref.name, pubspec.version, url: _serverFor(ref.description), - apiKey: _pubApiHeaders['X-API-Key']); + isHosted: ref.description['isHosted'], + serverHost: ref.description['serverHost']); final archiveUrlValue = map['archive_url']; final archiveUrl = archiveUrlValue is String ? Uri.tryParse(archiveUrlValue) : null; diff --git a/lib/src/validator.dart b/lib/src/validator.dart index a40d82e6f..852dfd669 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart @@ -123,7 +123,8 @@ abstract class Validator { List hints, List warnings, List errors, - String apiKey, + String serverHost, + bool isHosted, }) { var validators = [ PubspecValidator(entrypoint), @@ -142,7 +143,8 @@ abstract class Validator { StrictDependenciesValidator(entrypoint), FlutterPluginFormatValidator(entrypoint), LanguageVersionValidator(entrypoint), - RelativeVersionNumberingValidator(entrypoint, serverUrl), + RelativeVersionNumberingValidator( + entrypoint, serverUrl, serverHost, isHosted), NullSafetyMixedModeValidator(entrypoint), ]; if (packageSize != null) { diff --git a/lib/src/validator/relative_version_numbering.dart b/lib/src/validator/relative_version_numbering.dart index 52aecb3d6..0d14f995b 100644 --- a/lib/src/validator/relative_version_numbering.dart +++ b/lib/src/validator/relative_version_numbering.dart @@ -21,8 +21,11 @@ class RelativeVersionNumberingValidator extends Validator { 'https://dart.dev/tools/pub/versioning#semantic-versions'; final String _server; + final String _serverHost; + final bool _isHosted; - RelativeVersionNumberingValidator(Entrypoint entrypoint, this._server) + RelativeVersionNumberingValidator( + Entrypoint entrypoint, this._server, this._serverHost, this._isHosted) : super(entrypoint); @override @@ -35,6 +38,8 @@ class RelativeVersionNumberingValidator extends Validator { .getVersions(hostedSource.refFor( entrypoint.root.name, url: _server, + isHosted: _isHosted, + serverHost: _serverHost, )); } on PackageNotFoundException { existingVersions = []; diff --git a/test/validator/relative_version_numbering_test.dart b/test/validator/relative_version_numbering_test.dart index 7adcb1f6a..b571bb302 100644 --- a/test/validator/relative_version_numbering_test.dart +++ b/test/validator/relative_version_numbering_test.dart @@ -12,8 +12,8 @@ import '../descriptor.dart' as d; import '../test_pub.dart'; import 'utils.dart'; -Validator validator(Entrypoint entrypoint) => - RelativeVersionNumberingValidator(entrypoint, globalPackageServer.url); +Validator validator(Entrypoint entrypoint) => RelativeVersionNumberingValidator( + entrypoint, globalPackageServer.url, null, false); Future setup({String sdkConstraint}) async { await d.validPackage.create(); From 2f6e8acc72def9b093d8f607ee5a61bded9d18f8 Mon Sep 17 00:00:00 2001 From: vin-fandemand Date: Tue, 28 Jul 2020 23:16:47 -0400 Subject: [PATCH 3/8] pub get and publish workign with host url --- lib/src/command/lish.dart | 3 +- lib/src/source/hosted.dart | 65 +++++++++++-------- lib/src/validator.dart | 6 +- .../validator/relative_version_numbering.dart | 8 +-- 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index 2f66e4124..96092045a 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart @@ -181,8 +181,7 @@ class LishCommand extends PubCommand { hints: hints, warnings: warnings, errors: errors, - isHosted: isHosted, - serverHost: isHosted ? server.host : null, + useIdToken: useIdToken, ); if (errors.isNotEmpty) { diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index a765f6345..634c3a6ea 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart @@ -75,38 +75,32 @@ class HostedSource extends Source { /// /// If [url] is passed, it's the URL of the pub server from which the package /// should be downloaded. It can be a [Uri] or a [String]. - PackageRef refFor(String name, {url, bool isHosted, String serverHost}) { - return PackageRef( - name, this, _descriptionFor(name, url, isHosted, serverHost)); + PackageRef refFor(String name, {url, bool useIdToken}) { + return PackageRef(name, this, _descriptionFor(name, url, useIdToken)); } /// Returns an ID for a hosted package named [name] at [version]. /// /// If [url] is passed, it's the URL of the pub server from which the package /// should be downloaded. It can be a [Uri] or a [String]. - PackageId idFor(String name, Version version, - {url, bool isHosted, String serverHost}) { + PackageId idFor(String name, Version version, {url, bool useIdToken}) { return PackageId( - name, this, version, _descriptionFor(name, url, isHosted, serverHost)); + name, this, version, _descriptionFor(name, url, useIdToken)); } /// Returns the description for a hosted package named [name] with the /// given package server [url]. - dynamic _descriptionFor(String name, - [url, bool isHosted, String serverHost]) { + dynamic _descriptionFor(String name, [url, bool useIdToken]) { if (url == null) return name; if (url is! String && url is! Uri) { throw ArgumentError.value(url, 'url', 'must be a Uri or a String.'); } - return isHosted != null - ? { - 'name': name, - 'url': url.toString(), - 'isHosted': isHosted, - 'serverHost': serverHost - } - : {'name': name, 'url': url.toString()}; + return { + 'name': name, + 'url': url.toString(), + 'useIdToken': useIdToken ?? false + }; } @override @@ -194,10 +188,14 @@ class BoundHostedSource extends CachedSource { log.io('Get versions from $url.'); var _pubApiHeaders = {}; _pubApiHeaders.addEntries(pubApiHeaders.entries); - if (ref.description['serverHost'] != null) { - final client = await oauth2.getClient( - cache: systemCache, hostedURLName: ref.description['serverHost']); - _pubApiHeaders['Authorization'] = 'Bearer ${client.credentials.idToken}'; + if (url.host != 'pub.dartlang.org') { + final client = + await oauth2.getClient(cache: systemCache, hostedURLName: url.host); + _pubApiHeaders['Authorization'] = ref.description['useIdToken'] == null + ? 'Bearer ${client.credentials.accessToken}' + : ref.description['useIdToken'] == true + ? 'Bearer ${client.credentials.idToken}' + : 'Bearer ${client.credentials.accessToken}'; } String body; @@ -214,10 +212,12 @@ class BoundHostedSource extends CachedSource { final result = Map.fromEntries(versions.map((map) { var pubspec = Pubspec.fromMap(map['pubspec'], systemCache.sources, expectedName: ref.name, location: url); - var id = source.idFor(ref.name, pubspec.version, - url: _serverFor(ref.description), - isHosted: ref.description['isHosted'], - serverHost: ref.description['serverHost']); + var id = source.idFor( + ref.name, + pubspec.version, + url: _serverFor(ref.description), + useIdToken: ref.description['useIdToken'], + ); final archiveUrlValue = map['archive_url']; final archiveUrl = archiveUrlValue is String ? Uri.tryParse(archiveUrlValue) : null; @@ -418,8 +418,21 @@ class BoundHostedSource extends CachedSource { // Download and extract the archive to a temp directory. var tempDir = systemCache.createTempDir(); - var response = await httpClient.send(http.Request('GET', url)); - await extractTarGz(response.stream, tempDir); + + if (url.host != 'pub.dartlang.org') { + final client = + await oauth2.getClient(cache: systemCache, hostedURLName: url.host); + await io.HttpClient().getUrl(url).then((io.HttpClientRequest request) { + request.headers + .add('Authorization', 'Bearer ${client.credentials.idToken}'); + return request.close(); + }).then((io.HttpClientResponse response) async { + await extractTarGz(response.asBroadcastStream(), tempDir); + }); + } else { + var response = await httpClient.send(http.Request('GET', url)); + await extractTarGz(response.stream, tempDir); + } // Remove the existing directory if it exists. This will happen if // we're forcing a download to repair the cache. diff --git a/lib/src/validator.dart b/lib/src/validator.dart index 852dfd669..cb2509064 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart @@ -123,8 +123,7 @@ abstract class Validator { List hints, List warnings, List errors, - String serverHost, - bool isHosted, + bool useIdToken, }) { var validators = [ PubspecValidator(entrypoint), @@ -143,8 +142,7 @@ abstract class Validator { StrictDependenciesValidator(entrypoint), FlutterPluginFormatValidator(entrypoint), LanguageVersionValidator(entrypoint), - RelativeVersionNumberingValidator( - entrypoint, serverUrl, serverHost, isHosted), + RelativeVersionNumberingValidator(entrypoint, serverUrl, useIdToken), NullSafetyMixedModeValidator(entrypoint), ]; if (packageSize != null) { diff --git a/lib/src/validator/relative_version_numbering.dart b/lib/src/validator/relative_version_numbering.dart index 0d14f995b..bebcd3f84 100644 --- a/lib/src/validator/relative_version_numbering.dart +++ b/lib/src/validator/relative_version_numbering.dart @@ -21,11 +21,10 @@ class RelativeVersionNumberingValidator extends Validator { 'https://dart.dev/tools/pub/versioning#semantic-versions'; final String _server; - final String _serverHost; - final bool _isHosted; + final bool _useIdToken; RelativeVersionNumberingValidator( - Entrypoint entrypoint, this._server, this._serverHost, this._isHosted) + Entrypoint entrypoint, this._server, this._useIdToken) : super(entrypoint); @override @@ -38,8 +37,7 @@ class RelativeVersionNumberingValidator extends Validator { .getVersions(hostedSource.refFor( entrypoint.root.name, url: _server, - isHosted: _isHosted, - serverHost: _serverHost, + useIdToken: _useIdToken, )); } on PackageNotFoundException { existingVersions = []; From 185e6c2871ae4f99d19d9f2a3a5a5520136991a1 Mon Sep 17 00:00:00 2001 From: vin-fandemand Date: Wed, 29 Jul 2020 12:31:53 -0400 Subject: [PATCH 4/8] add loading auth config from file --- lib/oauth2 | 1 + lib/src/auth_config.dart | 122 ++++++++++++++++ lib/src/command/lish.dart | 10 +- lib/src/oauth2.dart | 135 ++++++++++++------ lib/src/source/hosted.dart | 35 ++--- lib/src/validator.dart | 3 +- .../validator/relative_version_numbering.dart | 5 +- .../relative_version_numbering_test.dart | 4 +- 8 files changed, 234 insertions(+), 81 deletions(-) create mode 160000 lib/oauth2 create mode 100644 lib/src/auth_config.dart diff --git a/lib/oauth2 b/lib/oauth2 new file mode 160000 index 000000000..a52e1d7df --- /dev/null +++ b/lib/oauth2 @@ -0,0 +1 @@ +Subproject commit a52e1d7df25dd8759eebad9d1c1a79a5f95c63de diff --git a/lib/src/auth_config.dart b/lib/src/auth_config.dart new file mode 100644 index 000000000..26eb028f0 --- /dev/null +++ b/lib/src/auth_config.dart @@ -0,0 +1,122 @@ +// Copyright (c) 2012, 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:convert'; +import 'package:meta/meta.dart'; + +/// [AuthConfig] is used for setting up parameters required +/// to establish an OAUTH2 authentitcation. This class will be used with +/// [AuthorizationCodeGrant] from `oath2` library. +class AuthConfig { + const AuthConfig({ + @required this.authorizationEndpoint, + @required this.identifier, + @required this.scopes, + @required this.secret, + @required this.tokenEndpoint, + this.useIdToken = false, + @required this.redirectOnAuthorization, + }); + + /// The URL of the authorization server endpoint that's used to authorize the + /// credentials. + /// + /// This may be `null`, indicating that the credentials can't be authenticated. + final Uri authorizationEndpoint; + + /// The URL of the authorization server endpoint that's used to refresh the + /// credentials. + /// + /// This may be `null`, indicating that the credentials can't be refreshed. + final Uri tokenEndpoint; + + /// The specific permissions being requested from the authorization server. + /// + /// The scope strings are specific to the authorization server and may be + /// found in its documentation. + final List scopes; + + /// OAUTH server secret + final String secret; + + /// OAUTH server client identifier. + final String identifier; + + /// Use Id Token instead of access token in Authorization header + final bool useIdToken; + + /// Url to redirect on successful authorization + final String redirectOnAuthorization; + + /// Loads a set of auth configuration from a JSON-serialized form. + /// + /// Throws a [FormatException] if the JSON is incorrectly formatted. + factory AuthConfig.fromJson(String json) { + void validate(condition, message) { + if (condition) return; + throw FormatException('Failed to load credentials: $message.\n\n$json'); + } + + dynamic parsed; + try { + parsed = jsonDecode(json); + } on FormatException { + validate(false, 'invalid JSON'); + } + + validate(parsed is Map, 'was not a JSON map'); + + for (var stringField in [ + 'identifier', + 'authorizationEndpoint', + 'tokenEndpoint', + 'secret', + 'redirectOnAuthorization' + ]) { + var value = parsed[stringField]; + validate(parsed.containsKey(stringField), + 'did not contain required field "$stringField"'); + validate(value == null || value is String, + 'field "$stringField" was not a string, was "$value"'); + } + + var mapScopes = parsed['scopes']; + validate(mapScopes == null || mapScopes is List, + 'field "scopes" was not a list, was "$mapScopes"'); + + var mapTokenEndpoint = parsed['tokenEndpoint']; + if (mapTokenEndpoint != null) { + mapTokenEndpoint = Uri.parse(mapTokenEndpoint); + } + + var mapAuthorizationEndpoint = parsed['authorizationEndpoint']; + if (mapAuthorizationEndpoint != null) { + mapAuthorizationEndpoint = Uri.parse(mapAuthorizationEndpoint); + } + + return AuthConfig( + authorizationEndpoint: mapAuthorizationEndpoint, + tokenEndpoint: mapTokenEndpoint, + identifier: parsed['identifier'], + secret: parsed['secret'], + redirectOnAuthorization: parsed['redirectOnAuthorization'], + scopes: [for (dynamic scope in mapScopes) scope.toString()], + useIdToken: parsed['useIdToken'] == null + ? false + : parsed['useIdToken'].toString().toLowerCase() == 'true'); + } + + /// Map representation of [AuthConfig]. + Map toMap() { + return { + 'authorizationEndpoint': authorizationEndpoint.path, + 'tokenEndpoint': tokenEndpoint.path, + 'identifier': identifier, + 'secret': secret, + 'redirectOnAuthorization': redirectOnAuthorization, + 'scopes': scopes, + 'useIdToken': useIdToken + }; + } +} diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index 96092045a..6dae26340 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart @@ -54,9 +54,6 @@ class LishCommand extends PubCommand { /// Optional API key for package server to which to upload this package. bool get isHosted => argResults['server'] != null; - /// Optional override to use Identity Token in Authorization Header - bool get useIdToken => argResults['useIdToken'] != null; - LishCommand() { argParser.addFlag('dry-run', abbr: 'n', @@ -68,10 +65,6 @@ class LishCommand extends PubCommand { help: 'Publish without confirmation if there are no errors.'); argParser.addOption('server', abbr: 's', help: 'The package server to which to upload this package.'); - argParser.addFlag('useIdToken', - negatable: false, - abbr: 'u', - help: 'Use Identity Token in Authorization Header'); } Future _publish(List packageBytes) async { @@ -109,7 +102,7 @@ class LishCommand extends PubCommand { if (location == null) throw PubHttpException(postResponse); handleJsonSuccess(await client.get(location, headers: pubApiHeaders)); }); - }, hostedURLName: isHosted ? server.host : null, useIdToken: useIdToken); + }, hostedURLName: isHosted ? server.host : null); } on PubHttpException catch (error) { var url = error.response.request.url; if (url == cloudStorageUrl) { @@ -181,7 +174,6 @@ class LishCommand extends PubCommand { hints: hints, warnings: warnings, errors: errors, - useIdToken: useIdToken, ); if (errors.isNotEmpty) { diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index baca80c73..9b9fe5412 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -12,6 +12,7 @@ import 'package:shelf/shelf_io.dart' as shelf_io; // ignore: avoid_relative_lib_imports import '../oauth2/lib/oauth2.dart'; +import 'auth_config.dart'; import 'http.dart'; import 'io.dart'; import 'log.dart' as log; @@ -66,6 +67,8 @@ final _scopes = ['openid', 'https://www.googleapis.com/auth/userinfo.email']; /// cache. final Map globalCredentials = {}; +final Map globalAuthConfig = {}; + /// Delete the cached credentials, if they exist. void _clearCredentials(SystemCache cache, String hostedURLName) { if (hostedURLName != null) { @@ -101,8 +104,8 @@ void logout(SystemCache cache, String hostedURLName) { /// prompting the user for their authorization. It will also re-authorize and /// re-run [fn] if a recoverable authorization error is detected. Future withClient(SystemCache cache, Future Function(Client) fn, - {String hostedURLName, bool useIdToken = false}) { - return _getClient(cache, hostedURLName, useIdToken).then((client) { + {String hostedURLName}) { + return _getClient(cache, hostedURLName).then((client) { return fn(client).whenComplete(() { client.close(); // Be sure to save the credentials even when an error happens. @@ -131,30 +134,40 @@ Future withClient(SystemCache cache, Future Function(Client) fn, /// /// If saved credentials are available, those are used; otherwise, the user is /// prompted to authorize the pub client. -Future getClient( - {SystemCache cache, String hostedURLName, bool useIdToken}) => - _getClient(cache, hostedURLName, useIdToken); +Future getClient({SystemCache cache, String hostedURLName}) => + _getClient(cache, hostedURLName); /// Gets a new OAuth2 client. /// /// If saved credentials are available, those are used; otherwise, the user is /// prompted to authorize the pub client. -Future _getClient( - SystemCache cache, String hostedURLName, bool useIdToken) async { +Future _getClient(SystemCache cache, String hostedURLName) async { var credentials = _loadCredentials(cache, hostedURLName); if (credentials == null) { return hostedURLName == null ? await _authorize() - : await _authorizeHostedUrl(hostedURLName, credentials, useIdToken); + : await _authorizeHostedUrl(hostedURLName, cache); } - var client = Client(credentials, - identifier: _identifier, - secret: _secret, - // Google's OAuth2 API doesn't support basic auth. - basicAuth: false, - httpClient: httpClient, - useIdToken: useIdToken); + var useIdToken = false; + if (hostedURLName != null) { + if (globalAuthConfig.containsKey(hostedURLName)) { + useIdToken = globalAuthConfig[hostedURLName].useIdToken; + } else { + _loadHostedAuthConfigFile(cache, hostedURLName); + useIdToken = globalAuthConfig[hostedURLName].useIdToken; + } + } + + var client = Client( + credentials, + identifier: _identifier, + secret: _secret, + // Google's OAuth2 API doesn't support basic auth. + basicAuth: false, + httpClient: httpClient, + useIdToken: useIdToken, + ); _saveCredentials(cache, client.credentials, hostedURLName); return client; } @@ -209,11 +222,21 @@ void _saveCredentials( } else { globalCredentials['default'] = credentials; } - var credentialsPath = hostedURLName != null - ? _hostedURLNameCredentialsFile(cache, hostedURLName) - : _credentialsFile(cache); - ensureDir(path.dirname(credentialsPath)); - writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); + + if (hostedURLName != null) { + var credentialsPath = + path.join(Directory.current.path, '${hostedURLName}_credentials.json'); + ensureDir(path.dirname(credentialsPath)); + writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); + credentialsPath = + path.join(cache.rootDir, '${hostedURLName}_credentials.json'); + ensureDir(path.dirname(credentialsPath)); + writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); + } else { + var credentialsPath = _credentialsFile(cache); + ensureDir(path.dirname(credentialsPath)); + writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); + } } /// The path to the file in which the user's OAuth2 credentials are stored. @@ -221,8 +244,14 @@ String _credentialsFile(SystemCache cache) => path.join(cache.rootDir, 'credentials.json'); /// The path to the file in which the user's OAuth2 credentials are stored. -String _hostedURLNameCredentialsFile(SystemCache cache, String hostedURLName) => - path.join(cache.rootDir, '$hostedURLName.json'); +String _hostedURLNameCredentialsFile(SystemCache cache, String hostedURLName) { + var p = + path.join(Directory.current.path, '${hostedURLName}_credentials.json'); + if (!fileExists(p)) { + p = path.join(cache.rootDir, '${hostedURLName}_credentials.json'); + } + return p; +} /// Gets the user to authorize pub as a client of pub.dartlang.org via oauth2. /// @@ -277,24 +306,15 @@ Future _authorize() async { /// /// Returns a Future that completes to a fully-authorized [Client]. Future _authorizeHostedUrl( - String hostedURLName, Credentials credentials, bool useIdToken) async { - const identifier = '32555940559.apps.googleusercontent.com'; - final authorizationEndpoint = _authorizationEndpoint; - const secret = 'ZmssLNjJy2998hD4CTg2ejr2'; - const scopes = [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/appengine.admin", - "openid", - "https://www.googleapis.com/auth/compute", - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/accounts.reauth" - ]; - var grant = - AuthorizationCodeGrant(identifier, authorizationEndpoint, tokenEndpoint, - secret: secret, - // Google's OAuth2 API doesn't support basic auth. - basicAuth: false, - httpClient: httpClient); + String hostedURLName, SystemCache cache) async { +// + final authConfig = _loadHostedAuthConfigFile(cache, hostedURLName); + var grant = AuthorizationCodeGrant(authConfig.identifier, + authConfig.authorizationEndpoint, authConfig.tokenEndpoint, + secret: authConfig.secret, + // Google's OAuth2 API doesn't support basic auth. + basicAuth: false, + httpClient: httpClient); // Spin up a one-shot HTTP server to receive the authorization code from the // Google OAuth2 server via redirect. This server will close itself as soon as @@ -314,14 +334,14 @@ Future _authorizeHostedUrl( server.close(); completer.complete(grant.handleAuthorizationResponse( queryToMap(queryString), - useIdToken: useIdToken)); + useIdToken: authConfig.useIdToken)); - return shelf.Response.found('https://pub.dartlang.org/authorized'); + return shelf.Response.found(authConfig.redirectOnAuthorization); }); var authUrl = grant.getAuthorizationUrl( Uri.parse('http://localhost:${server.port}'), - scopes: scopes); + scopes: authConfig.scopes); log.message( 'Pub needs your authorization to upload packages on your behalf.\n' @@ -334,3 +354,32 @@ Future _authorizeHostedUrl( log.message('Successfully authorized.\n'); return client; } + +AuthConfig _loadHostedAuthConfigFile(SystemCache cache, String hostedURLName) { + log.fine('Loading Hosted OAuth2 config.'); + + try { + if (globalAuthConfig.containsKey(hostedURLName)) { + return globalAuthConfig[hostedURLName]; + } + + var path = _hostedURLNameAuthConfigFile(cache, hostedURLName); + if (!fileExists(path)) return null; + + var authConfig = AuthConfig.fromJson(readTextFile(path)); + globalAuthConfig[hostedURLName] = authConfig; + return authConfig; + } catch (e) { + log.error('Warning: could not load the saved OAuth2 config: $e'); + return null; // null means config failed. + } +} + +/// The path to the file in which the host server's OAuth2 auth configuration is stored. +String _hostedURLNameAuthConfigFile(SystemCache cache, String hostedURLName) { + var p = path.join(Directory.current.path, '${hostedURLName}_config.json'); + if (!fileExists(p)) { + p = path.join(cache.rootDir, '${hostedURLName}_config.json'); + } + return p; +} diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 634c3a6ea..39d5d98a3 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart @@ -75,32 +75,25 @@ class HostedSource extends Source { /// /// If [url] is passed, it's the URL of the pub server from which the package /// should be downloaded. It can be a [Uri] or a [String]. - PackageRef refFor(String name, {url, bool useIdToken}) { - return PackageRef(name, this, _descriptionFor(name, url, useIdToken)); - } + PackageRef refFor(String name, {url}) => + PackageRef(name, this, _descriptionFor(name, url)); /// Returns an ID for a hosted package named [name] at [version]. /// /// If [url] is passed, it's the URL of the pub server from which the package /// should be downloaded. It can be a [Uri] or a [String]. - PackageId idFor(String name, Version version, {url, bool useIdToken}) { - return PackageId( - name, this, version, _descriptionFor(name, url, useIdToken)); - } + PackageId idFor(String name, Version version, {url}) => + PackageId(name, this, version, _descriptionFor(name, url)); /// Returns the description for a hosted package named [name] with the /// given package server [url]. - dynamic _descriptionFor(String name, [url, bool useIdToken]) { + dynamic _descriptionFor(String name, [url]) { if (url == null) return name; if (url is! String && url is! Uri) { throw ArgumentError.value(url, 'url', 'must be a Uri or a String.'); } - return { - 'name': name, - 'url': url.toString(), - 'useIdToken': useIdToken ?? false - }; + return {'name': name, 'url': url.toString()}; } @override @@ -191,11 +184,9 @@ class BoundHostedSource extends CachedSource { if (url.host != 'pub.dartlang.org') { final client = await oauth2.getClient(cache: systemCache, hostedURLName: url.host); - _pubApiHeaders['Authorization'] = ref.description['useIdToken'] == null - ? 'Bearer ${client.credentials.accessToken}' - : ref.description['useIdToken'] == true - ? 'Bearer ${client.credentials.idToken}' - : 'Bearer ${client.credentials.accessToken}'; + _pubApiHeaders['Authorization'] = client.useIdToken == true + ? 'Bearer ${client.credentials.idToken}' + : 'Bearer ${client.credentials.accessToken}'; } String body; @@ -216,7 +207,6 @@ class BoundHostedSource extends CachedSource { ref.name, pubspec.version, url: _serverFor(ref.description), - useIdToken: ref.description['useIdToken'], ); final archiveUrlValue = map['archive_url']; final archiveUrl = @@ -423,8 +413,11 @@ class BoundHostedSource extends CachedSource { final client = await oauth2.getClient(cache: systemCache, hostedURLName: url.host); await io.HttpClient().getUrl(url).then((io.HttpClientRequest request) { - request.headers - .add('Authorization', 'Bearer ${client.credentials.idToken}'); + request.headers.add( + 'Authorization', + client.useIdToken == true + ? 'Bearer ${client.credentials.idToken}' + : 'Bearer ${client.credentials.accessToken}'); return request.close(); }).then((io.HttpClientResponse response) async { await extractTarGz(response.asBroadcastStream(), tempDir); diff --git a/lib/src/validator.dart b/lib/src/validator.dart index cb2509064..a6d9dcbfa 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart @@ -123,7 +123,6 @@ abstract class Validator { List hints, List warnings, List errors, - bool useIdToken, }) { var validators = [ PubspecValidator(entrypoint), @@ -142,7 +141,7 @@ abstract class Validator { StrictDependenciesValidator(entrypoint), FlutterPluginFormatValidator(entrypoint), LanguageVersionValidator(entrypoint), - RelativeVersionNumberingValidator(entrypoint, serverUrl, useIdToken), + RelativeVersionNumberingValidator(entrypoint, serverUrl), NullSafetyMixedModeValidator(entrypoint), ]; if (packageSize != null) { diff --git a/lib/src/validator/relative_version_numbering.dart b/lib/src/validator/relative_version_numbering.dart index bebcd3f84..52aecb3d6 100644 --- a/lib/src/validator/relative_version_numbering.dart +++ b/lib/src/validator/relative_version_numbering.dart @@ -21,10 +21,8 @@ class RelativeVersionNumberingValidator extends Validator { 'https://dart.dev/tools/pub/versioning#semantic-versions'; final String _server; - final bool _useIdToken; - RelativeVersionNumberingValidator( - Entrypoint entrypoint, this._server, this._useIdToken) + RelativeVersionNumberingValidator(Entrypoint entrypoint, this._server) : super(entrypoint); @override @@ -37,7 +35,6 @@ class RelativeVersionNumberingValidator extends Validator { .getVersions(hostedSource.refFor( entrypoint.root.name, url: _server, - useIdToken: _useIdToken, )); } on PackageNotFoundException { existingVersions = []; diff --git a/test/validator/relative_version_numbering_test.dart b/test/validator/relative_version_numbering_test.dart index b571bb302..7adcb1f6a 100644 --- a/test/validator/relative_version_numbering_test.dart +++ b/test/validator/relative_version_numbering_test.dart @@ -12,8 +12,8 @@ import '../descriptor.dart' as d; import '../test_pub.dart'; import 'utils.dart'; -Validator validator(Entrypoint entrypoint) => RelativeVersionNumberingValidator( - entrypoint, globalPackageServer.url, null, false); +Validator validator(Entrypoint entrypoint) => + RelativeVersionNumberingValidator(entrypoint, globalPackageServer.url); Future setup({String sdkConstraint}) async { await d.validPackage.create(); From c636d4d818d08514a8c708e2da7bacfe16d0fc8f Mon Sep 17 00:00:00 2001 From: vin-fandemand Date: Wed, 29 Jul 2020 12:41:45 -0400 Subject: [PATCH 5/8] changed the licence year for new module --- lib/src/auth_config.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/auth_config.dart b/lib/src/auth_config.dart index 26eb028f0..355527e04 100644 --- a/lib/src/auth_config.dart +++ b/lib/src/auth_config.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2015, 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. From d32020219ef51e461b5f48d0dc3202192f8269d3 Mon Sep 17 00:00:00 2001 From: vin-fandemand Date: Wed, 29 Jul 2020 13:10:12 -0400 Subject: [PATCH 6/8] remove lint override --- lib/src/null_safety_analysis.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/null_safety_analysis.dart b/lib/src/null_safety_analysis.dart index d87e6979d..976ab18c3 100644 --- a/lib/src/null_safety_analysis.dart +++ b/lib/src/null_safety_analysis.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'package:analyzer/dart/analysis/context_builder.dart'; import 'package:analyzer/dart/analysis/context_locator.dart'; -// ignore: unused_import import 'package:analyzer/dart/ast/token.dart'; import 'package:cli_util/cli_util.dart'; import 'package:source_span/source_span.dart'; From 2deac20243727d90e098c54aeccca3c4e1bde70a Mon Sep 17 00:00:00 2001 From: vin-fandemand Date: Wed, 29 Jul 2020 14:35:48 -0400 Subject: [PATCH 7/8] add the null safety lint override back --- lib/src/null_safety_analysis.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/null_safety_analysis.dart b/lib/src/null_safety_analysis.dart index 976ab18c3..d87e6979d 100644 --- a/lib/src/null_safety_analysis.dart +++ b/lib/src/null_safety_analysis.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:analyzer/dart/analysis/context_builder.dart'; import 'package:analyzer/dart/analysis/context_locator.dart'; +// ignore: unused_import import 'package:analyzer/dart/ast/token.dart'; import 'package:cli_util/cli_util.dart'; import 'package:source_span/source_span.dart'; From ebb6f190180f693280acf9f1d92c911982b230ba Mon Sep 17 00:00:00 2001 From: vin-fandemand Date: Thu, 30 Jul 2020 12:52:20 -0400 Subject: [PATCH 8/8] changes to handle unauthenticated servers --- lib/src/oauth2.dart | 47 ++++++++++++++++++-------------------- lib/src/source/hosted.dart | 33 +++++++++++++++----------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index 9b9fe5412..df63c7d88 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -107,9 +107,11 @@ Future withClient(SystemCache cache, Future Function(Client) fn, {String hostedURLName}) { return _getClient(cache, hostedURLName).then((client) { return fn(client).whenComplete(() { - client.close(); - // Be sure to save the credentials even when an error happens. - _saveCredentials(cache, client.credentials, hostedURLName); + if (client != null) { + client.close(); + // Be sure to save the credentials even when an error happens. + _saveCredentials(cache, client.credentials, hostedURLName); + } }); }).catchError((error) { if (error is ExpirationException) { @@ -155,7 +157,9 @@ Future _getClient(SystemCache cache, String hostedURLName) async { useIdToken = globalAuthConfig[hostedURLName].useIdToken; } else { _loadHostedAuthConfigFile(cache, hostedURLName); - useIdToken = globalAuthConfig[hostedURLName].useIdToken; + if (globalAuthConfig.containsKey(hostedURLName)) { + useIdToken = globalAuthConfig[hostedURLName].useIdToken; + } } } @@ -223,20 +227,14 @@ void _saveCredentials( globalCredentials['default'] = credentials; } + String credentialsPath; if (hostedURLName != null) { - var credentialsPath = - path.join(Directory.current.path, '${hostedURLName}_credentials.json'); - ensureDir(path.dirname(credentialsPath)); - writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); - credentialsPath = - path.join(cache.rootDir, '${hostedURLName}_credentials.json'); - ensureDir(path.dirname(credentialsPath)); - writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); + credentialsPath = _hostedURLNameCredentialsFile(cache, hostedURLName); } else { - var credentialsPath = _credentialsFile(cache); - ensureDir(path.dirname(credentialsPath)); - writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); + credentialsPath = _credentialsFile(cache); } + ensureDir(path.dirname(credentialsPath)); + writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); } /// The path to the file in which the user's OAuth2 credentials are stored. @@ -244,19 +242,13 @@ String _credentialsFile(SystemCache cache) => path.join(cache.rootDir, 'credentials.json'); /// The path to the file in which the user's OAuth2 credentials are stored. -String _hostedURLNameCredentialsFile(SystemCache cache, String hostedURLName) { - var p = - path.join(Directory.current.path, '${hostedURLName}_credentials.json'); - if (!fileExists(p)) { - p = path.join(cache.rootDir, '${hostedURLName}_credentials.json'); - } - return p; -} +String _hostedURLNameCredentialsFile(SystemCache cache, String hostedURLName) => + path.join(cache.rootDir, '${hostedURLName}_credentials.json'); /// Gets the user to authorize pub as a client of pub.dartlang.org via oauth2. /// /// Returns a Future that completes to a fully-authorized [Client]. -Future _authorize() async { +Future _authorize({String hostedURLName}) async { var grant = AuthorizationCodeGrant(_identifier, _authorizationEndpoint, tokenEndpoint, secret: _secret, @@ -297,7 +289,7 @@ Future _authorize() async { 'Waiting for your authorization...'); var client = await completer.future; - globalCredentials['default'] = client.credentials; + globalCredentials[hostedURLName ?? 'default'] = client.credentials; log.message('Successfully authorized.\n'); return client; } @@ -309,6 +301,11 @@ Future _authorizeHostedUrl( String hostedURLName, SystemCache cache) async { // final authConfig = _loadHostedAuthConfigFile(cache, hostedURLName); + if (authConfig == null) { + return _authorize( + hostedURLName: + hostedURLName); // if there is no auth configuration, then fallback to default + } var grant = AuthorizationCodeGrant(authConfig.identifier, authConfig.authorizationEndpoint, authConfig.tokenEndpoint, secret: authConfig.secret, diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 39d5d98a3..4de83b7f6 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart @@ -184,9 +184,11 @@ class BoundHostedSource extends CachedSource { if (url.host != 'pub.dartlang.org') { final client = await oauth2.getClient(cache: systemCache, hostedURLName: url.host); - _pubApiHeaders['Authorization'] = client.useIdToken == true - ? 'Bearer ${client.credentials.idToken}' - : 'Bearer ${client.credentials.accessToken}'; + if (client != null) { + _pubApiHeaders['Authorization'] = client.useIdToken == true + ? 'Bearer ${client.credentials.idToken}' + : 'Bearer ${client.credentials.accessToken}'; + } } String body; @@ -412,16 +414,21 @@ class BoundHostedSource extends CachedSource { if (url.host != 'pub.dartlang.org') { final client = await oauth2.getClient(cache: systemCache, hostedURLName: url.host); - await io.HttpClient().getUrl(url).then((io.HttpClientRequest request) { - request.headers.add( - 'Authorization', - client.useIdToken == true - ? 'Bearer ${client.credentials.idToken}' - : 'Bearer ${client.credentials.accessToken}'); - return request.close(); - }).then((io.HttpClientResponse response) async { - await extractTarGz(response.asBroadcastStream(), tempDir); - }); + if (client != null) { + await io.HttpClient().getUrl(url).then((io.HttpClientRequest request) { + request.headers.add( + 'Authorization', + client.useIdToken == true + ? 'Bearer ${client.credentials.idToken}' + : 'Bearer ${client.credentials.accessToken}'); + return request.close(); + }).then((io.HttpClientResponse response) async { + await extractTarGz(response.asBroadcastStream(), tempDir); + }); + } else { + var response = await httpClient.send(http.Request('GET', url)); + await extractTarGz(response.stream, tempDir); + } } else { var response = await httpClient.send(http.Request('GET', url)); await extractTarGz(response.stream, tempDir);