Use package:vendor to vendor package:tar and package:oauth2 (#3638)
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 2b5d0cd..6f99b5b 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -15,13 +15,14 @@
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:pool/pool.dart';
+// ignore: prefer_relative_imports
+import 'package:pub/src/third_party/tar/lib/tar.dart';
import 'package:stack_trace/stack_trace.dart';
import 'error_group.dart';
import 'exceptions.dart';
import 'exit_codes.dart' as exit_codes;
import 'log.dart' as log;
-import 'third_party/tar/tar.dart';
import 'utils.dart';
export 'package:http/http.dart' show ByteStream;
diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart
index 41eb5f5..4715014 100644
--- a/lib/src/oauth2.dart
+++ b/lib/src/oauth2.dart
@@ -8,8 +8,9 @@
import 'package:collection/collection.dart' show IterableExtension;
import 'package:http/http.dart' as http;
import 'package:http/retry.dart';
-import 'package:oauth2/oauth2.dart';
import 'package:path/path.dart' as path;
+// ignore: prefer_relative_imports
+import 'package:pub/src/third_party/oauth2/lib/oauth2.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;
diff --git a/lib/src/third_party/oauth2/CHANGELOG.md b/lib/src/third_party/oauth2/CHANGELOG.md
new file mode 100644
index 0000000..0c0deb2
--- /dev/null
+++ b/lib/src/third_party/oauth2/CHANGELOG.md
@@ -0,0 +1,123 @@
+# 2.0.1
+
+* Handle `expires_in` when encoded as string.
+* Populate the pubspec `repository` field.
+* Increase the minimum Dart SDK to `2.17.0`.
+
+# 2.0.0
+
+* Migrate to null safety.
+
+# 1.6.3
+
+* Added optional `codeVerifier` parameter to `AuthorizationCodeGrant` constructor.
+
+# 1.6.1
+
+* Added fix to make sure that credentials are only refreshed once when multiple calls are made.
+
+# 1.6.0
+
+* Added PKCE support to `AuthorizationCodeGrant`.
+
+# 1.5.0
+
+* Added support for `clientCredentialsGrant`.
+
+# 1.4.0
+
+* OpenID's id_token treated.
+
+# 1.3.0
+
+* Added `onCredentialsRefreshed` option when creating `Client` objects.
+
+# 1.2.3
+
+* Support the latest `package:http` release.
+
+# 1.2.2
+
+* Allow the stable 2.0 SDK.
+
+# 1.2.1
+
+* Updated SDK version to 2.0.0-dev.17.0
+
+# 1.2.0
+
+* Add a `getParameter()` parameter to `new AuthorizationCodeGrant()`, `new
+ Credentials()`, and `resourceOwnerPasswordGrant()`. This controls how the
+ authorization server's response is parsed for servers that don't provide the
+ standard JSON response.
+
+# 1.1.1
+
+* `resourceOwnerPasswordGrant()` now properly uses its HTTP client for requests
+ made by the OAuth2 client it returns.
+
+# 1.1.0
+
+* Add a `delimiter` parameter to `new AuthorizationCodeGrant()`, `new
+ Credentials()`, and `resourceOwnerPasswordGrant()`. This controls the
+ delimiter between scopes, which some authorization servers require to be
+ different values than the specified `' '`.
+
+# 1.0.2
+
+* Fix all strong-mode warnings.
+
+* Support `crypto` 1.0.0.
+
+* Support `http_parser` 3.0.0.
+
+# 1.0.1
+
+* Support `http_parser` 2.0.0.
+
+# 1.0.0
+
+## Breaking changes
+
+* Requests that use client authentication, such as the
+ `AuthorizationCodeGrant`'s access token request and `Credentials`' refresh
+ request, now use HTTP Basic authentication by default. This form of
+ authentication is strongly recommended by the OAuth 2.0 spec. The new
+ `basicAuth` parameter may be set to `false` to force form-based authentication
+ for servers that require it.
+
+* `new AuthorizationCodeGrant()` now takes `secret` as an optional named
+ argument rather than a required argument. This matches the OAuth 2.0 spec,
+ which says that a client secret is only required for confidential clients.
+
+* `new Client()` and `Credentials.refresh()` now take both `identifier` and
+ `secret` as optional named arguments rather than required arguments. This
+ matches the OAuth 2.0 spec, which says that the server may choose not to
+ require client authentication for some flows.
+
+* `new Credentials()` now takes named arguments rather than optional positional
+ arguments.
+
+## Non-breaking changes
+
+* Added a `resourceOwnerPasswordGrant` method.
+
+* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` and
+ `new Credentials()` and the `newScopes` argument to `Credentials.refresh` now
+ take an `Iterable` rather than just a `List`.
+
+* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` now
+ defaults to `null` rather than `const []`.
+
+# 0.9.3
+
+* Update the `http` dependency.
+
+* Since `http` 0.11.0 now works in non-`dart:io` contexts, `oauth2` does as
+ well.
+
+# 0.9.2
+
+* Expand the dependency on the HTTP package to include 0.10.x.
+
+* Add a README file.
diff --git a/lib/src/third_party/oauth2/LICENSE b/lib/src/third_party/oauth2/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/lib/src/third_party/oauth2/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/src/third_party/oauth2/README.md b/lib/src/third_party/oauth2/README.md
new file mode 100644
index 0000000..196b9f7
--- /dev/null
+++ b/lib/src/third_party/oauth2/README.md
@@ -0,0 +1,260 @@
+[![Dart CI](https://github.com/dart-lang/oauth2/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/oauth2/actions/workflows/test-package.yml)
+[![pub package](https://img.shields.io/pub/v/oauth2.svg)](https://pub.dev/packages/oauth2)
+[![package publisher](https://img.shields.io/pub/publisher/oauth2.svg)](https://pub.dev/packages/oauth2/publisher)
+
+A client library for authenticating with a remote service via OAuth2 on behalf
+of a user, and making authorized HTTP requests with the user's OAuth2
+credentials.
+
+## About OAuth2
+
+OAuth2 allows a client (the program using this library) to access and manipulate
+a resource that's owned by a resource owner (the end user) and lives on a remote
+server. The client directs the resource owner to an authorization server
+(usually but not always the same as the server that hosts the resource), where
+the resource owner tells the authorization server to give the client an access
+token. This token serves as proof that the client has permission to access
+resources on behalf of the resource owner.
+
+OAuth2 provides several different methods for the client to obtain
+authorization. At the time of writing, this library only supports the
+[Authorization Code Grant][authorizationCodeGrantSection],
+[Client Credentials Grant][clientCredentialsGrantSection] and
+[Resource Owner Password Grant][resourceOwnerPasswordGrantSection] flows, but
+more may be added in the future.
+
+## Authorization Code Grant
+
+**Resources:** [Class summary][authorizationCodeGrantMethod],
+[OAuth documentation][authorizationCodeGrantDocs]
+
+```dart
+import 'dart:io';
+
+import 'package:oauth2/oauth2.dart' as oauth2;
+
+// These URLs are endpoints that are provided by the authorization
+// server. They're usually included in the server's documentation of its
+// OAuth2 API.
+final authorizationEndpoint =
+ Uri.parse('http://example.com/oauth2/authorization');
+final tokenEndpoint = Uri.parse('http://example.com/oauth2/token');
+
+// The authorization server will issue each client a separate client
+// identifier and secret, which allows the server to tell which client
+// is accessing it. Some servers may also have an anonymous
+// identifier/secret pair that any client may use.
+//
+// Note that clients whose source code or binary executable is readily
+// available may not be able to make sure the client secret is kept a
+// secret. This is fine; OAuth2 servers generally won't rely on knowing
+// with certainty that a client is who it claims to be.
+final identifier = 'my client identifier';
+final secret = 'my client secret';
+
+// This is a URL on your application's server. The authorization server
+// will redirect the resource owner here once they've authorized the
+// client. The redirection will include the authorization code in the
+// query parameters.
+final redirectUrl = Uri.parse('http://my-site.com/oauth2-redirect');
+
+/// A file in which the users credentials are stored persistently. If the server
+/// issues a refresh token allowing the client to refresh outdated credentials,
+/// these may be valid indefinitely, meaning the user never has to
+/// re-authenticate.
+final credentialsFile = File('~/.myapp/credentials.json');
+
+/// Either load an OAuth2 client from saved credentials or authenticate a new
+/// one.
+Future<oauth2.Client> createClient() async {
+ var exists = await credentialsFile.exists();
+
+ // If the OAuth2 credentials have already been saved from a previous run, we
+ // just want to reload them.
+ if (exists) {
+ var credentials =
+ oauth2.Credentials.fromJson(await credentialsFile.readAsString());
+ return oauth2.Client(credentials, identifier: identifier, secret: secret);
+ }
+
+ // If we don't have OAuth2 credentials yet, we need to get the resource owner
+ // to authorize us. We're assuming here that we're a command-line application.
+ var grant = oauth2.AuthorizationCodeGrant(
+ identifier, authorizationEndpoint, tokenEndpoint,
+ secret: secret);
+
+ // A URL on the authorization server (authorizationEndpoint with some additional
+ // query parameters). Scopes and state can optionally be passed into this method.
+ var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);
+
+ // Redirect the resource owner to the authorization URL. Once the resource
+ // owner has authorized, they'll be redirected to `redirectUrl` with an
+ // authorization code. The `redirect` should cause the browser to redirect to
+ // another URL which should also have a listener.
+ //
+ // `redirect` and `listen` are not shown implemented here. See below for the
+ // details.
+ await redirect(authorizationUrl);
+ var responseUrl = await listen(redirectUrl);
+
+ // Once the user is redirected to `redirectUrl`, pass the query parameters to
+ // the AuthorizationCodeGrant. It will validate them and extract the
+ // authorization code to create a new Client.
+ return await grant.handleAuthorizationResponse(responseUrl.queryParameters);
+}
+
+void main() async {
+ var client = await createClient();
+
+ // Once you have a Client, you can use it just like any other HTTP client.
+ print(await client.read('http://example.com/protected-resources.txt'));
+
+ // Once we're done with the client, save the credentials file. This ensures
+ // that if the credentials were automatically refreshed while using the
+ // client, the new credentials are available for the next run of the
+ // program.
+ await credentialsFile.writeAsString(client.credentials.toJson());
+}
+```
+
+<details>
+ <summary>Click here to learn how to implement `redirect` and `listen`.</summary>
+
+--------------------------------------------------------------------------------
+
+There is not a universal example for implementing `redirect` and `listen`,
+because different options exist for each platform.
+
+For Flutter apps, there's two popular approaches:
+
+1. Launch a browser using [url_launcher][] and listen for a redirect using
+ [uni_links][].
+
+ ```dart
+ if (await canLaunch(authorizationUrl.toString())) {
+ await launch(authorizationUrl.toString()); }
+
+ // ------- 8< -------
+
+ final linksStream = getLinksStream().listen((Uri uri) async {
+ if (uri.toString().startsWith(redirectUrl)) {
+ responseUrl = uri;
+ }
+ });
+ ```
+
+1. Launch a WebView inside the app and listen for a redirect using
+ [webview_flutter][].
+
+ ```dart
+ WebView(
+ javascriptMode: JavascriptMode.unrestricted,
+ initialUrl: authorizationUrl.toString(),
+ navigationDelegate: (navReq) {
+ if (navReq.url.startsWith(redirectUrl)) {
+ responseUrl = Uri.parse(navReq.url);
+ return NavigationDecision.prevent;
+ }
+ return NavigationDecision.navigate;
+ },
+ // ------- 8< -------
+ );
+ ```
+
+For Dart apps, the best approach depends on the available options for accessing
+a browser. In general, you'll need to launch the authorization URL through the
+client's browser and listen for the redirect URL.
+</details>
+
+## Client Credentials Grant
+
+**Resources:** [Method summary][clientCredentialsGrantMethod],
+[OAuth documentation][clientCredentialsGrantDocs]
+
+```dart
+// This URL is an endpoint that's provided by the authorization server. It's
+// usually included in the server's documentation of its OAuth2 API.
+final authorizationEndpoint =
+ Uri.parse('http://example.com/oauth2/authorization');
+
+// The OAuth2 specification expects a client's identifier and secret
+// to be sent when using the client credentials grant.
+//
+// Because the client credentials grant is not inherently associated with a user,
+// it is up to the server in question whether the returned token allows limited
+// API access.
+//
+// Either way, you must provide both a client identifier and a client secret:
+final identifier = 'my client identifier';
+final secret = 'my client secret';
+
+// Calling the top-level `clientCredentialsGrant` function will return a
+// [Client] instead.
+var client = await oauth2.clientCredentialsGrant(
+ authorizationEndpoint, identifier, secret);
+
+// With an authenticated client, you can make requests, and the `Bearer` token
+// returned by the server during the client credentials grant will be attached
+// to any request you make.
+var response =
+ await client.read('https://example.com/api/some_resource.json');
+
+// You can save the client's credentials, which consists of an access token, and
+// potentially a refresh token and expiry date, to a file. This way, subsequent runs
+// do not need to reauthenticate, and you can avoid saving the client identifier and
+// secret.
+await credentialsFile.writeAsString(client.credentials.toJson());
+```
+
+## Resource Owner Password Grant
+
+**Resources:** [Method summary][resourceOwnerPasswordGrantMethod],
+[OAuth documentation][resourceOwnerPasswordGrantDocs]
+
+```dart
+// This URL is an endpoint that's provided by the authorization server. It's
+// usually included in the server's documentation of its OAuth2 API.
+final authorizationEndpoint =
+ Uri.parse('http://example.com/oauth2/authorization');
+
+// The user should supply their own username and password.
+final username = 'example user';
+final password = 'example password';
+
+// The authorization server may issue each client a separate client
+// identifier and secret, which allows the server to tell which client
+// is accessing it. Some servers may also have an anonymous
+// identifier/secret pair that any client may use.
+//
+// Some servers don't require the client to authenticate itself, in which case
+// these should be omitted.
+final identifier = 'my client identifier';
+final secret = 'my client secret';
+
+// Make a request to the authorization endpoint that will produce the fully
+// authenticated Client.
+var client = await oauth2.resourceOwnerPasswordGrant(
+ authorizationEndpoint, username, password,
+ identifier: identifier, secret: secret);
+
+// Once you have the client, you can use it just like any other HTTP client.
+var result = await client.read('http://example.com/protected-resources.txt');
+
+// Once we're done with the client, save the credentials file. This will allow
+// us to re-use the credentials and avoid storing the username and password
+// directly.
+File('~/.myapp/credentials.json').writeAsString(client.credentials.toJson());
+```
+
+[authorizationCodeGrantDocs]: https://oauth.net/2/grant-types/authorization-code/
+[authorizationCodeGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/AuthorizationCodeGrant-class.html
+[authorizationCodeGrantSection]: #authorization-code-grant
+[clientCredentialsGrantDocs]: https://oauth.net/2/grant-types/client-credentials/
+[clientCredentialsGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/clientCredentialsGrant.html
+[clientCredentialsGrantSection]: #client-credentials-grant
+[resourceOwnerPasswordGrantDocs]: https://oauth.net/2/grant-types/password/
+[resourceOwnerPasswordGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/resourceOwnerPasswordGrant.html
+[resourceOwnerPasswordGrantSection]: #resource-owner-password-grant
+[uni_links]: https://pub.dev/packages/uni_links
+[url_launcher]: https://pub.dev/packages/url_launcher
+[webview_flutter]: https://pub.dev/packages/webview_flutter
diff --git a/lib/src/third_party/oauth2/analysis_options.yaml b/lib/src/third_party/oauth2/analysis_options.yaml
new file mode 100644
index 0000000..c8bc59c
--- /dev/null
+++ b/lib/src/third_party/oauth2/analysis_options.yaml
@@ -0,0 +1,40 @@
+include: package:lints/recommended.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ - always_declare_return_types
+ - avoid_catching_errors
+ - avoid_dynamic_calls
+ - avoid_private_typedef_functions
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - directives_ordering
+ - literal_only_boolean_expressions
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - omit_local_variable_types
+ - only_throw_errors
+ - package_api_docs
+ - prefer_asserts_in_initializer_lists
+ - prefer_const_constructors
+ - prefer_const_declarations
+ - prefer_relative_imports
+ - prefer_single_quotes
+ - sort_pub_dependencies
+ - test_types_in_equals
+ - throw_in_finally
+ - type_annotate_public_apis
+ - unawaited_futures
+ - unnecessary_await_in_return
+ - unnecessary_lambdas
+ - unnecessary_parenthesis
+ - unnecessary_statements
+ - use_is_even_rather_than_modulo
+ - use_string_buffers
+ - use_super_parameters
diff --git a/lib/src/third_party/oauth2/lib/oauth2.dart b/lib/src/third_party/oauth2/lib/oauth2.dart
new file mode 100644
index 0000000..45efc5c
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/oauth2.dart
@@ -0,0 +1,11 @@
+// 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.
+
+export 'src/authorization_code_grant.dart';
+export 'src/authorization_exception.dart';
+export 'src/client.dart';
+export 'src/client_credentials_grant.dart';
+export 'src/credentials.dart';
+export 'src/expiration_exception.dart';
+export 'src/resource_owner_password_grant.dart';
diff --git a/lib/src/third_party/oauth2/lib/src/authorization_code_grant.dart b/lib/src/third_party/oauth2/lib/src/authorization_code_grant.dart
new file mode 100644
index 0000000..fac56ba
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/authorization_code_grant.dart
@@ -0,0 +1,371 @@
+// 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:async';
+import 'dart:convert';
+import 'dart:math';
+
+import 'package:crypto/crypto.dart';
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'authorization_exception.dart';
+import 'client.dart';
+import 'credentials.dart';
+import 'handle_access_token_response.dart';
+import 'parameters.dart';
+import 'utils.dart';
+
+/// A class for obtaining credentials via an [authorization code grant][].
+///
+/// This method of authorization involves sending the resource owner to the
+/// authorization server where they will authorize the client. They're then
+/// redirected back to your server, along with an authorization code. This is
+/// used to obtain [Credentials] and create a fully-authorized [Client].
+///
+/// To use this class, you must first call [getAuthorizationUrl] to get the URL
+/// to which to redirect the resource owner. Then once they've been redirected
+/// back to your application, call [handleAuthorizationResponse] or
+/// [handleAuthorizationCode] to process the authorization server's response and
+/// construct a [Client].
+///
+/// [authorization code grant]: http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1
+class AuthorizationCodeGrant {
+ /// The function used to parse parameters from a host's response.
+ final GetParameters _getParameters;
+
+ /// The client identifier for this client.
+ ///
+ /// The authorization server will issue each client a separate client
+ /// identifier and secret, which allows the server to tell which client is
+ /// accessing it. Some servers may also have an anonymous identifier/secret
+ /// pair that any client may use.
+ ///
+ /// This is usually global to the program using this library.
+ final String identifier;
+
+ /// The client secret for this client.
+ ///
+ /// The authorization server will issue each client a separate client
+ /// identifier and secret, which allows the server to tell which client is
+ /// accessing it. Some servers may also have an anonymous identifier/secret
+ /// pair that any client may use.
+ ///
+ /// This is usually global to the program using this library.
+ ///
+ /// Note that clients whose source code or binary executable is readily
+ /// available may not be able to make sure the client secret is kept a secret.
+ /// This is fine; OAuth2 servers generally won't rely on knowing with
+ /// certainty that a client is who it claims to be.
+ final String? secret;
+
+ /// A URL provided by the authorization server that serves as the base for the
+ /// URL that the resource owner will be redirected to to authorize this
+ /// client.
+ ///
+ /// This will usually be listed in the authorization server's OAuth2 API
+ /// documentation.
+ final Uri authorizationEndpoint;
+
+ /// A URL provided by the authorization server that this library uses to
+ /// obtain long-lasting credentials.
+ ///
+ /// This will usually be listed in the authorization server's OAuth2 API
+ /// documentation.
+ final Uri tokenEndpoint;
+
+ /// Callback to be invoked whenever the credentials are refreshed.
+ ///
+ /// This will be passed as-is to the constructed [Client].
+ final CredentialsRefreshedCallback? _onCredentialsRefreshed;
+
+ /// Whether to use HTTP Basic authentication for authorizing the client.
+ final bool _basicAuth;
+
+ /// A [String] used to separate scopes; defaults to `" "`.
+ final String _delimiter;
+
+ /// The HTTP client used to make HTTP requests.
+ http.Client? _httpClient;
+
+ /// The URL to which the resource owner will be redirected after they
+ /// authorize this client with the authorization server.
+ Uri? _redirectEndpoint;
+
+ /// The scopes that the client is requesting access to.
+ List<String>? _scopes;
+
+ /// An opaque string that users of this library may specify that will be
+ /// included in the response query parameters.
+ String? _stateString;
+
+ /// The current state of the grant object.
+ _State _state = _State.initial;
+
+ /// Allowed characters for generating the _codeVerifier
+ static const String _charset =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
+
+ /// The PKCE code verifier. Will be generated if one is not provided in the
+ /// constructor.
+ final String _codeVerifier;
+
+ /// Creates a new grant.
+ ///
+ /// If [basicAuth] is `true` (the default), the client credentials are sent to
+ /// the server using using HTTP Basic authentication as defined in [RFC 2617].
+ /// Otherwise, they're included in the request body. Note that the latter form
+ /// is not recommended by the OAuth 2.0 spec, and should only be used if the
+ /// server doesn't support Basic authentication.
+ ///
+ /// [RFC 2617]: https://tools.ietf.org/html/rfc2617
+ ///
+ /// [httpClient] is used for all HTTP requests made by this grant, as well as
+ /// those of the [Client] is constructs.
+ ///
+ /// [onCredentialsRefreshed] will be called by the constructed [Client]
+ /// whenever the credentials are refreshed.
+ ///
+ /// [codeVerifier] String to be used as PKCE code verifier. If none is
+ /// provided a random codeVerifier will be generated.
+ /// The codeVerifier must meet requirements specified in [RFC 7636].
+ ///
+ /// [RFC 7636]: https://tools.ietf.org/html/rfc7636#section-4.1
+ ///
+ /// The scope strings will be separated by the provided [delimiter]. This
+ /// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
+ /// use non-standard delimiters.
+ ///
+ /// By default, this follows the OAuth2 spec and requires the server's
+ /// responses to be in JSON format. However, some servers return non-standard
+ /// response formats, which can be parsed using the [getParameters] function.
+ ///
+ /// This function is passed the `Content-Type` header of the response as well
+ /// as its body as a UTF-8-decoded string. It should return a map in the same
+ /// format as the [standard JSON response][].
+ ///
+ /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
+ AuthorizationCodeGrant(
+ this.identifier, this.authorizationEndpoint, this.tokenEndpoint,
+ {this.secret,
+ String? delimiter,
+ bool basicAuth = true,
+ http.Client? httpClient,
+ CredentialsRefreshedCallback? onCredentialsRefreshed,
+ Map<String, dynamic> Function(MediaType? contentType, String body)?
+ getParameters,
+ String? codeVerifier})
+ : _basicAuth = basicAuth,
+ _httpClient = httpClient ?? http.Client(),
+ _delimiter = delimiter ?? ' ',
+ _getParameters = getParameters ?? parseJsonParameters,
+ _onCredentialsRefreshed = onCredentialsRefreshed,
+ _codeVerifier = codeVerifier ?? _createCodeVerifier();
+
+ /// Returns the URL to which the resource owner should be redirected to
+ /// authorize this client.
+ ///
+ /// The resource owner will then be redirected to [redirect], which should
+ /// point to a server controlled by the client. This redirect will have
+ /// additional query parameters that should be passed to
+ /// [handleAuthorizationResponse].
+ ///
+ /// The specific permissions being requested from the authorization server may
+ /// be specified via [scopes]. The scope strings are specific to the
+ /// authorization server and may be found in its documentation. Note that you
+ /// may not be granted access to every scope you request; you may check the
+ /// [Credentials.scopes] field of [Client.credentials] to see which scopes you
+ /// were granted.
+ ///
+ /// An opaque [state] string may also be passed that will be present in the
+ /// query parameters provided to the redirect URL.
+ ///
+ /// It is a [StateError] to call this more than once.
+ Uri getAuthorizationUrl(Uri redirect,
+ {Iterable<String>? scopes, String? state}) {
+ if (_state != _State.initial) {
+ throw StateError('The authorization URL has already been generated.');
+ }
+ _state = _State.awaitingResponse;
+
+ var scopeList = scopes?.toList() ?? <String>[];
+ var codeChallenge = base64Url
+ .encode(sha256.convert(ascii.encode(_codeVerifier)).bytes)
+ .replaceAll('=', '');
+
+ _redirectEndpoint = redirect;
+ _scopes = scopeList;
+ _stateString = state;
+ var parameters = {
+ 'response_type': 'code',
+ 'client_id': identifier,
+ 'redirect_uri': redirect.toString(),
+ 'code_challenge': codeChallenge,
+ 'code_challenge_method': 'S256'
+ };
+
+ if (state != null) parameters['state'] = state;
+ if (scopeList.isNotEmpty) parameters['scope'] = scopeList.join(_delimiter);
+
+ return addQueryParameters(authorizationEndpoint, parameters);
+ }
+
+ /// Processes the query parameters added to a redirect from the authorization
+ /// server.
+ ///
+ /// Note that this "response" is not an HTTP response, but rather the data
+ /// passed to a server controlled by the client as query parameters on the
+ /// redirect URL.
+ ///
+ /// It is a [StateError] to call this more than once, to call it before
+ /// [getAuthorizationUrl] is called, or to call it after
+ /// [handleAuthorizationCode] is called.
+ ///
+ /// Throws [FormatException] if [parameters] is invalid according to the
+ /// OAuth2 spec or if the authorization server otherwise provides invalid
+ /// responses. If `state` was passed to [getAuthorizationUrl], this will throw
+ /// a [FormatException] if the `state` parameter doesn't match the original
+ /// value.
+ ///
+ /// Throws [AuthorizationException] if the authorization fails.
+ Future<Client> handleAuthorizationResponse(
+ Map<String, String> parameters) async {
+ if (_state == _State.initial) {
+ throw StateError('The authorization URL has not yet been generated.');
+ } else if (_state == _State.finished) {
+ throw StateError('The authorization code has already been received.');
+ }
+ _state = _State.finished;
+
+ if (_stateString != null) {
+ if (!parameters.containsKey('state')) {
+ throw FormatException('Invalid OAuth response for '
+ '"$authorizationEndpoint": parameter "state" expected to be '
+ '"$_stateString", was missing.');
+ } else if (parameters['state'] != _stateString) {
+ throw FormatException('Invalid OAuth response for '
+ '"$authorizationEndpoint": parameter "state" expected to be '
+ '"$_stateString", was "${parameters['state']}".');
+ }
+ }
+
+ if (parameters.containsKey('error')) {
+ var description = parameters['error_description'];
+ var uriString = parameters['error_uri'];
+ var uri = uriString == null ? null : Uri.parse(uriString);
+ throw AuthorizationException(parameters['error']!, description, uri);
+ } else if (!parameters.containsKey('code')) {
+ throw FormatException('Invalid OAuth response for '
+ '"$authorizationEndpoint": did not contain required parameter '
+ '"code".');
+ }
+
+ return _handleAuthorizationCode(parameters['code']);
+ }
+
+ /// Processes an authorization code directly.
+ ///
+ /// Usually [handleAuthorizationResponse] is preferable to this method, since
+ /// it validates all of the query parameters. However, some authorization
+ /// servers allow the user to copy and paste an authorization code into a
+ /// command-line application, in which case this method must be used.
+ ///
+ /// It is a [StateError] to call this more than once, to call it before
+ /// [getAuthorizationUrl] is called, or to call it after
+ /// [handleAuthorizationCode] is called.
+ ///
+ /// Throws [FormatException] if the authorization server provides invalid
+ /// responses while retrieving credentials.
+ ///
+ /// Throws [AuthorizationException] if the authorization fails.
+ Future<Client> handleAuthorizationCode(String authorizationCode) async {
+ if (_state == _State.initial) {
+ throw StateError('The authorization URL has not yet been generated.');
+ } else if (_state == _State.finished) {
+ throw StateError('The authorization code has already been received.');
+ }
+ _state = _State.finished;
+
+ return _handleAuthorizationCode(authorizationCode);
+ }
+
+ /// This works just like [handleAuthorizationCode], except it doesn't validate
+ /// the state beforehand.
+ Future<Client> _handleAuthorizationCode(String? authorizationCode) async {
+ var startTime = DateTime.now();
+
+ var headers = <String, String>{};
+
+ var body = {
+ 'grant_type': 'authorization_code',
+ 'code': authorizationCode,
+ 'redirect_uri': _redirectEndpoint.toString(),
+ 'code_verifier': _codeVerifier
+ };
+
+ var secret = this.secret;
+ if (_basicAuth && secret != null) {
+ headers['Authorization'] = basicAuthHeader(identifier, secret);
+ } else {
+ // The ID is required for this request any time basic auth isn't being
+ // used, even if there's no actual client authentication to be done.
+ body['client_id'] = identifier;
+ if (secret != null) body['client_secret'] = secret;
+ }
+
+ var response =
+ await _httpClient!.post(tokenEndpoint, headers: headers, body: body);
+
+ var credentials = handleAccessTokenResponse(
+ response, tokenEndpoint, startTime, _scopes, _delimiter,
+ getParameters: _getParameters);
+ return Client(credentials,
+ identifier: identifier,
+ secret: secret,
+ basicAuth: _basicAuth,
+ httpClient: _httpClient,
+ onCredentialsRefreshed: _onCredentialsRefreshed);
+ }
+
+ // Randomly generate a 128 character string to be used as the PKCE code
+ // verifier.
+ static String _createCodeVerifier() => List.generate(
+ 128,
+ (i) => _charset[Random.secure().nextInt(_charset.length)],
+ ).join();
+
+ /// Closes the grant and frees its resources.
+ ///
+ /// This will close the underlying HTTP client, which is shared by the
+ /// [Client] created by this grant, so it's not safe to close the grant and
+ /// continue using the client.
+ void close() {
+ _httpClient?.close();
+ _httpClient = null;
+ }
+}
+
+/// States that [AuthorizationCodeGrant] can be in.
+class _State {
+ /// [AuthorizationCodeGrant.getAuthorizationUrl] has not yet been called for
+ /// this grant.
+ static const initial = _State('initial');
+
+ // [AuthorizationCodeGrant.getAuthorizationUrl] has been called but neither
+ // [AuthorizationCodeGrant.handleAuthorizationResponse] nor
+ // [AuthorizationCodeGrant.handleAuthorizationCode] has been called.
+ static const awaitingResponse = _State('awaiting response');
+
+ // [AuthorizationCodeGrant.getAuthorizationUrl] and either
+ // [AuthorizationCodeGrant.handleAuthorizationResponse] or
+ // [AuthorizationCodeGrant.handleAuthorizationCode] have been called.
+ static const finished = _State('finished');
+
+ final String _name;
+
+ const _State(this._name);
+
+ @override
+ String toString() => _name;
+}
diff --git a/lib/src/third_party/oauth2/lib/src/authorization_exception.dart b/lib/src/third_party/oauth2/lib/src/authorization_exception.dart
new file mode 100644
index 0000000..14a5a3c
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/authorization_exception.dart
@@ -0,0 +1,39 @@
+// 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.
+
+/// An exception raised when OAuth2 authorization fails.
+class AuthorizationException implements Exception {
+ /// The name of the error.
+ ///
+ /// Possible names are enumerated in [the spec][].
+ ///
+ /// [the spec]: http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-5.2
+ final String error;
+
+ /// The description of the error, provided by the server.
+ ///
+ /// May be `null` if the server provided no description.
+ final String? description;
+
+ /// A URL for a page that describes the error in more detail, provided by the
+ /// server.
+ ///
+ /// May be `null` if the server provided no URL.
+ final Uri? uri;
+
+ /// Creates an AuthorizationException.
+ AuthorizationException(this.error, this.description, this.uri);
+
+ /// Provides a string description of the AuthorizationException.
+ @override
+ String toString() {
+ var header = 'OAuth authorization error ($error)';
+ if (description != null) {
+ header = '$header: $description';
+ } else if (uri != null) {
+ header = '$header: $uri';
+ }
+ return '$header.';
+ }
+}
diff --git a/lib/src/third_party/oauth2/lib/src/client.dart b/lib/src/third_party/oauth2/lib/src/client.dart
new file mode 100644
index 0000000..1dd2282
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/client.dart
@@ -0,0 +1,187 @@
+// 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:async';
+
+import 'package:collection/collection.dart';
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'authorization_exception.dart';
+import 'credentials.dart';
+import 'expiration_exception.dart';
+
+/// An OAuth2 client.
+///
+/// This acts as a drop-in replacement for an [http.Client], while sending
+/// OAuth2 authorization credentials along with each request.
+///
+/// The client also automatically refreshes its credentials if possible. When it
+/// makes a request, if its credentials are expired, it will first refresh them.
+/// This means that any request may throw an [AuthorizationException] if the
+/// refresh is not authorized for some reason, a [FormatException] if the
+/// authorization server provides ill-formatted responses, or an
+/// [ExpirationException] if the credentials are expired and can't be refreshed.
+///
+/// The client will also throw an [AuthorizationException] if the resource
+/// server returns a 401 response with a WWW-Authenticate header indicating that
+/// the current credentials are invalid.
+///
+/// If you already have a set of [Credentials], you can construct a [Client]
+/// directly. However, in order to first obtain the credentials, you must
+/// authorize. At the time of writing, the only authorization method this
+/// library supports is [AuthorizationCodeGrant].
+class Client extends http.BaseClient {
+ /// The client identifier for this client.
+ ///
+ /// The authorization server will issue each client a separate client
+ /// identifier and secret, which allows the server to tell which client is
+ /// accessing it. Some servers may also have an anonymous identifier/secret
+ /// pair that any client may use.
+ ///
+ /// This is usually global to the program using this library.
+ final String? identifier;
+
+ /// The client secret for this client.
+ ///
+ /// The authorization server will issue each client a separate client
+ /// identifier and secret, which allows the server to tell which client is
+ /// accessing it. Some servers may also have an anonymous identifier/secret
+ /// pair that any client may use.
+ ///
+ /// This is usually global to the program using this library.
+ ///
+ /// Note that clients whose source code or binary executable is readily
+ /// available may not be able to make sure the client secret is kept a secret.
+ /// This is fine; OAuth2 servers generally won't rely on knowing with
+ /// certainty that a client is who it claims to be.
+ final String? secret;
+
+ /// The credentials this client uses to prove to the resource server that it's
+ /// authorized.
+ ///
+ /// This may change from request to request as the credentials expire and the
+ /// client refreshes them automatically.
+ Credentials get credentials => _credentials;
+ Credentials _credentials;
+
+ /// Callback to be invoked whenever the credentials refreshed.
+ final CredentialsRefreshedCallback? _onCredentialsRefreshed;
+
+ /// Whether to use HTTP Basic authentication for authorizing the client.
+ final bool _basicAuth;
+
+ /// The underlying HTTP client.
+ http.Client? _httpClient;
+
+ /// Creates a new client from a pre-existing set of credentials.
+ ///
+ /// When authorizing a client for the first time, you should use
+ /// [AuthorizationCodeGrant] or [resourceOwnerPasswordGrant] instead of
+ /// constructing a [Client] directly.
+ ///
+ /// [httpClient] is the underlying client that this forwards requests to after
+ /// adding authorization credentials to them.
+ ///
+ /// Throws an [ArgumentError] if [secret] is passed without [identifier].
+ Client(this._credentials,
+ {this.identifier,
+ this.secret,
+ CredentialsRefreshedCallback? onCredentialsRefreshed,
+ bool basicAuth = true,
+ http.Client? httpClient})
+ : _basicAuth = basicAuth,
+ _onCredentialsRefreshed = onCredentialsRefreshed,
+ _httpClient = httpClient ?? http.Client() {
+ if (identifier == null && secret != null) {
+ throw ArgumentError('secret may not be passed without identifier.');
+ }
+ }
+
+ /// Sends an HTTP request with OAuth2 authorization credentials attached.
+ ///
+ /// This will also automatically refresh this client's [Credentials] before
+ /// sending the request if necessary.
+ @override
+ Future<http.StreamedResponse> send(http.BaseRequest request) async {
+ if (credentials.isExpired) {
+ if (!credentials.canRefresh) throw ExpirationException(credentials);
+ await refreshCredentials();
+ }
+
+ request.headers['authorization'] = 'Bearer ${credentials.accessToken}';
+ var response = await _httpClient!.send(request);
+
+ if (response.statusCode != 401) return response;
+ if (!response.headers.containsKey('www-authenticate')) return response;
+
+ List<AuthenticationChallenge> challenges;
+ try {
+ challenges = AuthenticationChallenge.parseHeader(
+ response.headers['www-authenticate']!);
+ } on FormatException {
+ return response;
+ }
+
+ var challenge = challenges
+ .firstWhereOrNull((challenge) => challenge.scheme == 'bearer');
+ if (challenge == null) return response;
+
+ var params = challenge.parameters;
+ if (!params.containsKey('error')) return response;
+
+ throw AuthorizationException(params['error']!, params['error_description'],
+ params['error_uri'] == null ? null : Uri.parse(params['error_uri']!));
+ }
+
+ /// A [Future] used to track whether [refreshCredentials] is running.
+ Future<Credentials>? _refreshingFuture;
+
+ /// Explicitly refreshes this client's credentials. Returns this client.
+ ///
+ /// This will throw a [StateError] if the [Credentials] can't be refreshed, an
+ /// [AuthorizationException] if refreshing the credentials fails, or a
+ /// [FormatException] if the authorization server returns invalid responses.
+ ///
+ /// You may request different scopes than the default by passing in
+ /// [newScopes]. These must be a subset of the scopes in the
+ /// [Credentials.scopes] field of [Client.credentials].
+ Future<Client> refreshCredentials([List<String>? newScopes]) async {
+ if (!credentials.canRefresh) {
+ var prefix = 'OAuth credentials';
+ if (credentials.isExpired) prefix = '$prefix have expired and';
+ throw StateError("$prefix can't be refreshed.");
+ }
+
+ // To make sure that only one refresh happens when credentials are expired
+ // we track it using the [_refreshingFuture]. And also make sure that the
+ // _onCredentialsRefreshed callback is only called once.
+ if (_refreshingFuture == null) {
+ try {
+ _refreshingFuture = credentials.refresh(
+ identifier: identifier,
+ secret: secret,
+ newScopes: newScopes,
+ basicAuth: _basicAuth,
+ httpClient: _httpClient,
+ );
+ _credentials = await _refreshingFuture!;
+ _onCredentialsRefreshed?.call(_credentials);
+ } finally {
+ _refreshingFuture = null;
+ }
+ } else {
+ await _refreshingFuture;
+ }
+
+ return this;
+ }
+
+ /// Closes this client and its underlying HTTP client.
+ @override
+ void close() {
+ _httpClient?.close();
+ _httpClient = null;
+ }
+}
diff --git a/lib/src/third_party/oauth2/lib/src/client_credentials_grant.dart b/lib/src/third_party/oauth2/lib/src/client_credentials_grant.dart
new file mode 100644
index 0000000..045d1a0
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/client_credentials_grant.dart
@@ -0,0 +1,79 @@
+// 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:async';
+
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'client.dart';
+import 'handle_access_token_response.dart';
+import 'utils.dart';
+
+/// Obtains credentials using a [client credentials grant](https://tools.ietf.org/html/rfc6749#section-1.3.4).
+///
+/// This mode of authorization uses the client's [identifier] and [secret]
+/// to obtain an authorization token from the authorization server, instead
+/// of sending a user through a dedicated flow.
+///
+/// The client [identifier] and [secret] are required, and are
+/// used to identify and authenticate your specific OAuth2 client. These are
+/// usually global to the program using this library.
+///
+/// The specific permissions being requested from the authorization server may
+/// be specified via [scopes]. The scope strings are specific to the
+/// authorization server and may be found in its documentation. Note that you
+/// may not be granted access to every scope you request; you may check the
+/// [Credentials.scopes] field of [Client.credentials] to see which scopes you
+/// were granted.
+///
+/// The scope strings will be separated by the provided [delimiter]. This
+/// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
+/// use non-standard delimiters.
+///
+/// By default, this follows the OAuth2 spec and requires the server's responses
+/// to be in JSON format. However, some servers return non-standard response
+/// formats, which can be parsed using the [getParameters] function.
+///
+/// This function is passed the `Content-Type` header of the response as well as
+/// its body as a UTF-8-decoded string. It should return a map in the same
+/// format as the [standard JSON response](https://tools.ietf.org/html/rfc6749#section-5.1)
+Future<Client> clientCredentialsGrant(
+ Uri authorizationEndpoint, String? identifier, String? secret,
+ {Iterable<String>? scopes,
+ bool basicAuth = true,
+ http.Client? httpClient,
+ String? delimiter,
+ Map<String, dynamic> Function(MediaType? contentType, String body)?
+ getParameters}) async {
+ delimiter ??= ' ';
+ var startTime = DateTime.now();
+
+ var body = {'grant_type': 'client_credentials'};
+
+ var headers = <String, String>{};
+
+ if (identifier != null) {
+ if (basicAuth) {
+ headers['Authorization'] = basicAuthHeader(identifier, secret!);
+ } else {
+ body['client_id'] = identifier;
+ if (secret != null) body['client_secret'] = secret;
+ }
+ }
+
+ if (scopes != null && scopes.isNotEmpty) {
+ body['scope'] = scopes.join(delimiter);
+ }
+
+ httpClient ??= http.Client();
+ var response = await httpClient.post(authorizationEndpoint,
+ headers: headers, body: body);
+
+ var credentials = handleAccessTokenResponse(response, authorizationEndpoint,
+ startTime, scopes?.toList() ?? [], delimiter,
+ getParameters: getParameters);
+ return Client(credentials,
+ identifier: identifier, secret: secret, httpClient: httpClient);
+}
diff --git a/lib/src/third_party/oauth2/lib/src/credentials.dart b/lib/src/third_party/oauth2/lib/src/credentials.dart
new file mode 100644
index 0000000..459e63e
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/credentials.dart
@@ -0,0 +1,267 @@
+// 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:async';
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'handle_access_token_response.dart';
+import 'parameters.dart';
+import 'utils.dart';
+
+/// Type of the callback when credentials are refreshed.
+typedef CredentialsRefreshedCallback = void Function(Credentials);
+
+/// Credentials that prove that a client is allowed to access a resource on the
+/// resource owner's behalf.
+///
+/// These credentials are long-lasting and can be safely persisted across
+/// multiple runs of the program.
+///
+/// Many authorization servers will attach an expiration date to a set of
+/// credentials, along with a token that can be used to refresh the credentials
+/// once they've expired. The [Client] will automatically refresh its
+/// credentials when necessary. It's also possible to explicitly refresh them
+/// via [Client.refreshCredentials] or [Credentials.refresh].
+///
+/// Note that a given set of credentials can only be refreshed once, so be sure
+/// to save the refreshed credentials for future use.
+class Credentials {
+ /// A [String] used to separate scopes; defaults to `" "`.
+ String _delimiter;
+
+ /// The token that is sent to the resource server to prove the authorization
+ /// of a client.
+ final String accessToken;
+
+ /// The token that is sent to the authorization server to refresh the
+ /// credentials.
+ ///
+ /// This may be `null`, indicating that the credentials can't be refreshed.
+ final String? refreshToken;
+
+ /// The token that is received from the authorization server to enable
+ /// End-Users to be Authenticated, contains Claims, represented as a
+ /// JSON Web Token (JWT).
+ ///
+ /// This may be `null`, indicating that the 'openid' scope was not
+ /// requested (or not supported).
+ ///
+ /// [spec]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
+ final String? idToken;
+
+ /// 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<String>? scopes;
+
+ /// The date at which these credentials will expire.
+ ///
+ /// This is likely to be a few seconds earlier than the server's idea of the
+ /// expiration date.
+ final DateTime? expiration;
+
+ /// The function used to parse parameters from a host's response.
+ final GetParameters _getParameters;
+
+ /// Whether or not these credentials have expired.
+ ///
+ /// Note that it's possible the credentials will expire shortly after this is
+ /// called. However, since the client's expiration date is kept a few seconds
+ /// earlier than the server's, there should be enough leeway to rely on this.
+ bool get isExpired {
+ var expiration = this.expiration;
+ return expiration != null && DateTime.now().isAfter(expiration);
+ }
+
+ /// Whether it's possible to refresh these credentials.
+ bool get canRefresh => refreshToken != null && tokenEndpoint != null;
+
+ /// Creates a new set of credentials.
+ ///
+ /// This class is usually not constructed directly; rather, it's accessed via
+ /// [Client.credentials] after a [Client] is created by
+ /// [AuthorizationCodeGrant]. Alternately, it may be loaded from a serialized
+ /// form via [Credentials.fromJson].
+ ///
+ /// The scope strings will be separated by the provided [delimiter]. This
+ /// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
+ /// use non-standard delimiters.
+ ///
+ /// By default, this follows the OAuth2 spec and requires the server's
+ /// responses to be in JSON format. However, some servers return non-standard
+ /// response formats, which can be parsed using the [getParameters] function.
+ ///
+ /// This function is passed the `Content-Type` header of the response as well
+ /// as its body as a UTF-8-decoded string. It should return a map in the same
+ /// format as the [standard JSON response][].
+ ///
+ /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
+ Credentials(this.accessToken,
+ {this.refreshToken,
+ this.idToken,
+ this.tokenEndpoint,
+ Iterable<String>? scopes,
+ this.expiration,
+ String? delimiter,
+ Map<String, dynamic> Function(MediaType? mediaType, String body)?
+ getParameters})
+ : scopes = UnmodifiableListView(
+ // Explicitly type-annotate the list literal to work around
+ // sdk#24202.
+ scopes == null ? <String>[] : scopes.toList()),
+ _delimiter = delimiter ?? ' ',
+ _getParameters = getParameters ?? parseJsonParameters;
+
+ /// Loads a set of credentials from a JSON-serialized form.
+ ///
+ /// Throws a [FormatException] if the JSON is incorrectly formatted.
+ factory Credentials.fromJson(String json) {
+ void validate(bool 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');
+
+ parsed = parsed as Map;
+ validate(parsed.containsKey('accessToken'),
+ 'did not contain required field "accessToken"');
+ validate(
+ parsed['accessToken'] is String,
+ 'required field "accessToken" was not a string, was '
+ '${parsed["accessToken"]}',
+ );
+
+ for (var stringField in ['refreshToken', 'idToken', 'tokenEndpoint']) {
+ var value = parsed[stringField];
+ validate(value == null || value is String,
+ 'field "$stringField" was not a string, was "$value"');
+ }
+
+ var scopes = parsed['scopes'];
+ validate(scopes == null || scopes is List,
+ 'field "scopes" was not a list, was "$scopes"');
+
+ var tokenEndpoint = parsed['tokenEndpoint'];
+ Uri? tokenEndpointUri;
+ if (tokenEndpoint != null) {
+ tokenEndpointUri = Uri.parse(tokenEndpoint as String);
+ }
+
+ var expiration = parsed['expiration'];
+ DateTime? expirationDateTime;
+ if (expiration != null) {
+ validate(expiration is int,
+ 'field "expiration" was not an int, was "$expiration"');
+ expiration = expiration as int;
+ expirationDateTime = DateTime.fromMillisecondsSinceEpoch(expiration);
+ }
+
+ return Credentials(
+ parsed['accessToken'] as String,
+ refreshToken: parsed['refreshToken'] as String?,
+ idToken: parsed['idToken'] as String?,
+ tokenEndpoint: tokenEndpointUri,
+ scopes: (scopes as List).map((scope) => scope as String),
+ expiration: expirationDateTime,
+ );
+ }
+
+ /// Serializes a set of credentials to JSON.
+ ///
+ /// Nothing is guaranteed about the output except that it's valid JSON and
+ /// compatible with [Credentials.toJson].
+ String toJson() => jsonEncode({
+ 'accessToken': accessToken,
+ 'refreshToken': refreshToken,
+ 'idToken': idToken,
+ 'tokenEndpoint': tokenEndpoint?.toString(),
+ 'scopes': scopes,
+ 'expiration': expiration?.millisecondsSinceEpoch
+ });
+
+ /// Returns a new set of refreshed credentials.
+ ///
+ /// See [Client.identifier] and [Client.secret] for explanations of those
+ /// parameters.
+ ///
+ /// You may request different scopes than the default by passing in
+ /// [newScopes]. These must be a subset of [scopes].
+ ///
+ /// This throws an [ArgumentError] if [secret] is passed without [identifier],
+ /// a [StateError] if these credentials can't be refreshed, an
+ /// [AuthorizationException] if refreshing the credentials fails, or a
+ /// [FormatException] if the authorization server returns invalid responses.
+ Future<Credentials> refresh(
+ {String? identifier,
+ String? secret,
+ Iterable<String>? newScopes,
+ bool basicAuth = true,
+ http.Client? httpClient}) async {
+ var scopes = this.scopes;
+ if (newScopes != null) scopes = newScopes.toList();
+ scopes ??= [];
+ httpClient ??= http.Client();
+
+ if (identifier == null && secret != null) {
+ throw ArgumentError('secret may not be passed without identifier.');
+ }
+
+ var startTime = DateTime.now();
+ var tokenEndpoint = this.tokenEndpoint;
+ if (refreshToken == null) {
+ throw StateError("Can't refresh credentials without a refresh "
+ 'token.');
+ } else if (tokenEndpoint == null) {
+ throw StateError("Can't refresh credentials without a token "
+ 'endpoint.');
+ }
+
+ var headers = <String, String>{};
+
+ var body = {'grant_type': 'refresh_token', 'refresh_token': refreshToken};
+ if (scopes.isNotEmpty) body['scope'] = scopes.join(_delimiter);
+
+ if (basicAuth && secret != null) {
+ headers['Authorization'] = basicAuthHeader(identifier!, secret);
+ } else {
+ if (identifier != null) body['client_id'] = identifier;
+ if (secret != null) body['client_secret'] = secret;
+ }
+
+ var response =
+ await httpClient.post(tokenEndpoint, headers: headers, body: body);
+ var credentials = handleAccessTokenResponse(
+ response, tokenEndpoint, startTime, scopes, _delimiter,
+ getParameters: _getParameters);
+
+ // The authorization server may issue a new refresh token. If it doesn't,
+ // we should re-use the one we already have.
+ if (credentials.refreshToken != null) return credentials;
+ return Credentials(credentials.accessToken,
+ refreshToken: refreshToken,
+ idToken: credentials.idToken,
+ tokenEndpoint: credentials.tokenEndpoint,
+ scopes: credentials.scopes,
+ expiration: credentials.expiration);
+ }
+}
diff --git a/lib/src/third_party/oauth2/lib/src/expiration_exception.dart b/lib/src/third_party/oauth2/lib/src/expiration_exception.dart
new file mode 100644
index 0000000..d72fcf6
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/expiration_exception.dart
@@ -0,0 +1,19 @@
+// 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 'credentials.dart';
+
+/// An exception raised when attempting to use expired OAuth2 credentials.
+class ExpirationException implements Exception {
+ /// The expired credentials.
+ final Credentials credentials;
+
+ /// Creates an ExpirationException.
+ ExpirationException(this.credentials);
+
+ /// Provides a string description of the ExpirationException.
+ @override
+ String toString() =>
+ "OAuth2 credentials have expired and can't be refreshed.";
+}
diff --git a/lib/src/third_party/oauth2/lib/src/handle_access_token_response.dart b/lib/src/third_party/oauth2/lib/src/handle_access_token_response.dart
new file mode 100644
index 0000000..931ae9d
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/handle_access_token_response.dart
@@ -0,0 +1,156 @@
+// 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 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'authorization_exception.dart';
+import 'credentials.dart';
+import 'parameters.dart';
+
+/// The amount of time to add as a "grace period" for credential expiration.
+///
+/// This allows credential expiration checks to remain valid for a reasonable
+/// amount of time.
+const _expirationGrace = Duration(seconds: 10);
+
+/// Handles a response from the authorization server that contains an access
+/// token.
+///
+/// This response format is common across several different components of the
+/// OAuth2 flow.
+///
+/// By default, this follows the OAuth2 spec and requires the server's responses
+/// to be in JSON format. However, some servers return non-standard response
+/// formats, which can be parsed using the [getParameters] function.
+///
+/// This function is passed the `Content-Type` header of the response as well as
+/// its body as a UTF-8-decoded string. It should return a map in the same
+/// format as the [standard JSON response][].
+///
+/// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
+Credentials handleAccessTokenResponse(http.Response response, Uri tokenEndpoint,
+ DateTime startTime, List<String>? scopes, String delimiter,
+ {Map<String, dynamic> Function(MediaType? contentType, String body)?
+ getParameters}) {
+ getParameters ??= parseJsonParameters;
+
+ try {
+ if (response.statusCode != 200) {
+ _handleErrorResponse(response, tokenEndpoint, getParameters);
+ }
+
+ var contentTypeString = response.headers['content-type'];
+ if (contentTypeString == null) {
+ throw const FormatException('Missing Content-Type string.');
+ }
+
+ var parameters =
+ getParameters(MediaType.parse(contentTypeString), response.body);
+
+ for (var requiredParameter in ['access_token', 'token_type']) {
+ if (!parameters.containsKey(requiredParameter)) {
+ throw FormatException(
+ 'did not contain required parameter "$requiredParameter"');
+ } else if (parameters[requiredParameter] is! String) {
+ throw FormatException(
+ 'required parameter "$requiredParameter" was not a string, was '
+ '"${parameters[requiredParameter]}"');
+ }
+ }
+
+ // TODO(nweiz): support the "mac" token type
+ // (http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01)
+ if ((parameters['token_type'] as String).toLowerCase() != 'bearer') {
+ throw FormatException(
+ '"$tokenEndpoint": unknown token type "${parameters['token_type']}"');
+ }
+
+ var expiresIn = parameters['expires_in'];
+ if (expiresIn != null) {
+ if (expiresIn is String) {
+ try {
+ expiresIn = double.parse(expiresIn).toInt();
+ } on FormatException {
+ throw FormatException(
+ 'parameter "expires_in" could not be parsed as in, was: "$expiresIn"');
+ }
+ } else if (expiresIn is! int) {
+ throw FormatException(
+ 'parameter "expires_in" was not an int, was: "$expiresIn"');
+ }
+ }
+
+ for (var name in ['refresh_token', 'id_token', 'scope']) {
+ var value = parameters[name];
+ if (value != null && value is! String) {
+ throw FormatException(
+ 'parameter "$name" was not a string, was "$value"');
+ }
+ }
+
+ var scope = parameters['scope'] as String?;
+ if (scope != null) scopes = scope.split(delimiter);
+
+ var expiration = expiresIn == null
+ ? null
+ : startTime.add(Duration(seconds: expiresIn as int) - _expirationGrace);
+
+ return Credentials(
+ parameters['access_token'] as String,
+ refreshToken: parameters['refresh_token'] as String?,
+ idToken: parameters['id_token'] as String?,
+ tokenEndpoint: tokenEndpoint,
+ scopes: scopes,
+ expiration: expiration,
+ );
+ } on FormatException catch (e) {
+ throw FormatException('Invalid OAuth response for "$tokenEndpoint": '
+ '${e.message}.\n\n${response.body}');
+ }
+}
+
+/// Throws the appropriate exception for an error response from the
+/// authorization server.
+void _handleErrorResponse(
+ http.Response response, Uri tokenEndpoint, GetParameters getParameters) {
+ // OAuth2 mandates a 400 or 401 response code for access token error
+ // responses. If it's not a 400 reponse, the server is either broken or
+ // off-spec.
+ if (response.statusCode != 400 && response.statusCode != 401) {
+ var reason = '';
+ var reasonPhrase = response.reasonPhrase;
+ if (reasonPhrase != null && reasonPhrase.isNotEmpty) {
+ reason = ' $reasonPhrase';
+ }
+ throw FormatException('OAuth request for "$tokenEndpoint" failed '
+ 'with status ${response.statusCode}$reason.\n\n${response.body}');
+ }
+
+ var contentTypeString = response.headers['content-type'];
+ var contentType =
+ contentTypeString == null ? null : MediaType.parse(contentTypeString);
+
+ var parameters = getParameters(contentType, response.body);
+
+ if (!parameters.containsKey('error')) {
+ throw const FormatException('did not contain required parameter "error"');
+ } else if (parameters['error'] is! String) {
+ throw FormatException('required parameter "error" was not a string, was '
+ '"${parameters["error"]}"');
+ }
+
+ for (var name in ['error_description', 'error_uri']) {
+ var value = parameters[name];
+
+ if (value != null && value is! String) {
+ throw FormatException('parameter "$name" was not a string, was "$value"');
+ }
+ }
+
+ var uriString = parameters['error_uri'] as String?;
+ var uri = uriString == null ? null : Uri.parse(uriString);
+ var description = parameters['error_description'] as String?;
+ throw AuthorizationException(parameters['error'] as String, description, uri);
+}
diff --git a/lib/src/third_party/oauth2/lib/src/parameters.dart b/lib/src/third_party/oauth2/lib/src/parameters.dart
new file mode 100644
index 0000000..ecc6559
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/parameters.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2018, 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:http_parser/http_parser.dart';
+
+/// The type of a callback that parses parameters from an HTTP response.
+typedef GetParameters = Map<String, dynamic> Function(
+ MediaType? contentType, String body);
+
+/// Parses parameters from a response with a JSON body, as per the
+/// [OAuth2 spec][].
+///
+/// [OAuth2 spec]: https://tools.ietf.org/html/rfc6749#section-5.1
+Map<String, dynamic> parseJsonParameters(MediaType? contentType, String body) {
+ // The spec requires a content-type of application/json, but some endpoints
+ // (e.g. Dropbox) serve it as text/javascript instead.
+ if (contentType == null ||
+ (contentType.mimeType != 'application/json' &&
+ contentType.mimeType != 'text/javascript')) {
+ throw FormatException(
+ 'Content-Type was "$contentType", expected "application/json"');
+ }
+
+ var untypedParameters = jsonDecode(body);
+ if (untypedParameters is Map<String, dynamic>) {
+ return untypedParameters;
+ }
+
+ throw FormatException('Parameters must be a map, was "$untypedParameters"');
+}
diff --git a/lib/src/third_party/oauth2/lib/src/resource_owner_password_grant.dart b/lib/src/third_party/oauth2/lib/src/resource_owner_password_grant.dart
new file mode 100644
index 0000000..96fb503
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/resource_owner_password_grant.dart
@@ -0,0 +1,94 @@
+// 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:async';
+
+import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
+
+import 'client.dart';
+import 'credentials.dart';
+import 'handle_access_token_response.dart';
+import 'utils.dart';
+
+/// Obtains credentials using a [resource owner password grant](https://tools.ietf.org/html/rfc6749#section-1.3.3).
+///
+/// This mode of authorization uses the user's username and password to obtain
+/// an authentication token, which can then be stored. This is safer than
+/// storing the username and password directly, but it should be avoided if any
+/// other authorization method is available, since it requires the user to
+/// provide their username and password to a third party (you).
+///
+/// The client [identifier] and [secret] may be issued by the server, and are
+/// used to identify and authenticate your specific OAuth2 client. These are
+/// usually global to the program using this library.
+///
+/// The specific permissions being requested from the authorization server may
+/// be specified via [scopes]. The scope strings are specific to the
+/// authorization server and may be found in its documentation. Note that you
+/// may not be granted access to every scope you request; you may check the
+/// [Credentials.scopes] field of [Client.credentials] to see which scopes you
+/// were granted.
+///
+/// The scope strings will be separated by the provided [delimiter]. This
+/// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
+/// use non-standard delimiters.
+///
+/// By default, this follows the OAuth2 spec and requires the server's responses
+/// to be in JSON format. However, some servers return non-standard response
+/// formats, which can be parsed using the [getParameters] function.
+///
+/// This function is passed the `Content-Type` header of the response as well as
+/// its body as a UTF-8-decoded string. It should return a map in the same
+/// format as the [standard JSON response][].
+///
+/// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
+Future<Client> resourceOwnerPasswordGrant(
+ Uri authorizationEndpoint, String username, String password,
+ {String? identifier,
+ String? secret,
+ Iterable<String>? scopes,
+ bool basicAuth = true,
+ CredentialsRefreshedCallback? onCredentialsRefreshed,
+ http.Client? httpClient,
+ String? delimiter,
+ Map<String, dynamic> Function(MediaType? contentType, String body)?
+ getParameters}) async {
+ delimiter ??= ' ';
+ var startTime = DateTime.now();
+
+ var body = {
+ 'grant_type': 'password',
+ 'username': username,
+ 'password': password
+ };
+
+ var headers = <String, String>{};
+
+ if (identifier != null) {
+ if (basicAuth) {
+ headers['Authorization'] = basicAuthHeader(identifier, secret!);
+ } else {
+ body['client_id'] = identifier;
+ if (secret != null) body['client_secret'] = secret;
+ }
+ }
+
+ if (scopes != null && scopes.isNotEmpty) {
+ body['scope'] = scopes.join(delimiter);
+ }
+
+ httpClient ??= http.Client();
+ var response = await httpClient.post(authorizationEndpoint,
+ headers: headers, body: body);
+
+ var credentials = handleAccessTokenResponse(
+ response, authorizationEndpoint, startTime, scopes?.toList(), delimiter,
+ getParameters: getParameters);
+ return Client(credentials,
+ identifier: identifier,
+ secret: secret,
+ httpClient: httpClient,
+ onCredentialsRefreshed: onCredentialsRefreshed);
+}
diff --git a/lib/src/third_party/oauth2/lib/src/utils.dart b/lib/src/third_party/oauth2/lib/src/utils.dart
new file mode 100644
index 0000000..2a22b9f
--- /dev/null
+++ b/lib/src/third_party/oauth2/lib/src/utils.dart
@@ -0,0 +1,15 @@
+// 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';
+
+/// Adds additional query parameters to [url], overwriting the original
+/// parameters if a name conflict occurs.
+Uri addQueryParameters(Uri url, Map<String, String> parameters) => url.replace(
+ queryParameters: Map.from(url.queryParameters)..addAll(parameters));
+
+String basicAuthHeader(String identifier, String secret) {
+ var userPass = '${Uri.encodeFull(identifier)}:${Uri.encodeFull(secret)}';
+ return 'Basic ${base64Encode(ascii.encode(userPass))}';
+}
diff --git a/lib/src/third_party/oauth2/vendored-pubspec.yaml b/lib/src/third_party/oauth2/vendored-pubspec.yaml
new file mode 100644
index 0000000..bb43bb7
--- /dev/null
+++ b/lib/src/third_party/oauth2/vendored-pubspec.yaml
@@ -0,0 +1,20 @@
+name: oauth2
+version: 2.0.1
+description: >-
+ A client library for authenticating with a remote service via OAuth2 on
+ behalf of a user, and making authorized HTTP requests with the user's
+ OAuth2 credentials.
+repository: https://github.com/dart-lang/oauth2
+
+environment:
+ sdk: '>=2.17.0 <3.0.0'
+
+dependencies:
+ collection: ^1.15.0
+ crypto: ^3.0.0
+ http: ^0.13.0
+ http_parser: ^4.0.0
+
+dev_dependencies:
+ lints: ^2.0.0
+ test: ^1.16.0
diff --git a/lib/src/third_party/tar/CHANGELOG.md b/lib/src/third_party/tar/CHANGELOG.md
new file mode 100644
index 0000000..a804552
--- /dev/null
+++ b/lib/src/third_party/tar/CHANGELOG.md
@@ -0,0 +1,86 @@
+## 0.5.6
+
+- Allow cancelling a `TarEntry.contents` subscription before reading more files.
+
+## 0.5.5+1
+
+- No user-visible changes.
+
+## 0.5.5
+
+- Fix a crash when pausing a subscription to `TarEntry.contents` right before
+ it ends.
+
+## 0.5.4
+
+- Fix generating corrupt tar files when adding lots of entries at very high
+ speeds [(#20)](https://github.com/simolus3/tar/issues/20).
+- Allow tar files with invalid utf8 content in PAX header values if those
+ values aren't used for anything important.
+
+## 0.5.3
+
+- Improve error messages when reading a tar entry after, or during, a call to
+ `moveNext()`.
+
+## 0.5.2
+
+- This package now supports being compiled to JavaScript.
+
+## 0.5.1
+
+- Improve performance when reading large archives
+
+## 0.5.0
+
+- Support sync encoding with `tarConverter`.
+
+## 0.4.0
+
+- Support generating tar files with GNU-style long link names
+ - Add `format` parameter to `tarWritingSink` and `tarWriterWith`
+
+## 0.3.3
+
+- Drop `chunked_stream` dependency in favor of `package:async`.
+
+## 0.3.2
+
+- Allow arbitrarily many zero bytes at the end of an archive when
+ `disallowTrailingData` is enabled.
+
+## 0.3.1
+
+- Add `disallowTrailingData` parameter to `TarReader`. When the option is set,
+ `readNext` will ensure that the input stream does not emit further data after
+ the tar archive has been read fully.
+
+## 0.3.0
+
+- Remove outdated references in the documentation
+
+## 0.3.0-nullsafety.0
+
+- Remove `TarReader.contents` and `TarReader.header`. Use `current.contents` and `current.header`, respectively.
+- Fix some minor implementation details
+
+## 0.2.0-nullsafety
+
+Most of the tar package has been rewritten, it's now based on the
+implementation written by [Garett Tok Ern Liang](https://github.com/walnutdust)
+in the GSoC 2020.
+
+- Added `tar` prefix to exported symbols.
+- Remove `MemoryEntry`. Use `TarEntry.data` to create a tar entry from bytes.
+- Make `WritingSink` private. Use `tarWritingSink` to create a general `StreamSink<tar.Entry>`.
+- `TarReader` is now a [`StreamIterator`](https://api.dart.dev/stable/2.10.4/dart-async/StreamIterator-class.html),
+ the transformer had some design flaws.
+
+## 0.1.0-nullsafety.1
+
+- Support writing user and group names
+- Better support for PAX-headers and large files
+
+## 0.1.0-nullsafety.0
+
+- Initial version
diff --git a/lib/src/third_party/tar/README.md b/lib/src/third_party/tar/README.md
index 5e12e5a..8d5a334 100644
--- a/lib/src/third_party/tar/README.md
+++ b/lib/src/third_party/tar/README.md
@@ -1,7 +1,214 @@
-# package:tar
+# tar
-Vendored elements from `package:tar` for use in creation and extraction of
-tar-archives.
+![Build status](https://github.com/simolus3/tar/workflows/build/badge.svg)
- * Repository: `https://github.com/simolus3/tar/`
- * Revision: `23ee71d667f003fba8c80ee126d5e1330d17c141`
+This package provides stream-based readers and writers for tar files.
+
+When working with large tar files, this library consumes considerably less memory
+than [package:archive](https://pub.dev/packages/archive), although it is slightly slower due to the async overhead.
+
+## Reading
+
+To read entries from a tar file, use a `TarReader` with a `Stream` emitting bytes (as `List<int>`):
+
+```dart
+import 'dart:convert';
+import 'dart:io';
+import 'package:tar/tar.dart';
+
+Future<void> main() async {
+ final reader = TarReader(File('file.tar').openRead());
+
+ while (await reader.moveNext()) {
+ final entry = reader.current;
+ // Use reader.header to see the header of the current tar entry
+ print(entry.header.name);
+ // And reader.contents to read the content of the current entry as a stream
+ print(await entry.contents.transform(utf8.decoder).first);
+ }
+ // Note that the reader will automatically close if moveNext() returns false or
+ // throws. If you want to close a tar stream before that happens, use
+ // reader.cancel();
+}
+```
+
+To read `.tar.gz` files, transform the stream with `gzip.decoder` before
+passing it to the `TarReader`.
+
+To easily go through all entries in a tar file, use `TarReader.forEach`:
+
+```dart
+Future<void> main() async {
+ final inputStream = File('file.tar').openRead();
+
+ await TarReader.forEach(inputStream, (entry) {
+ print(header.name);
+ print(await entry.contents.transform(utf8.decoder).first);
+ });
+}
+```
+
+__Warning__: Since the reader is backed by a single stream, concurrent calls to
+`read` are not allowed! Similarly, if you're reading from an entry's `contents`,
+make sure to fully drain the stream before calling `read()` again.
+_Not_ subscribing to `contents` before calling `moveNext()` is acceptable too.
+In this case, the reader will implicitly drain the stream.
+The reader detects concurrency misuses and will throw an error when they occur,
+there's no risk of reading faulty data.
+
+## Writing
+
+When writing archives, `package:tar` expects a `Stream` of tar entries to include in
+the archive.
+This stream can then be converted into a stream of byte-array chunks forming the
+encoded tar archive.
+
+To write a tar stream into a `StreamSink<List<int>>`, such as an `IOSink` returned by
+`File.openWrite`, use `tarWritingSink`:
+
+```dart
+import 'dart:convert';
+import 'dart:io';
+import 'package:tar/tar.dart';
+
+Future<void> main() async {
+ final output = File('test.tar').openWrite();
+ final tarEntries = Stream<TarEntry>.value(
+ TarEntry.data(
+ TarHeader(
+ name: 'hello.txt',
+ mode: int.parse('644', radix: 8),
+ ),
+ utf8.encode('Hello world'),
+ ),
+ );
+
+ await tarEntries.pipe(tarWritingSink(output));
+}
+```
+
+For more complex stream transformations, `tarWriter` can be used as a stream
+transformer converting a stream of tar entries into archive bytes.
+
+Together with the `gzip.encoder` transformer from `dart:io`, this can be used
+to write a `.tar.gz` file:
+
+```dart
+import 'dart:io';
+import 'package:tar/tar.dart';
+
+Future<void> write(Stream<TarEntry> entries) {
+ return entries
+ .transform(tarWriter) // convert entries into a .tar stream
+ .transform(gzip.encoder) // convert the .tar stream into a .tar.gz stream
+ .pipe(File('output.tar.gz').openWrite());
+}
+```
+
+A more complex example for writing files can be found in [`example/archive_self.dart`](example/archive_self.dart).
+
+### Encoding options
+
+By default, tar files are written in the pax format defined by the
+POSIX.1-2001 specification (`--format=posix` in GNU tar).
+When all entries have file names shorter than 100 chars and a size smaller
+than 8 GB, this is equivalent to the `ustar` format. This library won't write
+PAX headers when there is no reason to do so.
+If you prefer writing GNU-style long filenames instead, you can use the
+`format` option:
+
+```dart
+Future<void> write(Stream<TarEntry> entries) {
+ return entries
+ .pipe(
+ tarWritingSink(
+ File('output.tar').openWrite(),
+ format: OutputFormat.gnuLongName,
+ ));
+}
+```
+
+To change the output format on the `tarWriter` transformer, use
+`tarWriterWith`.
+
+### Synchronous writing
+
+As the content of tar entries is defined as an asynchronous stream, the tar encoder is asynchronous too.
+The more specific `SynchronousTarEntry` class stores tar content as a list of bytes, meaning that it can be
+written synchronously.
+
+To synchronously write tar files, use `tarConverter` (or `tarConverterWith` for options):
+
+```dart
+List<int> createTarArchive(Iterable<SynchronousTarEntry> entries) {
+ late List<int> result;
+ final sink = ByteConversionSink.withCallback((data) => result = data);
+
+ final output = tarConverter.startChunkedConversion(sink);
+ entries.forEach(output.add);
+ output.close();
+
+ return result;
+}
+```
+
+## Features
+
+- Supports v7, ustar, pax, gnu and star archives
+- Supports extended pax headers for long file or link names
+- Supports long file and link names generated by GNU-tar
+- Hardened against denial-of-service attacks with invalid tar files
+- Supports being compiled to JavaScript, tested on Node.js
+
+## Security considerations
+
+Internally, this package contains checks to guard against some invalid tar files.
+In particular,
+
+- The reader doesn't allocate memory based on values in a tar file (so there's
+ a guard against DoS attacks with tar files containing huge headers).
+- When encountering malformed tar files, the reader will throw a `TarException`.
+ Any other exception thrown indicates a bug in `package:tar` or how it's used.
+ The reader should never crash.
+- Reading a tar file can be cancelled mid-stream without leaking resources.
+
+However, the tar reader __does not__ throw exceptions for wellformed archives
+with suspicious contents, such as
+
+- File names beginning with `../`, `/` or names pointing out of the archive by
+ other means.
+- Link references to files outside of the archive.
+- Paths not using forward slashes.
+- Gzip + tar bombs.
+- Invalid permission bits in entries.
+- ...
+
+When reading or extracting untrusted tar files, it is your responsibility to
+detect and handle these cases.
+For instance, this naive extraction function is susceptible to invalid tar
+files containing paths outside of the target directory:
+
+```dart
+Future<void> extractTarGz(File tarGz, Directory target) async {
+ final input = tarGz.openRead().transform(gzip.decoder);
+
+ await TarReader.forEach(input, (entry) async {
+ final destination =
+ // DON'T DO THIS! If `entry.name` contained `../`, this may escape the
+ // target directory.
+ path.joinAll([target.path, ...path.posix.split(entry.name)]);
+
+ final f = File(destination);
+ await f.create(recursive: true);
+ await entry.contents.pipe(f.openWrite());
+ });
+}
+```
+
+For an idea on how to guard against this, see the [extraction logic](https://github.com/dart-lang/pub/blob/3082796f8ba9b3f509265ac3a223312fb5033988/lib/src/io.dart#L904-L991)
+used by the pub client.
+
+-----
+
+Big thanks to [Garett Tok Ern Liang](https://github.com/walnutdust) for writing the initial
+Dart tar reader that this library is based on.
diff --git a/lib/src/third_party/tar/analysis_options.yaml b/lib/src/third_party/tar/analysis_options.yaml
new file mode 100644
index 0000000..8b18047
--- /dev/null
+++ b/lib/src/third_party/tar/analysis_options.yaml
@@ -0,0 +1,20 @@
+include: package:extra_pedantic/analysis_options.2.0.0.yaml
+
+analyzer:
+ strong-mode:
+ implicit-casts: false
+ implicit-dynamic: false
+ language:
+ strict-inference: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ close_sinks: false # This rule has just too many false-positives...
+ comment_references: true
+ literal_only_boolean_expressions: false # Nothing wrong with a little while(true)
+ parameter_assignments: false
+ unnecessary_await_in_return: false
+ no_default_cases: false
+ prefer_asserts_with_message: false # We only use asserts for library-internal invariants
+ prefer_final_parameters: false # Too much noise
diff --git a/lib/src/third_party/tar/src/charcodes.dart b/lib/src/third_party/tar/lib/src/charcodes.dart
similarity index 100%
rename from lib/src/third_party/tar/src/charcodes.dart
rename to lib/src/third_party/tar/lib/src/charcodes.dart
diff --git a/lib/src/third_party/tar/src/constants.dart b/lib/src/third_party/tar/lib/src/constants.dart
similarity index 100%
rename from lib/src/third_party/tar/src/constants.dart
rename to lib/src/third_party/tar/lib/src/constants.dart
diff --git a/lib/src/third_party/tar/src/entry.dart b/lib/src/third_party/tar/lib/src/entry.dart
similarity index 100%
rename from lib/src/third_party/tar/src/entry.dart
rename to lib/src/third_party/tar/lib/src/entry.dart
diff --git a/lib/src/third_party/tar/src/exception.dart b/lib/src/third_party/tar/lib/src/exception.dart
similarity index 100%
rename from lib/src/third_party/tar/src/exception.dart
rename to lib/src/third_party/tar/lib/src/exception.dart
diff --git a/lib/src/third_party/tar/src/format.dart b/lib/src/third_party/tar/lib/src/format.dart
similarity index 100%
rename from lib/src/third_party/tar/src/format.dart
rename to lib/src/third_party/tar/lib/src/format.dart
diff --git a/lib/src/third_party/tar/src/header.dart b/lib/src/third_party/tar/lib/src/header.dart
similarity index 100%
rename from lib/src/third_party/tar/src/header.dart
rename to lib/src/third_party/tar/lib/src/header.dart
diff --git a/lib/src/third_party/tar/src/reader.dart b/lib/src/third_party/tar/lib/src/reader.dart
similarity index 100%
rename from lib/src/third_party/tar/src/reader.dart
rename to lib/src/third_party/tar/lib/src/reader.dart
diff --git a/lib/src/third_party/tar/src/sparse.dart b/lib/src/third_party/tar/lib/src/sparse.dart
similarity index 100%
rename from lib/src/third_party/tar/src/sparse.dart
rename to lib/src/third_party/tar/lib/src/sparse.dart
diff --git a/lib/src/third_party/tar/src/utils.dart b/lib/src/third_party/tar/lib/src/utils.dart
similarity index 100%
rename from lib/src/third_party/tar/src/utils.dart
rename to lib/src/third_party/tar/lib/src/utils.dart
diff --git a/lib/src/third_party/tar/src/writer.dart b/lib/src/third_party/tar/lib/src/writer.dart
similarity index 100%
rename from lib/src/third_party/tar/src/writer.dart
rename to lib/src/third_party/tar/lib/src/writer.dart
diff --git a/lib/src/third_party/tar/tar.dart b/lib/src/third_party/tar/lib/tar.dart
similarity index 100%
rename from lib/src/third_party/tar/tar.dart
rename to lib/src/third_party/tar/lib/tar.dart
diff --git a/lib/src/third_party/tar/vendored-pubspec.yaml b/lib/src/third_party/tar/vendored-pubspec.yaml
new file mode 100644
index 0000000..0556ca8
--- /dev/null
+++ b/lib/src/third_party/tar/vendored-pubspec.yaml
@@ -0,0 +1,24 @@
+name: tar
+description: Memory-efficient, streaming implementation of the tar file format
+version: 0.5.6
+repository: https://github.com/simolus3/tar/
+
+environment:
+ sdk: '>=2.12.0 <3.0.0'
+
+dependencies:
+ async: ^2.6.0
+ meta: ^1.3.0
+ typed_data: ^1.3.0
+
+dev_dependencies:
+ charcode: ^1.2.0
+ extra_pedantic: ^3.0.0
+ file: ^6.1.2
+ node_io: ^2.1.0
+ path: ^1.8.0
+ test: ^1.20.0
+
+dependency_overrides:
+ # Waiting for https://github.com/pulyaevskiy/node-interop/issues/110
+ file: '>=6.1.0 <6.1.3'
diff --git a/lib/src/third_party/vendor-state.yaml b/lib/src/third_party/vendor-state.yaml
new file mode 100644
index 0000000..09c5eeb
--- /dev/null
+++ b/lib/src/third_party/vendor-state.yaml
@@ -0,0 +1,29 @@
+# DO NOT EDIT: This file is generated by package:vendor version 0.9.0
+version: 0.9.0
+config:
+ import_rewrites:
+ oauth2: oauth2
+ tar: tar
+ vendored_dependencies:
+ oauth2:
+ package: oauth2
+ version: 2.0.1
+ import_rewrites: {}
+ include:
+ - pubspec.yaml
+ - README.md
+ - LICENSE
+ - CHANGELOG.md
+ - lib/**
+ - analysis_options.yaml
+ tar:
+ package: tar
+ version: 0.5.6
+ import_rewrites: {}
+ include:
+ - pubspec.yaml
+ - README.md
+ - LICENSE
+ - CHANGELOG.md
+ - lib/**
+ - analysis_options.yaml
diff --git a/pubspec.yaml b/pubspec.yaml
index 00c9829..cc655dc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -18,14 +18,15 @@
http_multi_server: ^3.0.1
http_parser: ^4.0.1
meta: ^1.3.0
- oauth2: ^2.0.0
path: ^1.8.0
pool: ^1.5.0
pub_semver: ^2.1.0
shelf: ^1.1.1
source_span: ^1.8.1
stack_trace: ^1.10.0
+ typed_data: ^1.3.1
usage: ^4.0.2
+ vendor: ^0.9.2
yaml: ^3.1.0
yaml_edit: ^2.0.0
@@ -35,3 +36,4 @@
test: ^1.21.5
test_descriptor: ^2.0.0
test_process: ^2.0.0
+
diff --git a/test/descriptor.dart b/test/descriptor.dart
index 6deecd7..ab3a7ec 100644
--- a/test/descriptor.dart
+++ b/test/descriptor.dart
@@ -5,10 +5,10 @@
/// Pub-specific test descriptors.
import 'dart:convert';
-import 'package:oauth2/oauth2.dart' as oauth2;
import 'package:path/path.dart' as p;
import 'package:pub/src/language_version.dart';
import 'package:pub/src/package_config.dart';
+import 'package:pub/src/third_party/oauth2/lib/oauth2.dart' as oauth2;
import 'package:test_descriptor/test_descriptor.dart';
import 'descriptor/git.dart';
diff --git a/test/io_test.dart b/test/io_test.dart
index 0657dd6..80bd1be 100644
--- a/test/io_test.dart
+++ b/test/io_test.dart
@@ -9,7 +9,7 @@
import 'package:path/path.dart' as path;
import 'package:pub/src/exceptions.dart';
import 'package:pub/src/io.dart';
-import 'package:pub/src/third_party/tar/tar.dart';
+import 'package:pub/src/third_party/tar/lib/tar.dart';
import 'package:test/test.dart';
import 'descriptor.dart' as d;
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 6a2e67b..a932b6b 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -27,7 +27,7 @@
import 'package:pub/src/package_name.dart';
import 'package:pub/src/source/hosted.dart';
import 'package:pub/src/system_cache.dart';
-import 'package:pub/src/third_party/tar/tar.dart';
+import 'package:pub/src/third_party/tar/lib/tar.dart';
import 'package:pub/src/utils.dart';
import 'package:pub/src/validator.dart';
import 'package:pub_semver/pub_semver.dart';
diff --git a/vendor.yaml b/vendor.yaml
new file mode 100644
index 0000000..27db379
--- /dev/null
+++ b/vendor.yaml
@@ -0,0 +1,10 @@
+import_rewrites:
+ oauth2: oauth2
+ tar: tar
+vendored_dependencies:
+ oauth2:
+ package: oauth2
+ version: 2.0.1
+ tar:
+ package: tar
+ version: 0.5.6