From bcf727d66e802d0d8cda6a3e69c8e1f47c254372 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 16:54:54 -0400 Subject: [PATCH 01/13] Refactor withClient to use http.BaseClient instead of oauth2.Client --- lib/src/oauth2.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index 50b7bd585..89b6380b8 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:io'; - +import 'package:http/http.dart' as http; import 'package:oauth2/oauth2.dart'; import 'package:path/path.dart' as path; import 'package:shelf/shelf.dart' as shelf; @@ -90,12 +90,16 @@ 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 fn(Client client)) { +Future withClient( + SystemCache cache, Future fn(http.BaseClient client)) { return _getClient(cache).then((client) { return fn(client).whenComplete(() { client.close(); - // Be sure to save the credentials even when an error happens. - _saveCredentials(cache, client.credentials); + if (client is Client) { + // Be sure to save the credentials even when an error happens. + // Note: this is only performed for the pub.dartlang.org client. + _saveCredentials(cache, client.credentials); + } }); }).catchError((error) { if (error is ExpirationException) { @@ -120,7 +124,7 @@ Future withClient(SystemCache cache, Future fn(Client client)) { /// /// If saved credentials are available, those are used; otherwise, the user is /// prompted to authorize the pub client. -Future _getClient(SystemCache cache) async { +Future _getClient(SystemCache cache) async { var credentials = _loadCredentials(cache); if (credentials == null) return await _authorize(); From 7be736eee70628633c8fbefd2fe0f26b6964adbb Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 16:56:05 -0400 Subject: [PATCH 02/13] Add _tokensFile resolver --- lib/src/oauth2.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index 89b6380b8..66c26cf94 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -181,6 +181,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 third-party Bearer tokens are stored. +String _tokensFile(SystemCache cache) => + path.join(cache.rootDir, 'tokens.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]. From e3134f377342647a3d636e8e1eb20b33e5f657a5 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 17:02:50 -0400 Subject: [PATCH 03/13] Add BearerTokenClient class --- lib/src/bearer_token_client.dart | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 lib/src/bearer_token_client.dart diff --git a/lib/src/bearer_token_client.dart b/lib/src/bearer_token_client.dart new file mode 100644 index 000000000..f08bc6d0e --- /dev/null +++ b/lib/src/bearer_token_client.dart @@ -0,0 +1,23 @@ +import 'dart:io'; +import 'package:http/http.dart'; + +/// An HTTP [BaseClient] implementation that adds an `authorization` +/// header containing the given `Bearer` [token] to each request. +class BearerTokenClient extends BaseClient { + /// The token to be sent with all requests. + /// + /// All requests will have the `authorization` header set to + /// `'Bearer $token'`. + final String token; + + /// The underlying [BaseClient] to use to send requests. + final BaseClient httpClient; + + BearerTokenClient(this.token, this.httpClient); + + @override + Future send(BaseRequest request) { + request.headers[HttpHeaders.authorizationHeader] = 'Bearer $token'; + return httpClient.send(request); + } +} From d63e712e6989857caa3e0ece883efcc33884ba7e Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 17:13:15 -0400 Subject: [PATCH 04/13] Add _loadTokens method --- lib/src/oauth2.dart | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index 66c26cf94..95e986380 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:oauth2/oauth2.dart'; @@ -10,6 +11,7 @@ import 'package:path/path.dart' as path; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; +import 'bearer_token_client.dart'; import 'http.dart'; import 'io.dart'; import 'log.dart' as log; @@ -125,6 +127,14 @@ Future withClient( /// If saved credentials are available, those are used; otherwise, the user is /// prompted to authorize the pub client. Future _getClient(SystemCache cache) async { + // Pub will default to searching for an OAuth2 token in + // $PUB_CACHE/credentials.json. + // + // However, if $PUB_HOSTED_URL is contained within $PUB_CACHE/tokens.json, + // then instead opt for an HTTP client that sends the provided token + // in the Authorization header. + var tokens = _loadTokens(cache); + var credentials = _loadCredentials(cache); if (credentials == null) return await _authorize(); @@ -167,6 +177,41 @@ Credentials _loadCredentials(SystemCache cache) { } } +/// Loads the user's stored bearer tokens from the in-memory cache or the +/// filesystem if possible. +/// +/// If the credentials can't be loaded for any reason, the returned [Future] +/// completes to `{}`. +Map _loadTokens(SystemCache cache) { + String path; + + try { + path = _tokensFile(cache); + if (!fileExists(path)) return {}; + + var data = json.decode(readTextFile(path)); + if (data is Map) { + // So that format errors can be caught as early as possible, + // eagerly iterate through and cast the set of tokens, rather + // than using a lazy alternative. + return Map.fromEntries( + data.entries.map((e) => MapEntry(e.key, e.value as String))); + } else { + log.error( + 'The format of "$path" is incorrect. It must be a map of string keys to string values.'); + return {}; + } + } on CastError { + var sourceOfError = path == null ? '' : '"$path"'; + log.error('The format of $sourceOfError is incorrect. ' + 'It must be a map of string keys to string values, ' + 'but at least one key or value was not a string.'); + return {}; + } catch (e) { + return {}; + } +} + /// Save the user's OAuth2 credentials to the in-memory cache and the /// filesystem. void _saveCredentials(SystemCache cache, Credentials credentials) { From fd8a8aa72535ff35febb086dc982e79660bd1bd9 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 17:14:37 -0400 Subject: [PATCH 05/13] Use a BearerTokenClient when a tokens.json entry is found --- lib/src/oauth2.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index 95e986380..4642cdca2 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -134,6 +134,11 @@ Future _getClient(SystemCache cache) async { // then instead opt for an HTTP client that sends the provided token // in the Authorization header. var tokens = _loadTokens(cache); + if (Platform.environment.containsKey('PUB_HOSTED_URL') && + tokens.containsKey('PUB_HOSTED_URL')) { + return BearerTokenClient( + Platform.environment['PUB_HOSTED_URL'], httpClient); + } var credentials = _loadCredentials(cache); if (credentials == null) return await _authorize(); From cbf87ac45b0468c2dac947f98ff30722a05cdc3b Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 17:14:56 -0400 Subject: [PATCH 06/13] Add BearerTokenClient.close() --- lib/src/bearer_token_client.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/bearer_token_client.dart b/lib/src/bearer_token_client.dart index f08bc6d0e..923ddd777 100644 --- a/lib/src/bearer_token_client.dart +++ b/lib/src/bearer_token_client.dart @@ -15,6 +15,12 @@ class BearerTokenClient extends BaseClient { BearerTokenClient(this.token, this.httpClient); + @override + void close() { + httpClient.close(); + super.close(); + } + @override Future send(BaseRequest request) { request.headers[HttpHeaders.authorizationHeader] = 'Bearer $token'; From b1a398c6bc9e59c102ba67ba93e65e60c3f87e0f Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 17:52:59 -0400 Subject: [PATCH 07/13] Handle pub.dev/dartlang, add token cache --- lib/src/oauth2.dart | 46 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index 4642cdca2..c3795996d 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -66,6 +66,12 @@ final _scopes = ['openid', 'https://www.googleapis.com/auth/userinfo.email']; /// cache. Credentials _credentials; +/// An in-memory cache of the user's bearer tokens. +/// +/// This should always be the same as the tokens file stored in the system +/// cache. +Map _tokens; + /// Delete the cached credentials, if they exist. void _clearCredentials(SystemCache cache) { _credentials = null; @@ -127,17 +133,24 @@ Future withClient( /// If saved credentials are available, those are used; otherwise, the user is /// prompted to authorize the pub client. Future _getClient(SystemCache cache) async { - // Pub will default to searching for an OAuth2 token in - // $PUB_CACHE/credentials.json. - // - // However, if $PUB_HOSTED_URL is contained within $PUB_CACHE/tokens.json, - // then instead opt for an HTTP client that sends the provided token - // in the Authorization header. - var tokens = _loadTokens(cache); - if (Platform.environment.containsKey('PUB_HOSTED_URL') && - tokens.containsKey('PUB_HOSTED_URL')) { - return BearerTokenClient( - Platform.environment['PUB_HOSTED_URL'], httpClient); + // For any server other than pub.dartlang.org and pub.dev, we will + // use $PUB_CACHE/tokens.json + var pubHostedUrl = + Platform.environment['PUB_HOSTED_URL'] ?? 'https://pub.dev'; + if (!['https://pub.dartlang.org', 'https://pub.dev'].contains(pubHostedUrl)) { + // Pub will default to searching for an OAuth2 token in + // $PUB_CACHE/credentials.json. + // + // However, if $PUB_HOSTED_URL is contained within $PUB_CACHE/tokens.json, + // then instead opt for an HTTP client that sends the provided token + // in the Authorization header. + var tokens = _loadTokens(cache); + + if (tokens.containsKey(pubHostedUrl)) { + return BearerTokenClient(pubHostedUrl, httpClient); + } else { + // If there is no entry for the given server, prompt the user for one. + } } var credentials = _loadCredentials(cache); @@ -227,6 +240,17 @@ void _saveCredentials(SystemCache cache, Credentials credentials) { writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); } +/// Save the user's bearer tokens to the in-memory cache and the +/// filesystem. +void _saveTokens(SystemCache cache, Map tokens) { + log.fine('Saving bearer tokens.'); + _tokens = tokens; + var encoder = JsonEncoder.withIndent(' '); + var tokensPath = _tokensFile(cache); + ensureDir(path.dirname(tokensPath)); + writeTextFile(tokensPath, encoder.convert(tokens), dontLogContents: true); +} + /// The path to the file in which the user's OAuth2 credentials are stored. String _credentialsFile(SystemCache cache) => path.join(cache.rootDir, 'credentials.json'); From 9027227d3e18c2a835ba8c67df1006196712f366 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 17:59:14 -0400 Subject: [PATCH 08/13] Prompt the user for a token if one does not exist --- lib/src/io.dart | 16 ++++++++++++++++ lib/src/oauth2.dart | 11 ++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/src/io.dart b/lib/src/io.dart index 3ad0c487b..d2760a699 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart @@ -587,6 +587,22 @@ Future confirm(String message) { return streamFirst(_stdinLines).then(RegExp(r"^[yY]").hasMatch); } +/// Displays a message and reads a text response from the user. +/// +/// Returns a [Future] that completes with the next line from stdin. +/// +/// This will automatically append ":" to the message, so [message] +/// should just be a fragment like, "Enter your name". +Future prompt(String message) { + log.fine('Showing prompt: $message'); + if (runningFromTest) { + log.message("$message: "); + } else { + stdout.write(log.format("$message: ")); + } + return streamFirst(_stdinLines); +} + /// Flushes the stdout and stderr streams, then exits the program with the given /// status code. /// diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index c3795996d..8536fd384 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -145,11 +145,18 @@ Future _getClient(SystemCache cache) async { // then instead opt for an HTTP client that sends the provided token // in the Authorization header. var tokens = _loadTokens(cache); + var tokensFile = _tokensFile(cache); if (tokens.containsKey(pubHostedUrl)) { - return BearerTokenClient(pubHostedUrl, httpClient); + return BearerTokenClient(tokens[pubHostedUrl], httpClient); } else { // If there is no entry for the given server, prompt the user for one. + log.message('Your PUB_HOSTED_URL is "$pubHostedUrl", but "$tokensFile" ' + 'contains no entry for that URL.'); + var token = await prompt('Enter your token for "$pubHostedUrl"'); + // Save the new credentials. + _saveTokens(cache, tokens..[pubHostedUrl] = token); + return BearerTokenClient(token, httpClient); } } @@ -204,6 +211,8 @@ Map _loadTokens(SystemCache cache) { String path; try { + if (_tokens != null) return _tokens; + path = _tokensFile(cache); if (!fileExists(path)) return {}; From f88cea55e5fdd3a6af09742012c07d127fa5b634 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 18:03:09 -0400 Subject: [PATCH 09/13] Prompt user if no token is present --- lib/src/oauth2.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index 8536fd384..ae7732f9b 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -151,7 +151,7 @@ Future _getClient(SystemCache cache) async { return BearerTokenClient(tokens[pubHostedUrl], httpClient); } else { // If there is no entry for the given server, prompt the user for one. - log.message('Your PUB_HOSTED_URL is "$pubHostedUrl", but "$tokensFile" ' + log.message('Your \$PUB_HOSTED_URL is "$pubHostedUrl", but "$tokensFile" ' 'contains no entry for that URL.'); var token = await prompt('Enter your token for "$pubHostedUrl"'); // Save the new credentials. From de98a481155f23f231dbf240c8207157015e19d2 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 18:27:55 -0400 Subject: [PATCH 10/13] Handle 401 replies from servers --- lib/src/bearer_token_client.dart | 47 ++++++++++++++++++++++++++------ lib/src/oauth2.dart | 13 +++++---- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/lib/src/bearer_token_client.dart b/lib/src/bearer_token_client.dart index 923ddd777..0d278bc1a 100644 --- a/lib/src/bearer_token_client.dart +++ b/lib/src/bearer_token_client.dart @@ -1,19 +1,27 @@ +import 'dart:async'; import 'dart:io'; import 'package:http/http.dart'; +import 'package:pub/src/exceptions.dart'; +import 'package:pub/src/http.dart'; /// An HTTP [BaseClient] implementation that adds an `authorization` -/// header containing the given `Bearer` [token] to each request. +/// header containing the given `Bearer` [getToken] to each request. class BearerTokenClient extends BaseClient { - /// The token to be sent with all requests. + /// The underlying [BaseClient] to use to send requests. + final BaseClient httpClient; + + /// A callback that takes a server response (potentially `null`), + /// and returns a token to be sent with all requests. + /// If the server returns a 401, this function will be invoked + /// and used to prompt the user for a new token. /// /// All requests will have the `authorization` header set to /// `'Bearer $token'`. - final String token; + final FutureOr Function(String) getToken; - /// The underlying [BaseClient] to use to send requests. - final BaseClient httpClient; + String _token; - BearerTokenClient(this.token, this.httpClient); + BearerTokenClient(this.httpClient, this._token, this.getToken); @override void close() { @@ -23,7 +31,30 @@ class BearerTokenClient extends BaseClient { @override Future send(BaseRequest request) { - request.headers[HttpHeaders.authorizationHeader] = 'Bearer $token'; - return httpClient.send(request); + if (_token != null) { + request.headers[HttpHeaders.authorizationHeader] = 'Bearer $_token'; + } + + return httpClient.send(request).then((response) async { + if (response.statusCode != HttpStatus.unauthorized) { + return response; + } else { + // If we get a 401, print the reply from the server, so + // that servers can give custom instructions to users on + // how to sign in. + String serverResponse; + + try { + handleJsonError(await Response.fromStream(response)); + } on ApplicationException catch (e) { + serverResponse = e.message; + } + + _token = null; // Remove the current token. + _token = await getToken(serverResponse); // Prompt again. + request.headers[HttpHeaders.authorizationHeader] = 'Bearer $_token'; + return httpClient.send(request); + } + }); } } diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart index ae7732f9b..b9fc85e67 100644 --- a/lib/src/oauth2.dart +++ b/lib/src/oauth2.dart @@ -146,18 +146,19 @@ Future _getClient(SystemCache cache) async { // in the Authorization header. var tokens = _loadTokens(cache); var tokensFile = _tokensFile(cache); - - if (tokens.containsKey(pubHostedUrl)) { - return BearerTokenClient(tokens[pubHostedUrl], httpClient); - } else { + return BearerTokenClient(httpClient, tokens[pubHostedUrl], + (serverReply) async { // If there is no entry for the given server, prompt the user for one. log.message('Your \$PUB_HOSTED_URL is "$pubHostedUrl", but "$tokensFile" ' 'contains no entry for that URL.'); + if (serverReply != null) { + log.warning(serverReply); + } var token = await prompt('Enter your token for "$pubHostedUrl"'); // Save the new credentials. _saveTokens(cache, tokens..[pubHostedUrl] = token); - return BearerTokenClient(token, httpClient); - } + return token; + }); } var credentials = _loadCredentials(cache); From d6b53e90b298b724c0f8277cd6a4642b9f1f258e Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 9 Jul 2019 09:28:35 -0400 Subject: [PATCH 11/13] Use withClient for publishing and downloading hosted packages --- lib/src/command/lish.dart | 5 ++++- lib/src/source/hosted.dart | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index 114bf783b..32caf4dbb 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart @@ -67,7 +67,10 @@ class LishCommand extends PubCommand { return log.progress('Uploading', () async { // 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 newUri = server.resolve("/api/packages/versions/new"); + var newUri = server.replace( + pathSegments: server.pathSegments + .followedBy(['api', 'packages', 'versions', 'new'])); var response = await client.get(newUri, headers: pubApiHeaders); var parameters = parseJsonResponse(response); diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 49a0d97db..fc7cd14bc 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart @@ -15,6 +15,7 @@ import '../exceptions.dart'; import '../http.dart'; import '../io.dart'; import '../log.dart' as log; +import '../oauth2.dart'; import '../package.dart'; import '../package_name.dart'; import '../pubspec.dart'; @@ -157,7 +158,8 @@ class BoundHostedSource extends CachedSource { String body; try { - body = await httpClient.read(url, headers: pubApiHeaders); + body = await withClient(systemCache, + (httpClient) => httpClient.read(url, headers: pubApiHeaders)); } catch (error, stackTrace) { var parsed = source._parseDescription(ref.description); _throwFriendlyError(error, stackTrace, parsed.first, parsed.last); @@ -198,7 +200,8 @@ class BoundHostedSource extends CachedSource { log.io("Describe package at $url."); Map version; try { - version = jsonDecode(await httpClient.read(url, headers: pubApiHeaders)); + version = jsonDecode(await withClient(systemCache, + (httpClient) => httpClient.read(url, headers: pubApiHeaders))); } catch (error, stackTrace) { var parsed = source._parseDescription(id.description); _throwFriendlyError(error, stackTrace, id.name, parsed.last); @@ -319,7 +322,8 @@ 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)); + var response = await withClient( + systemCache, (httpClient) => httpClient.send(http.Request("GET", url))); await extractTarGz(response.stream, tempDir); // Remove the existing directory if it exists. This will happen if From 2531f1ed254059471d7ba92d0f2d5e3290822bb2 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 9 Jul 2019 09:38:46 -0400 Subject: [PATCH 12/13] Resolve URLs relative to the entire pub url, not just the origin. --- lib/src/command/lish.dart | 1 - lib/src/command/uploader.dart | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index 32caf4dbb..dbe91a68a 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart @@ -67,7 +67,6 @@ class LishCommand extends PubCommand { return log.progress('Uploading', () async { // 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 newUri = server.replace( pathSegments: server.pathSegments .followedBy(['api', 'packages', 'versions', 'new'])); diff --git a/lib/src/command/uploader.dart b/lib/src/command/uploader.dart index 4adce3922..d90b38459 100644 --- a/lib/src/command/uploader.dart +++ b/lib/src/command/uploader.dart @@ -61,15 +61,16 @@ class UploaderCommand extends PubCommand { var uploader = rest[0]; return oauth2.withClient(cache, (client) { if (command == 'add') { - var url = server.resolve("/api/packages/" - "${Uri.encodeComponent(package)}/uploaders"); + var url = server.replace( + pathSegments: server.pathSegments + .followedBy(['api', 'packages', package, 'uploaders'])); return client .post(url, headers: pubApiHeaders, body: {"email": uploader}); } else { // command == 'remove' - var url = server.resolve("/api/packages/" - "${Uri.encodeComponent(package)}/uploaders/" - "${Uri.encodeComponent(uploader)}"); + var url = server.replace( + pathSegments: server.pathSegments.followedBy( + ['api', 'package', package, 'uploaders', uploader])); return client.delete(url, headers: pubApiHeaders); } }); From e850e3512edcf57ae6592ac83a46d027890ad43d Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 9 Jul 2019 09:39:48 -0400 Subject: [PATCH 13/13] Add copyright header to bearer client --- lib/src/bearer_token_client.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/bearer_token_client.dart b/lib/src/bearer_token_client.dart index 0d278bc1a..b68d3aadb 100644 --- a/lib/src/bearer_token_client.dart +++ b/lib/src/bearer_token_client.dart @@ -1,3 +1,6 @@ +// 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. import 'dart:async'; import 'dart:io'; import 'package:http/http.dart';